dead_bro 0.2.4 → 0.2.5
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/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: 95efdf7e1aadb84f27739eb59612c8ad3bf0c6a5315d12497322140fdd0ca676
|
|
4
|
+
data.tar.gz: c9285dd8f024f10d974b04b76e98a79f365bb6e07ab1fa665a8340cc34bc69e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b12f73db0672c45f624b0a3916b65c95c8d0929e491ef173832b5f960cfb073b2cf0b337a2071908f5a4bbe768551e851747c56e5a90a704c76719e13449773
|
|
7
|
+
data.tar.gz: b99e770dae2b68d773eee279b02c78821244bf9c0322e91776301611d0204a0ebdd3c3b5e0c4bf62aec9e5a403bdb4c66ee35f29907fb7d5e68ae978eca33c4d
|
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
|