Ruby Logo

Introspection & ObjectSpace

Master ObjectSpace, defined?, const_get/set for runtime introspection and debugging.

Home Ruby Introspection & ObjectSpace

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

Quick Navigation

Related Topics

Video Tutorial

Watch and learn introspection & objectspace

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!