Module: DeadBro::GcTracker

Defined in:
lib/dead_bro/gc_tracker.rb

Constant Summary collapse

THREAD_KEY =
:dead_bro_gc_start

Class Method Summary collapse

Class Method Details

.diff(before, after) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/dead_bro/gc_tracker.rb', line 47

def self.diff(before, after)
  return {} if before.empty? || after.empty?
  gc_time_ms = if before[:gc_time_ns] && after[:gc_time_ns]
    ((after[:gc_time_ns] - before[:gc_time_ns]) / 1_000_000.0).round(3)
  end
  result = {
    minor_gc_runs: (after[:minor_gc_count] || 0) - (before[:minor_gc_count] || 0),
    major_gc_runs: (after[:major_gc_count] || 0) - (before[:major_gc_count] || 0),
    allocated_objects: (after[:total_allocated_objects] || 0) - (before[:total_allocated_objects] || 0),
    gc_time_ms: gc_time_ms
  }

  # Present only when the enrichment was captured (memory tracking enabled).
  if after.key?(:heap_live_slots) || before.key?(:heap_live_slots)
    # Net change in live slots over the request. A small value alongside a
    # large allocated_objects means the memory was transient (reclaimed by
    # GC); a large value means objects were retained — the real leak signal.
    result[:heap_live_slots_growth] = (after[:heap_live_slots] || 0) - (before[:heap_live_slots] || 0)
    # Off-heap malloc pressure pending at request end (see snapshot).
    result[:malloc_increase_bytes] = after[:malloc_increase_bytes] || 0
    result[:oldmalloc_increase_bytes] = after[:oldmalloc_increase_bytes] || 0
  end

  result
rescue
  {}
end

.memory_tracking_enabled?Boolean

Returns:

  • (Boolean)


75
76
77
78
79
# File 'lib/dead_bro/gc_tracker.rb', line 75

def self.memory_tracking_enabled?
  DeadBro.configuration.memory_tracking_enabled
rescue
  false
end

.snapshotObject



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/dead_bro/gc_tracker.rb', line 19

def self.snapshot
  return {} unless defined?(GC) && GC.respond_to?(:stat)
  stat = GC.stat
  base = {
    minor_gc_count: stat[:minor_gc_count] || 0,
    major_gc_count: stat[:major_gc_count] || 0,
    total_allocated_objects: stat[:total_allocated_objects] || 0,
    gc_time_ns: GC.respond_to?(:total_time) ? GC.total_time : nil
  }

  # Memory-tracking enrichment (a few extra GC.stat reads). Only the base
  # GC pressure fields above are truly always-on.
  if memory_tracking_enabled?
    # Live heap slots — net retained objects. Comparing this delta against
    # allocated_objects separates transient churn from genuine retention.
    base[:heap_live_slots] = stat[:heap_live_slots] || 0
    # Bytes malloc'd outside the Ruby object heap (big strings/buffers, e.g.
    # parsed JSON response bodies). These are point-in-time gauges reset by
    # GC, so we report the request-end value rather than a diff.
    base[:malloc_increase_bytes] = stat[:malloc_increase_bytes] || 0
    base[:oldmalloc_increase_bytes] = stat[:oldmalloc_increase_bytes] || 0
  end

  base
rescue
  {}
end

.start_request_trackingObject



7
8
9
# File 'lib/dead_bro/gc_tracker.rb', line 7

def self.start_request_tracking
  Thread.current[THREAD_KEY] = snapshot
end

.stop_request_trackingObject



11
12
13
14
15
16
17
# File 'lib/dead_bro/gc_tracker.rb', line 11

def self.stop_request_tracking
  before = Thread.current[THREAD_KEY]
  return {} if before.nil? || before.empty?
  diff(before, snapshot)
ensure
  Thread.current[THREAD_KEY] = nil
end