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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -0
  3. data/app/assets/builds/profiler-toolbar.js +13 -6
  4. data/app/assets/builds/profiler.js +73 -37
  5. data/app/controllers/profiler/api/profiles_controller.rb +2 -0
  6. data/app/controllers/profiler/api/toolbar_controller.rb +2 -0
  7. data/app/controllers/profiler/profiles_controller.rb +2 -0
  8. data/lib/profiler/allocation_counter.rb +21 -0
  9. data/lib/profiler/collectors/base_collector.rb +39 -3
  10. data/lib/profiler/collectors/cache_collector.rb +8 -3
  11. data/lib/profiler/collectors/database_collector.rb +133 -10
  12. data/lib/profiler/collectors/env_collector.rb +12 -4
  13. data/lib/profiler/collectors/exception_collector.rb +12 -9
  14. data/lib/profiler/collectors/flamegraph_collector.rb +10 -5
  15. data/lib/profiler/collectors/function_profiler_collector.rb +28 -4
  16. data/lib/profiler/collectors/http_collector.rb +5 -0
  17. data/lib/profiler/collectors/log_collector.rb +5 -0
  18. data/lib/profiler/collectors/mailer_collector.rb +2 -8
  19. data/lib/profiler/collectors/request_collector.rb +14 -11
  20. data/lib/profiler/collectors/routes_collector.rb +7 -50
  21. data/lib/profiler/collectors/scoped_notifications.rb +229 -0
  22. data/lib/profiler/collectors/view_collector.rb +7 -2
  23. data/lib/profiler/configuration.rb +27 -4
  24. data/lib/profiler/console_profiler.rb +2 -17
  25. data/lib/profiler/instrumentation/executor_context_propagation.rb +47 -0
  26. data/lib/profiler/instrumentation/request_context.rb +77 -0
  27. data/lib/profiler/instrumentation/thread_context_propagation.rb +11 -16
  28. data/lib/profiler/job_profiler.rb +6 -17
  29. data/lib/profiler/mcp/resources/recent_requests.rb +2 -0
  30. data/lib/profiler/mcp/tools/get_profile_detail.rb +21 -2
  31. data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
  32. data/lib/profiler/middleware/capturing_body.rb +112 -0
  33. data/lib/profiler/middleware/profiler_middleware.rb +261 -53
  34. data/lib/profiler/middleware/streamed_profile.rb +135 -0
  35. data/lib/profiler/middleware/toolbar_injector.rb +3 -10
  36. data/lib/profiler/models/profile.rb +69 -5
  37. data/lib/profiler/process_snapshot.rb +157 -0
  38. data/lib/profiler/redaction.rb +10 -0
  39. data/lib/profiler/test_profiler.rb +2 -17
  40. data/lib/profiler/version.rb +1 -1
  41. data/lib/profiler.rb +6 -0
  42. 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
- @subscription = nil
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
- @subscription = ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |name, started, finished, unique_id, payload|
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
- def unsubscribe
72
- return unless @subscription
88
+ # Collect reads only what the collector gathered itself.
89
+ def collect_from_any_thread?
90
+ true
91
+ end
73
92
 
74
- ActiveSupport::Notifications.unsubscribe(@subscription)
75
- @subscription = nil
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
- caller_locations(5, 10)
118
- .reject { |loc| loc.path.include?("active_record") }
119
- .map { |loc| "#{loc.path}:#{loc.lineno}:in `#{loc.label}`" }
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
- variables = Profiler::Redaction.env_snapshot
29
- store_data({ variables: variables, total: variables.size })
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
- data = panel_content
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
- @subscriber = ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
35
- event = ActiveSupport::Notifications::Event.new(*args)
36
- ex = event.payload[:exception_object]
37
- capture_exception(ex) if ex && @exception_data.nil?
38
- end
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
- def unsubscribe
52
- return unless @subscriber
52
+ # Collect reads only what the collector gathered itself.
53
+ def collect_from_any_thread?
54
+ true
55
+ end
53
56
 
54
- ActiveSupport::Notifications.unsubscribe(@subscriber)
55
- @subscriber = nil
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 << ActiveSupport::Notifications.monotonic_subscribe("process_action.action_controller") do |_name, started, finished, _unique_id, payload|
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 << ActiveSupport::Notifications.monotonic_subscribe("render_template.action_view") do |_name, started, finished, _unique_id, payload|
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 << ActiveSupport::Notifications.monotonic_subscribe("render_partial.action_view") do |_name, started, finished, _unique_id, payload|
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 << ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |_name, started, finished, _unique_id, payload|
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 << ActiveSupport::Notifications.monotonic_subscribe(event_name) do |name, started, finished, _unique_id, payload|
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 — lite mode will fall back to TracePoint without memory tracking
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
- @trace.enable
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 << ActiveSupport::Notifications.monotonic_subscribe("process.action_mailer") do |_name, started, finished, _id, payload|
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 << ActiveSupport::Notifications.monotonic_subscribe("deliver.action_mailer") do |_name, started, finished, _id, payload|
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
- memory: @profile.memory,
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
- memory: format_memory(@profile.memory)
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 format_memory(bytes)
102
- return "0 B" unless bytes
110
+ def format_allocations(count)
111
+ return "0 objects" unless count
103
112
 
104
- if bytes < 1024
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
- unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
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: routes.size,
38
- matched: matched_route,
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] : "—", color: "blue" }
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