Memory::ProfilerSourceMemoryProfilerSampler

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

Number of stack frames to capture for call path analysis.

parameter filter Proc

Optional filter to exclude frames from call paths.

parameter increases_threshold Integer

Number of increases before enabling detailed tracking.

parameter prune_limit Integer

Keep only top N children per node during pruning (default: 5).

parameter prune_threshold Integer

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 interval Numeric

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 klass Class

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 klass Class

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