Memory::ProfilerSourceMemoryProfilerCallTreeNode

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 location Thread::Backtrace::Location

The source location for this frame.

parameter parent Node

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 limit Integer

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 location Thread::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 prefix Array

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