Module: Heapy::Alive

Defined in:
lib/heapy/alive.rb

Overview

This is an experimental module and likely to change. Don’t use in production.

Use at your own risk. APIs are not stable.

What

You can use it to trace objects to see if they are still “alive” in memory. Unlike the heapy CLI this is meant to be used in live running code.

This works by retaining an object’s address in memory, then running GC and taking a heap dump. If the object exists in the heap dump, it is retained. Since we have the whole heap dump we can also do things like find what is retaining your object preventing it from being collected.

Use It

You need to first start tracing objects:

Heapy::Alive.start_object_trace!(heap_file: "./tmp/heap.json")

Next in your code you want to specify the object ato trace

string = "hello world"
Heapy::Alive.trace_without_retain(string)

When the code is done executing you can get a reference to all “tracer” objects by running:

Heapy::Alive.traced_objects.each do |tracer|
  puts tracer.raw_json_hash if tracer.object_retained?
end

A few helpful methods on ‘tracer` objects:

  • ‘raw_json_hash` returns the hash of the object from the heap dump.

  • ‘object_retained?` returns truthy if the object was still present in the heap dump.

  • ‘address` a string of the memory address of the object you’re tracing.

  • ‘tracked_to_s` a string that represents the object you’re tracing (default

    is result of calling inspect on the method). You can pass in a custom representation
    when initializing the object. Can be useful for when `inspect` on the object you
    are tracing is too verbose.
    
  • ‘id2ref` returns the original object being traced (if it is still in memory).

  • ‘root?` returns false if the tracer isn’t the root object.

See ‘ObjectTracker` for more methods.

If you want to see what retains an object, you can use ‘ObectTracker#retained_by` method (caution this is extremely expensive and requires re-walking the whole heap dump:

Heapy::Alive.traced_objects.each do |tracer|
  if tracer.object_retained?
    puts "Traced: #{tracer.raw_json_hash}"
    tracer.retained_by.each do |retainer|
      puts "  Retained by: #{retainer.raw_json_hash}"
    end
  end
end

You can iterate up the whole retained tree by using the ‘retained_by` method on tracers returned. But again it’s expensive. If you have large heap dump or if you’re tracing a bunch of objects, continuously calling ‘retained_by` will take lots of time. We also don’t do any circular dependency detection so if you have two objects that depend on each other, you may hit an infinite loop.

If you know that you’ll need the retained objects of the main objects you’re tracing you can save re-walking the heap the first N times by using the ‘retained_by` flag:

Heapy::Alive.traced_objects(retained_by: true) do |tracer|
  # ...
end

This will pre-fetch the first level of “parents” for each object you’re tracing.

Did I mention this is all experimental and may change?

Defined Under Namespace

Classes: ObjectTracker, RootTracker

Class Method Summary collapse

Class Method Details

.address_to_object(address) ⇒ Object



86
87
88
89
90
91
# File 'lib/heapy/alive.rb', line 86

def self.address_to_object(address)
  obj_id = address.to_i(16) / 2
  ObjectSpace._id2ref(obj_id)
rescue RangeError
  nil
end

.retained_by(tracer: nil, address: nil) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/heapy/alive.rb', line 107

def self.retained_by(tracer: nil, address: nil)
  target_address = address || tracer.address
  tracer         = tracer  || @retain_hash[address]

  raise "not a valid address #{target_address}" if target_address.nil?

  retainer_array = []
  Analyzer.new(@heap_file).read do |json_hash|
    retainers_from_json_hash(json_hash, target_address: target_address, retainer_array: retainer_array)
  end

  retainer_array
end

.start_object_trace!(heap_file: "./tmp/heap.json") ⇒ Object



93
94
95
96
97
98
# File 'lib/heapy/alive.rb', line 93

def self.start_object_trace!(heap_file: "./tmp/heap.json")
  @mutex.synchronize do
    @started   ||= true && ObjectSpace.trace_object_allocations_start
    @heap_file ||= heap_file
  end
end

.trace_without_retain(object, to_s: nil) ⇒ Object



100
101
102
103
104
105
# File 'lib/heapy/alive.rb', line 100

def self.trace_without_retain(object, to_s: nil)
  tracker = ObjectTracker.new(object_id: object.object_id, to_s: to_s || object.inspect)
  @mutex.synchronize do
    @retain_hash[tracker.address] = tracker
  end
end

.traced_objects(retained_by: false) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/heapy/alive.rb', line 178

def self.traced_objects(retained_by: false)
  raise "You aren't tracing anything call Heapy::Alive.trace_without_retain first" if @retain_hash.empty?
  self.gc_start

  ObjectSpace.dump_all(output: File.open(@heap_file,'w'))

  retainer_address_array_hash = {}

  Analyzer.new(@heap_file).read do |json_hash|
    address = json_hash["address"]
    tracer = @retain_hash[address]
    next unless tracer
    tracer.raw_json_hash = json_hash

    if retained_by
      retainers_from_json_hash(json_hash, target_address: address, retainer_array: tracer.retained_by)
    end
  end
  @retain_hash.values
end