Module: DeadBro::MemoryPhaseTracker
- Defined in:
- lib/dead_bro/memory_phase_tracker.rb
Overview
Attributes a request’s object allocations to the phase that produced them —e.g. “92% of this request’s allocations happened during Elasticsearch”.
This is the always-on, low-overhead companion to GcTracker: GcTracker tells you *how many* objects a request allocated and whether they were retained; MemoryPhaseTracker tells you where they were allocated, so a 400MB request can be localized to ES deserialization vs view rendering vs controller code without running a full allocation profiler.
Attribution is exclusive: a sql.active_record event nested inside a view render is charged only to :sql, not to both. A thread-local stack records the allocation counter when each phase becomes active; entering a child phase flushes the parent’s accumulated delta and pauses it, leaving the child resumes the parent. Whatever isn’t captured by an instrumented phase stays “unattributed” (controller/application code) and is derivable on the backend as gc_pressure.allocated_objects minus the sum of these buckets.
Defined Under Namespace
Classes: Listener
Constant Summary collapse
- THREAD_KEY =
:dead_bro_memory_phases- EVENT_PHASES =
ActiveSupport event name => phase bucket. Each maps to a coarse phase so the breakdown stays readable (all view render events collapse to :view).
{ "sql.active_record" => :sql, "render_template.action_view" => :view, "render_partial.action_view" => :view, "render_collection.action_view" => :view, "render_layout.action_view" => :view, "request.elasticsearch" => :elasticsearch, "request.elastic_transport" => :elasticsearch }.freeze
Class Method Summary collapse
-
.allocated_objects ⇒ Object
Single-key GC.stat returns just the Integer (no hash allocation), so this is cheap enough to call on every instrumented event boundary.
- .allocation_counter_available? ⇒ Boolean
- .enter(phase) ⇒ Object
- .leave(phase) ⇒ Object
- .start_request_tracking ⇒ Object
-
.stop_request_tracking ⇒ Object
Returns { sql: n, view: n, elasticsearch: n } of objects allocated exclusively within each phase, omitting phases that allocated nothing.
- .subscribe! ⇒ Object
Class Method Details
.allocated_objects ⇒ Object
Single-key GC.stat returns just the Integer (no hash allocation), so this is cheap enough to call on every instrumented event boundary.
119 120 121 122 123 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 119 def self.allocated_objects GC.stat(:total_allocated_objects) rescue StandardError 0 end |
.allocation_counter_available? ⇒ Boolean
125 126 127 128 129 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 125 def self.allocation_counter_available? defined?(GC) && GC.respond_to?(:stat) && !GC.stat(:total_allocated_objects).nil? rescue StandardError false end |
.enter(phase) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 83 def self.enter(phase) state = Thread.current[THREAD_KEY] return unless state.is_a?(Hash) now = allocated_objects stack = state[:stack] if (parent = stack.last) # Pause the parent: bank what it allocated up to this point. state[:buckets][parent[:phase]] += now - parent[:checkpoint] end stack << {phase: phase, checkpoint: now} rescue StandardError # Best-effort only. end |
.leave(phase) ⇒ Object
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 98 def self.leave(phase) state = Thread.current[THREAD_KEY] return unless state.is_a?(Hash) stack = state[:stack] frame = stack.pop return unless frame now = allocated_objects state[:buckets][frame[:phase]] += now - frame[:checkpoint] # Resume the parent from this point so the child's allocations aren't # double-counted into it. if (parent = stack.last) parent[:checkpoint] = now end rescue StandardError # Best-effort only. end |
.start_request_tracking ⇒ Object
65 66 67 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 65 def self.start_request_tracking Thread.current[THREAD_KEY] = {buckets: Hash.new(0), stack: []} end |
.stop_request_tracking ⇒ Object
Returns { sql: n, view: n, elasticsearch: n } of objects allocated exclusively within each phase, omitting phases that allocated nothing.
71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 71 def self.stop_request_tracking state = Thread.current[THREAD_KEY] return {} unless state.is_a?(Hash) buckets = state[:buckets] buckets.each_with_object({}) do |(phase, count), result| result[phase] = count if count.positive? end ensure Thread.current[THREAD_KEY] = nil end |
.subscribe! ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/dead_bro/memory_phase_tracker.rb', line 53 def self.subscribe! return if @subscribed @subscribed = true return unless allocation_counter_available? EVENT_PHASES.each do |event_name, phase| ActiveSupport::Notifications.subscribe(event_name, Listener.new(phase)) end rescue StandardError # Never raise from instrumentation install. end |