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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/app/assets/builds/profiler.js +20 -4
- 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/collectors/base_collector.rb +8 -3
- data/lib/profiler/collectors/cache_collector.rb +3 -3
- data/lib/profiler/collectors/database_collector.rb +128 -10
- data/lib/profiler/collectors/env_collector.rb +12 -4
- data/lib/profiler/collectors/exception_collector.rb +7 -9
- data/lib/profiler/collectors/flamegraph_collector.rb +5 -5
- data/lib/profiler/collectors/function_profiler_collector.rb +28 -4
- data/lib/profiler/collectors/mailer_collector.rb +2 -8
- 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 +2 -2
- data/lib/profiler/configuration.rb +5 -1
- 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 +4 -2
- data/lib/profiler/mcp/tools/get_profile_detail.rb +8 -1
- data/lib/profiler/process_snapshot.rb +157 -0
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +5 -0
- metadata +5 -1
|
@@ -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
|
data/lib/profiler/version.rb
CHANGED
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.
|
|
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
|