CrackedRuby logo

CrackedRuby

YJIT Memory Management

Overview

YJIT (Yet another Ruby JIT) compiles Ruby bytecode to native machine code at runtime, requiring careful memory management for compiled code storage, cache maintenance, and garbage collection coordination. YJIT allocates memory for three primary areas: the code cache storing compiled machine instructions, metadata structures tracking compilation state, and runtime data supporting execution.

The code cache represents YJIT's largest memory allocation, defaulting to 128MB on 64-bit systems and 64MB on 32-bit systems. This cache stores compiled machine code alongside associated metadata including basic block boundaries, type assumptions, and jump targets. When the cache fills, YJIT employs eviction strategies to reclaim space for new compilations.

YJIT integrates with Ruby's garbage collector through write barriers and object tracking mechanisms. Compiled code references Ruby objects, requiring coordination during GC cycles to maintain memory safety and update object references after compaction.

# Enable YJIT with default memory settings
RubyVM::YJIT.enable

# Check current memory usage
stats = RubyVM::YJIT.runtime_stats
puts "Code cache size: #{stats[:code_gc_count]} MB"
puts "Compiled blocks: #{stats[:compiled_iseq_count]}"

Memory management becomes critical in long-running applications where YJIT must balance compilation overhead against performance gains while preventing memory exhaustion.

# Configure YJIT memory limits at startup
export RUBY_YJIT_CODE_GC_SIZE=256
ruby --yjit script.rb

Basic Usage

YJIT memory management operates automatically once enabled, but several configuration options control memory allocation behavior. The --yjit-code-gc-size option sets the code cache size in MB, while runtime statistics provide visibility into memory usage patterns.

# Enable YJIT with memory monitoring
RubyVM::YJIT.enable

def memory_intensive_method
  1000.times.map { |i| "string_#{i}" * 100 }
end

# Execute code to trigger compilation
5.times { memory_intensive_method }

# Check memory statistics
stats = RubyVM::YJIT.runtime_stats
puts "Allocated bytes: #{stats[:total_allocated]}"
puts "Freed bytes: #{stats[:total_freed]}"
puts "Peak RSS: #{stats[:peak_mem_usage]}"

The code cache fills as YJIT compiles methods. When approaching capacity, YJIT triggers garbage collection of compiled code, removing least recently used entries and associated metadata.

# Monitor code cache pressure
class YJITMonitor
  def self.check_cache_usage
    stats = RubyVM::YJIT.runtime_stats
    cache_size = stats[:code_gc_count] || 0
    total_allocated = stats[:total_allocated] || 0
    
    if cache_size > 0
      usage_ratio = total_allocated.to_f / (cache_size * 1024 * 1024)
      puts "Cache usage: #{(usage_ratio * 100).round(2)}%"
      
      if usage_ratio > 0.8
        puts "Warning: Code cache approaching capacity"
      end
    end
  end
end

YJITMonitor.check_cache_usage

Memory allocation tracking helps identify methods causing excessive compilation overhead. Methods with complex control flow or frequent deoptimization consume more cache space.

# Track per-method compilation costs
def track_compilation_memory
  before_stats = RubyVM::YJIT.runtime_stats
  
  # Method with multiple branches increases memory usage
  def complex_method(input)
    case input
    when String then input.upcase
    when Integer then input * 2
    when Array then input.compact
    else input.to_s
    end
  end
  
  # Trigger compilation with different types
  complex_method("test")
  complex_method(42)
  complex_method([1, nil, 3])
  complex_method({key: "value"})
  
  after_stats = RubyVM::YJIT.runtime_stats
  memory_increase = (after_stats[:total_allocated] || 0) - (before_stats[:total_allocated] || 0)
  puts "Memory allocated for compilation: #{memory_increase} bytes"
end

track_compilation_memory

Advanced Usage

Advanced YJIT memory management involves tuning cache sizes, controlling compilation thresholds, and implementing custom memory monitoring strategies. The compilation threshold determines when YJIT compiles methods, affecting memory allocation patterns.

# Advanced memory configuration with environment variables
ENV['RUBY_YJIT_CODE_GC_SIZE'] = '512'  # 512MB code cache
ENV['RUBY_YJIT_COMPILE_THRESHOLD'] = '30'  # Compile after 30 calls

# Custom memory pressure detection
class AdvancedYJITMonitor
  def initialize(warning_threshold: 0.75, critical_threshold: 0.9)
    @warning_threshold = warning_threshold
    @critical_threshold = critical_threshold
    @last_check = Time.now
    @stats_history = []
  end
  
  def monitor_memory_trends
    current_stats = RubyVM::YJIT.runtime_stats
    timestamp = Time.now
    
    @stats_history << {
      timestamp: timestamp,
      allocated: current_stats[:total_allocated] || 0,
      freed: current_stats[:total_freed] || 0,
      code_gc_count: current_stats[:code_gc_count] || 0
    }
    
    # Keep only last 100 measurements
    @stats_history = @stats_history.last(100)
    
    analyze_memory_pressure if @stats_history.length > 10
  end
  
  private
  
  def analyze_memory_pressure
    recent_stats = @stats_history.last(10)
    allocation_trend = calculate_allocation_trend(recent_stats)
    
    if allocation_trend > @critical_threshold
      handle_critical_pressure
    elsif allocation_trend > @warning_threshold
      handle_warning_pressure
    end
  end
  
  def calculate_allocation_trend(stats)
    return 0 if stats.length < 2
    
    first_allocated = stats.first[:allocated]
    last_allocated = stats.last[:allocated]
    time_span = stats.last[:timestamp] - stats.first[:timestamp]
    
    return 0 if time_span <= 0
    
    (last_allocated - first_allocated) / time_span.to_f
  end
  
  def handle_critical_pressure
    puts "CRITICAL: YJIT memory pressure detected"
    # Force garbage collection of compiled code
    GC.start
  end
  
  def handle_warning_pressure
    puts "WARNING: YJIT memory allocation increasing"
  end
end

Fine-grained control over compilation targets reduces memory usage by preventing compilation of infrequently executed methods. The --yjit-verify-ctx option enables context verification, trading memory for runtime safety.

# Selective compilation strategy
module SelectiveCompilation
  def self.setup_conditional_compilation
    # Monitor method call frequencies
    @call_counts = Hash.new(0)
    @compile_threshold = 50
    
    # Patch method definition to track calls
    Module.prepend(CallTracker)
  end
  
  module CallTracker
    def define_method(name, &block)
      super(name) do |*args, **kwargs|
        SelectiveCompilation.record_call(self.class, name)
        instance_exec(*args, **kwargs, &block)
      end
    end
  end
  
  def self.record_call(klass, method_name)
    key = "#{klass}##{method_name}"
    @call_counts[key] += 1
    
    if @call_counts[key] == @compile_threshold
      # Method reached compilation threshold
      puts "Compiling #{key} after #{@compile_threshold} calls"
    end
  end
  
  def self.compilation_candidates
    @call_counts.select { |_, count| count >= @compile_threshold }
  end
end

Memory-mapped code regions provide alternative allocation strategies for specific deployment environments. Large applications benefit from partitioning compiled code across multiple memory regions.

# Memory region management for large applications
class CodeRegionManager
  def initialize(region_size_mb: 64, max_regions: 8)
    @region_size = region_size_mb * 1024 * 1024
    @max_regions = max_regions
    @current_region = 0
    @regions = []
    
    allocate_initial_region
  end
  
  def allocate_initial_region
    puts "Allocating initial code region: #{@region_size / 1024 / 1024}MB"
    @regions << create_region(@region_size)
  end
  
  def monitor_region_usage
    stats = RubyVM::YJIT.runtime_stats
    total_allocated = stats[:total_allocated] || 0
    
    if total_allocated > (@current_region + 1) * @region_size * 0.8
      if @current_region < @max_regions - 1
        allocate_new_region
      else
        trigger_region_cleanup
      end
    end
  end
  
  private
  
  def allocate_new_region
    @current_region += 1
    puts "Allocating new code region #{@current_region}"
    @regions << create_region(@region_size)
  end
  
  def create_region(size)
    { size: size, allocated_at: Time.now }
  end
  
  def trigger_region_cleanup
    puts "Maximum regions reached, triggering cleanup"
    # Implementation would involve YJIT API extensions
  end
end

Performance & Memory

YJIT memory management directly impacts application performance through compilation overhead, cache hit rates, and garbage collection interactions. Measuring these interactions requires comprehensive monitoring of both YJIT-specific metrics and general memory usage patterns.

Code cache sizing significantly affects performance characteristics. Undersized caches cause frequent evictions and recompilations, while oversized caches waste memory without performance benefits. The optimal size depends on application working set size and method complexity.

# Benchmark different cache sizes
class CacheSizeBenchmark
  def initialize
    @test_methods = generate_test_methods
    @cache_sizes = [64, 128, 256, 512]  # MB
  end
  
  def run_benchmark
    @cache_sizes.each do |size|
      puts "\n=== Testing #{size}MB cache ==="
      
      # Restart with new cache size would require process restart
      # This shows measurement approach
      
      before_memory = measure_memory_usage
      before_time = Time.now
      
      # Execute workload
      execute_workload
      
      after_time = Time.now
      after_memory = measure_memory_usage
      
      stats = RubyVM::YJIT.runtime_stats
      
      puts "Execution time: #{after_time - before_time}s"
      puts "Memory increase: #{after_memory - before_memory}MB"
      puts "Compilation ratio: #{stats[:ratio_in_yjit] || 0}%"
      puts "Code GC events: #{stats[:code_gc_count] || 0}"
    end
  end
  
  private
  
  def generate_test_methods
    methods = []
    20.times do |i|
      method_name = :"test_method_#{i}"
      
      define_singleton_method(method_name) do |input|
        # Create different execution paths
        result = case input % 4
        when 0 then input * 2 + rand(100)
        when 1 then input.to_s.reverse.to_i
        when 2 then [input, input * 2, input * 3].sum
        else Math.sqrt(input).round
        end
        
        # Add some string operations
        result.to_s.upcase.length + input
      end
      
      methods << method_name
    end
    methods
  end
  
  def execute_workload
    1000.times do |iteration|
      @test_methods.each do |method_name|
        10.times { send(method_name, iteration) }
      end
    end
  end
  
  def measure_memory_usage
    GC.start
    `ps -o rss= -p #{Process.pid}`.to_i / 1024  # MB
  end
end

benchmark = CacheSizeBenchmark.new
benchmark.run_benchmark

Memory allocation patterns reveal optimization opportunities. Methods causing frequent deoptimizations consume disproportionate cache space and benefit from refactoring to reduce type variations.

# Memory allocation profiling
class AllocationProfiler
  def initialize
    @method_profiles = Hash.new { |h, k| h[k] = { calls: 0, allocations: 0 } }
  end
  
  def profile_method(object, method_name)
    original_method = object.method(method_name)
    
    object.define_singleton_method(method_name) do |*args, **kwargs|
      before_stats = RubyVM::YJIT.runtime_stats
      before_allocated = before_stats[:total_allocated] || 0
      
      result = original_method.call(*args, **kwargs)
      
      after_stats = RubyVM::YJIT.runtime_stats
      after_allocated = after_stats[:total_allocated] || 0
      
      profile = @method_profiles[method_name]
      profile[:calls] += 1
      profile[:allocations] += (after_allocated - before_allocated)
      
      result
    end
  end
  
  def analyze_hot_methods
    sorted_methods = @method_profiles.sort_by { |_, profile| 
      -profile[:allocations] 
    }
    
    puts "Top memory-allocating methods:"
    sorted_methods.first(10).each do |method_name, profile|
      avg_allocation = profile[:allocations] / profile[:calls].to_f
      puts "#{method_name}: #{profile[:allocations]} bytes total, " \
           "#{avg_allocation.round(2)} bytes/call"
    end
  end
  
  def detect_allocation_anomalies
    @method_profiles.each do |method_name, profile|
      avg_allocation = profile[:allocations] / profile[:calls].to_f
      
      if avg_allocation > 10_000  # 10KB per call threshold
        puts "WARNING: #{method_name} allocates #{avg_allocation.round(2)} bytes per call"
        puts "Consider optimizing for better cache efficiency"
      end
    end
  end
end

Garbage collection interaction patterns affect overall memory performance. YJIT's write barriers and object reference tracking introduce overhead during GC cycles, particularly in applications with high allocation rates.

# GC interaction analysis
class GCInteractionAnalyzer
  def initialize
    @gc_stats = []
    @yjit_stats = []
  end
  
  def start_monitoring
    @monitoring = true
    
    Thread.new do
      while @monitoring
        record_stats
        sleep(0.1)  # Sample every 100ms
      end
    end
  end
  
  def stop_monitoring
    @monitoring = false
    analyze_interaction_patterns
  end
  
  private
  
  def record_stats
    gc_stat = GC.stat
    yjit_stat = RubyVM::YJIT.runtime_stats
    
    @gc_stats << {
      timestamp: Time.now,
      major_gc_count: gc_stat[:major_gc_count],
      minor_gc_count: gc_stat[:minor_gc_count],
      heap_allocated_pages: gc_stat[:heap_allocated_pages],
      heap_live_slots: gc_stat[:heap_live_slots]
    }
    
    @yjit_stats << {
      timestamp: Time.now,
      total_allocated: yjit_stat[:total_allocated] || 0,
      code_gc_count: yjit_stat[:code_gc_count] || 0,
      invalidation_count: yjit_stat[:invalidation_count] || 0
    }
  end
  
  def analyze_interaction_patterns
    gc_events = detect_gc_events
    
    gc_events.each do |event|
      yjit_impact = measure_yjit_impact_during_gc(event)
      
      if yjit_impact[:significant]
        puts "GC event at #{event[:timestamp]} significantly impacted YJIT:"
        puts "  Invalidations: #{yjit_impact[:invalidations]}"
        puts "  Code GC triggered: #{yjit_impact[:code_gc]}"
      end
    end
  end
  
  def detect_gc_events
    events = []
    previous_major = 0
    
    @gc_stats.each do |stat|
      if stat[:major_gc_count] > previous_major
        events << {
          timestamp: stat[:timestamp],
          type: :major,
          heap_pages: stat[:heap_allocated_pages]
        }
        previous_major = stat[:major_gc_count]
      end
    end
    
    events
  end
  
  def measure_yjit_impact_during_gc(gc_event)
    # Find YJIT stats around GC event time
    event_time = gc_event[:timestamp]
    
    before_stats = @yjit_stats.reverse.find { |s| s[:timestamp] < event_time }
    after_stats = @yjit_stats.find { |s| s[:timestamp] > event_time }
    
    return { significant: false } unless before_stats && after_stats
    
    invalidation_increase = after_stats[:invalidation_count] - before_stats[:invalidation_count]
    code_gc_increase = after_stats[:code_gc_count] - before_stats[:code_gc_count]
    
    {
      significant: invalidation_increase > 0 || code_gc_increase > 0,
      invalidations: invalidation_increase,
      code_gc: code_gc_increase > 0
    }
  end
end

Production Patterns

Production YJIT deployments require systematic memory management strategies addressing varying workload patterns, resource constraints, and operational monitoring requirements. Applications serving different request types benefit from workload-aware compilation strategies.

Web applications experience request pattern variations that affect YJIT memory usage. High-traffic endpoints compiled early in the application lifecycle may dominate cache usage, while occasional endpoints remain uncompiled. Load balancing across multiple processes distributes compilation overhead.

# Production memory management strategy
class ProductionYJITManager
  def initialize(config = {})
    @config = {
      max_cache_size_mb: config[:max_cache_size_mb] || 256,
      warning_threshold: config[:warning_threshold] || 0.8,
      monitoring_interval: config[:monitoring_interval] || 30,
      log_file: config[:log_file] || '/var/log/yjit_memory.log'
    }.freeze
    
    @logger = setup_logger
    @monitoring_active = false
  end
  
  def start_production_monitoring
    return if @monitoring_active
    
    @monitoring_active = true
    @logger.info("Starting YJIT memory monitoring")
    
    # Background monitoring thread
    @monitor_thread = Thread.new { monitoring_loop }
    
    # Signal handlers for graceful shutdown
    setup_signal_handlers
    
    # Periodic memory reports
    setup_periodic_reporting
  end
  
  def stop_monitoring
    @monitoring_active = false
    @monitor_thread&.join(5)  # Wait up to 5 seconds
    @logger.info("YJIT memory monitoring stopped")
  end
  
  private
  
  def monitoring_loop
    while @monitoring_active
      begin
        check_memory_health
        sleep(@config[:monitoring_interval])
      rescue => e
        @logger.error("Monitoring error: #{e.message}")
        sleep(60)  # Back off on errors
      end
    end
  end
  
  def check_memory_health
    stats = RubyVM::YJIT.runtime_stats
    return unless stats
    
    memory_metrics = calculate_memory_metrics(stats)
    
    if memory_metrics[:cache_utilization] > @config[:warning_threshold]
      handle_high_memory_usage(memory_metrics)
    end
    
    log_memory_status(memory_metrics)
  end
  
  def calculate_memory_metrics(stats)
    total_allocated = stats[:total_allocated] || 0
    max_cache_bytes = @config[:max_cache_size_mb] * 1024 * 1024
    
    {
      total_allocated_mb: total_allocated / 1024.0 / 1024.0,
      cache_utilization: total_allocated.to_f / max_cache_bytes,
      compiled_methods: stats[:compiled_iseq_count] || 0,
      invalidation_count: stats[:invalidation_count] || 0,
      code_gc_count: stats[:code_gc_count] || 0
    }
  end
  
  def handle_high_memory_usage(metrics)
    @logger.warn("High YJIT memory usage detected:")
    @logger.warn("  Cache utilization: #{(metrics[:cache_utilization] * 100).round(1)}%")
    @logger.warn("  Total allocated: #{metrics[:total_allocated_mb].round(1)}MB")
    
    # Consider triggering code GC or alerting operations team
    if metrics[:cache_utilization] > 0.95
      @logger.error("Critical YJIT memory usage - consider process restart")
      # In production, this might trigger an alert to operations
      send_critical_alert(metrics)
    end
  end
  
  def send_critical_alert(metrics)
    # Production implementation would integrate with monitoring systems
    # like Datadog, New Relic, or custom alerting
    puts "CRITICAL ALERT: YJIT memory usage at #{(metrics[:cache_utilization] * 100).round(1)}%"
  end
  
  def setup_logger
    require 'logger'
    Logger.new(@config[:log_file], 'daily').tap do |logger|
      logger.level = Logger::INFO
    end
  end
  
  def setup_signal_handlers
    trap('USR1') { log_detailed_stats }
    trap('USR2') { force_memory_cleanup }
  end
  
  def log_detailed_stats
    stats = RubyVM::YJIT.runtime_stats
    @logger.info("Detailed YJIT stats: #{stats}")
  end
  
  def force_memory_cleanup
    @logger.info("Forcing memory cleanup")
    GC.start
  end
  
  def setup_periodic_reporting
    # Daily memory usage reports
    Thread.new do
      while @monitoring_active
        sleep(24 * 60 * 60)  # 24 hours
        generate_daily_report if @monitoring_active
      end
    end
  end
  
  def generate_daily_report
    stats = RubyVM::YJIT.runtime_stats
    return unless stats
    
    report = <<~REPORT
      Daily YJIT Memory Report
      ========================
      Date: #{Time.now.strftime('%Y-%m-%d')}
      Total Allocated: #{((stats[:total_allocated] || 0) / 1024.0 / 1024.0).round(2)}MB
      Compiled Methods: #{stats[:compiled_iseq_count] || 0}
      Code GC Events: #{stats[:code_gc_count] || 0}
      Invalidations: #{stats[:invalidation_count] || 0}
      Compilation Ratio: #{stats[:ratio_in_yjit] || 0}%
    REPORT
    
    @logger.info(report)
  end
end

# Initialize in production
production_manager = ProductionYJITManager.new(
  max_cache_size_mb: ENV['YJIT_MAX_CACHE_MB']&.to_i || 256,
  warning_threshold: 0.85,
  monitoring_interval: 60
)

production_manager.start_production_monitoring

Container environments require memory limit awareness to prevent YJIT cache sizes from exceeding available memory. Dynamic cache sizing based on container memory limits prevents out-of-memory conditions.

# Container-aware memory management
class ContainerYJITManager
  def initialize
    @container_memory_limit = detect_container_memory_limit
    @recommended_cache_size = calculate_optimal_cache_size
    
    configure_yjit_for_container
  end
  
  def self.setup_for_container
    new.tap(&:apply_container_optimizations)
  end
  
  private
  
  def detect_container_memory_limit
    # Check cgroup memory limit
    cgroup_limit = read_cgroup_memory_limit
    return cgroup_limit if cgroup_limit && cgroup_limit > 0
    
    # Fallback to system memory
    system_memory = `grep MemTotal /proc/meminfo`.split[1].to_i * 1024
    puts "Using system memory: #{system_memory / 1024 / 1024}MB"
    system_memory
  end
  
  def read_cgroup_memory_limit
    limit_file = '/sys/fs/cgroup/memory/memory.limit_in_bytes'
    return nil unless File.exist?(limit_file)
    
    limit = File.read(limit_file).strip.to_i
    # cgroups v1 uses a very large number for unlimited
    limit > (1 << 50) ? nil : limit
  rescue
    nil
  end
  
  def calculate_optimal_cache_size
    return 64 unless @container_memory_limit
    
    memory_mb = @container_memory_limit / 1024 / 1024
    
    # Reserve memory for Ruby heap, gems, and system overhead
    case memory_mb
    when 0..512 then 32      # Small containers
    when 513..1024 then 64   # Medium containers  
    when 1025..2048 then 128 # Large containers
    when 2049..4096 then 256 # XL containers
    else 512                 # XXL containers
    end
  end
  
  def configure_yjit_for_container
    # Set environment variable for child processes
    ENV['RUBY_YJIT_CODE_GC_SIZE'] = @recommended_cache_size.to_s
    
    puts "Container memory: #{@container_memory_limit / 1024 / 1024}MB"
    puts "YJIT cache size: #{@recommended_cache_size}MB"
  end
  
  def apply_container_optimizations
    # Monitor memory pressure in containers
    setup_memory_pressure_monitoring
    
    # Configure compilation thresholds for container workloads
    configure_compilation_strategy
  end
  
  def setup_memory_pressure_monitoring
    Thread.new do
      loop do
        check_container_memory_pressure
        sleep(30)
      end
    end
  end
  
  def check_container_memory_pressure
    return unless @container_memory_limit
    
    current_usage = get_current_memory_usage
    pressure_ratio = current_usage.to_f / @container_memory_limit
    
    if pressure_ratio > 0.9
      puts "High memory pressure: #{(pressure_ratio * 100).round(1)}%"
      consider_cache_reduction
    end
  end
  
  def get_current_memory_usage
    `ps -o rss= -p #{Process.pid}`.to_i * 1024  # Convert KB to bytes
  rescue
    0
  end
  
  def consider_cache_reduction
    # In extreme cases, could trigger YJIT code GC
    # or temporarily reduce compilation aggressiveness
    puts "Consider reducing YJIT cache size or restarting process"
  end
  
  def configure_compilation_strategy
    # Lower compilation threshold in containers to reduce memory pressure
    ENV['RUBY_YJIT_COMPILE_THRESHOLD'] = '50'
  end
end

# Auto-configure for container environment
ContainerYJITManager.setup_for_container if ENV['CONTAINER_ENV']

Common Pitfalls

YJIT memory management contains several subtle behaviors that cause unexpected memory growth, performance degradation, or compilation failures. Understanding these pitfalls prevents common deployment issues and performance problems.

The most frequent pitfall involves cache size misconfiguration. Developers often set cache sizes too small for their application's working set, causing excessive code garbage collection and recompilation overhead. Conversely, oversized caches waste memory without providing performance benefits.

# Pitfall: Cache size too small for working set
class CacheSizePitfallDemo
  def demonstrate_undersized_cache
    # Simulating an app with many method variants
    puts "=== Demonstrating undersized cache pitfall ==="
    
    # Set artificially small cache for demonstration
    # In practice, this would be set via environment variables
    puts "Simulating 8MB cache for large application"
    
    # Track recompilation events
    compilation_events = []
    
    # Define many methods that will compete for cache space
    50.times do |i|
      method_name = :"method_#{i}"
      
      define_singleton_method(method_name) do |input|
        # Each method has complex logic requiring significant cache space
        case input % 5
        when 0 then fibonacci_recursive(input % 20)
        when 1 then string_manipulations(input)
        when 2 then array_operations(input)
        when 3 then hash_operations(input)
        else mathematical_operations(input)
        end
      end
    end
    
    # Exercise all methods to trigger compilations
    initial_stats = RubyVM::YJIT.runtime_stats
    
    3.times do |round|
      puts "Execution round #{round + 1}"
      
      50.times do |method_index|
        method_name = :"method_#{method_index}"
        20.times { |i| send(method_name, i) }
      end
      
      current_stats = RubyVM::YJIT.runtime_stats
      if current_stats[:code_gc_count] && initial_stats[:code_gc_count]
        new_gc_events = current_stats[:code_gc_count] - initial_stats[:code_gc_count]
        if new_gc_events > 0
          puts "  Code GC events this round: #{new_gc_events}"
          puts "  Performance likely degraded due to recompilation"
        end
      end
    end
  end
  
  private
  
  def fibonacci_recursive(n)
    return n if n <= 1
    fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
  end
  
  def string_manipulations(input)
    str = "base_string_#{input}"
    str.upcase.reverse.downcase.capitalize
  end
  
  def array_operations(input)
    array = (0..input).to_a
    array.map(&:to_s).select { |s| s.length.even? }.join(',')
  end
  
  def hash_operations(input)
    hash = { key: input, value: input * 2 }
    hash.transform_keys(&:to_s).transform_values { |v| v + 1 }
  end
  
  def mathematical_operations(input)
    Math.sqrt(input) + Math.sin(input) + Math.cos(input)
  end
end

# CacheSizePitfallDemo.new.demonstrate_undersized_cache

Memory leaks occur when YJIT retains references to objects that should be garbage collected. This happens particularly with dynamic method definitions and metaprogramming that creates compilation contexts referencing temporary objects.

# Pitfall: Memory leaks from dynamic method generation
class MemoryLeakDemo
  def initialize
    @dynamic_method_count = 0
    @leaked_objects = []
  end
  
  def demonstrate_metaprogramming_leak
    puts "=== Demonstrating metaprogramming memory leak ==="
    
    initial_memory = measure_memory
    initial_stats = RubyVM::YJIT.runtime_stats
    
    # This pattern creates memory leaks in YJIT
    100.times do |i|
      create_leaky_dynamic_method(i)
    end
    
    # Force compilation of dynamic methods
    exercise_dynamic_methods
    
    final_memory = measure_memory
    final_stats = RubyVM::YJIT.runtime_stats
    
    memory_increase = final_memory - initial_memory
    yjit_allocation = (final_stats[:total_allocated] || 0) - (initial_stats[:total_allocated] || 0)
    
    puts "Memory increase: #{memory_increase}KB"
    puts "YJIT allocation increase: #{yjit_allocation} bytes"
    puts "Dynamic methods created: #{@dynamic_method_count}"
    puts "Potential leaked objects: #{@leaked_objects.length}"
    
    if memory_increase > 1000  # More than 1MB increase
      puts "WARNING: Significant memory increase detected"
      puts "This pattern may cause memory leaks in production"
    end
  end
  
  def create_leaky_dynamic_method(index)
    # This creates a closure that captures the current context
    # YJIT may retain references to these objects
    captured_data = generate_large_data_structure(index)
    @leaked_objects << captured_data  # Simulating retained reference
    
    method_name = :"dynamic_method_#{index}"
    
    # The closure captures captured_data, potentially preventing GC
    define_singleton_method(method_name) do
      # Method uses captured data, creating reference in compiled code
      captured_data[:computed_value] + index
    end
    
    @dynamic_method_count += 1
  end
  
  def generate_large_data_structure(index)
    {
      id: index,
      data: Array.new(1000) { |i| "string_#{index}_#{i}" },
      metadata: {
        created_at: Time.now,
        description: "Large data structure #{index}",
        tags: Array.new(50) { |i| "tag_#{i}" }
      },
      computed_value: index * 42
    }
  end
  
  def exercise_dynamic_methods
    @dynamic_method_count.times do |i|
      method_name = :"dynamic_method_#{i}"
      10.times { send(method_name) }
    end
  end
  
  def measure_memory
    GC.start
    `ps -o rss= -p #{Process.pid}`.to_i  # KB
  rescue
    0
  end
  
  def demonstrate_proper_cleanup
    puts "\n=== Demonstrating proper cleanup ==="
    
    initial_memory = measure_memory
    
    # Proper pattern: avoid capturing large objects in closures
    100.times do |i|
      create_clean_dynamic_method(i)
    end
    
    exercise_clean_methods
    
    final_memory = measure_memory
    memory_increase = final_memory - initial_memory
    
    puts "Clean pattern memory increase: #{memory_increase}KB"
    puts "This should be significantly less than the leaky pattern"
  end
  
  def create_clean_dynamic_method(index)
    # Don't capture large objects - pass minimal data
    value = index * 42  # Simple value, not large structure
    
    method_name = :"clean_method_#{index}"
    
    define_singleton_method(method_name) do
      value + 1  # Uses simple value, not large structure
    end
  end
  
  def exercise_clean_methods
    100.times do |i|
      method_name = :"clean_method_#{i}"
      10.times { send(method_name) }
    end
  end
end

# MemoryLeakDemo.new.demonstrate_metaprogramming_leak
# MemoryLeakDemo.new.demonstrate_proper_cleanup

Type assumption invalidations cause unexpected memory usage spikes when YJIT repeatedly recompiles methods due to changing object types. Methods that handle multiple types unpredictably consume excessive cache space.

# Pitfall: Type assumption invalidations
class TypeInvalidationDemo
  def demonstrate_type_invalidation_pitfall
    puts "=== Demonstrating type invalidation pitfall ==="
    
    # Method that will be compiled with initial type assumptions
    def polymorphic_method(input)
      input.to_s.length + input.hash.abs
    end
    
    # First phase: establish type assumptions with consistent types
    puts "Phase 1: Establishing type assumptions with String inputs"
    before_stats = RubyVM::YJIT.runtime_stats
    
    100.times { |i| polymorphic_method("string_#{i}") }
    
    after_phase1_stats = RubyVM::YJIT.runtime_stats
    phase1_allocations = (after_phase1_stats[:total_allocated] || 0) - (before_stats[:total_allocated] || 0)
    
    puts "Memory allocated in phase 1: #{phase1_allocations} bytes"
    puts "Invalidations: #{after_phase1_stats[:invalidation_count] || 0}"
    
    # Second phase: violate type assumptions with different types
    puts "\nPhase 2: Violating assumptions with mixed types"
    
    mixed_inputs = [
      42,                    # Integer
      3.14,                 # Float  
      [1, 2, 3],           # Array
      {key: 'value'},      # Hash
      :symbol,             # Symbol
      true,                # Boolean
      nil                  # NilClass
    ]
    
    # This will cause repeated invalidations and recompilations
    50.times do |round|
      mixed_inputs.each { |input| polymorphic_method(input) }
    end
    
    after_phase2_stats = RubyVM::YJIT.runtime_stats
    phase2_allocations = (after_phase2_stats[:total_allocated] || 0) - (after_phase1_stats[:total_allocated] || 0)
    invalidation_increase = (after_phase2_stats[:invalidation_count] || 0) - (after_phase1_stats[:invalidation_count] || 0)
    
    puts "Memory allocated in phase 2: #{phase2_allocations} bytes"
    puts "Additional invalidations: #{invalidation_increase}"
    
    if phase2_allocations > phase1_allocations * 3
      puts "WARNING: Phase 2 used significantly more memory due to invalidations"
      puts "Consider refactoring to handle types more predictably"
    end
    
    demonstrate_type_stable_alternative
  end
  
  def demonstrate_type_stable_alternative
    puts "\n=== Demonstrating type-stable alternative ==="
    
    # Better pattern: separate methods for different types
    def handle_string(input)
      input.length + input.hash.abs
    end
    
    def handle_numeric(input)
      input.to_s.length + input.hash.abs
    end
    
    def handle_collection(input)
      input.to_s.length + input.hash.abs
    end
    
    def handle_other(input)
      input.to_s.length + input.hash.abs
    end
    
    # Type-dispatch method
    def type_stable_method(input)
      case input
      when String then handle_string(input)
      when Numeric then handle_numeric(input)
      when Array, Hash then handle_collection(input)
      else handle_other(input)
      end
    end
    
    before_stats = RubyVM::YJIT.runtime_stats
    
    # Same mixed inputs, but handled through type-stable dispatch
    mixed_inputs = [42, 3.14, [1, 2, 3], {key: 'value'}, :symbol, true, nil]
    
    50.times do |round|
      mixed_inputs.each { |input| type_stable_method(input) }
    end
    
    after_stats = RubyVM::YJIT.runtime_stats
    stable_allocations = (after_stats[:total_allocated] || 0) - (before_stats[:total_allocated] || 0)
    
    puts "Type-stable pattern memory usage: #{stable_allocations} bytes"
    puts "This should be more consistent and efficient"
  end
end

# TypeInvalidationDemo.new.demonstrate_type_invalidation_pitfall

Reference

YJIT Memory Configuration

Environment Variable Default Description
RUBY_YJIT_CODE_GC_SIZE 128 (64-bit), 64 (32-bit) Code cache size in MB
RUBY_YJIT_COMPILE_THRESHOLD 10 Method calls before compilation
RUBY_YJIT_COLD_THRESHOLD 200000 Calls before considering method cold
RUBY_YJIT_MAX_VERSIONS 4 Maximum compiled versions per method

Runtime Statistics Methods

Method Returns Description
RubyVM::YJIT.runtime_stats Hash Complete runtime statistics
RubyVM::YJIT.enabled? Boolean YJIT compilation status
RubyVM::YJIT.reset_stats! nil Reset runtime statistics

Key Statistics Fields

Field Type Description
:total_allocated Integer Total bytes allocated by YJIT
:total_freed Integer Total bytes freed by code GC
:code_gc_count Integer Number of code GC events
:compiled_iseq_count Integer Number of compiled instruction sequences
:invalidation_count Integer Type assumption invalidations
:ratio_in_yjit Float Percentage of execution in compiled code

Memory Monitoring Commands

# Basic memory check
stats = RubyVM::YJIT.runtime_stats
puts "Allocated: #{stats[:total_allocated]} bytes"

# Cache utilization check
cache_size_bytes = ENV['RUBY_YJIT_CODE_GC_SIZE'].to_i * 1024 * 1024
utilization = stats[:total_allocated].to_f / cache_size_bytes
puts "Cache utilization: #{(utilization * 100).round(1)}%"

# Compilation ratio
puts "Compiled execution: #{stats[:ratio_in_yjit]}%"

Common Error Patterns

Error Pattern Cause Solution
High invalidation count Type instability Use type-stable method dispatch
Excessive code GC events Undersized cache Increase RUBY_YJIT_CODE_GC_SIZE
Memory growth without GC Reference retention Avoid capturing large objects in closures
Low compilation ratio High compile threshold Decrease RUBY_YJIT_COMPILE_THRESHOLD

Production Monitoring Checklist

  • Set cache size based on container memory limits
  • Monitor invalidation rates for type stability issues
  • Track compilation ratios for performance optimization
  • Set up alerts for excessive code GC events
  • Log daily memory usage reports
  • Monitor GC interaction patterns
  • Configure compilation thresholds for workload patterns
  • Implement graceful degradation for memory pressure