rails-profiler 0.31.2 → 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.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "redaction"
4
+
5
+ module Profiler
6
+ # What is the same for every request of a process, kept out of the profiles: the route table
7
+ # and ENV. A profile stores the route its request matched; the Routes and Env tabs, the
8
+ # toolbar and the MCP tools get the table and the variables from the process that displays the
9
+ # profile (#hydrate).
10
+ #
11
+ # The route table is built once and built again when the routes are reloaded, in development
12
+ # (reload_routes!, or a change picked up by the reloader): the routes are new objects then.
13
+ # ENV is read when the profile is displayed, masked as it always was: it can change while the
14
+ # process runs, through the profiler's own env overrides among others.
15
+ module ProcessSnapshot
16
+ @mutex = Mutex.new
17
+ @routes = nil # [fingerprint, table, routes by [controller, action]]
18
+
19
+ class << self
20
+ # The application's routes, the engine's and Rails' own left out, as the Routes tab lists
21
+ # them: { name:, pattern:, verb:, controller_action: }.
22
+ def route_table
23
+ routes_snapshot[1]
24
+ end
25
+
26
+ # The route of the table a request to +path+ with +method+ is dispatched to, or nil.
27
+ def matched_route(path, method)
28
+ return nil unless rails_routes?
29
+
30
+ recognized = Rails.application.routes.recognize_path(path, method: method)
31
+ candidates = routes_snapshot[2][[recognized[:controller], recognized[:action]]]
32
+ return nil unless candidates
33
+
34
+ # A PUT goes to the action of the PATCH route too: give the route of the request's verb.
35
+ verb = method.to_s.upcase == "HEAD" ? "GET" : method.to_s.upcase
36
+ candidates.find { |route| route[:verb].split("|").include?(verb) } ||
37
+ candidates.find { |route| route[:verb] == "ANY" } ||
38
+ candidates.first
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
43
+ # ENV as the Env tab shows it.
44
+ def env_variables
45
+ Redaction.hide_credentials(Redaction.env_snapshot)
46
+ end
47
+
48
+ # Puts the route table and ENV back into +profile+'s data for display. A profile saved by
49
+ # an earlier version has its own table and variables, and is left as it is.
50
+ def hydrate(profile)
51
+ # A job, a console expression or a test ran in another process, whose ENV its profile keeps.
52
+ return profile unless profile && profile.profile_type.to_s == "http"
53
+
54
+ data = profile.collectors_data
55
+ if (routes = data["routes"]) && !key?(routes, :routes)
56
+ data["routes"] = with_route_table(routes)
57
+ end
58
+ if (env = data["env"]) && !key?(env, :variables)
59
+ variables = env_variables
60
+ data["env"] = merge(env, variables: variables, total: variables.size)
61
+ end
62
+ profile
63
+ rescue StandardError => e
64
+ warn "Profiler: could not add the routes and ENV to profile #{profile.token}: #{e.message}"
65
+ profile
66
+ end
67
+
68
+ private
69
+
70
+ def rails_routes?
71
+ defined?(Rails) && Rails.respond_to?(:application) && Rails.application
72
+ end
73
+
74
+ def routes_snapshot
75
+ return [nil, [].freeze, {}.freeze] unless rails_routes?
76
+
77
+ journey = Rails.application.routes.routes
78
+ list = journey.respond_to?(:routes) ? journey.routes : journey.to_a
79
+ fingerprint = [journey.object_id, list.size, list.first&.object_id, list.last&.object_id]
80
+ snapshot = @routes
81
+ return snapshot if snapshot && snapshot[0] == fingerprint
82
+
83
+ @mutex.synchronize do
84
+ @routes = build_snapshot(fingerprint, list) unless @routes && @routes[0] == fingerprint
85
+ @routes
86
+ end
87
+ rescue StandardError
88
+ [nil, [].freeze, {}.freeze]
89
+ end
90
+
91
+ def build_snapshot(fingerprint, list)
92
+ index = {}
93
+ table = list.filter_map do |route|
94
+ next if route.respond_to?(:internal) && route.internal
95
+
96
+ controller = route.defaults[:controller]
97
+ action = route.defaults[:action]
98
+ next if controller.nil?
99
+ next if controller.start_with?("rails/", "profiler/")
100
+
101
+ verb = route.verb
102
+ entry = {
103
+ name: route.name,
104
+ pattern: route.path.spec.to_s.sub(/\(\.:format\)$/, ""),
105
+ verb: (verb && !verb.empty?) ? verb : "ANY",
106
+ controller_action: controller_action(controller, action)
107
+ }.freeze
108
+ (index[[controller, action]] ||= []) << entry
109
+ entry
110
+ end
111
+ [fingerprint, table.freeze, index.each_value(&:freeze).freeze]
112
+ end
113
+
114
+ def controller_action(controller, action)
115
+ return nil unless action
116
+
117
+ "#{controller.split("/").map { |s| ActiveSupport::Inflector.camelize(s) }.join("::")}Controller##{action}"
118
+ end
119
+
120
+ def with_route_table(routes)
121
+ matched = fetch(routes, :matched)
122
+ table = route_table.map { |route| route.merge(matched: same_route?(route, matched)) }
123
+ merge(routes, routes: table, total: table.size)
124
+ end
125
+
126
+ # Every route of the matched action, as the table always marked them.
127
+ def same_route?(route, matched)
128
+ return false unless matched.is_a?(Hash)
129
+
130
+ action = fetch(matched, :controller_action)
131
+ action ? route[:controller_action] == action : route[:pattern] == fetch(matched, :pattern)
132
+ end
133
+
134
+ # Collector data has symbol keys as collected, string keys once read back from storage.
135
+ def key?(hash, key)
136
+ hash.key?(key) || hash.key?(key.to_s)
137
+ end
138
+
139
+ def fetch(hash, key)
140
+ hash.key?(key) ? hash[key] : hash[key.to_s]
141
+ end
142
+
143
+ def merge(hash, values)
144
+ strings = hash.keys.first.is_a?(String)
145
+ hash.merge(strings ? deep_stringify(values) : values)
146
+ end
147
+
148
+ def deep_stringify(value)
149
+ case value
150
+ when Hash then value.to_h { |k, v| [k.to_s, deep_stringify(v)] }
151
+ when Array then value.map { |v| deep_stringify(v) }
152
+ else value
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.31.2"
4
+ VERSION = "0.31.3"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -16,6 +16,9 @@ module Profiler
16
16
  attr_accessor :function_profiling_max_frames
17
17
  attr_accessor :function_profiling_mode
18
18
  attr_accessor :function_profiling_clock
19
+ # Without stackprof, the function profiler stays off unless this is true: it then traces
20
+ # every method call of the request's thread with a TracePoint, several times slower.
21
+ attr_accessor :function_profiling_tracepoint_fallback
19
22
 
20
23
  def configuration
21
24
  @configuration ||= Configuration.new
@@ -114,6 +117,7 @@ module Profiler
114
117
  self.function_profiling_max_frames = 2000
115
118
  self.function_profiling_mode = "lite"
116
119
  self.function_profiling_clock = "wall"
120
+ self.function_profiling_tracepoint_fallback = false
117
121
  end
118
122
 
119
123
  # Require core components
@@ -138,6 +142,7 @@ require_relative "profiler/storage/token"
138
142
  require_relative "profiler/storage/private_files"
139
143
  require_relative "profiler/env_override_store"
140
144
  require_relative "profiler/instrumentation/thread_context_propagation"
145
+ require_relative "profiler/instrumentation/executor_context_propagation"
141
146
  require_relative "profiler/sse/event_bus"
142
147
  require_relative "profiler/sse/redis_event_bus"
143
148
  require_relative "profiler/sse/bus"
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.31.2
4
+ version: 0.31.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
@@ -158,6 +158,7 @@ files:
158
158
  - lib/profiler/collectors/mailer_collector.rb
159
159
  - lib/profiler/collectors/request_collector.rb
160
160
  - lib/profiler/collectors/routes_collector.rb
161
+ - lib/profiler/collectors/scoped_notifications.rb
161
162
  - lib/profiler/collectors/test_collector.rb
162
163
  - lib/profiler/collectors/view_collector.rb
163
164
  - lib/profiler/configuration.rb
@@ -167,8 +168,10 @@ files:
167
168
  - lib/profiler/env_override_store.rb
168
169
  - lib/profiler/explain_runner.rb
169
170
  - lib/profiler/instrumentation/active_job_instrumentation.rb
171
+ - lib/profiler/instrumentation/executor_context_propagation.rb
170
172
  - lib/profiler/instrumentation/irb_instrumentation.rb
171
173
  - lib/profiler/instrumentation/net_http_instrumentation.rb
174
+ - lib/profiler/instrumentation/request_context.rb
172
175
  - lib/profiler/instrumentation/sidekiq_middleware.rb
173
176
  - lib/profiler/instrumentation/thread_context_propagation.rb
174
177
  - lib/profiler/job_profiler.rb
@@ -215,6 +218,7 @@ files:
215
218
  - lib/profiler/models/profile.rb
216
219
  - lib/profiler/models/sql_query.rb
217
220
  - lib/profiler/models/timeline_event.rb
221
+ - lib/profiler/process_snapshot.rb
218
222
  - lib/profiler/railtie.rb
219
223
  - lib/profiler/redaction.rb
220
224
  - lib/profiler/sql_statement.rb