class Sampler
Periodic sampler for monitoring memory growth over time.
Samples class allocation counts at regular intervals and detects potential memory leaks by tracking when counts increase beyond a threshold. When a class exceeds the increases threshold, automatically enables detailed call path tracking for diagnosis.
Nested
Definitions
def initialize(depth: 4, filter: nil, increases_threshold: 10, prune_limit: 5, prune_threshold: nil)
Create a new memory sampler.
Signature
-
parameter
depthInteger Number of stack frames to capture for call path analysis.
-
parameter
filterProc Optional filter to exclude frames from call paths.
-
parameter
increases_thresholdInteger Number of increases before enabling detailed tracking.
-
parameter
prune_limitInteger Keep only top N children per node during pruning (default: 5).
-
parameter
prune_thresholdInteger Number of insertions before auto-pruning (nil = no auto-pruning).
Implementation
def initialize(depth: 4, filter: nil, increases_threshold: 10, prune_limit: 5, prune_threshold: nil)
@depth = depth
@filter = filter || default_filter
@increases_threshold = increases_threshold
@prune_limit = prune_limit
@prune_threshold = prune_threshold
@capture = Capture.new
@call_trees = {}
@samples = {}
end
attr :depth
Signature
-
attribute
Integer The depth of the call tree.
attr :filter
Signature
-
attribute
Proc The filter to exclude frames from call paths.
attr :increases_threshold
Signature
-
attribute
Integer The number of increases before enabling detailed tracking.
attr :prune_limit
Signature
-
attribute
Integer The number of insertions before auto-pruning (nil = no auto-pruning).
attr :prune_threshold
Signature
-
attribute
Integer | Nil The number of insertions before auto-pruning (nil = no auto-pruning).
attr :capture
Signature
-
attribute
Capture The capture object.
attr :call_trees
Signature
-
attribute
Hash The call trees.
attr :samples
Signature
-
attribute
Hash The samples for each class being tracked.
def start
Start capturing allocations.
Implementation
def start
@capture.start
end
def stop
Stop capturing allocations.
Implementation
def stop
@capture.stop
end
def run(interval: 60, &block)
Run periodic sampling in a loop.
Samples allocation counts at the specified interval and reports when classes show sustained memory growth. Automatically tracks ALL classes that allocate objects - no need to specify them upfront.
Signature
-
parameter
intervalNumeric Seconds between samples.
-
yields
{|sample| ...} Called when a class shows significant growth.
Implementation
def run(interval: 60, &block)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
while true
sample!(&block)
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
delta = interval - (now - start_time)
sleep(delta) if delta > 0
start_time = now
end
end
def sample!
Take a single sample of memory usage for all tracked classes.
Signature
-
yields
{|sample| ...} Called when a class shows significant growth.
Implementation
def sample!
@capture.each do |klass, allocations|
count = allocations.retained_count
sample = @samples[klass] ||= Sample.new(klass, count)
if sample.sample!(count)
# Check if we should enable detailed tracking
if sample.increases >= @increases_threshold && !@call_trees.key?(klass)
track(klass, allocations)
end
# Notify about growth if block given
yield sample if block_given?
end
end
# Prune call trees to control memory usage
prune_call_trees!
end
def track(klass, allocations = nil)
Start tracking with call path analysis.
Signature
-
parameter
klassClass The class to track with detailed analysis.
Implementation
def track(klass, allocations = nil)
# Track the class and get the allocations object
allocations ||= @capture.track(klass)
# Set up call tree for this class
tree = @call_trees[klass] = CallTree.new
depth = @depth
filter = @filter
# Register callback on allocations object:
# - On :newobj - returns state (leaf node) which C extension stores
# - On :freeobj - receives state back from C extension
allocations.track do |klass, event, state|
case event
when :newobj
# Capture call stack and record in tree
locations = caller_locations(1, depth)
filtered = locations.select(&filter)
unless filtered.empty?
# Record returns the leaf node - return it so C can store it:
tree.record(filtered)
end
# Return nil or the node - C will store whatever we return.
when :freeobj
# Decrement using the state (leaf node) passed back from then native extension:
state&.decrement_path!
end
rescue Exception => error
warn "Error in allocation tracking: #{error.message}\n#{error.backtrace.join("\n")}"
end
end
def untrack(klass)
Stop tracking a specific class.
Implementation
def untrack(klass)
@capture.untrack(klass)
@call_trees.delete(klass)
end
def tracking?(klass)
Check if a class is being tracked.
Implementation
def tracking?(klass)
@capture.tracking?(klass)
end
def count(klass)
Get live object count for a class.
Implementation
def count(klass)
@capture.count_for(klass)
end
def call_tree(klass)
Get the call tree for a specific class.
Implementation
def call_tree(klass)
@call_trees[klass]
end
def statistics(klass)
Get allocation statistics for a tracked class.
Signature
-
parameter
klassClass The class to get statistics for.
-
returns
Hash Statistics including total, retained, paths, and hotspots.
Implementation
def statistics(klass)
tree = @call_trees[klass]
return nil unless tree
{
live_count: @capture.count_for(klass),
total_allocations: tree.total_allocations,
retained_allocations: tree.retained_allocations,
top_paths: tree.top_paths(10).map {|path, total, retained|
{ path: path, total_count: total, retained_count: retained }
},
hotspots: tree.hotspots(20).transform_values {|total, retained|
{ total_count: total, retained_count: retained }
}
}
end
def all_statistics
Get statistics for all tracked classes.
Implementation
def all_statistics
@call_trees.keys.each_with_object({}) do |klass, result|
result[klass] = statistics(klass) if tracking?(klass)
end
end
def clear(klass)
Clear tracking data for a class.
Implementation
def clear(klass)
tree = @call_trees[klass]
tree&.clear!
end
def clear_all!
Clear all tracking data.
Implementation
def clear_all!
@call_trees.each_value(&:clear!)
@capture.clear
end
def stop!
Stop all tracking and clean up.
Implementation
def stop!
@capture.stop
@call_trees.each_key do |klass|
@capture.untrack(klass)
end
@capture.clear
@call_trees.clear
end