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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6e2fe55b896183a75b3590cb0939c3cc1326822eb6be81bbd147cebb9ebbee2
4
- data.tar.gz: fed15de1f8aa27c1fb6df413dc3cf8b1d6de9f09ec983f28c52d00afbccacd41
3
+ metadata.gz: 7e977f41766dfbcf7ca52cf14aafb217595f35860746a553bd9386b1ddfe7797
4
+ data.tar.gz: 24858e179f45181d6d144352347ed3f760a19abe53cead83c80eb4644e281f39
5
5
  SHA512:
6
- metadata.gz: 278bcd7b6da8de36b01a8c41c5325af407c150b3094aff5b358d6609ecea052970be33cfcba901378ddbdfe9fbfdeeb1a228571b5786e1538b6252c116595fc2
7
- data.tar.gz: 27bc67cfa0f02510c4b52426a66151d4c1a828229dedc2468bfc899d589bed1d6e7e132c864f4e55c263162ead93be22ab91bf71e5ba3c95ed028fbf67c36cd6
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.utc.to_i,
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.utc.to_i
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
- class_name: class_name,
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.4"
4
+ VERSION = "0.2.6"
5
5
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dead_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa