class Node
Represents a node in the call tree.
Each node tracks how many allocations occurred at a specific point in a call path. Nodes form a tree structure where each path from root to leaf represents a unique call stack that led to allocations.
Nodes now track both total allocations and currently retained (live) allocations.
Definitions
def initialize(location = nil, parent = nil)
Create a new call tree node.
Signature
-
parameter
locationThread::Backtrace::Location The source location for this frame.
-
parameter
parentNode The parent node in the tree.
Implementation
def initialize(location = nil, parent = nil)
@location = location
@parent = parent
@total_count = 0 # Total allocations (never decrements)
@retained_count = 0 # Current live objects (decrements on free)
@children = nil
end
attr_reader :location, :parent, :children
Signature
-
attribute
Thread::Backtrace::Location The location of the call.
def increment_path!
Increment both total and retained counts up the entire path to root.
Implementation
def increment_path!
current = self
while current
current.total_count += 1
current.retained_count += 1
current = current.parent
end
end
def decrement_path!
Decrement retained count up the entire path to root.
Implementation
def decrement_path!
current = self
while current
current.retained_count -= 1
current = current.parent
end
end
def prune!(limit)
Prune this node's children, keeping only the top N by retained count. Prunes current level first, then recursively prunes retained children (top-down).
Signature
-
parameter
limitInteger Number of children to keep.
-
returns
Integer Total number of nodes pruned (discarded).
Implementation
def prune!(limit)
return 0 if @children.nil?
pruned_count = 0
# Prune at this level first - keep only top N children by retained count
if @children.size > limit
sorted = @children.sort_by do |_location, child|
-child.retained_count # Sort descending
end
# Detach and count discarded subtrees before we discard them:
discarded = sorted.drop(limit)
discarded.each do |_location, child|
# detach! breaks references to aid GC and returns node count
pruned_count += child.detach!
end
@children = sorted.first(limit).to_h
end
# Now recursively prune the retained children (avoid pruning nodes we just discarded)
@children.each_value{|child| pruned_count += child.prune!(limit)}
# Clean up if we ended up with no children
@children = nil if @children.empty?
pruned_count
end
def detach!
Detach this node from the tree, breaking parent/child relationships. This helps GC collect pruned nodes that might be retained in object_states.
Recursively detaches all descendants and returns total nodes detached.
Signature
-
returns
Integer Number of nodes detached (including self).
Implementation
def detach!
count = 1 # Self
# Recursively detach all children first and sum their counts
if @children
@children.each_value{|child| count += child.detach!}
end
# Break all references
@parent = nil
@children = nil
@location = nil
return count
end
def leaf?
Check if this node is a leaf (end of a call path).
Signature
-
returns
Boolean True if this node has no children.
Implementation
def leaf?
@children.nil?
end
def find_or_create_child(location)
Find or create a child node for the given location.
Signature
-
parameter
locationThread::Backtrace::Location The frame location for the child node.
-
returns
Node The child node for this location.
Implementation
def find_or_create_child(location)
@children ||= {}
@children[location.to_s] ||= Node.new(location, self)
end
def each_child(&block)
Iterate over child nodes.
Signature
-
yields
{|child| ...} If a block is given, yields each child node.
Implementation
def each_child(&block)
@children&.each_value(&block)
end
def each_path(prefix = [], &block)
Enumerate all paths from this node to leaves with their counts
Signature
-
parameter
prefixArray The path prefix (nodes traversed so far).
-
yields
{|path, total_count, retained_count| ...} For each leaf path.
Implementation
def each_path(prefix = [], &block)
current = prefix + [self]
if leaf?
yield current, @total_count, @retained_count
end
@children&.each_value do |child|
child.each_path(current, &block)
end
end