rails-profiler 0.34.3 → 0.34.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 066e4af89b9741273fe1740d6dfbcecaa32df311835699642cafad8e703ca4ec
4
- data.tar.gz: 05f65764c8e80a2b61ceea1f164dd85fa1d3d920b71452c3abdd35044698107b
3
+ metadata.gz: d4912dd474a748e657c35111e3224d83500e75960ef2592e5cd652155dc62617
4
+ data.tar.gz: d0d4cb0f34f25d98f5be08875bdc59ec4cc84894f7cbe0b512fd1ecd153dda4b
5
5
  SHA512:
6
- metadata.gz: 7eeb6e60ace7bbfccb32d3a5eeb0d42243a5bd39d551d08aeaefe05ae8cb79a76b91aee3e1e2a5e2edcaba11d1cd99a89d9860c788841915b16a62483bc457bb
7
- data.tar.gz: ae5f198e1f98ca9f964b25a1fba4d419d4c2544e5c651d7921f984626be85d64f4afc494501602e4a19e306ff6cacc8d16f9e5c6f2895a45778af1f444c7e3e3
6
+ metadata.gz: 3ca18ad3dd582e68c2bb935a5ace8628dc466a4999fa655e54117879548d543c5a291ec98765eb25456b0ff8fa774ee0f98f939544a346aa9b3368d37c421f72
7
+ data.tar.gz: e144011d2fd93d37aa3166748bf2d3671d8d246325a475cf13d756b188dd99999ef6f95b232fd36eca444c91e24e2715c1902695697ea1930407bc932b2e952d
data/CHANGELOG.md CHANGED
@@ -19,6 +19,24 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.34.4] - 2026-10-16
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Sidekiq:** A job run through the ActiveJob adapter is no longer profiled twice. It reaches the
29
+ Sidekiq middleware wrapped (`job["wrapped"]`) and is already profiled by the ActiveJob
30
+ `around_perform` callback, so the middleware now leaves it alone when ActiveJob is instrumented
31
+ and the same work is recorded once.
32
+ - **Logs:** The log buffer behind `CaptureLogger` is now guarded by a `Mutex`, so lines recorded
33
+ from several threads at once (a streamed body iterated by another thread, work handed the buffer)
34
+ keep the byte count and the stored lines consistent.
35
+ - **Flame graph and views:** Nested durations are counted once. The total of the flame graph and
36
+ the total of the views and partials now cover each moment a single time, so a template that
37
+ renders partials, or a controller action that runs queries, is no longer added to what it
38
+ contains.
39
+
22
40
  ## [0.34.3] - 2026-10-15
23
41
 
24
42
  <!-- stamped -->
@@ -124,6 +124,24 @@ module Profiler
124
124
 
125
125
  protected
126
126
 
127
+ # The time covered by +intervals+ ([started, finished] pairs of Time or Float seconds), in
128
+ # milliseconds, each moment counted once: a template that renders partials, or a controller
129
+ # action that runs queries, is not added to what it contains.
130
+ def covered_ms(intervals)
131
+ covered = 0.0
132
+ current_end = nil
133
+ intervals.sort_by(&:first).each do |started, finished|
134
+ if current_end.nil? || started >= current_end
135
+ covered += finished - started
136
+ current_end = finished
137
+ elsif finished > current_end
138
+ covered += finished - current_end
139
+ current_end = finished
140
+ end
141
+ end
142
+ (covered * 1000).round(2)
143
+ end
144
+
127
145
  def store_data(data)
128
146
  @data = data
129
147
  @profile.add_collector_data(name, data)
@@ -139,7 +139,7 @@ module Profiler
139
139
 
140
140
  store_data({
141
141
  total_events: @events.size,
142
- total_duration: @events.empty? ? 0 : @events.map(&:duration).sum.round(2),
142
+ total_duration: @events.empty? ? 0 : covered_ms(@events.map { |e| [e.started_at, e.finished_at] }),
143
143
  root_events: root_events.map(&:to_h)
144
144
  })
145
145
  end
@@ -20,13 +20,22 @@ module Profiler
20
20
  @bytes = 0
21
21
  @dropped = 0
22
22
  @truncated = false
23
+ @mutex = Mutex.new
23
24
  end
24
25
 
25
26
  def truncated?
26
27
  @truncated
27
28
  end
28
29
 
30
+ # A streamed body iterated by another thread, or work handed the buffer, can record while
31
+ # the request's own thread does: the byte count and the lines are updated together.
29
32
  def record(entry)
33
+ @mutex.synchronize { record_unlocked(entry) }
34
+ end
35
+
36
+ private
37
+
38
+ def record_unlocked(entry)
30
39
  message = entry[:message]
31
40
  if @limit
32
41
  room = @limit - @bytes
@@ -9,6 +9,7 @@ module Profiler
9
9
  super
10
10
  @views = []
11
11
  @partials = []
12
+ @intervals = []
12
13
  @subscriptions = []
13
14
  end
14
15
 
@@ -36,6 +37,7 @@ module Profiler
36
37
 
37
38
  @subscriptions << subscribe_notification("render_template.action_view") do |name, started, finished, unique_id, payload|
38
39
  duration = ((finished - started) * 1000).round(2)
40
+ @intervals << [started, finished]
39
41
  @views << {
40
42
  identifier: payload[:identifier],
41
43
  layout: payload[:layout],
@@ -45,6 +47,7 @@ module Profiler
45
47
 
46
48
  @subscriptions << subscribe_notification("render_partial.action_view") do |name, started, finished, unique_id, payload|
47
49
  duration = ((finished - started) * 1000).round(2)
50
+ @intervals << [started, finished]
48
51
  @partials << {
49
52
  identifier: payload[:identifier],
50
53
  duration: duration
@@ -60,7 +63,7 @@ module Profiler
60
63
  partials: @partials,
61
64
  total_views: @views.size,
62
65
  total_partials: @partials.size,
63
- total_duration: (@views + @partials).sum { |v| v[:duration] }.round(2)
66
+ total_duration: covered_ms(@intervals)
64
67
  }
65
68
 
66
69
  store_data(data)
@@ -76,7 +79,7 @@ module Profiler
76
79
  end
77
80
 
78
81
  def toolbar_summary
79
- total_duration = (@views + @partials).sum { |v| v[:duration] }.round(2)
82
+ total_duration = covered_ms(@intervals)
80
83
 
81
84
  {
82
85
  text: "#{@views.size} views, #{@partials.size} partials (#{total_duration}ms)",
@@ -12,6 +12,11 @@ module Profiler
12
12
  class SidekiqMiddleware
13
13
  def call(worker, job, queue, &block)
14
14
  Profiler.env_override_store.apply!
15
+ # A job run through the ActiveJob adapter reaches Sidekiq wrapped (job["wrapped"] names the
16
+ # real class) and is profiled by ActiveJobInstrumentation's around_perform: profiling the
17
+ # wrapper too would record the same work twice.
18
+ return block.call if job["wrapped"] && active_job_profiled?
19
+
15
20
  Profiler::JobProfiler.profile(
16
21
  job_class: job["class"],
17
22
  job_id: job["jid"],
@@ -22,6 +27,13 @@ module Profiler
22
27
  &block
23
28
  )
24
29
  end
30
+
31
+ private
32
+
33
+ def active_job_profiled?
34
+ defined?(ActiveJob::Base) && defined?(Profiler::Instrumentation::ActiveJobInstrumentation) &&
35
+ ActiveJob::Base.include?(Profiler::Instrumentation::ActiveJobInstrumentation)
36
+ end
25
37
  end
26
38
  end
27
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.34.3"
4
+ VERSION = "0.34.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.3
4
+ version: 0.34.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy