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 +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/dead_bro/gc_tracker.rb +24 -5
- data/lib/dead_bro/job_subscriber.rb +3 -0
- data/lib/dead_bro/memory_leak_detector.rb +2 -1
- data/lib/dead_bro/subscriber.rb +28 -19
- data/lib/dead_bro/version.rb +1 -1
- 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: 85f75545759422dfd14db796ac17035d9d6722e8e68e9c31e9ef0382358e93f8
|
|
4
|
+
data.tar.gz: beed782f1043421e364b1548db3dcd29dd1389d54d46440c2ff65aaf81a4b6af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/dead_bro/gc_tracker.rb
CHANGED
|
@@ -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
|
|
33
|
-
#
|
|
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
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
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 = []
|
data/lib/dead_bro/subscriber.rb
CHANGED
|
@@ -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:
|
|
147
|
-
heap_pages:
|
|
148
|
-
object_count:
|
|
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:
|
|
166
|
-
sql_count: sql_count(
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
data/lib/dead_bro/version.rb
CHANGED