Introspection & ObjectSpace
Why Master Ruby Introspection & ObjectSpace?
Ruby's introspection capabilities and ObjectSpace module provide powerful tools for examining and manipulating the runtime environment. These features enable debugging tools, performance analyzers, testing frameworks, and advanced metaprogramming techniques.
Debugging & Analysis
Memory profiling, object tracking, runtime inspection
Framework Development
Dynamic constant loading, plugin systems, auto-discovery
Testing & Mocking
Object state verification, method stubbing, test utilities
ObjectSpace - Ruby's Object Universe
ObjectSpace provides access to all objects in the Ruby runtime, enabling powerful introspection and debugging capabilities. It's essential for memory profiling, garbage collection analysis, and understanding object relationships.
Basic ObjectSpace Operations
require 'objspace'
class MemoryAnalyzer
def self.object_count_by_class
counts = Hash.new(0)
ObjectSpace.each_object do |obj|
counts[obj.class] += 1
end
# Sort by count descending
counts.sort_by { |klass, count| -count }.to_h
end
def self.find_objects_of_type(klass)
objects = []
ObjectSpace.each_object(klass) do |obj|
objects << obj
end
objects
end
def self.memory_usage_by_class
usage = Hash.new(0)
ObjectSpace.each_object do |obj|
size = ObjectSpace.memsize_of(obj)
usage[obj.class] += size
end
# Convert to human readable format
usage.transform_values { |bytes| humanize_bytes(bytes) }
.sort_by { |klass, size| -usage[klass] }
.to_h
end
def self.object_allocation_trace
# Enable allocation tracing
ObjectSpace.trace_object_allocations_start
yield if block_given?
allocations = []
ObjectSpace.each_object do |obj|
file = ObjectSpace.allocation_sourcefile(obj)
line = ObjectSpace.allocation_sourceline(obj)
method = ObjectSpace.allocation_method_id(obj)
if file && line
allocations << {
object: obj,
class: obj.class,
file: file,
line: line,
method: method,
size: ObjectSpace.memsize_of(obj)
}
end
end
ObjectSpace.trace_object_allocations_stop
allocations.sort_by { |alloc| -alloc[:size] }
end
private
def self.humanize_bytes(bytes)
units = ['B', 'KB', 'MB', 'GB']
unit_index = 0
size = bytes.to_f
while size >= 1024 && unit_index < units.length - 1
size /= 1024
unit_index += 1
end
"#{size.round(2)} #{units[unit_index]}"
end
end
# Analyze object counts
puts "=== Object Counts by Class ==="
MemoryAnalyzer.object_count_by_class.first(10).each do |klass, count|
puts "#{klass}: #{count}"
end
# Find all string objects
strings = MemoryAnalyzer.find_objects_of_type(String)
puts "\nFound #{strings.length} String objects"
# Analyze memory usage
puts "\n=== Memory Usage by Class ==="
MemoryAnalyzer.memory_usage_by_class.first(5).each do |klass, size|
puts "#{klass}: #{size}"
end
# Trace allocations during code execution
allocations = MemoryAnalyzer.object_allocation_trace do
# Code to analyze
1000.times { |i| "String #{i}" }
Array.new(100) { Hash.new }
end
puts "\n=== Top Allocations ==="
allocations.first(5).each do |alloc|
puts "#{alloc[:class]} (#{MemoryAnalyzer.send(:humanize_bytes, alloc[:size])}) at #{File.basename(alloc[:file])}:#{alloc[:line]}"
end
Object Lifecycle Tracking
class ObjectTracker
def initialize
@tracked_objects = {}
@finalizers = {}
end
def track_object(obj, identifier = nil)
object_id = obj.object_id
identifier ||= "#{obj.class}:#{object_id}"
# Store weak reference to avoid preventing GC
@tracked_objects[object_id] = {
identifier: identifier,
class: obj.class,
created_at: Time.now,
alive: true
}
# Set up finalizer to detect when object is GC'd
finalizer = proc do
@tracked_objects[object_id][:alive] = false
@tracked_objects[object_id][:gc_at] = Time.now
puts "🗑️ Object #{identifier} was garbage collected"
end
ObjectSpace.define_finalizer(obj, finalizer)
@finalizers[object_id] = finalizer
puts "👁️ Tracking object #{identifier}"
object_id
end
def untrack_object(obj)
object_id = obj.object_id
if finalizer = @finalizers[object_id]
ObjectSpace.undefine_finalizer(obj)
@finalizers.delete(object_id)
@tracked_objects.delete(object_id)
puts "👋 Stopped tracking object #{object_id}"
end
end
def status_report
alive_count = @tracked_objects.count { |_, data| data[:alive] }
total_count = @tracked_objects.size
puts "\n=== Object Tracking Report ==="
puts "Total tracked: #{total_count}"
puts "Still alive: #{alive_count}"
puts "Garbage collected: #{total_count - alive_count}"
puts "\n--- Alive Objects ---"
@tracked_objects.select { |_, data| data[:alive] }.each do |id, data|
age = Time.now - data[:created_at]
puts "#{data[:identifier]} (alive for #{age.round(2)}s)"
end
puts "\n--- Garbage Collected Objects ---"
@tracked_objects.reject { |_, data| data[:alive] }.each do |id, data|
lifetime = data[:gc_at] - data[:created_at]
puts "#{data[:identifier]} (lived for #{lifetime.round(2)}s)"
end
end
def force_gc
puts "🧹 Forcing garbage collection..."
GC.start
sleep(0.1) # Give finalizers time to run
end
end
# Example usage
tracker = ObjectTracker.new
# Track some objects
obj1 = "This is a string"
obj2 = [1, 2, 3, 4, 5]
obj3 = { name: "test", value: 42 }
tracker.track_object(obj1, "test_string")
tracker.track_object(obj2, "test_array")
tracker.track_object(obj3, "test_hash")
# Create and release objects
10.times do |i|
temp_obj = "Temporary string #{i}"
tracker.track_object(temp_obj, "temp_#{i}")
end
# temp_obj goes out of scope here
tracker.status_report
tracker.force_gc
tracker.status_report
# Remove reference to one tracked object
obj2 = nil
tracker.force_gc
tracker.status_report
⚠️ ObjectSpace Performance Notes
- ObjectSpace.each_object can be slow with many objects - use sparingly in production
- Allocation tracing adds significant overhead - only enable during profiling
- Finalizers have performance implications and should be used carefully
- WeakRef may be preferable to finalizers for simple object tracking
defined? - Existence Checking
The defined? operator checks whether variables, constants, methods, or other Ruby constructs are defined. It returns a string describing what was found, or nil if nothing is defined.
Comprehensive defined? Usage
class DefinedChecker
CONSTANT_VALUE = "I'm a constant"
def initialize
@instance_var = "I'm an instance variable"
@@class_var = "I'm a class variable"
end
def demo_defined_checks
local_var = "I'm a local variable"
puts "=== Variable Checks ==="
puts "Local variable: #{defined?(local_var)}" # => "local-variable"
puts "Instance variable: #{defined?(@instance_var)}" # => "instance-variable"
puts "Class variable: #{defined?(@@class_var)}" # => "class-variable"
puts "Global variable: #{defined?($LOAD_PATH)}" # => "global-variable"
puts "Undefined variable: #{defined?(undefined_var)}" # => nil
puts "\n=== Constant Checks ==="
puts "Class constant: #{defined?(CONSTANT_VALUE)}" # => "constant"
puts "Built-in constant: #{defined?(Array)}" # => "constant"
puts "Undefined constant: #{defined?(UndefinedClass)}" # => nil
puts "\n=== Method Checks ==="
puts "Instance method: #{defined?(demo_defined_checks)}" # => "method"
puts "Built-in method: #{defined?(puts)}" # => "method"
puts "Undefined method: #{defined?(undefined_method)}" # => nil
puts "\n=== Expression Checks ==="
puts "Yield: #{defined?(yield)}" # => nil (no block given)
puts "Super: #{defined?(super)}" # => nil (no superclass method)
puts "Assignment: #{defined?(x = 1)}" # => "assignment"
end
def demo_with_block
puts "\n=== Block Context ==="
puts "Yield available: #{defined?(yield)}" # => "yield"
end
end
checker = DefinedChecker.new
checker.demo_defined_checks
# Test with block
checker.demo_with_block { puts "Block executed" }
# Practical usage in conditional loading
if defined?(Rails)
puts "Rails is available"
else
puts "Rails is not loaded"
end
# Safe constant access
database_config = if defined?(Rails) && defined?(Rails.application)
Rails.application.database_configuration
else
{ 'default' => { 'adapter' => 'sqlite3' } }
end
# Conditional method definition
unless defined?(debug_log)
def debug_log(message)
puts "[DEBUG] #{message}" if ENV['DEBUG']
end
end
Safe Navigation with defined?
module SafeAccess
def self.safe_constant_get(const_path)
const_path.split('::').reduce(Object) do |scope, const_name|
if defined?(scope.const_get(const_name))
scope.const_get(const_name)
else
return nil
end
end
end
def self.safe_method_call(object, method_name, *args)
if object.respond_to?(method_name) && defined?(object.send(method_name))
object.send(method_name, *args)
else
nil
end
end
def self.conditional_require(gem_name, &block)
begin
require gem_name
if defined?(yield)
yield
end
true
rescue LoadError
puts "Optional gem '#{gem_name}' not available"
false
end
end
end
# Safe constant access
puts SafeAccess.safe_constant_get('ActiveRecord::Base') # nil if AR not loaded
puts SafeAccess.safe_constant_get('String') # String class
# Safe method calling
obj = "hello"
puts SafeAccess.safe_method_call(obj, :upcase) # "HELLO"
puts SafeAccess.safe_method_call(obj, :undefined_method) # nil
# Conditional gem loading
SafeAccess.conditional_require('json') do
puts "JSON gem loaded, can use JSON.parse"
end
SafeAccess.conditional_require('nonexistent_gem') do
puts "This won't run"
end
const_get & const_set - Dynamic Constant Access
const_get and const_set enable dynamic access and modification of constants. They're essential for plugin systems, auto-loading, and runtime constant manipulation.
Dynamic Class Loading System
class DynamicLoader
def self.load_class(class_path)
# Handle nested constants like "MyModule::MyClass"
constants = class_path.split('::')
constants.reduce(Object) do |scope, const_name|
if scope.const_defined?(const_name)
scope.const_get(const_name)
else
raise NameError, "Constant #{const_name} not defined in #{scope}"
end
end
end
def self.safe_load_class(class_path, default = nil)
load_class(class_path)
rescue NameError
default
end
def self.create_nested_constant(path, value)
*namespace_parts, const_name = path.split('::')
# Create nested modules if they don't exist
namespace = namespace_parts.reduce(Object) do |scope, module_name|
if scope.const_defined?(module_name)
scope.const_get(module_name)
else
new_module = Module.new
scope.const_set(module_name, new_module)
new_module
end
end
namespace.const_set(const_name, value)
end
def self.list_constants(scope = Object, pattern = nil)
constants = scope.constants.map do |const_name|
begin
const_value = scope.const_get(const_name)
{
name: const_name,
value: const_value,
type: const_value.class,
full_path: scope == Object ? const_name.to_s : "#{scope}::#{const_name}"
}
rescue => e
{
name: const_name,
error: e.message,
full_path: scope == Object ? const_name.to_s : "#{scope}::#{const_name}"
}
end
end
if pattern
constants.select { |const| const[:name].to_s.match?(pattern) }
else
constants
end
end
end
# Example: Create nested modules and classes dynamically
DynamicLoader.create_nested_constant('MyApp::Services::EmailService', Class.new do
def self.send_email(to, subject, body)
puts "Sending email to #{to}: #{subject}"
end
end)
# Load and use the dynamically created class
email_service = DynamicLoader.load_class('MyApp::Services::EmailService')
email_service.send_email('user@example.com', 'Welcome!', 'Thanks for joining!')
# Safe loading with fallback
logger_class = DynamicLoader.safe_load_class('Rails::Logger', Logger)
puts "Using logger class: #{logger_class}"
# List all constants matching a pattern
puts "\n=== Constants matching /Service/ ==="
DynamicLoader.list_constants(Object, /Service/).each do |const_info|
puts "#{const_info[:full_path]} => #{const_info[:type]}"
end
Plugin System with Dynamic Loading
module PluginSystem
class Plugin
attr_reader :name, :version, :description
def initialize(name, version, description)
@name = name
@version = version
@description = description
end
def activate
raise NotImplementedError, "Plugins must implement #activate"
end
def deactivate
# Default implementation - override if needed
puts "Plugin #{@name} deactivated"
end
end
class Manager
def initialize
@plugins = {}
@active_plugins = {}
end
def register_plugin_class(plugin_class)
unless plugin_class < Plugin
raise ArgumentError, "Plugin class must inherit from Plugin"
end
@plugins[plugin_class.name] = plugin_class
puts "Registered plugin class: #{plugin_class.name}"
end
def discover_plugins(namespace = 'Plugins')
return unless Object.const_defined?(namespace)
namespace_module = Object.const_get(namespace)
namespace_module.constants.each do |const_name|
plugin_class = namespace_module.const_get(const_name)
if plugin_class.is_a?(Class) && plugin_class < Plugin
register_plugin_class(plugin_class)
end
end
end
def activate_plugin(plugin_class_name, *args)
plugin_class = @plugins[plugin_class_name]
unless plugin_class
raise ArgumentError, "Plugin #{plugin_class_name} not registered"
end
plugin_instance = plugin_class.new(*args)
plugin_instance.activate
@active_plugins[plugin_class_name] = plugin_instance
puts "Activated plugin: #{plugin_instance.name} v#{plugin_instance.version}"
plugin_instance
end
def deactivate_plugin(plugin_class_name)
plugin_instance = @active_plugins.delete(plugin_class_name)
if plugin_instance
plugin_instance.deactivate
puts "Deactivated plugin: #{plugin_instance.name}"
end
end
def list_plugins
puts "\n=== Available Plugins ==="
@plugins.each do |class_name, plugin_class|
status = @active_plugins.key?(class_name) ? "ACTIVE" : "inactive"
puts "#{class_name} [#{status}]"
end
end
def active_plugins
@active_plugins.values
end
end
end
# Create some sample plugins
module Plugins
class LoggingPlugin < PluginSystem::Plugin
def initialize
super("Logger", "1.0.0", "Adds logging functionality")
end
def activate
puts "📝 Logging plugin activated - all events will be logged"
# Add logging to Object class
Object.class_eval do
alias_method :original_send, :send
define_method :send do |method_name, *args, &block|
puts "[LOG] Calling #{method_name} on #{self.class}"
original_send(method_name, *args, &block)
end
end
end
def deactivate
# Restore original send method
Object.class_eval do
alias_method :send, :original_send
remove_method :original_send
end
super
end
end
class CachePlugin < PluginSystem::Plugin
def initialize
super("Cache", "2.1.0", "Provides caching functionality")
@cache = {}
end
def activate
puts "🗄️ Cache plugin activated"
# Make cache globally available
Object.const_set('GLOBAL_CACHE', @cache)
# Add cache methods to Object
Object.class_eval do
def cache_get(key)
GLOBAL_CACHE[key]
end
def cache_set(key, value)
GLOBAL_CACHE[key] = value
end
end
end
def deactivate
Object.send(:remove_method, :cache_get) if Object.method_defined?(:cache_get)
Object.send(:remove_method, :cache_set) if Object.method_defined?(:cache_set)
Object.send(:remove_const, 'GLOBAL_CACHE') if Object.const_defined?('GLOBAL_CACHE')
super
end
end
end
# Use the plugin system
manager = PluginSystem::Manager.new
# Discover plugins automatically
manager.discover_plugins('Plugins')
manager.list_plugins
# Activate plugins
cache_plugin = manager.activate_plugin('Plugins::CachePlugin')
logging_plugin = manager.activate_plugin('Plugins::LoggingPlugin')
# Test the plugins
cache_set('user:123', { name: 'Alice', email: 'alice@example.com' })
user_data = cache_get('user:123')
puts "Cached user data: #{user_data}"
# Deactivate plugins
manager.deactivate_plugin('Plugins::LoggingPlugin')
manager.deactivate_plugin('Plugins::CachePlugin')
🔒 Security Warning
- Never use const_get/const_set with untrusted input - it can execute arbitrary code
- Validate constant names and paths before dynamic access
- Be cautious when modifying core classes or built-in constants
- Consider using a whitelist approach for allowed constants
Advanced Introspection Patterns
Method Signature Analysis
class MethodAnalyzer
def self.analyze_method(klass, method_name)
method = klass.instance_method(method_name)
{
name: method_name,
arity: method.arity,
parameters: method.parameters,
source_location: method.source_location,
owner: method.owner,
visibility: method_visibility(klass, method_name),
signature: method_signature(method)
}
end
def self.method_visibility(klass, method_name)
if klass.private_instance_methods.include?(method_name)
:private
elsif klass.protected_instance_methods.include?(method_name)
:protected
else
:public
end
end
def self.method_signature(method)
params = method.parameters.map do |type, name|
case type
when :req then name.to_s
when :opt then "#{name} = default"
when :rest then "*#{name}"
when :keyreq then "#{name}:"
when :key then "#{name}: default"
when :keyrest then "**#{name}"
when :block then "{name}"
end
end
"#{method.name}(#{params.join(', ')})"
end
def self.find_methods_by_pattern(klass, pattern)
methods = klass.instance_methods(false) +
klass.private_instance_methods(false) +
klass.protected_instance_methods(false)
methods.select { |method_name| method_name.to_s.match?(pattern) }
.map { |method_name| analyze_method(klass, method_name) }
end
def self.method_call_trace(object, method_name, *args, &block)
original_method = object.method(method_name)
trace_info = []
# Create tracing wrapper
object.define_singleton_method(method_name) do |*method_args, &method_block|
call_info = {
timestamp: Time.now,
arguments: method_args,
caller_location: caller_locations(1, 1).first
}
trace_info << call_info
puts "🔍 Tracing call to #{method_name} with args: #{method_args.inspect}"
# Call original method
result = original_method.call(*method_args, &method_block)
call_info[:result] = result
call_info[:duration] = Time.now - call_info[:timestamp]
result
end
# Execute the traced call
result = object.send(method_name, *args, &block)
# Restore original method
object.singleton_class.send(:remove_method, method_name)
{ result: result, trace: trace_info }
end
end
class SampleClass
def initialize(name)
@name = name
end
def greet(message = "Hello", excited: false)
greeting = excited ? "#{message}!!" : message
"#{greeting}, I'm #{@name}"
end
def calculate(*numbers, operation: :sum)
case operation
when :sum then numbers.sum
when :product then numbers.reduce(:*)
else raise ArgumentError, "Unknown operation"
end
end
private
def secret_method
"This is private!"
end
end
# Analyze methods
puts "=== Method Analysis ==="
greet_analysis = MethodAnalyzer.analyze_method(SampleClass, :greet)
puts "Method: #{greet_analysis[:signature]}"
puts "Visibility: #{greet_analysis[:visibility]}"
puts "Parameters: #{greet_analysis[:parameters]}"
# Find methods by pattern
puts "\n=== Methods containing 'calc' ==="
calc_methods = MethodAnalyzer.find_methods_by_pattern(SampleClass, /calc/)
calc_methods.each do |method_info|
puts "#{method_info[:signature]} (#{method_info[:visibility]})"
end
# Trace method calls
puts "\n=== Method Call Tracing ==="
obj = SampleClass.new("Alice")
trace_result = MethodAnalyzer.method_call_trace(obj, :greet, "Hi there", excited: true)
puts "Result: #{trace_result[:result]}"
puts "Trace info: #{trace_result[:trace].first}"
Inheritance Chain Analysis
class InheritanceAnalyzer
def self.inheritance_chain(klass)
chain = []
current = klass
while current
chain << {
class: current,
type: current.class == Class ? :class : :module,
methods: current.instance_methods(false),
constants: current.constants(false),
included_modules: current.included_modules - current.superclass&.included_modules.to_a
}
current = current.superclass
end
chain
end
def self.method_lookup_path(klass)
klass.ancestors.map do |ancestor|
{
name: ancestor.name || ancestor.to_s,
type: ancestor.class == Class ? :class : :module,
methods_count: ancestor.instance_methods(false).size
}
end
end
def self.find_method_definition(klass, method_name)
klass.ancestors.each do |ancestor|
if ancestor.instance_methods(false).include?(method_name)
method = ancestor.instance_method(method_name)
return {
defined_in: ancestor,
method: method,
source_location: method.source_location,
overridden_by: []
}
end
end
nil
end
def self.method_override_analysis(klass, method_name)
definitions = []
klass.ancestors.each do |ancestor|
if ancestor.instance_methods(false).include?(method_name)
method = ancestor.instance_method(method_name)
definitions << {
class: ancestor,
method: method,
source_location: method.source_location
}
end
end
{
method_name: method_name,
total_definitions: definitions.size,
definitions: definitions,
active_definition: definitions.first
}
end
def self.class_hierarchy_diagram(klass)
diagram = []
indent_level = 0
inheritance_chain(klass).reverse.each do |level|
indent = " " * indent_level
class_info = level[:class]
methods_count = level[:methods].size
modules = level[:included_modules].map(&:name).compact
line = "#{indent}#{class_info.name || class_info.to_s}"
line += " (#{methods_count} methods)"
line += " includes: #{modules.join(', ')}" unless modules.empty?
diagram << line
indent_level += 1
end
diagram.join("\n")
end
end
# Example classes for analysis
module Greetings
def say_hello
"Hello from #{self.class}"
end
end
module Farewells
def say_goodbye
"Goodbye from #{self.class}"
end
end
class Animal
include Greetings
def initialize(name)
@name = name
end
def speak
"Animal sound"
end
def move
"Moving around"
end
end
class Mammal < Animal
include Farewells
def speak
"Mammal sound"
end
def nurse_young
"Nursing babies"
end
end
class Dog < Mammal
def speak
"Woof!"
end
def fetch
"Fetching the ball"
end
end
# Analyze inheritance
puts "=== Inheritance Chain for Dog ==="
InheritanceAnalyzer.inheritance_chain(Dog).each_with_index do |level, index|
puts "Level #{index}: #{level[:class].name} (#{level[:methods].size} methods)"
puts " Methods: #{level[:methods].join(', ')}" unless level[:methods].empty?
puts " Modules: #{level[:included_modules].map(&:name).join(', ')}" unless level[:included_modules].empty?
end
puts "\n=== Method Lookup Path ==="
InheritanceAnalyzer.method_lookup_path(Dog).each_with_index do |ancestor, index|
puts "#{index + 1}. #{ancestor[:name]} (#{ancestor[:type]}) - #{ancestor[:methods_count]} methods"
end
puts "\n=== Method Override Analysis for 'speak' ==="
override_info = InheritanceAnalyzer.method_override_analysis(Dog, :speak)
puts "Method '#{override_info[:method_name]}' defined #{override_info[:total_definitions]} times"
override_info[:definitions].each_with_index do |definition, index|
status = index == 0 ? " (ACTIVE)" : " (overridden)"
puts " #{definition[:class].name}#{status}"
end
puts "\n=== Class Hierarchy Diagram ==="
puts InheritanceAnalyzer.class_hierarchy_diagram(Dog)
Performance & Memory Profiling
Memory Leak Detection System
require 'objspace'
class MemoryProfiler
def initialize
@snapshots = []
end
def snapshot(label = "snapshot_#{Time.now.to_i}")
GC.start # Ensure clean snapshot
snapshot_data = {
label: label,
timestamp: Time.now,
object_counts: object_counts_by_class,
memory_usage: memory_usage_by_class,
total_objects: ObjectSpace.count_objects,
gc_stats: GC.stat
}
@snapshots << snapshot_data
snapshot_data
end
def compare_snapshots(label1, label2)
snap1 = @snapshots.find { |s| s[:label] == label1 }
snap2 = @snapshots.find { |s| s[:label] == label2 }
unless snap1 && snap2
raise ArgumentError, "Snapshots not found"
end
comparison = {
time_diff: snap2[:timestamp] - snap1[:timestamp],
object_diff: {},
memory_diff: {},
total_objects_diff: snap2[:total_objects][:T_OBJECT] - snap1[:total_objects][:T_OBJECT]
}
# Compare object counts
all_classes = (snap1[:object_counts].keys + snap2[:object_counts].keys).uniq
all_classes.each do |klass|
count1 = snap1[:object_counts][klass] || 0
count2 = snap2[:object_counts][klass] || 0
diff = count2 - count1
comparison[:object_diff][klass] = diff if diff != 0
end
# Compare memory usage
all_classes.each do |klass|
mem1 = snap1[:memory_usage][klass] || 0
mem2 = snap2[:memory_usage][klass] || 0
diff = mem2 - mem1
comparison[:memory_diff][klass] = diff if diff != 0
end
comparison
end
def detect_leaks(threshold_objects: 100, threshold_memory: 1024 * 1024)
return nil if @snapshots.size < 2
latest = @snapshots.last
previous = @snapshots[-2]
comparison = compare_snapshots(previous[:label], latest[:label])
potential_leaks = []
comparison[:object_diff].each do |klass, object_diff|
memory_diff = comparison[:memory_diff][klass] || 0
if object_diff > threshold_objects || memory_diff > threshold_memory
potential_leaks << {
class: klass,
object_increase: object_diff,
memory_increase: memory_diff,
severity: calculate_severity(object_diff, memory_diff)
}
end
end
potential_leaks.sort_by { |leak| -leak[:severity] }
end
def generate_report
return "No snapshots available" if @snapshots.empty?
latest = @snapshots.last
report = []
report << "=== Memory Profile Report ==="
report << "Timestamp: #{latest[:timestamp]}"
report << "Total Objects: #{latest[:total_objects][:T_OBJECT]}"
report << "GC Count: #{latest[:gc_stats][:count]}"
report << ""
report << "Top 10 Classes by Object Count:"
latest[:object_counts].sort_by { |_, count| -count }.first(10).each do |klass, count|
memory = latest[:memory_usage][klass] || 0
report << " #{klass}: #{count} objects (#{humanize_bytes(memory)})"
end
if @snapshots.size > 1
report << ""
report << "Potential Memory Leaks:"
leaks = detect_leaks
if leaks.any?
leaks.first(5).each do |leak|
report << " #{leak[:class]}: +#{leak[:object_increase]} objects (+#{humanize_bytes(leak[:memory_increase])})"
end
else
report << " No significant leaks detected"
end
end
report.join("\n")
end
private
def object_counts_by_class
counts = Hash.new(0)
ObjectSpace.each_object { |obj| counts[obj.class] += 1 }
counts
end
def memory_usage_by_class
usage = Hash.new(0)
ObjectSpace.each_object { |obj| usage[obj.class] += ObjectSpace.memsize_of(obj) }
usage
end
def calculate_severity(object_diff, memory_diff)
# Simple severity calculation - customize as needed
(object_diff * 0.1) + (memory_diff / 1024.0)
end
def humanize_bytes(bytes)
units = ['B', 'KB', 'MB', 'GB']
unit_index = 0
size = bytes.to_f
while size >= 1024 && unit_index < units.length - 1
size /= 1024
unit_index += 1
end
"#{size.round(2)} #{units[unit_index]}"
end
end
# Example usage
profiler = MemoryProfiler.new
# Initial snapshot
profiler.snapshot("baseline")
# Simulate some memory allocation
1000.times { |i| "String #{i}" }
big_array = Array.new(500) { { key: "value" } }
profiler.snapshot("after_allocation")
# Simulate potential leak
leaked_objects = []
100.times { leaked_objects << Object.new }
profiler.snapshot("after_potential_leak")
# Generate and display report
puts profiler.generate_report
# Compare specific snapshots
comparison = profiler.compare_snapshots("baseline", "after_potential_leak")
puts "\n=== Snapshot Comparison ==="
puts "Time difference: #{comparison[:time_diff].round(2)} seconds"
puts "Total object difference: #{comparison[:total_objects_diff]}"
puts "\nTop object increases:"
comparison[:object_diff].sort_by { |_, diff| -diff }.first(5).each do |klass, diff|
puts " #{klass}: +#{diff}"
end
Best Practices & Guidelines
✅ Introspection Best Practices
- Use defined? for safe feature detection before calling methods or accessing constants
- Cache introspection results when possible to avoid repeated expensive operations
- Prefer respond_to? over method rescue for method existence checking
- Use ObjectSpace sparingly in production due to performance impact
⚠️ Performance Considerations
- ObjectSpace.each_object iterates through ALL objects - use filters early
- Allocation tracing has significant overhead - only enable for profiling
- const_get/const_set can be slow with complex namespace paths
- Finalizers delay garbage collection - use WeakRef when possible
🔒 Security Guidelines
- Validate constant paths before using const_get to prevent code injection
- Never use introspection with untrusted user input without validation
- Be careful with ObjectSpace as it can expose sensitive object data
- Limit introspection scope in security-sensitive applications