CrackedRuby logo

CrackedRuby

YJIT Statistics

Overview

YJIT Statistics provides runtime visibility into Ruby's just-in-time compiler behavior through the RubyVM::YJIT module. Ruby exposes compilation metrics, execution statistics, and performance data that applications can collect and analyze during program execution.

The statistics system tracks method compilations, inline cache hits, code generation efficiency, and memory usage patterns. Ruby maintains these counters internally and exposes them through several methods that return detailed hash structures containing numeric data.

# Check if YJIT is enabled and collect basic stats
if RubyVM::YJIT.enabled?
  stats = RubyVM::YJIT.runtime_stats
  puts "Compiled methods: #{stats[:compiled_iseq_count]}"
end

The primary interface centers around RubyVM::YJIT.runtime_stats, which returns a hash containing dozens of metrics. Additional methods like RubyVM::YJIT.reset_stats! and RubyVM::YJIT.dump_stats provide control over data collection and output formatting.

# Detailed statistics collection
stats = RubyVM::YJIT.runtime_stats
puts "Side exits: #{stats[:side_exit_count]}"
puts "Inline cache misses: #{stats[:inline_code_asm_bytes]}"
puts "Compilation ratio: #{stats[:ratio_in_yjit]}"

YJIT statistics operate independently of Ruby's garbage collector metrics and profiling tools. The data reflects compiler-specific behavior rather than general Ruby performance characteristics. Applications typically collect these statistics periodically for monitoring or after specific operations for analysis.

Basic Usage

Statistics collection requires YJIT activation through the --yjit command line flag or RubyVM::YJIT.enable method call. Ruby only populates statistics when YJIT actively compiles and executes code.

# Enable YJIT programmatically if not already active
RubyVM::YJIT.enable unless RubyVM::YJIT.enabled?

# Collect current statistics
current_stats = RubyVM::YJIT.runtime_stats

# Key metrics for basic monitoring
puts "YJIT enabled: #{RubyVM::YJIT.enabled?}"
puts "Compiled ISEQs: #{current_stats[:compiled_iseq_count]}"
puts "Code memory used: #{current_stats[:code_gc_count]} GC cycles"
puts "Execution in YJIT: #{(current_stats[:ratio_in_yjit] * 100).round(2)}%"

The runtime_stats method returns a comprehensive hash with over 50 different metrics. Most applications focus on compilation counts, execution ratios, and memory usage indicators for basic monitoring purposes.

# Monitor compilation progress over time
def track_compilation_progress(duration_seconds)
  initial_stats = RubyVM::YJIT.runtime_stats
  sleep(duration_seconds)
  final_stats = RubyVM::YJIT.runtime_stats
  
  compiled_methods = final_stats[:compiled_iseq_count] - initial_stats[:compiled_iseq_count]
  puts "New compilations: #{compiled_methods}"
  puts "Current ratio: #{(final_stats[:ratio_in_yjit] * 100).round(2)}%"
end

track_compilation_progress(5)

Resetting statistics provides clean measurement windows for specific operations or time periods. The reset_stats! method zeroes all counters while preserving the current YJIT operational state.

# Reset counters before measuring specific operations
RubyVM::YJIT.reset_stats!

# Execute code to measure
1000.times do |i|
  "string_#{i}".upcase.reverse
end

# Collect measurements
measurement_stats = RubyVM::YJIT.runtime_stats
puts "Operations compiled: #{measurement_stats[:compiled_iseq_count]}"
puts "Side exits during test: #{measurement_stats[:side_exit_count]}"

Statistics persistence across program execution depends on when collection occurs. Ruby resets YJIT statistics when the interpreter starts, but maintains counters throughout the program's lifetime unless explicitly reset.

Performance & Memory

YJIT statistics directly reflect compilation efficiency and runtime performance characteristics. The ratio_in_yjit metric indicates what percentage of execution time occurs in compiled code versus interpreted code.

# Measure compilation efficiency over workload
def analyze_yjit_efficiency(workload_proc)
  RubyVM::YJIT.reset_stats!
  start_time = Time.now
  
  workload_proc.call
  
  elapsed = Time.now - start_time
  stats = RubyVM::YJIT.runtime_stats
  
  {
    elapsed_seconds: elapsed,
    yjit_ratio: stats[:ratio_in_yjit],
    compiled_methods: stats[:compiled_iseq_count],
    side_exits: stats[:side_exit_count],
    compilation_rate: stats[:compiled_iseq_count] / elapsed
  }
end

# Test with method-heavy workload
results = analyze_yjit_efficiency(proc {
  10_000.times do |i|
    Math.sqrt(i) * Math.log(i + 1)
  end
})

puts "YJIT handled #{(results[:yjit_ratio] * 100).round(1)}% of execution"
puts "Compilation rate: #{results[:compilation_rate].round(2)} methods/sec"

Memory usage tracking focuses on code generation and garbage collection patterns. YJIT maintains separate memory regions for compiled code, and statistics track both allocation and cleanup cycles.

# Monitor memory usage patterns
def memory_usage_analysis
  stats = RubyVM::YJIT.runtime_stats
  
  code_bytes = stats[:inline_code_asm_bytes] + stats[:outlined_code_asm_bytes]
  total_pages = stats[:code_page_count]
  gc_cycles = stats[:code_gc_count]
  
  {
    total_code_bytes: code_bytes,
    bytes_per_page: total_pages > 0 ? code_bytes / total_pages : 0,
    gc_pressure: gc_cycles,
    memory_efficiency: stats[:compiled_iseq_count] > 0 ? code_bytes / stats[:compiled_iseq_count] : 0
  }
end

memory_stats = memory_usage_analysis
puts "Code memory: #{memory_stats[:total_code_bytes]} bytes"
puts "Average per method: #{memory_stats[:memory_efficiency].round(2)} bytes"
puts "GC cycles: #{memory_stats[:gc_pressure]}"

Side exit frequency indicates deoptimization events where compiled code transfers control back to the interpreter. High side exit counts suggest compilation assumptions that don't match runtime behavior.

# Analyze side exit patterns for optimization hints
def side_exit_analysis(test_code)
  initial_stats = RubyVM::YJIT.runtime_stats
  test_code.call
  final_stats = RubyVM::YJIT.runtime_stats
  
  side_exits = final_stats[:side_exit_count] - initial_stats[:side_exit_count]
  compilations = final_stats[:compiled_iseq_count] - initial_stats[:compiled_iseq_count]
  
  if compilations > 0
    exit_ratio = side_exits.to_f / compilations
    puts "Side exits per compilation: #{exit_ratio.round(3)}"
    puts "Compilation stability: #{exit_ratio < 0.1 ? 'Good' : 'Needs optimization'}"
  end
  
  side_exits
end

# Test with type-unstable code
exits = side_exit_analysis(proc {
  values = [1, "string", 3.14, :symbol]
  1000.times { values.each(&:to_s) }
})

Benchmark comparisons between YJIT-enabled and interpreted execution require careful measurement timing. Statistics provide data for calculating performance improvements and identifying bottlenecks.

# Compare YJIT vs interpreter performance
def performance_comparison(workload)
  # Measure without YJIT
  RubyVM::YJIT.disable if RubyVM::YJIT.enabled?
  start_time = Time.now
  workload.call
  interpreted_time = Time.now - start_time
  
  # Measure with YJIT
  RubyVM::YJIT.enable
  RubyVM::YJIT.reset_stats!
  start_time = Time.now
  workload.call
  yjit_time = Time.now - start_time
  stats = RubyVM::YJIT.runtime_stats
  
  speedup = interpreted_time / yjit_time
  {
    interpreted_seconds: interpreted_time,
    yjit_seconds: yjit_time,
    speedup_factor: speedup,
    yjit_ratio: stats[:ratio_in_yjit],
    compiled_methods: stats[:compiled_iseq_count]
  }
end

Production Patterns

Production monitoring typically involves periodic statistics collection and alerting on performance degradation indicators. Applications integrate YJIT statistics into existing monitoring infrastructure through custom metrics and logging systems.

# Production monitoring service
class YJITMonitor
  def initialize(collection_interval: 60)
    @interval = collection_interval
    @baseline_stats = nil
    @monitoring_thread = nil
  end
  
  def start_monitoring
    return unless RubyVM::YJIT.enabled?
    
    @baseline_stats = RubyVM::YJIT.runtime_stats
    @monitoring_thread = Thread.new { monitor_loop }
  end
  
  def stop_monitoring
    @monitoring_thread&.kill
    @monitoring_thread = nil
  end
  
  private
  
  def monitor_loop
    loop do
      sleep @interval
      current_stats = RubyVM::YJIT.runtime_stats
      analyze_performance_trends(current_stats)
      log_metrics(current_stats)
    end
  rescue => e
    Rails.logger.error "YJIT monitoring error: #{e.message}"
    retry
  end
  
  def analyze_performance_trends(stats)
    if @baseline_stats
      ratio_change = stats[:ratio_in_yjit] - @baseline_stats[:ratio_in_yjit]
      side_exit_increase = stats[:side_exit_count] - @baseline_stats[:side_exit_count]
      
      if ratio_change < -0.05  # 5% drop in YJIT execution
        Rails.logger.warn "YJIT efficiency decreased by #{(ratio_change * 100).round(2)}%"
      end
      
      if side_exit_increase > 10000  # Significant deoptimization
        Rails.logger.warn "High side exit activity: #{side_exit_increase} new exits"
      end
    end
  end
  
  def log_metrics(stats)
    metrics = {
      yjit_ratio: (stats[:ratio_in_yjit] * 100).round(2),
      compiled_methods: stats[:compiled_iseq_count],
      side_exits: stats[:side_exit_count],
      code_memory_mb: (stats[:inline_code_asm_bytes] + stats[:outlined_code_asm_bytes]) / (1024 * 1024),
      gc_cycles: stats[:code_gc_count]
    }
    
    Rails.logger.info "YJIT Stats: #{metrics.to_json}"
  end
end

# Initialize in Rails application
monitor = YJITMonitor.new(collection_interval: 30)
monitor.start_monitoring

Health check endpoints often include YJIT statistics to verify compiler operation in production environments. These endpoints provide visibility into JIT compiler status without impacting application performance.

# Health check endpoint with YJIT status
class HealthController < ApplicationController
  def yjit_status
    if RubyVM::YJIT.enabled?
      stats = RubyVM::YJIT.runtime_stats
      
      status = {
        yjit_enabled: true,
        execution_ratio: (stats[:ratio_in_yjit] * 100).round(2),
        compiled_methods: stats[:compiled_iseq_count],
        memory_usage_mb: calculate_memory_usage(stats),
        performance_indicators: {
          side_exit_rate: calculate_side_exit_rate(stats),
          compilation_efficiency: calculate_efficiency(stats)
        }
      }
      
      render json: { yjit: status }, status: :ok
    else
      render json: { yjit: { enabled: false } }, status: :ok
    end
  end
  
  private
  
  def calculate_memory_usage(stats)
    total_bytes = stats[:inline_code_asm_bytes] + stats[:outlined_code_asm_bytes]
    (total_bytes / (1024.0 * 1024.0)).round(2)
  end
  
  def calculate_side_exit_rate(stats)
    return 0 if stats[:compiled_iseq_count] == 0
    (stats[:side_exit_count].to_f / stats[:compiled_iseq_count]).round(4)
  end
  
  def calculate_efficiency(stats)
    return 0 if stats[:compiled_iseq_count] == 0
    code_bytes = stats[:inline_code_asm_bytes] + stats[:outlined_code_asm_bytes]
    (code_bytes.to_f / stats[:compiled_iseq_count]).round(2)
  end
end

Deployment strategies often include YJIT warmup periods where applications execute representative workloads to trigger compilation before handling production traffic. Statistics guide warmup duration and effectiveness.

# Application warmup with YJIT statistics
class ApplicationWarmer
  def self.warmup_with_monitoring(warmup_duration: 30)
    return unless RubyVM::YJIT.enabled?
    
    Rails.logger.info "Starting YJIT warmup period"
    RubyVM::YJIT.reset_stats!
    warmup_start = Time.now
    
    # Execute representative application workloads
    warmup_workloads.each { |workload| workload.call }
    
    # Monitor compilation progress
    while Time.now - warmup_start < warmup_duration
      stats = RubyVM::YJIT.runtime_stats
      
      Rails.logger.info "Warmup progress: #{stats[:compiled_iseq_count]} methods compiled, " \
                       "#{(stats[:ratio_in_yjit] * 100).round(1)}% in YJIT"
      
      sleep 5
    end
    
    final_stats = RubyVM::YJIT.runtime_stats
    Rails.logger.info "Warmup completed: #{final_stats[:compiled_iseq_count]} methods ready"
    
    final_stats
  end
  
  def self.warmup_workloads
    [
      -> { User.first&.full_name },  # Database access patterns
      -> { Rails.application.routes.recognize_path("/") },  # Routing
      -> { ApplicationController.new.process(:index) },  # Controller logic
      -> { ActionView::Base.new.render(inline: "<%= 'test' %>") }  # View rendering
    ]
  end
end

Error Handling & Debugging

YJIT statistics collection handles several error conditions related to compiler availability and operational state. Applications must verify YJIT activation before attempting statistics collection to avoid runtime errors.

# Safe statistics collection with error handling
def collect_yjit_stats_safely
  unless RubyVM::YJIT.enabled?
    Rails.logger.warn "YJIT statistics requested but YJIT not enabled"
    return { error: "YJIT not available", enabled: false }
  end
  
  begin
    stats = RubyVM::YJIT.runtime_stats
    
    # Validate statistics structure
    required_keys = [:compiled_iseq_count, :ratio_in_yjit, :side_exit_count]
    missing_keys = required_keys - stats.keys
    
    if missing_keys.any?
      Rails.logger.error "YJIT statistics missing expected keys: #{missing_keys}"
      return { error: "Incomplete statistics", missing_keys: missing_keys }
    end
    
    stats
  rescue => e
    Rails.logger.error "Failed to collect YJIT statistics: #{e.message}"
    { error: e.message, backtrace: e.backtrace.first(5) }
  end
end

# Usage with error handling
stats_result = collect_yjit_stats_safely
if stats_result[:error]
  handle_statistics_error(stats_result)
else
  process_valid_statistics(stats_result)
end

Statistics interpretation requires understanding normal value ranges and identifying anomalous patterns. Debugging performance issues involves correlating statistics with application behavior and workload characteristics.

# YJIT performance debugging toolkit
class YJITDebugger
  def self.diagnose_performance_issues(stats = nil)
    stats ||= RubyVM::YJIT.runtime_stats
    issues = []
    
    # Check compilation ratio
    if stats[:ratio_in_yjit] < 0.3
      issues << {
        severity: :warning,
        issue: "Low YJIT execution ratio",
        value: (stats[:ratio_in_yjit] * 100).round(2),
        recommendation: "Code may not be suitable for JIT compilation"
      }
    end
    
    # Analyze side exit frequency
    if stats[:compiled_iseq_count] > 0
      exit_ratio = stats[:side_exit_count].to_f / stats[:compiled_iseq_count]
      if exit_ratio > 5.0
        issues << {
          severity: :error,
          issue: "High side exit ratio",
          value: exit_ratio.round(2),
          recommendation: "Review code for type instability or dynamic behavior"
        }
      end
    end
    
    # Memory usage analysis
    code_bytes = stats[:inline_code_asm_bytes] + stats[:outlined_code_asm_bytes]
    if code_bytes > 100 * 1024 * 1024  # 100MB
      issues << {
        severity: :warning,
        issue: "High code memory usage",
        value: "#{(code_bytes / (1024 * 1024)).round(2)} MB",
        recommendation: "Monitor for memory pressure from compiled code"
      }
    end
    
    # GC frequency check
    if stats[:code_gc_count] > 10
      issues << {
        severity: :info,
        issue: "Frequent code GC cycles",
        value: stats[:code_gc_count],
        recommendation: "Code memory pressure may indicate excessive compilation"
      }
    end
    
    issues
  end
  
  def self.detailed_analysis_report
    stats = RubyVM::YJIT.runtime_stats
    issues = diagnose_performance_issues(stats)
    
    report = {
      timestamp: Time.current,
      yjit_enabled: RubyVM::YJIT.enabled?,
      statistics: stats,
      analysis: {
        performance_issues: issues,
        health_score: calculate_health_score(stats, issues)
      }
    }
    
    Rails.logger.info "YJIT Analysis: #{report.to_json}"
    report
  end
  
  private
  
  def self.calculate_health_score(stats, issues)
    base_score = 100
    
    issues.each do |issue|
      case issue[:severity]
      when :error then base_score -= 30
      when :warning then base_score -= 15
      when :info then base_score -= 5
      end
    end
    
    [base_score, 0].max
  end
end

Debugging compilation failures or unexpected performance requires systematic analysis of statistics changes over time. Applications can track statistics deltas to identify when performance degradation occurs.

# Statistics change tracking for debugging
class YJITChangeTracker
  def initialize
    @snapshots = []
    @max_snapshots = 50
  end
  
  def capture_snapshot(label = nil)
    return unless RubyVM::YJIT.enabled?
    
    snapshot = {
      timestamp: Time.current,
      label: label,
      stats: RubyVM::YJIT.runtime_stats.dup
    }
    
    @snapshots << snapshot
    @snapshots.shift if @snapshots.size > @max_snapshots
    
    snapshot
  end
  
  def analyze_changes(since_label: nil, last_n: 2)
    return {} if @snapshots.size < 2
    
    if since_label
      start_index = @snapshots.find_index { |s| s[:label] == since_label }
      return {} unless start_index
      
      start_snapshot = @snapshots[start_index]
      end_snapshot = @snapshots.last
    else
      recent_snapshots = @snapshots.last(last_n)
      start_snapshot = recent_snapshots.first
      end_snapshot = recent_snapshots.last
    end
    
    calculate_deltas(start_snapshot[:stats], end_snapshot[:stats])
  end
  
  private
  
  def calculate_deltas(start_stats, end_stats)
    deltas = {}
    
    numeric_keys = [:compiled_iseq_count, :side_exit_count, :code_gc_count,
                   :inline_code_asm_bytes, :outlined_code_asm_bytes]
    
    numeric_keys.each do |key|
      if start_stats[key] && end_stats[key]
        deltas[key] = end_stats[key] - start_stats[key]
      end
    end
    
    # Calculate ratio change
    if start_stats[:ratio_in_yjit] && end_stats[:ratio_in_yjit]
      deltas[:ratio_change] = end_stats[:ratio_in_yjit] - start_stats[:ratio_in_yjit]
    end
    
    deltas
  end
end

# Usage for debugging performance regression
tracker = YJITChangeTracker.new
tracker.capture_snapshot("before_deploy")
# ... application changes or load ...
tracker.capture_snapshot("after_deploy")

changes = tracker.analyze_changes(since_label: "before_deploy")
puts "Compilation changes: #{changes[:compiled_iseq_count]}"
puts "Side exit changes: #{changes[:side_exit_count]}"
puts "Ratio change: #{(changes[:ratio_change] * 100).round(3)}%" if changes[:ratio_change]

Reference

Core Methods

Method Parameters Returns Description
RubyVM::YJIT.enabled? None Boolean Check if YJIT compiler is active
RubyVM::YJIT.enable None Boolean Activate YJIT compilation
RubyVM::YJIT.disable None Boolean Deactivate YJIT compilation
RubyVM::YJIT.runtime_stats None Hash Current compilation and execution statistics
RubyVM::YJIT.reset_stats! None true Zero all statistics counters
RubyVM::YJIT.dump_stats context: nil String Formatted statistics output

Statistics Hash Keys

Key Type Description
:compiled_iseq_count Integer Number of compiled instruction sequences
:ratio_in_yjit Float Execution time percentage in compiled code (0.0-1.0)
:side_exit_count Integer Total deoptimization events from compiled to interpreted code
:inline_code_asm_bytes Integer Memory used by inline compiled code
:outlined_code_asm_bytes Integer Memory used by outlined compiled code
:code_page_count Integer Number of memory pages allocated for compiled code
:code_gc_count Integer Number of garbage collection cycles for compiled code
:iseq_recompilation_count Integer Number of instruction sequences recompiled
:binding_allocations Integer Memory allocations for variable bindings
:binding_set Integer Variable binding update operations
:constant_state_bumps Integer Constant cache invalidation events
:vm_insns_count Integer Total VM instructions executed
:yjit_insns_count Integer Instructions executed in compiled code

Performance Interpretation Guidelines

Metric Range Interpretation Action
ratio_in_yjit > 0.7 Excellent compilation coverage Monitor for regressions
ratio_in_yjit 0.4-0.7 Good compilation effectiveness Profile for optimization opportunities
ratio_in_yjit < 0.4 Limited compilation benefit Review code patterns and workload
Side exits / compiled methods > 10 High deoptimization rate Investigate type stability
Side exits / compiled methods < 1 Stable compilation Normal operation
Code memory > 50MB High memory usage Monitor for excessive compilation

Common Statistics Patterns

Pattern Cause Resolution
High side_exit_count with low ratio_in_yjit Type instability or dynamic code Review polymorphic method calls
Growing code_gc_count Memory pressure Monitor total code memory usage
Low compiled_iseq_count Limited compilation triggers Increase workload or warmup time
High iseq_recompilation_count Invalidation cycles Check for constant modifications
Large binding_allocations Variable access patterns Profile closure and block usage

Integration Examples

# Periodic monitoring
Thread.new do
  loop do
    sleep 60
    stats = RubyVM::YJIT.runtime_stats if RubyVM::YJIT.enabled?
    Logger.info "YJIT: #{stats[:compiled_iseq_count]} methods, #{(stats[:ratio_in_yjit] * 100).round(1)}%"
  end
end

# Performance testing
def measure_yjit_impact(iterations = 1000)
  RubyVM::YJIT.reset_stats!
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  
  iterations.times { yield }
  
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
  stats = RubyVM::YJIT.runtime_stats
  
  {
    elapsed: elapsed,
    yjit_ratio: stats[:ratio_in_yjit],
    compilations: stats[:compiled_iseq_count]
  }
end

# Health check
def yjit_health_status
  return { enabled: false } unless RubyVM::YJIT.enabled?
  
  stats = RubyVM::YJIT.runtime_stats
  {
    enabled: true,
    compiled_methods: stats[:compiled_iseq_count],
    execution_ratio: (stats[:ratio_in_yjit] * 100).round(2),
    side_exit_rate: stats[:compiled_iseq_count] > 0 ? 
      (stats[:side_exit_count].to_f / stats[:compiled_iseq_count]).round(3) : 0,
    memory_mb: ((stats[:inline_code_asm_bytes] + stats[:outlined_code_asm_bytes]) / 1_048_576.0).round(2)
  }
end

Error Conditions

Condition Error Resolution
YJIT not enabled NoMethodError on statistics methods Check RubyVM::YJIT.enabled? first
Statistics collection failure Method returns empty hash Verify YJIT operational state
Memory constraints Statistics may show zero values Check system memory availability
Ruby version compatibility NameError for YJIT constants Verify Ruby 3.1+ with YJIT support