rails-profiler 0.34.2 → 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 +4 -4
- data/CHANGELOG.md +38 -0
- data/lib/profiler/collectors/base_collector.rb +18 -0
- data/lib/profiler/collectors/flamegraph_collector.rb +1 -1
- data/lib/profiler/collectors/log_collector.rb +9 -0
- data/lib/profiler/collectors/mailer_collector.rb +3 -2
- data/lib/profiler/collectors/view_collector.rb +5 -2
- data/lib/profiler/configuration.rb +14 -2
- data/lib/profiler/instrumentation/sidekiq_middleware.rb +12 -0
- data/lib/profiler/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4912dd474a748e657c35111e3224d83500e75960ef2592e5cd652155dc62617
|
|
4
|
+
data.tar.gz: d0d4cb0f34f25d98f5be08875bdc59ec4cc84894f7cbe0b512fd1ecd153dda4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ca18ad3dd582e68c2bb935a5ace8628dc466a4999fa655e54117879548d543c5a291ec98765eb25456b0ff8fa774ee0f98f939544a346aa9b3368d37c421f72
|
|
7
|
+
data.tar.gz: e144011d2fd93d37aa3166748bf2d3671d8d246325a475cf13d756b188dd99999ef6f95b232fd36eca444c91e24e2715c1902695697ea1930407bc932b2e952d
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,44 @@ 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
|
+
|
|
40
|
+
## [0.34.3] - 2026-10-15
|
|
41
|
+
|
|
42
|
+
<!-- stamped -->
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Test suite:** `bundle exec rspec` now measures the line coverage of the gem's Ruby code with
|
|
47
|
+
SimpleCov, and the CI fails below 92.0%, the level measured on Ruby 3.3 once the MCP
|
|
48
|
+
tools, the function profiler, the storage base class and the cluster client got specs of their
|
|
49
|
+
own. A run of a few spec files is held to no floor outside the CI.
|
|
50
|
+
- **Test suite:** The suite loads and passes under any locale, `LANG=C` included: `spec_helper.rb`
|
|
51
|
+
reads files as UTF-8 whatever the locale, and a `.rspec` file loads it for every spec. The specs
|
|
52
|
+
of the masking of sensitive data now live together under `spec/redaction/`.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **Middleware:** The default `skip_paths` are now anchored to the whole path, so a page such as `/users/favicon.ico.html` is no longer skipped by mistake, and the Sprockets assets (`/assets`) and Action Cable (`/cable`) requests are no longer profiled. A `skip_paths` list you set yourself is used as given.
|
|
57
|
+
- **Mailers:** Visiting the mailer previews (`/rails/mailers`) no longer records phantom delivered emails in the profile.
|
|
58
|
+
- **Security:** An `authorize_with` block that raises is now treated as a denial: the error is logged server side and the request is served unprofiled, instead of breaking every request of the application.
|
|
59
|
+
|
|
22
60
|
## [0.34.2] - 2026-10-15
|
|
23
61
|
|
|
24
62
|
<!-- 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
|
|
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
|
|
@@ -316,8 +316,9 @@ module Profiler
|
|
|
316
316
|
end
|
|
317
317
|
|
|
318
318
|
def rails_preview_request?
|
|
319
|
-
path
|
|
320
|
-
|
|
319
|
+
# The profile of the request being served carries its path; the thread-local is kept
|
|
320
|
+
# for a caller that sets it by hand.
|
|
321
|
+
[profile&.path, Thread.current[:profiler_request_path]].any? { |path| path.to_s.match?(%r{\A/rails/mailers(/|\z)}) }
|
|
321
322
|
end
|
|
322
323
|
|
|
323
324
|
def to_path(class_name)
|
|
@@ -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: (@
|
|
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 = (@
|
|
82
|
+
total_duration = covered_ms(@intervals)
|
|
80
83
|
|
|
81
84
|
{
|
|
82
85
|
text: "#{@views.size} views, #{@partials.size} partials (#{total_duration}ms)",
|
|
@@ -65,7 +65,8 @@ module Profiler
|
|
|
65
65
|
@storage = :memory
|
|
66
66
|
@storage_options = {}
|
|
67
67
|
@collectors = []
|
|
68
|
-
@skip_paths = [%r{
|
|
68
|
+
@skip_paths = [%r{\A/_profiler}, %r{\A/\.well-known/}, %r{\A/favicon\.ico\z}, %r{\A(?:/[^/]+)*/manifest\.json\z},
|
|
69
|
+
%r{\A/assets/}, %r{\A/cable(/|\z)}]
|
|
69
70
|
@slow_query_threshold = 100 # milliseconds
|
|
70
71
|
@max_queries_warning = 50
|
|
71
72
|
# Where each query comes from: :first_and_slow captures the caller the first time a
|
|
@@ -284,7 +285,7 @@ module Profiler
|
|
|
284
285
|
LocalRequest.warn_once(reason)
|
|
285
286
|
false
|
|
286
287
|
when :allow_authorized
|
|
287
|
-
|
|
288
|
+
run_authorize_block(request)
|
|
288
289
|
else
|
|
289
290
|
false
|
|
290
291
|
end
|
|
@@ -373,5 +374,16 @@ module Profiler
|
|
|
373
374
|
raise ArgumentError, "config.mcp_allowed_hosts: #{pattern.inspect} cannot be matched against the whole " \
|
|
374
375
|
"host name (#{e.message})"
|
|
375
376
|
end
|
|
377
|
+
|
|
378
|
+
# An exception in the application's own authorization code is a denial, never an error
|
|
379
|
+
# for the request: it would otherwise break every request. The detail stays in the log.
|
|
380
|
+
def run_authorize_block(request)
|
|
381
|
+
return false unless @authorize_block
|
|
382
|
+
|
|
383
|
+
@authorize_block.call(request) ? true : false
|
|
384
|
+
rescue StandardError => e
|
|
385
|
+
Profiler.log_error("authorize_with block raised, treating the request as unauthorized", e)
|
|
386
|
+
false
|
|
387
|
+
end
|
|
376
388
|
end
|
|
377
389
|
end
|
|
@@ -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
|
data/lib/profiler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.34.
|
|
4
|
+
version: 0.34.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|