CrackedRuby logo

CrackedRuby

YJIT Configuration

Overview

YJIT (Yet Another Ruby Just-In-Time compiler) compiles frequently executed Ruby code to native machine code during runtime. Ruby enables YJIT through command-line options, environment variables, and runtime configuration methods. The compiler monitors method execution patterns and selectively compiles hot code paths to improve performance.

Ruby activates YJIT with the --yjit command-line flag or by setting the RUBY_YJIT_ENABLE environment variable. Once enabled, YJIT tracks method calls and basic block execution frequency. When execution counts exceed configured thresholds, YJIT compiles the code to optimized machine instructions.

The configuration system controls compilation behavior, memory usage limits, and debugging output. YJIT maintains separate configuration for development and production environments through environment variables and runtime options.

# Enable YJIT at startup
# ruby --yjit script.rb

# Check YJIT status
puts RubyVM::YJIT.enabled?
# => true or false

# Runtime statistics
stats = RubyVM::YJIT.runtime_stats
puts stats[:compiled_iseq_count]
# => number of compiled instruction sequences

YJIT configuration affects three primary areas: compilation thresholds that determine when code gets compiled, memory limits that constrain JIT compilation overhead, and instrumentation settings that control performance monitoring and debugging output.

Basic Usage

Enabling YJIT

Ruby provides multiple methods to activate YJIT compilation. The most common approach uses the command-line flag when starting Ruby applications:

# Command line activation
# ruby --yjit application.rb

# Environment variable activation
# RUBY_YJIT_ENABLE=1 ruby application.rb

# Runtime detection
if RubyVM::YJIT.enabled?
  puts "YJIT compilation active"
  puts "Compiled methods: #{RubyVM::YJIT.runtime_stats[:compiled_iseq_count]}"
end

Runtime Configuration

YJIT accepts configuration through environment variables that modify compilation behavior. These variables must be set before Ruby process startup:

# Set compilation threshold (default: 30)
# RUBY_YJIT_CALL_THRESHOLD=50 ruby --yjit app.rb

# Limit compiled code size (default: 128MB)
# RUBY_YJIT_CODE_GC_THRESHOLD=64 ruby --yjit app.rb

# Enable execution tracing
# RUBY_YJIT_TRACE_EXITS=1 ruby --yjit app.rb

# Runtime configuration check
def yjit_config_info
  return "YJIT disabled" unless RubyVM::YJIT.enabled?
  
  stats = RubyVM::YJIT.runtime_stats
  {
    compiled_methods: stats[:compiled_iseq_count],
    code_size: stats[:code_region_size],
    invalidations: stats[:invalidation_count]
  }
end

puts yjit_config_info

Compilation Thresholds

YJIT tracks method execution frequency and compiles methods that exceed the call threshold. The default threshold balances compilation overhead against performance benefits:

class BenchmarkTarget
  def hot_method(data)
    data.map(&:upcase).select { |s| s.length > 5 }
  end
  
  def cold_method
    "This method runs infrequently"
  end
end

target = BenchmarkTarget.new
test_data = ["hello", "world", "ruby", "programming"]

# Trigger compilation through repeated calls
1000.times { target.hot_method(test_data) }

# Check compilation status
stats = RubyVM::YJIT.runtime_stats
puts "Compiled instruction sequences: #{stats[:compiled_iseq_count]}"
puts "Compiled method bodies: #{stats[:compiled_method_count]}"

Memory Management

YJIT allocates memory regions for compiled code and maintains metadata for optimization decisions. The code garbage collector reclaims memory when limits are exceeded:

# Monitor YJIT memory usage
def yjit_memory_stats
  return {} unless RubyVM::YJIT.enabled?
  
  stats = RubyVM::YJIT.runtime_stats
  {
    code_region_size: stats[:code_region_size],
    outlined_code_size: stats[:outlined_code_size],
    code_gc_count: stats[:code_gc_count],
    inline_code_size: stats[:inline_code_size]
  }
end

# Before heavy computation
initial_memory = yjit_memory_stats
puts "Initial code size: #{initial_memory[:code_region_size]} bytes"

# Execute code that triggers compilation
10_000.times do |i|
  Math.sqrt(i) + Math.log(i + 1)
end

# After compilation
final_memory = yjit_memory_stats
puts "Final code size: #{final_memory[:code_region_size]} bytes"
puts "Memory growth: #{final_memory[:code_region_size] - initial_memory[:code_region_size]} bytes"

Performance & Memory

Compilation Impact Measurement

YJIT performance benefits vary significantly based on code patterns and execution characteristics. Methods with tight loops, mathematical computations, and repeated object allocations typically see the largest improvements:

require 'benchmark'

class PerformanceTest
  def array_processing(size)
    (1..size).map { |n| n * n + Math.sqrt(n) }.select(&:even?)
  end
  
  def string_manipulation(text, iterations)
    result = text
    iterations.times do
      result = result.upcase.gsub(/[aeiou]/, '*').reverse
    end
    result
  end
  
  def numeric_computation(limit)
    sum = 0
    (1..limit).each do |i|
      sum += Math.sin(i) * Math.cos(i) + i**0.5
    end
    sum
  end
end

test = PerformanceTest.new

# Warm up YJIT compilation
100.times { test.array_processing(1000) }
100.times { test.string_manipulation("test string", 100) }
100.times { test.numeric_computation(1000) }

# Performance comparison
Benchmark.bm(20) do |x|
  x.report("Array processing:") do
    1000.times { test.array_processing(1000) }
  end
  
  x.report("String manipulation:") do
    1000.times { test.string_manipulation("ruby programming", 50) }
  end
  
  x.report("Numeric computation:") do
    1000.times { test.numeric_computation(500) }
  end
end

# Display compilation statistics
stats = RubyVM::YJIT.runtime_stats
puts "\nCompilation Statistics:"
puts "Compiled methods: #{stats[:compiled_method_count]}"
puts "Code region size: #{stats[:code_region_size]} bytes"
puts "Exit locations: #{stats[:traced_exits_count]}"

Memory Usage Optimization

YJIT maintains several memory regions for different types of compiled code. Understanding these regions helps optimize memory usage in memory-constrained environments:

def analyze_yjit_memory_usage
  return "YJIT not enabled" unless RubyVM::YJIT.enabled?
  
  stats = RubyVM::YJIT.runtime_stats
  
  total_memory = stats[:code_region_size] + stats[:outlined_code_size]
  
  analysis = {
    inline_code: {
      size: stats[:inline_code_size],
      percentage: (stats[:inline_code_size].to_f / total_memory * 100).round(2)
    },
    outlined_code: {
      size: stats[:outlined_code_size], 
      percentage: (stats[:outlined_code_size].to_f / total_memory * 100).round(2)
    },
    total_compiled: total_memory,
    gc_collections: stats[:code_gc_count],
    freed_pages: stats[:freed_code_pages]
  }
  
  analysis
end

# Before and after memory analysis
initial_analysis = analyze_yjit_memory_usage

# Execute memory-intensive compilation workload
class MemoryIntensiveClass
  1000.times do |i|
    define_method("method_#{i}") do |param|
      param.to_s.length * i + Math.sqrt(param)
    end
  end
end

obj = MemoryIntensiveClass.new
1000.times do |i|
  obj.send("method_#{i % 1000}", rand(100))
end

final_analysis = analyze_yjit_memory_usage
puts "Memory usage analysis:"
puts "Total memory: #{final_analysis[:total_compiled]} bytes"
puts "Inline code: #{final_analysis[:inline_code][:size]} bytes (#{final_analysis[:inline_code][:percentage]}%)"
puts "Outlined code: #{final_analysis[:outlined_code][:size]} bytes (#{final_analysis[:outlined_code][:percentage]}%)"
puts "GC collections: #{final_analysis[:gc_collections]}"

Compilation Overhead Analysis

YJIT compilation introduces overhead during the initial execution phase while methods are being compiled. This overhead typically pays off through improved execution speed:

class CompilationOverheadTest
  def measure_compilation_phases(method_name, iterations)
    times = []
    
    # Cold execution (compilation phase)
    cold_start = Time.now
    30.times { send(method_name) }
    cold_time = Time.now - cold_start
    
    # Warm execution (compiled code)
    warm_start = Time.now 
    iterations.times { send(method_name) }
    warm_time = Time.now - warm_start
    
    {
      cold_phase: cold_time,
      warm_phase: warm_time,
      per_call_cold: cold_time / 30,
      per_call_warm: warm_time / iterations,
      speedup: (cold_time / 30) / (warm_time / iterations)
    }
  end
  
  def cpu_intensive_method
    result = 0
    1000.times do |i|
      result += Math.sin(i) * Math.cos(i) + i**2
    end
    result
  end
  
  def memory_allocation_method
    arrays = []
    100.times do |i|
      arrays << (1..i).to_a.map(&:to_s)
    end
    arrays.flatten.size
  end
end

test = CompilationOverheadTest.new

cpu_results = test.measure_compilation_phases(:cpu_intensive_method, 1000)
memory_results = test.measure_compilation_phases(:memory_allocation_method, 1000)

puts "CPU-intensive method results:"
puts "Cold phase: #{(cpu_results[:per_call_cold] * 1000).round(3)}ms per call"
puts "Warm phase: #{(cpu_results[:per_call_warm] * 1000).round(3)}ms per call" 
puts "Speedup: #{cpu_results[:speedup].round(2)}x"

puts "\nMemory allocation method results:"
puts "Cold phase: #{(memory_results[:per_call_cold] * 1000).round(3)}ms per call"
puts "Warm phase: #{(memory_results[:per_call_warm] * 1000).round(3)}ms per call"
puts "Speedup: #{memory_results[:speedup].round(2)}x"

Production Patterns

Application Server Integration

YJIT configuration in production web applications requires coordination with application servers and deployment processes. Most application servers support YJIT through environment variable configuration:

# config/application.rb or deployment script
class ProductionYJITConfig
  def self.configure_for_environment
    return unless production_environment?
    
    ENV['RUBY_YJIT_ENABLE'] = '1'
    ENV['RUBY_YJIT_CALL_THRESHOLD'] = '50'  # Higher threshold for stability
    ENV['RUBY_YJIT_CODE_GC_THRESHOLD'] = '256'  # More memory for large apps
    
    # Disable tracing in production for performance
    ENV.delete('RUBY_YJIT_TRACE_EXITS')
  end
  
  def self.production_environment?
    ENV['RAILS_ENV'] == 'production' || ENV['RACK_ENV'] == 'production'
  end
  
  def self.yjit_health_check
    return { status: 'disabled', reason: 'YJIT not enabled' } unless RubyVM::YJIT.enabled?
    
    stats = RubyVM::YJIT.runtime_stats
    memory_mb = stats[:code_region_size] / (1024 * 1024)
    
    health_status = {
      status: 'healthy',
      compiled_methods: stats[:compiled_method_count],
      memory_usage_mb: memory_mb,
      invalidations: stats[:invalidation_count],
      code_gc_count: stats[:code_gc_count]
    }
    
    # Alert conditions
    if memory_mb > 200
      health_status[:warnings] = ['High memory usage']
    end
    
    if stats[:invalidation_count] > stats[:compiled_method_count] * 0.1
      health_status[:warnings] ||= []
      health_status[:warnings] << 'High invalidation rate'
    end
    
    health_status
  end
end

# Puma server configuration example
# config/puma.rb
on_worker_boot do
  ProductionYJITConfig.configure_for_environment
end

# Health check endpoint
get '/health/yjit' do
  content_type :json
  ProductionYJITConfig.yjit_health_check.to_json
end

Monitoring and Alerting

Production YJIT deployments require monitoring of compilation rates, memory usage, and performance regression detection:

class YJITMonitoring
  def initialize(statsd_client = nil)
    @statsd = statsd_client
    @baseline_stats = initial_stats_snapshot
  end
  
  def collect_metrics
    return unless RubyVM::YJIT.enabled?
    
    current_stats = RubyVM::YJIT.runtime_stats
    metrics = calculate_metrics(current_stats)
    
    report_to_monitoring(metrics) if @statsd
    
    metrics
  end
  
  private
  
  def initial_stats_snapshot
    return {} unless RubyVM::YJIT.enabled?
    
    RubyVM::YJIT.runtime_stats.dup
  end
  
  def calculate_metrics(current_stats)
    {
      compilation_rate: compilation_rate(current_stats),
      memory_efficiency: memory_efficiency(current_stats),
      invalidation_rate: invalidation_rate(current_stats),
      code_gc_pressure: code_gc_pressure(current_stats)
    }
  end
  
  def compilation_rate(stats)
    total_executed = stats[:exec_instruction] || 1
    compiled_ratio = stats[:compiled_iseq_count].to_f / total_executed
    (compiled_ratio * 100).round(2)
  end
  
  def memory_efficiency(stats)
    total_memory = stats[:code_region_size] + stats[:outlined_code_size]
    return 0 if total_memory == 0
    
    methods_per_mb = stats[:compiled_method_count].to_f / (total_memory / (1024 * 1024))
    methods_per_mb.round(2)
  end
  
  def invalidation_rate(stats)
    return 0 if stats[:compiled_method_count] == 0
    
    invalidation_ratio = stats[:invalidation_count].to_f / stats[:compiled_method_count]
    (invalidation_ratio * 100).round(2)
  end
  
  def code_gc_pressure(stats)
    baseline_gcs = @baseline_stats[:code_gc_count] || 0
    current_gcs = stats[:code_gc_count] || 0
    current_gcs - baseline_gcs
  end
  
  def report_to_monitoring(metrics)
    @statsd.gauge('yjit.compilation_rate', metrics[:compilation_rate])
    @statsd.gauge('yjit.memory_efficiency', metrics[:memory_efficiency])
    @statsd.gauge('yjit.invalidation_rate', metrics[:invalidation_rate])
    @statsd.gauge('yjit.code_gc_pressure', metrics[:code_gc_pressure])
  end
end

# Background monitoring job
class YJITMetricsJob
  def self.perform
    monitor = YJITMonitoring.new($statsd_client)
    metrics = monitor.collect_metrics
    
    # Log significant changes
    if metrics[:invalidation_rate] > 15
      Rails.logger.warn("High YJIT invalidation rate: #{metrics[:invalidation_rate]}%")
    end
    
    if metrics[:code_gc_pressure] > 10
      Rails.logger.warn("Frequent YJIT code GC: #{metrics[:code_gc_pressure]} collections")
    end
  end
end

Load Testing and Performance Validation

Production YJIT deployments require comprehensive performance validation to ensure compilation improves application throughput:

class YJITLoadTest
  def initialize(endpoints)
    @endpoints = endpoints
    @request_count = 0
    @response_times = []
  end
  
  def run_load_test(duration_seconds, concurrent_requests)
    test_start = Time.now
    threads = []
    
    concurrent_requests.times do
      threads << Thread.new do
        while Time.now - test_start < duration_seconds
          endpoint = @endpoints.sample
          response_time = measure_request(endpoint)
          
          Thread.current[:responses] ||= []
          Thread.current[:responses] << response_time
        end
      end
    end
    
    threads.each(&:join)
    
    # Collect results from all threads
    all_responses = threads.flat_map { |t| t[:responses] || [] }
    
    generate_performance_report(all_responses, duration_seconds)
  end
  
  private
  
  def measure_request(endpoint)
    start_time = Time.now
    
    # Simulate application request
    case endpoint[:type]
    when :computation
      perform_computation_work
    when :database
      simulate_database_work
    when :api
      simulate_api_processing
    end
    
    Time.now - start_time
  end
  
  def perform_computation_work
    result = 0
    1000.times do |i|
      result += Math.sqrt(i) * Math.sin(i) + i**2
    end
    result
  end
  
  def simulate_database_work
    # Simulate ORM-style object processing
    data = Array.new(100) { |i| { id: i, value: "item_#{i}", score: rand(100) } }
    data.select { |item| item[:score] > 50 }
        .sort_by { |item| item[:score] }
        .map { |item| item[:value].upcase }
  end
  
  def simulate_api_processing
    # Simulate JSON processing and validation
    json_data = { users: Array.new(50) { |i| { name: "User #{i}", email: "user#{i}@example.com" } } }
    json_data[:users].map { |user| user[:email].include?('@') }.all?
  end
  
  def generate_performance_report(response_times, duration)
    sorted_times = response_times.sort
    
    {
      duration_seconds: duration,
      total_requests: response_times.length,
      requests_per_second: response_times.length.to_f / duration,
      average_response_time: (response_times.sum / response_times.length * 1000).round(2),
      median_response_time: (sorted_times[sorted_times.length / 2] * 1000).round(2),
      p95_response_time: (sorted_times[(sorted_times.length * 0.95).to_i] * 1000).round(2),
      p99_response_time: (sorted_times[(sorted_times.length * 0.99).to_i] * 1000).round(2),
      yjit_stats: RubyVM::YJIT.enabled? ? RubyVM::YJIT.runtime_stats : 'disabled'
    }
  end
end

# Performance comparison test
endpoints = [
  { type: :computation, weight: 0.4 },
  { type: :database, weight: 0.4 },
  { type: :api, weight: 0.2 }
]

load_test = YJITLoadTest.new(endpoints)
results = load_test.run_load_test(60, 10)  # 60 seconds, 10 concurrent requests

puts "Load Test Results:"
puts "Requests per second: #{results[:requests_per_second].round(2)}"
puts "Average response time: #{results[:average_response_time]}ms"
puts "95th percentile: #{results[:p95_response_time]}ms"
puts "99th percentile: #{results[:p99_response_time]}ms"

if results[:yjit_stats] != 'disabled'
  puts "YJIT compiled methods: #{results[:yjit_stats][:compiled_method_count]}"
  puts "YJIT memory usage: #{(results[:yjit_stats][:code_region_size] / 1024.0 / 1024.0).round(2)}MB"
end

Error Handling & Debugging

Compilation Failure Diagnosis

YJIT may fail to compile certain code patterns or encounter internal errors during optimization. Understanding these failures helps identify problematic code sections:

class YJITDebugger
  def self.analyze_compilation_issues
    return "YJIT not enabled" unless RubyVM::YJIT.enabled?
    
    stats = RubyVM::YJIT.runtime_stats
    
    analysis = {
      total_methods_seen: stats[:binding_allocations] || 0,
      successfully_compiled: stats[:compiled_method_count],
      compilation_failures: detect_compilation_failures(stats),
      exit_reasons: analyze_exit_patterns(stats),
      invalidation_causes: analyze_invalidations(stats)
    }
    
    generate_debug_recommendations(analysis)
  end
  
  def self.detect_compilation_failures(stats)
    # Estimate failures from difference in seen vs compiled methods
    seen_methods = stats[:binding_allocations] || 0
    compiled_methods = stats[:compiled_method_count] || 0
    
    failure_estimate = [seen_methods - compiled_methods, 0].max
    
    {
      estimated_failures: failure_estimate,
      success_rate: seen_methods > 0 ? (compiled_methods.to_f / seen_methods * 100).round(2) : 0
    }
  end
  
  def self.analyze_exit_patterns(stats)
    # Common exit reasons indicate why YJIT falls back to interpreter
    {
      traced_exits: stats[:traced_exits_count] || 0,
      exit_locations: stats[:exit_locations] || 0,
      side_exits: stats[:side_exit_count] || 0
    }
  end
  
  def self.analyze_invalidations(stats)
    invalidation_count = stats[:invalidation_count] || 0
    compiled_count = stats[:compiled_method_count] || 1
    
    {
      total_invalidations: invalidation_count,
      invalidation_rate: (invalidation_count.to_f / compiled_count * 100).round(2),
      constant_invalidations: stats[:constant_invalidations] || 0
    }
  end
  
  def self.generate_debug_recommendations(analysis)
    recommendations = []
    
    if analysis[:compilation_failures][:success_rate] < 50
      recommendations << "Low compilation success rate suggests complex metaprogramming or unsupported patterns"
    end
    
    if analysis[:exit_reasons][:traced_exits] > analysis[:successfully_compiled] * 10
      recommendations << "High exit rate indicates type instability or polymorphic call sites"
    end
    
    if analysis[:invalidation_causes][:invalidation_rate] > 10
      recommendations << "Frequent invalidations suggest dynamic constant modification or monkey patching"
    end
    
    analysis.merge(recommendations: recommendations)
  end
end

# Method-specific compilation tracking
class CompilationTracker
  def self.track_method_compilation(klass, method_name)
    original_method = klass.instance_method(method_name)
    call_count = 0
    
    klass.define_method(method_name) do |*args, **kwargs|
      call_count += 1
      
      if call_count == 30  # Default compilation threshold
        puts "Method #{klass}##{method_name} should compile after this call"
        
        before_stats = RubyVM::YJIT.runtime_stats
        result = original_method.bind(self).call(*args, **kwargs)
        after_stats = RubyVM::YJIT.runtime_stats
        
        if after_stats[:compiled_method_count] > before_stats[:compiled_method_count]
          puts "✓ Method compiled successfully"
        else
          puts "✗ Method compilation may have failed"
        end
        
        result
      else
        original_method.bind(self).call(*args, **kwargs)
      end
    end
  end
end

# Usage example
class TestClass
  def problematic_method(param)
    # Code patterns that might prevent compilation
    if param.respond_to?(:each)
      param.each_with_object({}) do |item, hash|
        # Dynamic method calls can prevent optimization
        hash[item.send(:to_s)] = item.send([:length, :size].sample)
      end
    else
      param.to_s.length
    end
  end
end

CompilationTracker.track_method_compilation(TestClass, :problematic_method)

obj = TestClass.new
35.times { |i| obj.problematic_method(["test", "array"]) }  # Trigger compilation tracking

Performance Regression Detection

YJIT can occasionally introduce performance regressions due to compilation overhead or suboptimal code generation. Systematic performance monitoring detects these issues:

class PerformanceRegressionDetector
  def initialize
    @baseline_measurements = {}
    @regression_threshold = 0.15  # 15% performance drop threshold
  end
  
  def establish_baseline(test_name, test_proc, iterations = 1000)
    # Disable YJIT for baseline measurement
    if RubyVM::YJIT.enabled?
      puts "Warning: YJIT enabled during baseline measurement"
    end
    
    times = []
    iterations.times do
      start_time = Time.now
      test_proc.call
      times << Time.now - start_time
    end
    
    @baseline_measurements[test_name] = {
      mean: times.sum / times.length,
      median: times.sort[times.length / 2],
      p95: times.sort[(times.length * 0.95).to_i],
      iterations: iterations
    }
  end
  
  def measure_with_yjit(test_name, test_proc, warmup_iterations = 100, measurement_iterations = 1000)
    return "YJIT not enabled" unless RubyVM::YJIT.enabled?
    return "No baseline for #{test_name}" unless @baseline_measurements[test_name]
    
    # Warm up compilation
    warmup_iterations.times { test_proc.call }
    
    # Measure performance
    times = []
    measurement_iterations.times do
      start_time = Time.now
      test_proc.call
      times << Time.now - start_time
    end
    
    current_measurement = {
      mean: times.sum / times.length,
      median: times.sort[times.length / 2],
      p95: times.sort[(times.length * 0.95).to_i],
      iterations: measurement_iterations
    }
    
    analyze_regression(test_name, current_measurement)
  end
  
  private
  
  def analyze_regression(test_name, current_measurement)
    baseline = @baseline_measurements[test_name]
    
    mean_change = (current_measurement[:mean] - baseline[:mean]) / baseline[:mean]
    median_change = (current_measurement[:median] - baseline[:median]) / baseline[:median]
    p95_change = (current_measurement[:p95] - baseline[:p95]) / baseline[:p95]
    
    regression_detected = [mean_change, median_change, p95_change].any? { |change| change > @regression_threshold }
    
    {
      test_name: test_name,
      regression_detected: regression_detected,
      performance_changes: {
        mean: (mean_change * 100).round(2),
        median: (median_change * 100).round(2),
        p95: (p95_change * 100).round(2)
      },
      baseline: baseline,
      current: current_measurement,
      yjit_stats: RubyVM::YJIT.runtime_stats
    }
  end
end

# Regression testing example
detector = PerformanceRegressionDetector.new

# Define test cases
array_processing_test = proc do
  data = (1..1000).to_a
  data.map { |n| n * 2 + 1 }.select(&:even?).sum
end

string_processing_test = proc do
  text = "Ruby YJIT performance testing"
  100.times do
    text = text.reverse.upcase.gsub(/[AEIOU]/, '*').downcase
  end
  text
end

# Establish baselines (run with YJIT disabled)
unless RubyVM::YJIT.enabled?
  detector.establish_baseline("array_processing", array_processing_test)
  detector.establish_baseline("string_processing", string_processing_test)
  puts "Baselines established. Restart with YJIT enabled for regression testing."
else
  # Run regression tests (with YJIT enabled)
  array_results = detector.measure_with_yjit("array_processing", array_processing_test)
  string_results = detector.measure_with_yjit("string_processing", string_processing_test)
  
  [array_results, string_results].each do |result|
    next unless result.is_a?(Hash)
    
    puts "\nTest: #{result[:test_name]}"
    puts "Regression detected: #{result[:regression_detected]}"
    puts "Performance changes:"
    puts "  Mean: #{result[:performance_changes][:mean]}%"
    puts "  Median: #{result[:performance_changes][:median]}%"
    puts "  95th percentile: #{result[:performance_changes][:p95]}%"
    
    if result[:regression_detected]
      puts "⚠️  Performance regression detected!"
      puts "YJIT compiled methods: #{result[:yjit_stats][:compiled_method_count]}"
      puts "YJIT invalidations: #{result[:yjit_stats][:invalidation_count]}"
    end
  end
end

Reference

Environment Variables

Variable Default Description
RUBY_YJIT_ENABLE 0 Enable YJIT compilation (1 to enable)
RUBY_YJIT_CALL_THRESHOLD 30 Method call count before compilation
RUBY_YJIT_CODE_GC_THRESHOLD 128 Code region size limit in MB before GC
RUBY_YJIT_TRACE_EXITS 0 Enable exit tracing (1 to enable)
RUBY_YJIT_GREEDY_VERSIONING 0 Compile more aggressively (1 to enable)
RUBY_YJIT_STATS 0 Collect detailed statistics (1 to enable)

Runtime Statistics Methods

Method Returns Description
RubyVM::YJIT.enabled? Boolean Check if YJIT is active
RubyVM::YJIT.runtime_stats Hash Complete statistics hash
RubyVM::YJIT.reset_stats! nil Reset all statistics counters
RubyVM::YJIT.enable Boolean Enable YJIT at runtime (limited)
RubyVM::YJIT.code_gc Integer Force code garbage collection

Statistics Hash Keys

Key Type Description
:compiled_iseq_count Integer Number of compiled instruction sequences
:compiled_method_count Integer Number of compiled method bodies
:code_region_size Integer Total compiled code size in bytes
:inline_code_size Integer Inline code region size in bytes
:outlined_code_size Integer Outlined code region size in bytes
:invalidation_count Integer Number of compilation invalidations
:constant_invalidations Integer Invalidations due to constant changes
:code_gc_count Integer Number of code garbage collections
:freed_code_pages Integer Pages freed during code GC
:exit_locations Integer Number of unique exit locations
:traced_exits_count Integer Total traced exits from compiled code
:side_exit_count Integer Side exits to interpreter
:exec_instruction Integer Total executed instructions
:binding_allocations Integer Method binding allocations tracked

Command Line Options

Option Description
--yjit Enable YJIT compilation
--yjit-stats Enable YJIT statistics collection
--yjit-trace-exits Enable exit location tracing
--yjit-call-threshold=N Set compilation threshold to N calls
--yjit-code-gc Enable code garbage collection

Performance Characteristics

Code Pattern YJIT Impact Compilation Likelihood
Tight loops with arithmetic High positive Very likely
Array/Hash processing Moderate positive Likely
String manipulation Moderate positive Likely
Method calls with stable types Moderate positive Likely
Dynamic method dispatch Low/negative Unlikely
Heavy metaprogramming Negative Very unlikely
Constant modification Negative Causes invalidation
Polymorphic call sites Low positive May cause exits

Memory Usage Guidelines

Application Size Recommended Code GC Threshold Expected Memory Usage
Small applications 64MB 10-50MB
Medium web applications 128MB (default) 50-150MB
Large web applications 256MB 100-300MB
Memory-constrained environments 32MB 5-30MB

Troubleshooting Checklist

Issue Diagnostic Steps Common Solutions
YJIT not improving performance Check compilation rates, measure warmup time Increase call threshold, verify hot paths
High memory usage Monitor code region size, GC frequency Lower code GC threshold, check for leaks
Frequent invalidations Track constant modifications, method redefinitions Avoid runtime constant changes
Compilation failures Enable exit tracing, check for unsupported patterns Refactor dynamic code, reduce metaprogramming
Performance regressions Compare with/without YJIT, profile exit patterns Adjust thresholds, identify problematic methods