Module: DeadBro::AllocationSourceSampler
- Defined in:
- lib/dead_bro/allocation_source_sampler.rb
Overview
Deep, opt-in memory diagnostics that answer “what code allocated this?”. Only active when allocation tracking is on (see Configuration #allocation_tracking_active?), because turning on object allocation tracing adds ~2-5ms of per-request overhead.
Two complementary breakdowns, both produced from a single ObjectSpace walk after the request finishes:
* by_type_bytes — total *retained bytes* per Ruby class. This catches the
"death by a million small strings" pattern (e.g. a deserialized
Elasticsearch response) that MemoryTrackingSubscriber's >1MB
single-object scan structurally misses, because it sums bytes per type
instead of flagging individually-large objects.
* by_source — top allocation sites (file:line) by retained bytes. This is
the gold-standard "this line allocated 300MB" attribution, available
because trace_object_allocations was running for the request.
Constant Summary collapse
- SAMPLE_RATE =
Fraction of live objects inspected during the post-request walk. Reported back as sample_rate so the consumer can extrapolate to the full heap.
0.10- MAX_RESULTS =
20- LARGE_TYPE_MIN_BYTES =
Only report a type if its sampled retained bytes clear this floor (keeps the breakdown to things that actually matter).
100_000- DEFAULT_MIN_GROWTH_MB =
Skip the walk entirely below this growth — no point profiling a request that didn’t move memory. Gates the expensive path even when the flag is on.
50
Class Method Summary collapse
-
.analyze(memory_growth_mb: nil, min_growth_mb: DEFAULT_MIN_GROWTH_MB) ⇒ Object
Walk live objects once and build the two breakdowns.
- .available? ⇒ Boolean
-
.start ⇒ Object
Begin recording allocation source locations.
-
.stop ⇒ Object
Stop and discard recorded allocation data.
- .top_by_bytes(hash, min_bytes) ⇒ Object
Class Method Details
.analyze(memory_growth_mb: nil, min_growth_mb: DEFAULT_MIN_GROWTH_MB) ⇒ Object
Walk live objects once and build the two breakdowns. Returns {} when tracing is unavailable, or … when growth was below threshold.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/dead_bro/allocation_source_sampler.rb', line 72 def self.analyze(memory_growth_mb: nil, min_growth_mb: DEFAULT_MIN_GROWTH_MB) return {} unless available? if memory_growth_mb && memory_growth_mb < min_growth_mb return {skipped: "memory_growth_below_threshold", min_growth_mb: min_growth_mb} end by_type = Hash.new { |h, k| h[k] = {count: 0, bytes: 0} } by_source = Hash.new { |h, k| h[k] = {count: 0, bytes: 0} } ObjectSpace.each_object do |obj| next unless rand < SAMPLE_RATE size = begin ObjectSpace.memsize_of(obj) rescue StandardError 0 end next unless size && size > 0 klass = begin obj.class.name rescue StandardError nil end || "Unknown" type_bucket = by_type[klass] type_bucket[:count] += 1 type_bucket[:bytes] += size file = begin ObjectSpace.allocation_sourcefile(obj) rescue StandardError nil end next unless file line = begin ObjectSpace.allocation_sourceline(obj) rescue StandardError nil end source_bucket = by_source["#{file}:#{line}"] source_bucket[:count] += 1 source_bucket[:bytes] += size end { sample_rate: SAMPLE_RATE, by_type_bytes: top_by_bytes(by_type, LARGE_TYPE_MIN_BYTES), by_source: top_by_bytes(by_source, 0) } rescue StandardError {} end |
.available? ⇒ Boolean
43 44 45 46 47 48 49 |
# File 'lib/dead_bro/allocation_source_sampler.rb', line 43 def self.available? defined?(ObjectSpace) && ObjectSpace.respond_to?(:trace_object_allocations_start) && ObjectSpace.respond_to?(:memsize_of) rescue StandardError false end |
.start ⇒ Object
Begin recording allocation source locations. Must be called before the request allocates the objects we want to attribute.
53 54 55 56 57 58 |
# File 'lib/dead_bro/allocation_source_sampler.rb', line 53 def self.start return unless available? ObjectSpace.trace_object_allocations_start rescue StandardError # Best-effort only. end |
.stop ⇒ Object
Stop and discard recorded allocation data. Call this AFTER analyze, since clearing wipes the source locations analyze reads.
62 63 64 65 66 67 68 |
# File 'lib/dead_bro/allocation_source_sampler.rb', line 62 def self.stop return unless available? ObjectSpace.trace_object_allocations_stop ObjectSpace.trace_object_allocations_clear rescue StandardError # Best-effort only. end |
.top_by_bytes(hash, min_bytes) ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/dead_bro/allocation_source_sampler.rb', line 126 def self.top_by_bytes(hash, min_bytes) hash.select { |_, v| v[:bytes] >= min_bytes } .sort_by { |_, v| -v[:bytes] } .first(MAX_RESULTS) .map do |key, v| { name: key, count: v[:count], bytes: v[:bytes], mb: (v[:bytes] / 1_000_000.0).round(2) } end end |