class Sampler
Sample memory allocations.
sampler = Sampler.capture do
5.times { "foo" }
end
Definitions
def self.load_object_space_dump(io, &block)
Load allocations from an ObjectSpace heap dump.
If a block is given, it will be called periodically with progress information.
Signature
-
parameter
ioIO The IO stream containing the heap dump JSON.
-
returns
Sampler A new sampler populated with allocations from the heap dump.
Implementation
def self.load_object_space_dump(io, &block)
sampler = new
cache = sampler.cache
line_count = 0
object_count = 0
report_interval = 10000
io.each_line do |line|
line_count += 1
begin
object = JSON.parse(line)
rescue JSON::ParserError
# Skip invalid JSON lines
next
end
# Skip non-object entries (ROOT, SHAPE, etc.)
next unless object["address"]
# Get allocation information (may be nil if tracing wasn't enabled)
file = object["file"] || "(unknown)"
line_number = object["line"] || 0
# Get object type/class
type = object["type"] || "unknown"
# Get memory size
memsize = object["memsize"] || 0
# Get value for strings
value = object["value"]
allocation = Allocation.new(
cache,
type, # class_name
file, # file
line_number, # line
memsize, # memsize
value, # value (for strings)
true # retained (all objects in heap dump are live)
)
sampler.allocated << allocation
object_count += 1
# Report progress periodically
if block && (object_count % report_interval == 0)
block.call(line_count, object_count)
end
end
# Final progress report
block.call(line_count, object_count) if block
return sampler
end
def initialize(&filter)
Initialize a new sampler.
Signature
-
parameter
filterBlock | Nil Optional filter block to select which allocations to track.
Implementation
def initialize(&filter)
@filter = filter
@cache = Cache.new
@wrapper = Wrapper.new(@cache)
@allocated = Array.new
end
def inspect
Generate a human-readable representation of this sampler.
Signature
-
returns
String String showing the number of allocations tracked.
Implementation
def inspect
"#<#{self.class} #{@allocated.size} allocations>"
end
def start
Start tracking memory allocations. Disables GC and begins ObjectSpace allocation tracing.
Implementation
def start
GC.disable
3.times{GC.start}
# Ensure any allocations related to the block are freed:
GC.start
@generation = GC.count
ObjectSpace.trace_object_allocations_start
end
def stop
Stop tracking allocations and determine which ones were retained. Re-enables GC and marks retained allocations.
Implementation
def stop
ObjectSpace.trace_object_allocations_stop
allocated = track_allocations(@generation)
# **WARNING** Do not allocate any new Objects between the call to GC.start and the completion of the retained lookups. It is likely that a new Object would reuse an object_id from a GC'd object.
# Overwrite any immediate values on the C stack to avoid retaining them.
ObjectSpace.dump(Object.new)
GC.enable
3.times{GC.start}
# See above.
GC.start
ObjectSpace.each_object do |object|
next unless ObjectSpace.allocation_generation(object) == @generation
if found = allocated[object.__id__]
found.retained = true
end
end
ObjectSpace.trace_object_allocations_clear
end
def dump(io = nil)
Serialize allocations to MessagePack format.
Signature
-
parameter
ioIO | Nil Optional IO stream to write to. If nil, returns serialized data.
-
returns
String | Nil Serialized data if no IO stream provided, otherwise nil.
Implementation
def dump(io = nil)
Console.logger.debug(self, "Dumping allocations: #{@allocated.size}")
if io
packer = @wrapper.packer(io)
packer.pack(@allocated)
packer.flush
else
@wrapper.dump(@allocated)
end
end
def load(data)
Load allocations from MessagePack-serialized data.
Signature
-
parameter
dataString The serialized allocation data.
Implementation
def load(data)
allocations = @wrapper.load(data)
Console.logger.debug(self, "Loading allocations: #{allocations.size}")
@allocated.concat(allocations)
end
def report(**options)
Generate a report from the tracked allocations.
Signature
-
parameter
optionsHash Options to pass to the report constructor.
-
returns
Report A report containing allocation statistics.
Implementation
def report(**options)
report = Report.general(**options)
report.concat(@allocated)
return report
end
def as_json(options = nil)
Convert this sampler to a JSON-compatible summary. Returns the allocation count without iterating through all allocations.
Signature
-
parameter
optionsHash | Nil Optional JSON serialization options.
-
returns
Hash JSON-compatible summary of sampler data.
Implementation
def as_json(options = nil)
{
allocations: @allocated.size
}
end
def to_json(...)
Convert this sampler to a JSON string.
Signature
-
returns
String JSON representation of this sampler summary.
Implementation
def to_json(...)
as_json.to_json(...)
end
def run(&block)
Collects object allocation and memory of ruby code inside of passed block.
Implementation
def run(&block)
start
begin
# We do this to avoid retaining the result of the block.
yield && false
ensure
stop
end
end
def track_allocations(generation)
Iterates through objects in memory of a given generation. Stores results along with meta data of objects collected.
Implementation
def track_allocations(generation)
rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
allocated = Hash.new
ObjectSpace.each_object do |object|
next unless ObjectSpace.allocation_generation(object) == generation
file = ObjectSpace.allocation_sourcefile(object) || "(no name)"
klass = object.class rescue nil
unless Class === klass
# attempt to determine the true Class when .class returns something other than a Class
klass = Kernel.instance_method(:class).bind(object).call
end
next if @filter && !@filter.call(klass, file)
line = ObjectSpace.allocation_sourceline(object)
# we do memsize first to avoid freezing as a side effect and shifting
# storage to the new frozen string, this happens on @hash[s] in lookup_string
memsize = ObjectSpace.memsize_of(object)
class_name = @cache.lookup_class_name(klass)
value = (klass == String) ? @cache.lookup_string(object) : nil
# compensate for API bug
memsize = rvalue_size if memsize > 100_000_000_000
allocation = Allocation.new(@cache, class_name, file, line, memsize, value, false)
@allocated << allocation
allocated[object.__id__] = allocation
end
return allocated
end