rails-profiler 0.34.3 → 0.34.5
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 +34 -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/view_collector.rb +5 -2
- data/lib/profiler/instrumentation/sidekiq_middleware.rb +12 -0
- data/lib/profiler/middleware/profiler_middleware.rb +11 -1
- data/lib/profiler/models/profile.rb +46 -4
- data/lib/profiler/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b498ec1274af5787fe96b70e64613b009298b578a7b084d65d450ec86a431bc
|
|
4
|
+
data.tar.gz: b575d9afef090eee5539cfca2ffd536887d90b4c08a3b5099504a8e31ca69eab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7da413fad8fd79420dddc04242b1ce30c05aac1106073219b7c0dcda26dada8c59a1b8e0d6a6c3c403d92ee6cd519271285d5f99fe6670b0e970179f79be119
|
|
7
|
+
data.tar.gz: d36516568229d7fa228dad51b70a30e8ca2753cc07fa114279f322200d32bd9a4f5e02891ff0ea96b0c904dc1594d528f677129b840fe0acadd2f106f29ea9cc
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,40 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.34.5] - 2026-10-16
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Request capture:** the parameters of a request are now read once the application has returned, so
|
|
29
|
+
route parameters are present and `param_encoding` is applied, and they are converted to UTF-8. They
|
|
30
|
+
used to be read in the outermost middleware, before the router ran.
|
|
31
|
+
- **Request capture:** a text body keeps its accents. It is now read in the `charset` of its
|
|
32
|
+
`Content-Type`, UTF-8 when it declares none, and invalid bytes are replaced, instead of every
|
|
33
|
+
accent being turned into a replacement character.
|
|
34
|
+
- **Request capture:** a request whose `Content-Type` declares a charset Ruby cannot convert from
|
|
35
|
+
(`charset=utf-7`, `charset=internal`) no longer loses its profile; the body is read as UTF-8 with
|
|
36
|
+
invalid bytes replaced.
|
|
37
|
+
|
|
38
|
+
## [0.34.4] - 2026-10-16
|
|
39
|
+
|
|
40
|
+
<!-- stamped -->
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **Sidekiq:** A job run through the ActiveJob adapter is no longer profiled twice. It reaches the
|
|
45
|
+
Sidekiq middleware wrapped (`job["wrapped"]`) and is already profiled by the ActiveJob
|
|
46
|
+
`around_perform` callback, so the middleware now leaves it alone when ActiveJob is instrumented
|
|
47
|
+
and the same work is recorded once.
|
|
48
|
+
- **Logs:** The log buffer behind `CaptureLogger` is now guarded by a `Mutex`, so lines recorded
|
|
49
|
+
from several threads at once (a streamed body iterated by another thread, work handed the buffer)
|
|
50
|
+
keep the byte count and the stored lines consistent.
|
|
51
|
+
- **Flame graph and views:** Nested durations are counted once. The total of the flame graph and
|
|
52
|
+
the total of the views and partials now cover each moment a single time, so a template that
|
|
53
|
+
renders partials, or a controller action that runs queries, is no longer added to what it
|
|
54
|
+
contains.
|
|
55
|
+
|
|
22
56
|
## [0.34.3] - 2026-10-15
|
|
23
57
|
|
|
24
58
|
<!-- 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
|
|
@@ -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)",
|
|
@@ -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
|
|
@@ -28,7 +28,7 @@ module Profiler
|
|
|
28
28
|
kept = nil
|
|
29
29
|
begin
|
|
30
30
|
begin
|
|
31
|
-
profile = Models::Profile.new(build_request(env))
|
|
31
|
+
profile = Models::Profile.new(build_request(env), params: false)
|
|
32
32
|
profile.gem_version = Profiler::VERSION
|
|
33
33
|
Profiler::CurrentContext.token = profile.token
|
|
34
34
|
|
|
@@ -57,10 +57,12 @@ module Profiler
|
|
|
57
57
|
begin
|
|
58
58
|
response = @app.call(env)
|
|
59
59
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
60
|
+
capture_params(env, profile) if failed_request?(e)
|
|
60
61
|
record_failed_request(env, profile, collectors, request_body, allocations_before, e) if failed_request?(e)
|
|
61
62
|
raise
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
capture_params(env, profile)
|
|
64
66
|
response, kept = complete_profile(env, profile, collectors, request_body, allocations_before, response)
|
|
65
67
|
response
|
|
66
68
|
ensure
|
|
@@ -74,6 +76,14 @@ module Profiler
|
|
|
74
76
|
|
|
75
77
|
private
|
|
76
78
|
|
|
79
|
+
# Read after the application has run, so after the routing: the route parameters are there,
|
|
80
|
+
# and the params carry the encoding the route declared, as the controller saw them.
|
|
81
|
+
def capture_params(env, profile)
|
|
82
|
+
profile.capture_params(build_request(env))
|
|
83
|
+
rescue => e
|
|
84
|
+
Profiler.log_error("ProfilerMiddleware: could not read the request params", e)
|
|
85
|
+
end
|
|
86
|
+
|
|
77
87
|
def complete_profile(env, profile, collectors, request_body, allocations_before, response)
|
|
78
88
|
status, headers, body = response
|
|
79
89
|
|
|
@@ -22,7 +22,9 @@ module Profiler
|
|
|
22
22
|
:collectors_released_after_seconds, :params_truncated,
|
|
23
23
|
:gem_version
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# +params+: false leaves the params to capture_params, for a caller that builds the profile
|
|
26
|
+
# before the router has run.
|
|
27
|
+
def initialize(request = nil, params: true)
|
|
26
28
|
@token = SecureRandom.hex(16)
|
|
27
29
|
@started_at = Time.now
|
|
28
30
|
@collectors_data = {}
|
|
@@ -34,12 +36,19 @@ module Profiler
|
|
|
34
36
|
if request
|
|
35
37
|
@path = request.path
|
|
36
38
|
@method = request.request_method
|
|
37
|
-
|
|
38
|
-
Profiler.configuration.max_captured_body_bytes)
|
|
39
|
+
capture_params(request) if params
|
|
39
40
|
@headers = extract_headers(request.env)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# The request's params as the controller sees them. Read once the router has run: Rails
|
|
45
|
+
# adds the route parameters (:id...) and applies the routes' param_encoding only then, so
|
|
46
|
+
# a read before it keeps neither.
|
|
47
|
+
def capture_params(request)
|
|
48
|
+
@params, @params_truncated = cap_params(sanitize_params(utf8_params(request.params)),
|
|
49
|
+
Profiler.configuration.max_captured_body_bytes)
|
|
50
|
+
end
|
|
51
|
+
|
|
43
52
|
# The sizes are those of the whole bodies, when a body was cut at max_captured_body_bytes;
|
|
44
53
|
# *_size_is_minimum when the whole size is not known (no Content-Length, a stream that
|
|
45
54
|
# stopped), only that it is at least that.
|
|
@@ -252,7 +261,7 @@ module Profiler
|
|
|
252
261
|
# compressed format (zip, png, gzip...) does not hold the secret as it is.
|
|
253
262
|
{ body: Base64.strict_encode64(Redaction.hide_credentials(raw.b)), encoding: "base64" }
|
|
254
263
|
else
|
|
255
|
-
text = Redaction.filter_body(raw, content_type)
|
|
264
|
+
text = utf8_text(Redaction.filter_body(raw, content_type), content_type)
|
|
256
265
|
if compress_body?(text)
|
|
257
266
|
# The fastest level: paid on the request, a quarter larger than the default level.
|
|
258
267
|
{ body: Base64.strict_encode64(Zlib::Deflate.deflate(text, Zlib::BEST_SPEED)), encoding: "gzip+base64" }
|
|
@@ -262,6 +271,39 @@ module Profiler
|
|
|
262
271
|
end
|
|
263
272
|
end
|
|
264
273
|
|
|
274
|
+
# The text as UTF-8: a body read from the wire is binary, and is taken for the charset its
|
|
275
|
+
# Content-Type declares (UTF-8 when it declares none, or one Ruby does not know). Bytes
|
|
276
|
+
# that do not belong to it are replaced, never raised on.
|
|
277
|
+
def utf8_text(text, content_type)
|
|
278
|
+
return text.encode("UTF-8", invalid: :replace, undef: :replace) unless text.encoding == Encoding::BINARY
|
|
279
|
+
|
|
280
|
+
charset = content_type.to_s[/charset\s*=\s*"?([^\s";]+)/i, 1]
|
|
281
|
+
source = begin
|
|
282
|
+
charset ? Encoding.find(charset) : Encoding::UTF_8
|
|
283
|
+
rescue ArgumentError
|
|
284
|
+
Encoding::UTF_8
|
|
285
|
+
end
|
|
286
|
+
# "internal" is a name Ruby knows but resolves to nil, "utf-7" is a dummy encoding with no converter.
|
|
287
|
+
source ||= Encoding::UTF_8
|
|
288
|
+
begin
|
|
289
|
+
text.dup.force_encoding(source).encode("UTF-8", invalid: :replace, undef: :replace)
|
|
290
|
+
rescue EncodingError
|
|
291
|
+
text.dup.force_encoding(Encoding::UTF_8).encode("UTF-8", invalid: :replace, undef: :replace)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# The params with their strings in UTF-8: a route's param_encoding gives some another
|
|
296
|
+
# encoding, which the profile must not carry into its JSON.
|
|
297
|
+
def utf8_params(value)
|
|
298
|
+
case value
|
|
299
|
+
when Hash then value.to_h { |k, v| [utf8_params(k), utf8_params(v)] }
|
|
300
|
+
when Array then value.map { |v| utf8_params(v) }
|
|
301
|
+
when String
|
|
302
|
+
value.encoding == Encoding::UTF_8 ? value : value.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
303
|
+
else value
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
265
307
|
def body_size(captured, total)
|
|
266
308
|
captured_size = captured.to_s.bytesize
|
|
267
309
|
total ||= captured_size
|
data/lib/profiler/version.rb
CHANGED