rails-profiler 0.31.1 → 0.31.3
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 +122 -0
- data/app/assets/builds/profiler-toolbar.js +13 -6
- data/app/assets/builds/profiler.js +73 -37
- data/app/controllers/profiler/api/profiles_controller.rb +2 -0
- data/app/controllers/profiler/api/toolbar_controller.rb +2 -0
- data/app/controllers/profiler/profiles_controller.rb +2 -0
- data/lib/profiler/allocation_counter.rb +21 -0
- data/lib/profiler/collectors/base_collector.rb +39 -3
- data/lib/profiler/collectors/cache_collector.rb +8 -3
- data/lib/profiler/collectors/database_collector.rb +133 -10
- data/lib/profiler/collectors/env_collector.rb +12 -4
- data/lib/profiler/collectors/exception_collector.rb +12 -9
- data/lib/profiler/collectors/flamegraph_collector.rb +10 -5
- data/lib/profiler/collectors/function_profiler_collector.rb +28 -4
- data/lib/profiler/collectors/http_collector.rb +5 -0
- data/lib/profiler/collectors/log_collector.rb +5 -0
- data/lib/profiler/collectors/mailer_collector.rb +2 -8
- data/lib/profiler/collectors/request_collector.rb +14 -11
- data/lib/profiler/collectors/routes_collector.rb +7 -50
- data/lib/profiler/collectors/scoped_notifications.rb +229 -0
- data/lib/profiler/collectors/view_collector.rb +7 -2
- data/lib/profiler/configuration.rb +27 -4
- data/lib/profiler/console_profiler.rb +2 -17
- data/lib/profiler/instrumentation/executor_context_propagation.rb +47 -0
- data/lib/profiler/instrumentation/request_context.rb +77 -0
- data/lib/profiler/instrumentation/thread_context_propagation.rb +11 -16
- data/lib/profiler/job_profiler.rb +6 -17
- data/lib/profiler/mcp/resources/recent_requests.rb +2 -0
- data/lib/profiler/mcp/tools/get_profile_detail.rb +21 -2
- data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
- data/lib/profiler/middleware/capturing_body.rb +112 -0
- data/lib/profiler/middleware/profiler_middleware.rb +261 -53
- data/lib/profiler/middleware/streamed_profile.rb +135 -0
- data/lib/profiler/middleware/toolbar_injector.rb +3 -10
- data/lib/profiler/models/profile.rb +69 -5
- data/lib/profiler/process_snapshot.rb +157 -0
- data/lib/profiler/redaction.rb +10 -0
- data/lib/profiler/test_profiler.rb +2 -17
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +6 -0
- metadata +12 -5
|
@@ -6,10 +6,24 @@ require_relative "../models/sql_query"
|
|
|
6
6
|
module Profiler
|
|
7
7
|
module Collectors
|
|
8
8
|
class DatabaseCollector < BaseCollector
|
|
9
|
+
BACKTRACE_DEPTH = 10
|
|
10
|
+
MAX_SCANNED_FRAMES = 100
|
|
11
|
+
FRAMES_PAST_APPLICATION = 15
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Frames between the application's code and the subscriber: the profiler itself, the
|
|
15
|
+
# notifications bus and Active Record.
|
|
16
|
+
INTERNAL_FRAMES = [
|
|
17
|
+
File.expand_path("..", __dir__) + "/",
|
|
18
|
+
"/active_support/notifications",
|
|
19
|
+
"/active_record/"
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
9
22
|
def initialize(profile)
|
|
10
23
|
super
|
|
11
24
|
@queries = []
|
|
12
|
-
@
|
|
25
|
+
@subscriptions = []
|
|
26
|
+
@seen_statements = {}
|
|
13
27
|
end
|
|
14
28
|
|
|
15
29
|
def icon
|
|
@@ -34,7 +48,10 @@ module Profiler
|
|
|
34
48
|
def subscribe
|
|
35
49
|
return unless defined?(ActiveSupport::Notifications)
|
|
36
50
|
|
|
37
|
-
@
|
|
51
|
+
@backtrace_mode = Profiler.configuration.sql_backtrace&.to_sym
|
|
52
|
+
@slow_query_threshold = Profiler.configuration.slow_query_threshold
|
|
53
|
+
|
|
54
|
+
@subscriptions << subscribe_notification("sql.active_record") do |name, started, finished, unique_id, payload|
|
|
38
55
|
duration = ((finished - started) * 1000).round(2) # milliseconds
|
|
39
56
|
|
|
40
57
|
# Skip schema queries and internal Rails queries
|
|
@@ -47,7 +64,7 @@ module Profiler
|
|
|
47
64
|
binds: extract_binds(payload[:binds]),
|
|
48
65
|
name: payload[:name],
|
|
49
66
|
connection: payload[:connection],
|
|
50
|
-
backtrace: extract_backtrace
|
|
67
|
+
backtrace: backtrace?(payload[:sql], duration) ? extract_backtrace : []
|
|
51
68
|
)
|
|
52
69
|
|
|
53
70
|
@queries << query
|
|
@@ -68,11 +85,13 @@ module Profiler
|
|
|
68
85
|
store_data(data)
|
|
69
86
|
end
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
# Collect reads only what the collector gathered itself.
|
|
89
|
+
def collect_from_any_thread?
|
|
90
|
+
true
|
|
91
|
+
end
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
@
|
|
93
|
+
def unsubscribe
|
|
94
|
+
unsubscribe_notifications(@subscriptions)
|
|
76
95
|
end
|
|
77
96
|
|
|
78
97
|
def toolbar_summary
|
|
@@ -113,10 +132,114 @@ module Profiler
|
|
|
113
132
|
binds.map { Profiler::Redaction::MASK }
|
|
114
133
|
end
|
|
115
134
|
|
|
135
|
+
# Capturing the caller costs tens of microseconds. The first run of a statement locates
|
|
136
|
+
# it, an N+1 loop included (its repeats are the same statement with other values); a slow
|
|
137
|
+
# query is always located.
|
|
138
|
+
def backtrace?(sql, duration)
|
|
139
|
+
case @backtrace_mode
|
|
140
|
+
when :all then true
|
|
141
|
+
when :none then false
|
|
142
|
+
else
|
|
143
|
+
key = statement_key(sql)
|
|
144
|
+
first = !@seen_statements.key?(key)
|
|
145
|
+
@seen_statements[key] = true if first
|
|
146
|
+
first || duration > @slow_query_threshold
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The statement with its values left out, exactly as the N+1 detection of the Database tab
|
|
151
|
+
# (DatabaseTab.tsx) and of the MCP (Resources::N1Patterns) groups queries: the first query
|
|
152
|
+
# of each group is then always one whose caller was captured, values inlined in the SQL
|
|
153
|
+
# (MySQL) included.
|
|
154
|
+
def statement_key(sql)
|
|
155
|
+
sql.to_s
|
|
156
|
+
.gsub(/\$\d+/, "?")
|
|
157
|
+
.gsub(/\b\d+\b/, "?")
|
|
158
|
+
.gsub(/'[^']*'/, "?")
|
|
159
|
+
.gsub(/"[^"]*"/, "?")
|
|
160
|
+
.strip
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The code that ran the query: the frames Rails.backtrace_cleaner keeps, as the exception
|
|
164
|
+
# tab shows them, or without Rails, the frames outside gems and Ruby itself. Whatever stands
|
|
165
|
+
# in between (the profiler, Active Support, Active Record, the driver, other gems) is left
|
|
166
|
+
# out, so that the first frames are the application's.
|
|
116
167
|
def extract_backtrace
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
cleaner = rails_backtrace_cleaner
|
|
169
|
+
cleaned = []
|
|
170
|
+
plain = []
|
|
171
|
+
scanned = 0
|
|
172
|
+
last_kept = nil
|
|
173
|
+
while scanned < MAX_SCANNED_FRAMES && cleaned.size < BACKTRACE_DEPTH
|
|
174
|
+
chunk = caller_locations(1 + scanned, 25)
|
|
175
|
+
break if chunk.nil? || chunk.empty?
|
|
176
|
+
|
|
177
|
+
chunk.each do |loc|
|
|
178
|
+
scanned += 1
|
|
179
|
+
path = loc.path.to_s
|
|
180
|
+
next if INTERNAL_FRAMES.any? { |internal| path.include?(internal) }
|
|
181
|
+
# Gems and Ruby are never the application's code, and Rails' cleaner silences them
|
|
182
|
+
# too: a string test spares it most frames.
|
|
183
|
+
next if library_frame?(path)
|
|
184
|
+
|
|
185
|
+
line = "#{path}:#{loc.lineno}:in `#{loc.label}`"
|
|
186
|
+
if cleaner && (kept = clean_frame(cleaner, line))
|
|
187
|
+
cleaned << kept
|
|
188
|
+
last_kept = scanned
|
|
189
|
+
end
|
|
190
|
+
plain << line if plain.size < BACKTRACE_DEPTH
|
|
191
|
+
break if cleaned.size == BACKTRACE_DEPTH
|
|
192
|
+
end
|
|
193
|
+
# Without Rails, the first frames outside gems are enough; with it, the application's
|
|
194
|
+
# frames come together: a few frames past the last one kept, the rest is the framework.
|
|
195
|
+
break if cleaner.nil? && plain.size >= BACKTRACE_DEPTH
|
|
196
|
+
break if last_kept && scanned - last_kept > FRAMES_PAST_APPLICATION
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
frames = cleaned.empty? ? plain : cleaned
|
|
200
|
+
frames.empty? ? fallback_backtrace : frames
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def rails_backtrace_cleaner
|
|
204
|
+
Rails.backtrace_cleaner if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
|
|
205
|
+
rescue StandardError
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The frame as the cleaner shows it, or nil when it silences it.
|
|
210
|
+
def clean_frame(cleaner, line)
|
|
211
|
+
cleaner.respond_to?(:clean_frame) ? cleaner.clean_frame(line) : cleaner.clean([line]).first
|
|
212
|
+
rescue StandardError
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def fallback_backtrace
|
|
217
|
+
(caller_locations(1, BACKTRACE_DEPTH + 20) || []).filter_map do |loc|
|
|
218
|
+
path = loc.path.to_s
|
|
219
|
+
"#{path}:#{loc.lineno}:in `#{loc.label}`" unless INTERNAL_FRAMES.any? { |internal| path.include?(internal) }
|
|
220
|
+
end.first(BACKTRACE_DEPTH)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Ruby itself and the directories gems are installed in; an application that lives under a
|
|
224
|
+
# directory called gems is not one.
|
|
225
|
+
def library_frame?(path)
|
|
226
|
+
path.start_with?("<internal:") || library_prefixes.any? { |dir| path.start_with?(dir) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def library_prefixes
|
|
230
|
+
@library_prefixes ||= begin
|
|
231
|
+
dirs = [RbConfig::CONFIG["rubylibdir"], RbConfig::CONFIG["rubyarchdir"], RbConfig::CONFIG["bindir"]]
|
|
232
|
+
dirs.concat(Gem.path) if defined?(Gem)
|
|
233
|
+
dirs << bundle_path
|
|
234
|
+
dirs.compact.reject(&:empty?).map { |dir| File.join(File.expand_path(dir), "") }.uniq.freeze
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Bundler raises without a Gemfile.
|
|
239
|
+
def bundle_path
|
|
240
|
+
Bundler.bundle_path.to_s if defined?(Bundler) && Bundler.respond_to?(:bundle_path)
|
|
241
|
+
rescue StandardError
|
|
242
|
+
nil
|
|
120
243
|
end
|
|
121
244
|
end
|
|
122
245
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_collector"
|
|
4
|
+
require_relative "../process_snapshot"
|
|
4
5
|
|
|
5
6
|
module Profiler
|
|
6
7
|
module Collectors
|
|
@@ -24,14 +25,21 @@ module Profiler
|
|
|
24
25
|
}
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# ENV is the process's, not the request's: an HTTP profile keeps none of it, and the Env tab
|
|
29
|
+
# shows the variables of the process that displays the profile, which is the one that served
|
|
30
|
+
# the request (ProcessSnapshot). A job, a console expression or a test runs in another
|
|
31
|
+
# process (Sidekiq, the console, rspec): its profile keeps that process's ENV, masked.
|
|
27
32
|
def collect
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
if @profile.profile_type.to_s == "http"
|
|
34
|
+
store_data({ scope: "process" })
|
|
35
|
+
else
|
|
36
|
+
variables = Profiler::Redaction.env_snapshot
|
|
37
|
+
store_data({ variables: variables, total: variables.size })
|
|
38
|
+
end
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
def toolbar_summary
|
|
33
|
-
|
|
34
|
-
{ text: "#{data[:total] || 0} vars", color: "gray" }
|
|
42
|
+
{ text: "#{ENV.size} vars", color: "gray" }
|
|
35
43
|
end
|
|
36
44
|
end
|
|
37
45
|
end
|
|
@@ -31,11 +31,12 @@ module Profiler
|
|
|
31
31
|
def subscribe
|
|
32
32
|
@exception_data = nil
|
|
33
33
|
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
@subscriptions = [
|
|
35
|
+
subscribe_notification("process_action.action_controller") do |_name, _started, _finished, _id, payload|
|
|
36
|
+
ex = payload[:exception_object]
|
|
37
|
+
capture_exception(ex) if ex && @exception_data.nil?
|
|
38
|
+
end
|
|
39
|
+
]
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def capture(ex)
|
|
@@ -48,11 +49,13 @@ module Profiler
|
|
|
48
49
|
store_data(@exception_data || {})
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
# Collect reads only what the collector gathered itself.
|
|
53
|
+
def collect_from_any_thread?
|
|
54
|
+
true
|
|
55
|
+
end
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
@
|
|
57
|
+
def unsubscribe
|
|
58
|
+
unsubscribe_notifications(@subscriptions) if @subscriptions
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def has_data?
|
|
@@ -38,7 +38,7 @@ module Profiler
|
|
|
38
38
|
claim_thread_slot(:profiler_flamegraph_collector, self)
|
|
39
39
|
|
|
40
40
|
# Controller action
|
|
41
|
-
@subscriptions <<
|
|
41
|
+
@subscriptions << subscribe_notification("process_action.action_controller") do |_name, started, finished, _unique_id, payload|
|
|
42
42
|
add_event Models::TimelineEvent.new(
|
|
43
43
|
name: "#{payload[:controller]}##{payload[:action]}",
|
|
44
44
|
started_at: started,
|
|
@@ -56,7 +56,7 @@ module Profiler
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Template rendering
|
|
59
|
-
@subscriptions <<
|
|
59
|
+
@subscriptions << subscribe_notification("render_template.action_view") do |_name, started, finished, _unique_id, payload|
|
|
60
60
|
identifier = short_identifier(payload[:identifier])
|
|
61
61
|
add_event Models::TimelineEvent.new(
|
|
62
62
|
name: "Render: #{identifier}",
|
|
@@ -68,7 +68,7 @@ module Profiler
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Partial rendering
|
|
71
|
-
@subscriptions <<
|
|
71
|
+
@subscriptions << subscribe_notification("render_partial.action_view") do |_name, started, finished, _unique_id, payload|
|
|
72
72
|
identifier = short_identifier(payload[:identifier])
|
|
73
73
|
add_event Models::TimelineEvent.new(
|
|
74
74
|
name: "Partial: #{identifier}",
|
|
@@ -80,7 +80,7 @@ module Profiler
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
# SQL queries
|
|
83
|
-
@subscriptions <<
|
|
83
|
+
@subscriptions << subscribe_notification("sql.active_record") do |_name, started, finished, _unique_id, payload|
|
|
84
84
|
next if payload[:name] == "SCHEMA"
|
|
85
85
|
next if payload[:sql] =~ /^(BEGIN|COMMIT|ROLLBACK|SAVEPOINT)/i
|
|
86
86
|
|
|
@@ -96,7 +96,7 @@ module Profiler
|
|
|
96
96
|
|
|
97
97
|
# Cache operations
|
|
98
98
|
%w[cache_read.active_support cache_write.active_support cache_delete.active_support].each do |event_name|
|
|
99
|
-
@subscriptions <<
|
|
99
|
+
@subscriptions << subscribe_notification(event_name) do |name, started, finished, _unique_id, payload|
|
|
100
100
|
op = name.split(".").first.sub("cache_", "")
|
|
101
101
|
key = payload[:key].to_s
|
|
102
102
|
add_event Models::TimelineEvent.new(
|
|
@@ -144,6 +144,11 @@ module Profiler
|
|
|
144
144
|
})
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
# Collect reads only what the collector gathered itself.
|
|
148
|
+
def collect_from_any_thread?
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
147
152
|
def unsubscribe
|
|
148
153
|
unsubscribe_notifications(@subscriptions)
|
|
149
154
|
restore_thread_slots
|
|
@@ -5,7 +5,7 @@ require_relative "base_collector"
|
|
|
5
5
|
begin
|
|
6
6
|
require "stackprof"
|
|
7
7
|
rescue LoadError
|
|
8
|
-
# stackprof not available
|
|
8
|
+
# stackprof not available: lite mode stays off, unless Profiler.function_profiling_tracepoint_fallback
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
module Profiler
|
|
@@ -55,6 +55,20 @@ module Profiler
|
|
|
55
55
|
|
|
56
56
|
mode = Profiler.function_profiling_mode
|
|
57
57
|
clock = Profiler.function_profiling_clock
|
|
58
|
+
|
|
59
|
+
# Sampling needs stackprof, which the gem does not depend on. Tracing every method call
|
|
60
|
+
# instead costs several times the rest of the profiler: only when asked for.
|
|
61
|
+
if stackprof_missing?(mode)
|
|
62
|
+
store_data({
|
|
63
|
+
enabled: false,
|
|
64
|
+
reason: "stackprof_missing",
|
|
65
|
+
max_frames: Profiler.function_profiling_max_frames,
|
|
66
|
+
mode: mode,
|
|
67
|
+
clock: clock
|
|
68
|
+
})
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
@subscribed = true
|
|
59
73
|
claim_thread_slot(:fn_profiler_mode, mode)
|
|
60
74
|
claim_thread_slot(:fn_profiler_clock, clock)
|
|
@@ -119,6 +133,7 @@ module Profiler
|
|
|
119
133
|
|
|
120
134
|
def toolbar_summary
|
|
121
135
|
return { text: "off", color: "gray" } unless Profiler.function_profiling_enabled
|
|
136
|
+
return { text: "off", color: "gray" } if stackprof_missing?(Profiler.function_profiling_mode)
|
|
122
137
|
case Profiler.function_profiling_mode
|
|
123
138
|
when "lite" then { text: "sampling on", color: "blue" }
|
|
124
139
|
else { text: "fn profiling on", color: "purple" }
|
|
@@ -127,6 +142,10 @@ module Profiler
|
|
|
127
142
|
|
|
128
143
|
private
|
|
129
144
|
|
|
145
|
+
def stackprof_missing?(mode)
|
|
146
|
+
mode == "lite" && !defined?(StackProf) && !Profiler.function_profiling_tracepoint_fallback
|
|
147
|
+
end
|
|
148
|
+
|
|
130
149
|
# ── StackProf (lite mode) ──────────────────────────────────────────────────
|
|
131
150
|
|
|
132
151
|
def store_sampler_busy(mode, clock)
|
|
@@ -297,10 +316,9 @@ module Profiler
|
|
|
297
316
|
claim_thread_slot(:fn_profiler_depth, Hash.new(0))
|
|
298
317
|
|
|
299
318
|
@trace = TracePoint.new(:call, :return) do |tp|
|
|
300
|
-
next unless tp.path&.start_with?(app_root)
|
|
301
|
-
|
|
302
319
|
stack = Thread.current[:fn_profiler_stack]
|
|
303
320
|
next unless stack
|
|
321
|
+
next unless tp.path&.start_with?(app_root)
|
|
304
322
|
|
|
305
323
|
case tp.event
|
|
306
324
|
when :call
|
|
@@ -339,7 +357,13 @@ module Profiler
|
|
|
339
357
|
end
|
|
340
358
|
end
|
|
341
359
|
|
|
342
|
-
|
|
360
|
+
# On the request's thread only: a TracePoint enabled for the whole process slows down
|
|
361
|
+
# every other thread, other requests included.
|
|
362
|
+
begin
|
|
363
|
+
@trace.enable(target_thread: Thread.current)
|
|
364
|
+
rescue ArgumentError
|
|
365
|
+
@trace.enable # a Ruby without target_thread: the hook returns at once on other threads
|
|
366
|
+
end
|
|
343
367
|
end
|
|
344
368
|
|
|
345
369
|
def collect_tracepoint(mode)
|
|
@@ -39,6 +39,11 @@ module Profiler
|
|
|
39
39
|
claim_thread_slot(:profiler_http_collector, self)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Collect reads only what the collector gathered itself.
|
|
43
|
+
def collect_from_any_thread?
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
# The Net::HTTP patch installed by subscribe stays: it is installed once per process and
|
|
43
48
|
# records nothing when no collector holds the thread-local slot.
|
|
44
49
|
def unsubscribe
|
|
@@ -93,6 +93,11 @@ module Profiler
|
|
|
93
93
|
warn "LogCollector#subscribe failed: #{e.message}"
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Collect reads only what the collector gathered itself.
|
|
97
|
+
def collect_from_any_thread?
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
96
101
|
def unsubscribe
|
|
97
102
|
if @capture_logger
|
|
98
103
|
@broadcaster.stop_broadcasting_to(@capture_logger) if @broadcaster.respond_to?(:stop_broadcasting_to)
|
|
@@ -40,15 +40,10 @@ module Profiler
|
|
|
40
40
|
return unless defined?(ActiveSupport::Notifications)
|
|
41
41
|
return unless Profiler.configuration.track_mailers
|
|
42
42
|
|
|
43
|
-
# Capture the subscriber thread so notifications from other threads (e.g. async
|
|
44
|
-
# job threads delivering mail enqueued via deliver_later) are ignored by this collector.
|
|
45
|
-
@subscriber_thread = Thread.current
|
|
46
|
-
|
|
47
43
|
# Use a stack so multiple deliver_later calls in the same request are all tracked.
|
|
48
44
|
claim_thread_slot(:profiler_pending_processes, [])
|
|
49
45
|
|
|
50
|
-
@subscriptions <<
|
|
51
|
-
next unless Thread.current.equal?(@subscriber_thread)
|
|
46
|
+
@subscriptions << subscribe_notification("process.action_mailer") do |_name, started, finished, _id, payload|
|
|
52
47
|
next if rails_preview_request?
|
|
53
48
|
|
|
54
49
|
mailer_class = payload[:mailer].to_s
|
|
@@ -64,8 +59,7 @@ module Profiler
|
|
|
64
59
|
}
|
|
65
60
|
end
|
|
66
61
|
|
|
67
|
-
@subscriptions <<
|
|
68
|
-
next unless Thread.current.equal?(@subscriber_thread)
|
|
62
|
+
@subscriptions << subscribe_notification("deliver.action_mailer") do |_name, started, finished, _id, payload|
|
|
69
63
|
next unless payload[:perform_deliveries]
|
|
70
64
|
next if rails_preview_request?
|
|
71
65
|
|
|
@@ -30,7 +30,8 @@ module Profiler
|
|
|
30
30
|
method: @profile.method,
|
|
31
31
|
status: @profile.status,
|
|
32
32
|
duration: @profile.duration,
|
|
33
|
-
|
|
33
|
+
allocated_objects: @profile.allocated_objects,
|
|
34
|
+
memory: @profile.memory, # deprecated, see Profile#memory
|
|
34
35
|
params: @profile.params,
|
|
35
36
|
headers: @profile.headers,
|
|
36
37
|
response_headers: @profile.response_headers,
|
|
@@ -38,6 +39,14 @@ module Profiler
|
|
|
38
39
|
request_body_encoding: @profile.request_body_encoding,
|
|
39
40
|
response_body: @profile.response_body,
|
|
40
41
|
response_body_encoding: @profile.response_body_encoding,
|
|
42
|
+
request_body_size: @profile.request_body_size,
|
|
43
|
+
request_body_truncated: @profile.request_body_truncated || false,
|
|
44
|
+
request_body_size_is_minimum: @profile.request_body_size_is_minimum || false,
|
|
45
|
+
response_body_size: @profile.response_body_size,
|
|
46
|
+
response_body_truncated: @profile.response_body_truncated || false,
|
|
47
|
+
response_body_size_is_minimum: @profile.response_body_size_is_minimum || false,
|
|
48
|
+
# A streamed response whose collectors were released before its body was closed.
|
|
49
|
+
collectors_released_after_seconds: @profile.collectors_released_after_seconds,
|
|
41
50
|
started_at: @profile.started_at&.iso8601,
|
|
42
51
|
finished_at: @profile.finished_at&.iso8601
|
|
43
52
|
}
|
|
@@ -59,7 +68,7 @@ module Profiler
|
|
|
59
68
|
text: "#{@profile.method} #{@profile.status}",
|
|
60
69
|
color: status_color,
|
|
61
70
|
duration: @profile.duration,
|
|
62
|
-
|
|
71
|
+
allocated_objects: format_allocations(@profile.allocated_objects)
|
|
63
72
|
}
|
|
64
73
|
end
|
|
65
74
|
|
|
@@ -98,16 +107,10 @@ module Profiler
|
|
|
98
107
|
{}
|
|
99
108
|
end
|
|
100
109
|
|
|
101
|
-
def
|
|
102
|
-
return "0
|
|
110
|
+
def format_allocations(count)
|
|
111
|
+
return "0 objects" unless count
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
"#{bytes} B"
|
|
106
|
-
elsif bytes < 1024 * 1024
|
|
107
|
-
"#{(bytes / 1024.0).round(2)} KB"
|
|
108
|
-
else
|
|
109
|
-
"#{(bytes / 1024.0 / 1024.0).round(2)} MB"
|
|
110
|
-
end
|
|
113
|
+
"#{count.to_s.reverse.scan(/\d{1,3}/).join(",").reverse} objects"
|
|
111
114
|
end
|
|
112
115
|
end
|
|
113
116
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_collector"
|
|
4
|
+
require_relative "../process_snapshot"
|
|
4
5
|
|
|
5
6
|
module Profiler
|
|
6
7
|
module Collectors
|
|
@@ -24,65 +25,21 @@ module Profiler
|
|
|
24
25
|
}
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# The route the request matched, and the size of the table: the table itself is the same
|
|
29
|
+
# for every request, and is added back when the profile is displayed (ProcessSnapshot).
|
|
27
30
|
def collect
|
|
28
|
-
|
|
29
|
-
return store_data({ routes: [], total: 0 })
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
matched_controller, matched_action = recognize_current_route
|
|
33
|
-
routes = build_routes_list(matched_controller, matched_action)
|
|
34
|
-
matched_route = routes.find { |r| r[:matched] }
|
|
31
|
+
matched = ProcessSnapshot.matched_route(@profile.path, @profile.method)
|
|
35
32
|
|
|
36
33
|
store_data({
|
|
37
|
-
total:
|
|
38
|
-
matched:
|
|
39
|
-
routes: routes
|
|
34
|
+
total: ProcessSnapshot.route_table.size,
|
|
35
|
+
matched: matched && matched.merge(matched: true)
|
|
40
36
|
})
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
def toolbar_summary
|
|
44
40
|
data = panel_content
|
|
45
41
|
matched = data[:matched]
|
|
46
|
-
{ text: matched ? matched[:pattern] : "
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def recognize_current_route
|
|
52
|
-
recognized = Rails.application.routes.recognize_path(@profile.path, method: @profile.method)
|
|
53
|
-
[recognized[:controller], recognized[:action]]
|
|
54
|
-
rescue StandardError
|
|
55
|
-
[nil, nil]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def build_routes_list(matched_controller, matched_action)
|
|
59
|
-
Rails.application.routes.routes.filter_map do |route|
|
|
60
|
-
next if route.respond_to?(:internal) && route.internal
|
|
61
|
-
|
|
62
|
-
controller = route.defaults[:controller]
|
|
63
|
-
action = route.defaults[:action]
|
|
64
|
-
next if controller.nil?
|
|
65
|
-
next if controller.start_with?("rails/", "profiler/")
|
|
66
|
-
|
|
67
|
-
name = route.name
|
|
68
|
-
pattern = route.path.spec.to_s.sub(/\(\.:format\)$/, "")
|
|
69
|
-
v = route.verb
|
|
70
|
-
verb = (v && !v.empty?) ? v : "ANY"
|
|
71
|
-
|
|
72
|
-
controller_action = if controller && action
|
|
73
|
-
"#{controller.split("/").map { |s| ActiveSupport::Inflector.camelize(s) }.join("::")}Controller##{action}"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
name: name,
|
|
78
|
-
pattern: pattern,
|
|
79
|
-
verb: verb,
|
|
80
|
-
controller_action: controller_action,
|
|
81
|
-
matched: controller == matched_controller && action == matched_action
|
|
82
|
-
}
|
|
83
|
-
end
|
|
84
|
-
rescue StandardError
|
|
85
|
-
[]
|
|
42
|
+
{ text: matched ? matched[:pattern] : "\u2014", color: "blue" }
|
|
86
43
|
end
|
|
87
44
|
end
|
|
88
45
|
end
|