dead_bro 0.2.35 → 0.2.37

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: 2544ca10e878c96153137ba46f40a543e157eff3c877b1bd3211eec15b01a9e7
4
- data.tar.gz: 046c6f63e058e9273e8f978f42a727164c4e366c65111169c1a3d86b29ebae43
3
+ metadata.gz: 85f75545759422dfd14db796ac17035d9d6722e8e68e9c31e9ef0382358e93f8
4
+ data.tar.gz: beed782f1043421e364b1548db3dcd29dd1389d54d46440c2ff65aaf81a4b6af
5
5
  SHA512:
6
- metadata.gz: badda478e44e521b88d558413370bde506ecb7693c5a77864fc9953f9a082a97722c5c942386e16c8eeb3a15488e196d96da799a494ff9e9ee87cd304ea437bc
7
- data.tar.gz: 18f19c4e1dc982cb7e6ec422a11ff4e443f1759b73e9841757987c4d8e9b5b3c914dc6995c65cbccf0bdd48110e3214e357b62546585558a3e6cab7ee2de83a1
6
+ metadata.gz: 1dd65b6b589c013f24d18a548491aa6203c990cf0522c23c0e9000939898aed0e6927a26793c1a8eb80e4fe96e7a9c72176436f2cf8d3ae84848562b2fb82c2d
7
+ data.tar.gz: c0c020935c96898d9c54bd1b9f39ee77bf7040b8ad0cf2a9d51487af14199dca91b46e6b89b66e99c73cce470452136cb71d49ae2535f725b05edcd8b1511bdc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.37] - 2026-10-26
4
+
5
+ ### Fixed
6
+ - **`sql_count` stopped at 100 on any page that ran more queries.** It was never a query count: the subscriber read `ActiveRecord::Base.connection.query_cache.size`, the number of distinct SELECTs in the query cache — an LRU capped at 100 entries since Rails 7.1. An N+1 page that ran 188 queries reported "SQL Count 100" and "100 calls/request". `sql_count` is now the sum of the per-statement aggregate counts the SQL subscriber already keeps (every query, not only the first `MAX_TRACKED_QUERIES`), with query-cache hits included the way Rails counts them in its `(N queries, M cached)` log line. The old lookup also leased a database connection on requests that ran no SQL; nothing in the subscriber touches `ActiveRecord::Base.connection` any more.
7
+ - Background jobs sent no `sql_count` at all, so the dashboard showed 0 database calls for every job; job payloads now carry the same count.
8
+
9
+ ### Added
10
+ - `sql_cached_count` on request and job payloads: how many of `sql_count` were answered by the query cache.
11
+
12
+ ## [0.2.36] - 2026-10-25
13
+
14
+ ### Added
15
+ - **`gc_pressure` now says whether the window's GC had finished.** `gc_state_at_end` is the GC's phase when a request or job window closes — `"none"`, `"marking"` or `"sweeping"`, from `GC.latest_gc_info(:state)` — sent with the other memory-tracking fields. Ruby marks and sweeps in steps between allocations, so a GC counted in the window is often still running when it closes, and until it finishes `heap_live_slots_growth` still counts garbage it hasn't freed. The dashboard uses it so a sample measured mid-GC isn't flagged as a possible leak. It's a single-key read; no hash is built.
16
+
17
+ ### Fixed
18
+ - **The in-process leak detector's `object_count` was always 0.** `gc_stats` never read `heap_live_slots`, so every `MemoryLeakDetector` sample recorded `object_count: 0` and the `object_stats` / `memory_efficiency` figures from `DeadBro::MemoryHelpers.analyze_memory` were meaningless. `gc_stats` now includes `heap_live_slots` — in request and job payloads alike — and the request's leak sample and payload share one `GC.stat` read. `average_memory_per_object_kb` is now actually in KB; it divided RSS in MB by the object count.
19
+
3
20
  ## [0.2.35] - 2026-10-21
4
21
 
5
22
  ### Fixed
@@ -29,14 +29,17 @@ module DeadBro
29
29
  # Memory-tracking enrichment (a few extra GC.stat reads). Only the base
30
30
  # GC pressure fields above are truly always-on.
31
31
  if memory_tracking_enabled?
32
- # Live heap slots — net retained objects. Comparing this delta against
33
- # allocated_objects separates transient churn from genuine retention.
32
+ # Live heap slots, process-wide. How to read their delta is explained
33
+ # in self.diff below.
34
34
  base[:heap_live_slots] = stat[:heap_live_slots] || 0
35
35
  # Bytes malloc'd outside the Ruby object heap (big strings/buffers, e.g.
36
36
  # parsed JSON response bodies). These are point-in-time gauges reset by
37
37
  # GC, so we report the request-end value rather than a diff.
38
38
  base[:malloc_increase_bytes] = stat[:malloc_increase_bytes] || 0
39
39
  base[:oldmalloc_increase_bytes] = stat[:oldmalloc_increase_bytes] || 0
40
+ # The GC's phase right after those counters were read: :none, :marking
41
+ # or :sweeping. See self.diff below.
42
+ base[:gc_state] = gc_state
40
43
  end
41
44
 
42
45
  base
@@ -44,6 +47,13 @@ module DeadBro
44
47
  {}
45
48
  end
46
49
 
50
+ # A single-key read: no hash is built. nil where the VM doesn't report it.
51
+ def self.gc_state
52
+ GC.latest_gc_info(:state) if GC.respond_to?(:latest_gc_info)
53
+ rescue
54
+ nil
55
+ end
56
+
47
57
  def self.diff(before, after)
48
58
  return {} if before.empty? || after.empty?
49
59
  gc_time_ms = if before[:gc_time_ns] && after[:gc_time_ns]
@@ -58,10 +68,19 @@ module DeadBro
58
68
 
59
69
  # Present only when the enrichment was captured (memory tracking enabled).
60
70
  if after.key?(:heap_live_slots) || before.key?(:heap_live_slots)
61
- # Net change in live slots over the request. A small value alongside a
62
- # large allocated_objects means the memory was transient (reclaimed by
63
- # GC); a large value means objects were retained — the real leak signal.
71
+ # Signed net change in live slots over the request. Like allocated_objects
72
+ # it is process-wide: every thread's allocations and frees count. It only
73
+ # separates transient churn from retention once a GC in the window has
74
+ # finished: without one, slots are freed only by the lazy sweep of an
75
+ # earlier GC, and a GC still running when the window closes hasn't freed
76
+ # all its garbage yet. It goes negative when more is freed than
77
+ # allocated. Read it together with minor_gc_runs/major_gc_runs and
78
+ # gc_state_at_end.
64
79
  result[:heap_live_slots_growth] = (after[:heap_live_slots] || 0) - (before[:heap_live_slots] || 0)
80
+ # The GC's phase when the window closed: "none" once the last GC has
81
+ # finished, "marking" or "sweeping" while it's still running (Ruby marks
82
+ # and sweeps in steps between allocations).
83
+ result[:gc_state_at_end] = after[:gc_state].to_s if after[:gc_state]
65
84
  # Off-heap malloc pressure pending at request end (see snapshot).
66
85
  result[:malloc_increase_bytes] = after[:malloc_increase_bytes] || 0
67
86
  result[:oldmalloc_increase_bytes] = after[:oldmalloc_increase_bytes] || 0
@@ -141,6 +141,8 @@ module DeadBro
141
141
  gc_pressure: gc_pressure,
142
142
  ar_instantiation_count: ar_instantiation_count,
143
143
  status: has_error ? "failed" : "completed",
144
+ sql_count: DeadBro::Subscriber.sql_count(sql_queries),
145
+ sql_cached_count: DeadBro::Subscriber.sql_cached_count(sql_queries),
144
146
  sql_queries: sql_queries,
145
147
  transaction_events: transaction_events,
146
148
  rails_env: DeadBro.env,
@@ -307,6 +309,7 @@ module DeadBro
307
309
  count: stats[:count] || 0,
308
310
  heap_allocated_pages: stats[:heap_allocated_pages] || 0,
309
311
  heap_sorted_pages: stats[:heap_sorted_pages] || 0,
312
+ heap_live_slots: stats[:heap_live_slots] || 0,
310
313
  total_allocated_objects: stats[:total_allocated_objects] || 0
311
314
  }
312
315
  else
@@ -141,8 +141,9 @@ module DeadBro
141
141
  def calculate_memory_efficiency(samples)
142
142
  return {} if samples.length < 2
143
143
 
144
+ # memory_usage is RSS in MB; object_count is live heap slots.
144
145
  memory_per_object = samples.map do |sample|
145
- (sample[:object_count] > 0) ? sample[:memory_usage] / sample[:object_count] : 0
146
+ (sample[:object_count] > 0) ? sample[:memory_usage] * 1024.0 / sample[:object_count] : 0
146
147
  end
147
148
 
148
149
  gc_efficiency = []
@@ -139,13 +139,16 @@ module DeadBro
139
139
  }
140
140
  end
141
141
 
142
+ # One GC.stat read for both the leak-detection sample and the payload.
143
+ end_gc_stats = gc_stats
144
+
142
145
  # Record memory sample for leak detection (only if memory tracking enabled)
143
146
  if DeadBro.configuration.memory_tracking_enabled
144
147
  DeadBro::MemoryLeakDetector.record_memory_sample({
145
148
  memory_usage: memory_usage_mb,
146
- gc_count: gc_stats[:count],
147
- heap_pages: gc_stats[:heap_allocated_pages],
148
- object_count: gc_stats[:heap_live_slots],
149
+ gc_count: end_gc_stats[:count],
150
+ heap_pages: end_gc_stats[:heap_allocated_pages],
151
+ object_count: end_gc_stats[:heap_live_slots],
149
152
  request_id: data[:request_id],
150
153
  controller: data[:controller],
151
154
  action: data[:action]
@@ -162,8 +165,9 @@ module DeadBro
162
165
  view_runtime_ms: data[:view_runtime],
163
166
  db_runtime_ms: data[:db_runtime],
164
167
  memory_usage: memory_usage_mb,
165
- gc_stats: gc_stats,
166
- sql_count: sql_count(data),
168
+ gc_stats: end_gc_stats,
169
+ sql_count: sql_count(sql_queries),
170
+ sql_cached_count: sql_cached_count(sql_queries),
167
171
  sql_queries: sql_queries,
168
172
  transaction_events: transaction_events,
169
173
  http_outgoing: Thread.current[:dead_bro_http_events] || [],
@@ -459,6 +463,7 @@ module DeadBro
459
463
  count: stats[:count] || 0,
460
464
  heap_allocated_pages: stats[:heap_allocated_pages] || 0,
461
465
  heap_sorted_pages: stats[:heap_sorted_pages] || 0,
466
+ heap_live_slots: stats[:heap_live_slots] || 0,
462
467
  total_allocated_objects: stats[:total_allocated_objects] || 0
463
468
  }
464
469
  else
@@ -468,20 +473,24 @@ module DeadBro
468
473
  {}
469
474
  end
470
475
 
471
- def self.sql_count(data)
472
- # Count SQL queries from the payload if available
473
- if data[:sql_count]
474
- data[:sql_count]
475
- elsif defined?(ActiveRecord) && ActiveRecord::Base.connection
476
- # Try to get from ActiveRecord connection
477
- begin
478
- ActiveRecord::Base.connection.query_cache.size
479
- rescue
480
- 0
481
- end
482
- else
483
- 0
484
- end
476
+ # Number of SQL statements run during the request, query-cache hits included
477
+ # (the same count as Rails' "ActiveRecord: …ms (N queries, M cached)" log
478
+ # line). Summed from SqlSubscriber.stop_request_tracking's per-statement
479
+ # aggregates, which count every query — the raw query list stops at
480
+ # MAX_TRACKED_QUERIES. This used to read
481
+ # ActiveRecord::Base.connection.query_cache.size: the number of distinct
482
+ # cached SELECTs, an LRU capped at 100 since Rails 7.1, so every N+1 page
483
+ # reported "100" — and it leased a DB connection on requests with no SQL.
484
+ def self.sql_count(sql_queries)
485
+ Array(sql_queries).sum { |agg| agg[:count].to_i }
486
+ rescue
487
+ 0
488
+ end
489
+
490
+ # How many of sql_count were answered by the query cache, sent separately so
491
+ # the dashboard can tell repeated cache hits from database round trips.
492
+ def self.sql_cached_count(sql_queries)
493
+ Array(sql_queries).sum { |agg| agg[:cached_count].to_i }
485
494
  rescue
486
495
  0
487
496
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.35"
4
+ VERSION = "0.2.37"
5
5
  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.35
4
+ version: 0.2.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa