dead_bro 0.2.4 → 0.2.6
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/lib/dead_bro/memory_tracking_subscriber.rb +47 -3
- data/lib/dead_bro/version.rb +1 -1
- data/lib/dead_bro.rb +271 -0
- 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: 7e977f41766dfbcf7ca52cf14aafb217595f35860746a553bd9386b1ddfe7797
|
|
4
|
+
data.tar.gz: 24858e179f45181d6d144352347ed3f760a19abe53cead83c80eb4644e281f39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7310571657927f6404088b2605d710cb9e107d5e63231cddcf785ffc9cc22a78ff634d5bc5969e34d08e634ed0cdc46f85f8f2d6937bc05ebe9af14b00c4968c
|
|
7
|
+
data.tar.gz: eaaec05db2cdf1d2da03cfa873c333286d891e33cb2e37d6b347b319a3ae8481f142182d87c3ce20bd2990829075aa236e52c9f52388d2799fc862b1511ceabd
|
|
@@ -6,6 +6,7 @@ module DeadBro
|
|
|
6
6
|
class MemoryTrackingSubscriber
|
|
7
7
|
# Object allocation events
|
|
8
8
|
ALLOCATION_EVENT = "object_allocations.active_support"
|
|
9
|
+
PROCESS_ACTION_EVENT = "process_action.action_controller"
|
|
9
10
|
|
|
10
11
|
THREAD_LOCAL_KEY = :dead_bro_memory_events
|
|
11
12
|
# Consider objects larger than this many bytes as "large"
|
|
@@ -28,6 +29,23 @@ module DeadBro
|
|
|
28
29
|
next unless rand < ALLOCATION_SAMPLING_RATE
|
|
29
30
|
track_allocation(data, started, finished)
|
|
30
31
|
end
|
|
32
|
+
|
|
33
|
+
# Subscribe to process_action to capture request-level allocation counters
|
|
34
|
+
ActiveSupport::Notifications.subscribe(PROCESS_ACTION_EVENT) do |*args|
|
|
35
|
+
event = if args.length == 1 && args.first.is_a?(ActiveSupport::Notifications::Event)
|
|
36
|
+
args.first
|
|
37
|
+
else
|
|
38
|
+
ActiveSupport::Notifications::Event.new(*args)
|
|
39
|
+
end
|
|
40
|
+
allocations = event.respond_to?(:allocations) ? event.allocations : event.payload[:allocations]
|
|
41
|
+
allocated_bytes = event.respond_to?(:allocated_bytes) ? event.allocated_bytes : event.payload[:allocated_bytes]
|
|
42
|
+
next unless allocations || allocated_bytes
|
|
43
|
+
|
|
44
|
+
record_request_allocations(
|
|
45
|
+
allocations: allocations,
|
|
46
|
+
allocated_bytes: allocated_bytes
|
|
47
|
+
)
|
|
48
|
+
end
|
|
31
49
|
rescue
|
|
32
50
|
# Allocation tracking might not be available in all Ruby versions
|
|
33
51
|
end
|
|
@@ -44,9 +62,10 @@ module DeadBro
|
|
|
44
62
|
allocations: [],
|
|
45
63
|
memory_snapshots: [],
|
|
46
64
|
large_objects: [],
|
|
65
|
+
request_allocations: nil,
|
|
47
66
|
gc_before: gc_stats,
|
|
48
67
|
memory_before: memory_usage_mb,
|
|
49
|
-
start_time: Time.now.
|
|
68
|
+
start_time: Time.now.to_f,
|
|
50
69
|
object_counts_before: count_objects_snapshot
|
|
51
70
|
}
|
|
52
71
|
end
|
|
@@ -58,7 +77,7 @@ module DeadBro
|
|
|
58
77
|
if events
|
|
59
78
|
events[:gc_after] = gc_stats
|
|
60
79
|
events[:memory_after] = memory_usage_mb
|
|
61
|
-
events[:end_time] = Time.now.
|
|
80
|
+
events[:end_time] = Time.now.to_f
|
|
62
81
|
events[:duration_seconds] = events[:end_time] - events[:start_time]
|
|
63
82
|
events[:object_counts_after] = count_objects_snapshot
|
|
64
83
|
|
|
@@ -71,6 +90,16 @@ module DeadBro
|
|
|
71
90
|
events || {}
|
|
72
91
|
end
|
|
73
92
|
|
|
93
|
+
# Record request-level allocation counters from Rails instrumentation.
|
|
94
|
+
def self.record_request_allocations(allocations:, allocated_bytes:)
|
|
95
|
+
return unless Thread.current[THREAD_LOCAL_KEY]
|
|
96
|
+
|
|
97
|
+
Thread.current[THREAD_LOCAL_KEY][:request_allocations] = {
|
|
98
|
+
allocations: allocations,
|
|
99
|
+
allocated_bytes: allocated_bytes
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
74
103
|
def self.track_allocation(data, started, finished)
|
|
75
104
|
return unless Thread.current[THREAD_LOCAL_KEY]
|
|
76
105
|
|
|
@@ -122,6 +151,7 @@ module DeadBro
|
|
|
122
151
|
allocations = memory_events[:allocations] || []
|
|
123
152
|
large_objects = memory_events[:large_objects] || []
|
|
124
153
|
snapshots = memory_events[:memory_snapshots] || []
|
|
154
|
+
request_allocations = memory_events[:request_allocations]
|
|
125
155
|
|
|
126
156
|
# Calculate memory growth
|
|
127
157
|
memory_growth = 0
|
|
@@ -132,6 +162,19 @@ module DeadBro
|
|
|
132
162
|
# Calculate allocation totals
|
|
133
163
|
total_allocations = allocations.sum { |a| a[:count] }
|
|
134
164
|
total_allocated_size = allocations.sum { |a| a[:size] }
|
|
165
|
+
if request_allocations
|
|
166
|
+
total_allocated_size = request_allocations[:allocated_bytes].to_i if total_allocated_size.zero?
|
|
167
|
+
end
|
|
168
|
+
gc_allocations = nil
|
|
169
|
+
if memory_events[:gc_before] && memory_events[:gc_after]
|
|
170
|
+
gc_allocations = (memory_events[:gc_after][:total_allocated_objects] || 0) -
|
|
171
|
+
(memory_events[:gc_before][:total_allocated_objects] || 0)
|
|
172
|
+
end
|
|
173
|
+
if gc_allocations.to_i > 0
|
|
174
|
+
total_allocations = gc_allocations
|
|
175
|
+
elsif total_allocations.zero? && request_allocations
|
|
176
|
+
total_allocations = request_allocations[:allocations].to_i
|
|
177
|
+
end
|
|
135
178
|
|
|
136
179
|
# Group allocations by class
|
|
137
180
|
allocations_by_class = allocations.group_by { |a| a[:class_name] }
|
|
@@ -174,7 +217,8 @@ module DeadBro
|
|
|
174
217
|
(total_allocations.to_f / memory_events[:duration_seconds]).round(2) : 0,
|
|
175
218
|
top_allocating_classes: top_allocating_classes.map { |class_name, data|
|
|
176
219
|
{
|
|
177
|
-
|
|
220
|
+
class: class_name,
|
|
221
|
+
name: class_name,
|
|
178
222
|
count: data[:count],
|
|
179
223
|
size: data[:size],
|
|
180
224
|
size_mb: (data[:size] / 1_000_000.0).round(2)
|
data/lib/dead_bro/version.rb
CHANGED
data/lib/dead_bro.rb
CHANGED
|
@@ -84,4 +84,275 @@ module DeadBro
|
|
|
84
84
|
# Shared constant for tracking start time (used by all subscribers)
|
|
85
85
|
TRACKING_START_TIME_KEY = :dead_bro_tracking_start_time
|
|
86
86
|
MAX_TRACKING_DURATION_SECONDS = 3600 # 1 hour
|
|
87
|
+
|
|
88
|
+
# Analyze a block of code by tracking its runtime, SQL queries, and memory usage.
|
|
89
|
+
#
|
|
90
|
+
# Usage:
|
|
91
|
+
# DeadBro.analyze("load users") do
|
|
92
|
+
# User.where(active: true).to_a
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# This will print a summary to the console (or Rails logger) including:
|
|
96
|
+
# - total time the block took
|
|
97
|
+
# - number of SQL queries executed
|
|
98
|
+
# - total SQL time
|
|
99
|
+
# - breakdown of distinct SQL query patterns (count and total time)
|
|
100
|
+
# - memory before/after and delta
|
|
101
|
+
# - when detailed memory tracking is enabled, GC and allocation stats
|
|
102
|
+
#
|
|
103
|
+
# The return value of this method is a hash with the following keys:
|
|
104
|
+
# - :label
|
|
105
|
+
# - :total_time_ms
|
|
106
|
+
# - :sql_count
|
|
107
|
+
# - :sql_time_ms
|
|
108
|
+
# - :sql_queries (array of distinct query patterns with counts and timings)
|
|
109
|
+
# - :memory_before_mb
|
|
110
|
+
# - :memory_after_mb
|
|
111
|
+
# - :memory_delta_mb
|
|
112
|
+
# - :memory_details (detailed GC/allocation stats when available)
|
|
113
|
+
def self.analyze(label = nil)
|
|
114
|
+
raise ArgumentError, "DeadBro.analyze requires a block" unless block_given?
|
|
115
|
+
|
|
116
|
+
label ||= "block"
|
|
117
|
+
|
|
118
|
+
memory_tracking_started = false
|
|
119
|
+
memory_before_mb = 0.0
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
if defined?(DeadBro::MemoryTrackingSubscriber) &&
|
|
123
|
+
!Thread.current[DeadBro::MemoryTrackingSubscriber::THREAD_LOCAL_KEY]
|
|
124
|
+
DeadBro::MemoryTrackingSubscriber.start_request_tracking
|
|
125
|
+
memory_tracking_started = true
|
|
126
|
+
end
|
|
127
|
+
rescue
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
if defined?(DeadBro::MemoryTrackingSubscriber)
|
|
132
|
+
memory_before_mb = DeadBro::MemoryTrackingSubscriber.memory_usage_mb
|
|
133
|
+
else
|
|
134
|
+
memory_before_mb = 0.0
|
|
135
|
+
end
|
|
136
|
+
rescue
|
|
137
|
+
memory_before_mb = 0.0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Local SQL tracking just for this block.
|
|
141
|
+
# We subscribe directly to ActiveSupport::Notifications instead of relying
|
|
142
|
+
# on DeadBro's global SqlSubscriber tracking so we don't interfere with or
|
|
143
|
+
# depend on request/job instrumentation.
|
|
144
|
+
current_thread = Thread.current
|
|
145
|
+
local_sql_queries = []
|
|
146
|
+
sql_notification_subscription = nil
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
if defined?(ActiveSupport) && defined?(ActiveSupport::Notifications)
|
|
150
|
+
# Ensure SqlSubscriber is loaded so SQL_EVENT_NAME is defined
|
|
151
|
+
DeadBro::SqlSubscriber
|
|
152
|
+
event_name = DeadBro::SqlSubscriber::SQL_EVENT_NAME
|
|
153
|
+
|
|
154
|
+
sql_notification_subscription =
|
|
155
|
+
ActiveSupport::Notifications.subscribe(event_name) do |_name, started, finished, _id, data|
|
|
156
|
+
# Only count queries executed on this thread and skip schema queries
|
|
157
|
+
next unless Thread.current == current_thread
|
|
158
|
+
next if data[:name] == "SCHEMA"
|
|
159
|
+
|
|
160
|
+
duration_ms = begin
|
|
161
|
+
((finished - started) * 1000.0).round(2)
|
|
162
|
+
rescue
|
|
163
|
+
0.0
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
raw_sql = data[:sql].to_s
|
|
167
|
+
# Normalize SQL so identical logical queries group together
|
|
168
|
+
normalized_sql = begin
|
|
169
|
+
sql = DeadBro::SqlSubscriber.sanitize_sql(raw_sql)
|
|
170
|
+
# Collapse whitespace
|
|
171
|
+
sql = sql.gsub(/\s+/, " ").strip
|
|
172
|
+
# Normalize numeric literals and quoted strings to '?'
|
|
173
|
+
sql = sql.gsub(/=\s*\d+(\.\d+)?/i, "= ?")
|
|
174
|
+
sql = sql.gsub(/=\s*'[^']*'/i, "= ?")
|
|
175
|
+
sql
|
|
176
|
+
rescue
|
|
177
|
+
raw_sql.to_s.strip
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
query_type = begin
|
|
181
|
+
raw_sql.strip.split.first.to_s.upcase
|
|
182
|
+
rescue
|
|
183
|
+
"SQL"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
local_sql_queries << {
|
|
187
|
+
duration_ms: duration_ms,
|
|
188
|
+
sql: normalized_sql,
|
|
189
|
+
query_type: query_type
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
rescue
|
|
194
|
+
sql_notification_subscription = nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
block_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
198
|
+
|
|
199
|
+
error = nil
|
|
200
|
+
result = nil
|
|
201
|
+
analysis_result = nil
|
|
202
|
+
|
|
203
|
+
begin
|
|
204
|
+
result = yield
|
|
205
|
+
rescue => e
|
|
206
|
+
error = e
|
|
207
|
+
ensure
|
|
208
|
+
# Always unsubscribe our local SQL subscriber
|
|
209
|
+
begin
|
|
210
|
+
if sql_notification_subscription && defined?(ActiveSupport) && defined?(ActiveSupport::Notifications)
|
|
211
|
+
ActiveSupport::Notifications.unsubscribe(sql_notification_subscription)
|
|
212
|
+
end
|
|
213
|
+
rescue
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
total_time_ms = begin
|
|
217
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - block_start
|
|
218
|
+
(elapsed * 1000.0).round(2)
|
|
219
|
+
rescue
|
|
220
|
+
0.0
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Aggregate SQL metrics from our local subscription
|
|
224
|
+
sql_count = local_sql_queries.length
|
|
225
|
+
sql_time_ms = local_sql_queries.sum { |q| (q[:duration_ms] || 0.0).to_f }.round(2)
|
|
226
|
+
|
|
227
|
+
# Group SQL queries by normalized pattern to show frequency and cost
|
|
228
|
+
query_signatures = Hash.new { |h, k| h[k] = { count: 0, total_time_ms: 0.0, type: nil } }
|
|
229
|
+
local_sql_queries.each do |q|
|
|
230
|
+
sig = (q[:sql] || "UNKNOWN").to_s
|
|
231
|
+
entry = query_signatures[sig]
|
|
232
|
+
entry[:count] += 1
|
|
233
|
+
entry[:total_time_ms] += (q[:duration_ms] || 0.0).to_f
|
|
234
|
+
entry[:type] ||= q[:query_type]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
top_query_signatures = query_signatures.sort_by { |_, data| -data[:count] }.first(3)
|
|
238
|
+
|
|
239
|
+
memory_after_mb = memory_before_mb
|
|
240
|
+
memory_delta_mb = 0.0
|
|
241
|
+
detailed_memory_summary = nil
|
|
242
|
+
|
|
243
|
+
raw_events = {}
|
|
244
|
+
if memory_tracking_started
|
|
245
|
+
begin
|
|
246
|
+
raw_events = DeadBro::MemoryTrackingSubscriber.stop_request_tracking || {}
|
|
247
|
+
rescue
|
|
248
|
+
raw_events = {}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
begin
|
|
253
|
+
# Prefer values from detailed tracking when available
|
|
254
|
+
if raw_events[:memory_before]
|
|
255
|
+
memory_before_mb = raw_events[:memory_before]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if raw_events[:memory_after]
|
|
259
|
+
memory_after_mb = raw_events[:memory_after]
|
|
260
|
+
else
|
|
261
|
+
if defined?(DeadBro::MemoryTrackingSubscriber)
|
|
262
|
+
memory_after_mb = DeadBro::MemoryTrackingSubscriber.memory_usage_mb
|
|
263
|
+
else
|
|
264
|
+
memory_after_mb = memory_before_mb
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
rescue
|
|
268
|
+
memory_after_mb = memory_before_mb
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
memory_delta_mb = (memory_after_mb - memory_before_mb).round(2)
|
|
272
|
+
|
|
273
|
+
if memory_tracking_started && !raw_events.empty?
|
|
274
|
+
begin
|
|
275
|
+
perf = DeadBro::MemoryTrackingSubscriber.analyze_memory_performance(raw_events) || {}
|
|
276
|
+
|
|
277
|
+
detailed_memory_summary = {
|
|
278
|
+
memory_growth_mb: (perf[:memory_growth_mb] || memory_delta_mb).to_f,
|
|
279
|
+
gc_count_increase: perf.dig(:gc_efficiency, :gc_count_increase) || 0,
|
|
280
|
+
heap_pages_increase: perf.dig(:gc_efficiency, :heap_pages_increase) || 0,
|
|
281
|
+
total_allocated_size_mb: (perf[:total_allocated_size_mb] || 0.0).to_f,
|
|
282
|
+
top_allocating_classes: (perf[:top_allocating_classes] || []).first(3)
|
|
283
|
+
}
|
|
284
|
+
rescue
|
|
285
|
+
detailed_memory_summary = nil
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
sql_queries_segment = ""
|
|
290
|
+
unless top_query_signatures.empty?
|
|
291
|
+
formatted_queries = top_query_signatures.map do |sig, data|
|
|
292
|
+
type = data[:type] || "SQL"
|
|
293
|
+
count = data[:count]
|
|
294
|
+
total_ms = data[:total_time_ms].round(2)
|
|
295
|
+
"#{type} #{sig} (#{count}x, #{total_ms}ms)"
|
|
296
|
+
end
|
|
297
|
+
sql_queries_segment = ", sql_top_queries=[#{formatted_queries.join(" | ")}]"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
base_summary = "Analysis for #{label} - total_time=#{total_time_ms}ms, " \
|
|
301
|
+
"sql_queries=#{sql_count}, sql_time=#{sql_time_ms}ms, " \
|
|
302
|
+
"memory_before=#{memory_before_mb.round(2)}MB, " \
|
|
303
|
+
"memory_after=#{memory_after_mb.round(2)}MB, " \
|
|
304
|
+
"memory_delta=#{memory_delta_mb}MB" \
|
|
305
|
+
"#{sql_queries_segment}"
|
|
306
|
+
|
|
307
|
+
summary =
|
|
308
|
+
if detailed_memory_summary
|
|
309
|
+
top_classes = (detailed_memory_summary[:top_allocating_classes] || []).map { |c|
|
|
310
|
+
"#{c[:class_name]}:#{c[:size_mb]}MB"
|
|
311
|
+
}.join(", ")
|
|
312
|
+
|
|
313
|
+
"#{base_summary}, " \
|
|
314
|
+
"memory_growth=#{detailed_memory_summary[:memory_growth_mb].round(2)}MB, " \
|
|
315
|
+
"gc_runs=+#{detailed_memory_summary[:gc_count_increase]}, " \
|
|
316
|
+
"heap_pages=+#{detailed_memory_summary[:heap_pages_increase]}, " \
|
|
317
|
+
"allocated=#{detailed_memory_summary[:total_allocated_size_mb].round(2)}MB, " \
|
|
318
|
+
"top_allocators=[#{top_classes}]"
|
|
319
|
+
else
|
|
320
|
+
base_summary
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
begin
|
|
324
|
+
DeadBro.logger.info(summary)
|
|
325
|
+
rescue
|
|
326
|
+
begin
|
|
327
|
+
$stdout.puts("[DeadBro] #{summary}")
|
|
328
|
+
rescue
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Build structured result hash to return to the caller
|
|
333
|
+
sql_queries_detail = query_signatures.map do |sig, data|
|
|
334
|
+
{
|
|
335
|
+
sql: sig,
|
|
336
|
+
query_type: data[:type] || "SQL",
|
|
337
|
+
count: data[:count],
|
|
338
|
+
total_time_ms: data[:total_time_ms].round(2)
|
|
339
|
+
}
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
analysis_result = {
|
|
343
|
+
label: label,
|
|
344
|
+
total_time_ms: total_time_ms,
|
|
345
|
+
sql_count: sql_count,
|
|
346
|
+
sql_time_ms: sql_time_ms,
|
|
347
|
+
sql_queries: sql_queries_detail,
|
|
348
|
+
memory_before_mb: memory_before_mb,
|
|
349
|
+
memory_after_mb: memory_after_mb,
|
|
350
|
+
memory_delta_mb: memory_delta_mb,
|
|
351
|
+
memory_details: detailed_memory_summary
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
raise error if error
|
|
356
|
+
analysis_result
|
|
357
|
+
end
|
|
87
358
|
end
|