CrackedRuby logo

CrackedRuby

YJIT Logging

YJIT Logging provides comprehensive debugging and performance monitoring capabilities for Ruby's Just-In-Time compiler through environment variables, programmatic APIs, and detailed compilation statistics.

Performance Optimization YJIT Compiler
7.1.4

Overview

YJIT Logging exposes the internal workings of Ruby's Just-In-Time compiler through multiple interfaces. The logging system tracks compilation events, method performance, memory usage, and optimization decisions in real-time. Ruby provides both environment variable controls for immediate debugging and programmatic APIs for runtime introspection.

The logging infrastructure operates at multiple levels. Environment variables control compilation verbosity and output destinations. The RubyVM::YJIT module exposes runtime statistics and compilation data through method calls. Internal hooks capture compilation events, method entries, and optimization failures.

# Enable basic YJIT logging
ENV['RUBY_YJIT_ENABLE'] = '1'
ENV['RUBY_YJIT_LOG'] = '1'

# Access runtime statistics
stats = RubyVM::YJIT.runtime_stats
puts "Compiled methods: #{stats[:compiled_iseq_count]}"

YJIT Logging integrates with Ruby's existing debugging infrastructure while providing JIT-specific insights. The system captures method compilation boundaries, inline decisions, and guard failures. This data helps developers understand which code benefits from JIT compilation and identify performance bottlenecks.

# Monitor compilation during method execution
def hot_method(data)
  data.map { |x| x * 2 }.select { |x| x > 10 }
end

# YJIT logs will show compilation of hot_method after threshold
1000.times { hot_method([1, 2, 3, 4, 5, 6]) }

Basic Usage

YJIT Logging activates through environment variables set before Ruby execution. The RUBY_YJIT_ENABLE variable activates the JIT compiler, while RUBY_YJIT_LOG controls logging verbosity. Ruby writes compilation events to standard error by default.

# Terminal: RUBY_YJIT_ENABLE=1 RUBY_YJIT_LOG=1 ruby script.rb

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

# This will trigger compilation logging
result = fibonacci(20)

The RubyVM::YJIT.runtime_stats method returns a hash containing detailed compilation metrics. This hash includes counters for compiled methods, inline cache hits, guard failures, and memory consumption. The statistics reset only when Ruby restarts.

# Capture baseline statistics
baseline = RubyVM::YJIT.runtime_stats

# Execute code that triggers compilation
1000.times do
  "hello".upcase + "world"
end

# Compare against baseline
current = RubyVM::YJIT.runtime_stats
compiled = current[:compiled_iseq_count] - baseline[:compiled_iseq_count]
puts "Newly compiled methods: #{compiled}"

The RUBY_YJIT_STATS environment variable enables extended statistics collection. This mode captures additional performance counters but introduces slight overhead. The extended statistics include memory allocation patterns and detailed compilation timings.

# Terminal: RUBY_YJIT_STATS=1 ruby -e "puts RubyVM::YJIT.runtime_stats.keys.size"

# Access extended statistics
stats = RubyVM::YJIT.runtime_stats
puts "Code region size: #{stats[:code_region_size]}"
puts "Inline code size: #{stats[:inline_code_size]}"
puts "Outlined code size: #{stats[:outlined_code_size]}"

Method-level logging captures individual compilation decisions. Ruby logs when methods cross the compilation threshold, when inline optimizations occur, and when guards prevent further optimization. This granular data pinpoints exactly which code paths benefit from JIT compilation.

class Calculator
  def add(a, b)
    a + b  # Will be compiled after sufficient calls
  end
  
  def complex_calculation(data)
    data.reduce(0) do |sum, item|
      sum + (item[:value] * item[:multiplier])
    end
  end
end

calc = Calculator.new
# These calls will trigger compilation logging
1000.times { calc.add(rand(100), rand(100)) }

Advanced Usage

YJIT Logging supports custom output destinations through file redirection and programmatic capture. The RUBY_YJIT_LOG_FILE environment variable redirects compilation logs to specified files. Ruby appends to existing files and creates new files as needed.

# Terminal: RUBY_YJIT_LOG_FILE=/tmp/yjit.log ruby script.rb

# Alternative: programmatic log capture
original_stderr = $stderr
log_buffer = StringIO.new
$stderr = log_buffer

ENV['RUBY_YJIT_ENABLE'] = '1'
ENV['RUBY_YJIT_LOG'] = '1'

# Execute code that triggers logging
def intensive_method(n)
  (1..n).map { |i| i ** 2 }.sum
end

100.times { intensive_method(1000) }

# Restore and analyze logs
$stderr = original_stderr
compilation_events = log_buffer.string.split("\n").select { |line| line.include?("compile") }
puts "Compilation events: #{compilation_events.size}"

The logging system supports filtering by compilation phases. Different log levels expose varying amounts of detail about optimization decisions, guard insertions, and code generation. Ruby provides fine-grained control over which compilation stages generate log output.

# Selective logging configuration
ENV['RUBY_YJIT_VERBOSE'] = '1'  # Maximum verbosity
ENV['RUBY_YJIT_GREEDY_VERSIONING'] = '1'  # Aggressive compilation

class DataProcessor
  def process_records(records)
    records
      .filter { |r| r[:active] }
      .map { |r| transform_record(r) }
      .group_by { |r| r[:category] }
  end
  
  private
  
  def transform_record(record)
    {
      id: record[:id],
      value: record[:value] * 1.1,
      category: record[:category]&.downcase,
      processed_at: Time.now
    }
  end
end

processor = DataProcessor.new
sample_data = 1000.times.map do |i|
  {
    id: i,
    active: i.even?,
    value: rand(100),
    category: ["A", "B", "C"].sample
  }
end

# This generates detailed compilation logs for each optimization
10.times { processor.process_records(sample_data) }

Custom log processors can extract specific compilation metrics from log streams. Ruby's logging format follows predictable patterns that enable automated parsing and analysis. These processors track compilation ratios, optimization success rates, and performance regressions.

class YJITLogAnalyzer
  def initialize(log_content)
    @log_lines = log_content.split("\n")
    @stats = Hash.new(0)
  end
  
  def analyze_compilation_patterns
    @log_lines.each do |line|
      case line
      when /compile: (.+) \((\d+) bytes\)/
        @stats[:compiled_methods] += 1
        @stats[:total_compiled_bytes] += $2.to_i
      when /inline: (.+)/
        @stats[:inlined_calls] += 1
      when /guard_failure: (.+)/
        @stats[:guard_failures] += 1
      when /deopt: (.+)/
        @stats[:deoptimizations] += 1
      end
    end
    
    @stats[:average_method_size] = @stats[:total_compiled_bytes] / @stats[:compiled_methods] if @stats[:compiled_methods] > 0
    @stats
  end
  
  def compilation_efficiency
    return 0.0 if @stats[:compiled_methods] == 0
    (@stats[:compiled_methods] - @stats[:deoptimizations]).to_f / @stats[:compiled_methods]
  end
end

# Usage with captured logs
log_content = File.read('/tmp/yjit.log')
analyzer = YJITLogAnalyzer.new(log_content)
patterns = analyzer.analyze_compilation_patterns

puts "Compilation efficiency: #{(analyzer.compilation_efficiency * 100).round(2)}%"
puts "Average compiled method size: #{patterns[:average_method_size]} bytes"

Performance & Memory

YJIT Logging introduces minimal overhead when disabled but can impact performance when active. Basic logging adds approximately 2-5% runtime overhead, while verbose logging can increase execution time by 10-20%. Memory usage increases proportionally with log volume and retention duration.

The RubyVM::YJIT.runtime_stats call itself has negligible performance cost, making it suitable for production monitoring. Ruby calculates statistics incrementally during compilation rather than on-demand during method calls. This design keeps monitoring overhead constant regardless of application complexity.

require 'benchmark'

def performance_sensitive_method(data)
  data.map { |item| 
    item.transform_keys(&:to_s)
        .transform_values { |v| v.to_s.upcase }
  }.group_by { |item| item['category'] }
end

sample_data = 10000.times.map do
  {
    id: rand(1000),
    name: "item_#{rand(1000)}",
    category: rand(5),
    value: rand(100.0)
  }
end

# Benchmark with and without logging
Benchmark.bm(20) do |x|
  x.report("without_logging:") do
    ENV.delete('RUBY_YJIT_LOG')
    10.times { performance_sensitive_method(sample_data) }
  end
  
  x.report("with_basic_logging:") do
    ENV['RUBY_YJIT_LOG'] = '1'
    10.times { performance_sensitive_method(sample_data) }
  end
  
  x.report("with_verbose_logging:") do
    ENV['RUBY_YJIT_VERBOSE'] = '1'
    10.times { performance_sensitive_method(sample_data) }
  end
end

Memory consumption patterns vary significantly based on compilation success rates. Successfully compiled methods consume additional memory for native code storage, while failed compilations only retain metadata. The code_region_size statistic tracks total memory allocated for JIT-compiled native code.

def analyze_memory_usage
  initial_stats = RubyVM::YJIT.runtime_stats
  initial_memory = initial_stats[:code_region_size] || 0
  
  # Generate methods that will trigger compilation
  methods = []
  100.times do |i|
    method_name = "dynamic_method_#{i}"
    define_singleton_method(method_name) do |x|
      x * 2 + 1
    end
    methods << method_name
  end
  
  # Exercise the methods to trigger compilation
  methods.each do |method_name|
    1000.times { send(method_name, rand(100)) }
  end
  
  final_stats = RubyVM::YJIT.runtime_stats
  final_memory = final_stats[:code_region_size] || 0
  memory_growth = final_memory - initial_memory
  
  {
    methods_compiled: final_stats[:compiled_iseq_count] - initial_stats[:compiled_iseq_count],
    memory_used: memory_growth,
    average_per_method: methods.size > 0 ? memory_growth / methods.size : 0
  }
end

usage = analyze_memory_usage
puts "Memory per compiled method: #{usage[:average_per_method]} bytes"

Compilation statistics help identify performance bottlenecks and optimization opportunities. High guard failure rates indicate polymorphic call sites that prevent effective optimization. Deoptimization events suggest changing execution patterns that invalidate compiled code.

class PerformanceAnalyzer
  def initialize
    @baseline = RubyVM::YJIT.runtime_stats
  end
  
  def capture_metrics
    current = RubyVM::YJIT.runtime_stats
    
    {
      compilation_ratio: calculate_ratio(current[:compiled_iseq_count], current[:iseq_count]),
      inline_success_rate: calculate_ratio(current[:inline_code_size], current[:code_region_size]),
      guard_failure_rate: calculate_ratio(current[:guard_failure_count], current[:compiled_iseq_count]),
      memory_efficiency: calculate_ratio(current[:code_region_size], current[:compiled_iseq_count])
    }
  end
  
  private
  
  def calculate_ratio(numerator, denominator)
    return 0.0 if denominator.nil? || denominator.zero?
    (numerator || 0).to_f / denominator
  end
end

# Monitor performance during application execution
analyzer = PerformanceAnalyzer.new

# Simulate varied workload
1000.times do |i|
  case i % 3
  when 0
    "string_#{i}".upcase.reverse
  when 1
    [i, i+1, i+2].map(&:to_s).join(',')
  when 2
    { id: i, value: i * 2 }.transform_values(&:to_s)
  end
end

metrics = analyzer.capture_metrics
puts "Compilation ratio: #{(metrics[:compilation_ratio] * 100).round(2)}%"
puts "Guard failure rate: #{(metrics[:guard_failure_rate] * 100).round(2)}%"

Production Patterns

Production YJIT Logging requires careful balance between observability and performance impact. Continuous statistics collection through RubyVM::YJIT.runtime_stats provides ongoing visibility into JIT performance without significant overhead. This approach suits production monitoring systems and performance dashboards.

class YJITMonitor
  def initialize(interval: 60)
    @interval = interval
    @baseline = RubyVM::YJIT.runtime_stats
    @monitoring = false
  end
  
  def start_monitoring
    return if @monitoring
    @monitoring = true
    
    Thread.new do
      while @monitoring
        sleep @interval
        report_metrics
      end
    end
  end
  
  def stop_monitoring
    @monitoring = false
  end
  
  private
  
  def report_metrics
    current = RubyVM::YJIT.runtime_stats
    return unless current
    
    metrics = {
      timestamp: Time.now.iso8601,
      compiled_methods: current[:compiled_iseq_count] || 0,
      memory_usage: current[:code_region_size] || 0,
      inline_cache_hits: current[:inline_code_size] || 0,
      compilation_failures: current[:compilation_failure_count] || 0
    }
    
    # Send to monitoring system
    send_to_monitoring(metrics)
  end
  
  def send_to_monitoring(metrics)
    # Integration with monitoring systems like DataDog, NewRelic, etc.
    puts "YJIT Metrics: #{metrics.to_json}"
  end
end

# Initialize monitoring in application startup
monitor = YJITMonitor.new(interval: 30)
monitor.start_monitoring

# Register shutdown handler
at_exit { monitor.stop_monitoring }

Log aggregation patterns help manage log volume in production environments. Sampling strategies reduce log overhead while preserving visibility into compilation patterns. Ruby applications can implement smart logging that activates detailed logging based on performance thresholds or error conditions.

class SmartYJITLogger
  def initialize(threshold: 0.1)
    @performance_threshold = threshold
    @last_check = Time.now
    @baseline_performance = nil
    @logging_active = false
  end
  
  def maybe_enable_logging
    return if @logging_active
    
    current_time = Time.now
    return if current_time - @last_check < 300  # Check every 5 minutes
    
    performance_degraded = check_performance_degradation
    
    if performance_degraded
      enable_detailed_logging
      schedule_logging_disable
    end
    
    @last_check = current_time
  end
  
  private
  
  def check_performance_degradation
    stats = RubyVM::YJIT.runtime_stats
    return false unless stats
    
    if @baseline_performance.nil?
      @baseline_performance = stats[:compiled_iseq_count] || 0
      return false
    end
    
    current_performance = stats[:compiled_iseq_count] || 0
    improvement_rate = (current_performance - @baseline_performance).to_f / 300  # Methods per second
    
    improvement_rate < @performance_threshold
  end
  
  def enable_detailed_logging
    ENV['RUBY_YJIT_LOG'] = '1'
    @logging_active = true
    puts "Enabled detailed YJIT logging due to performance concerns"
  end
  
  def schedule_logging_disable
    Thread.new do
      sleep 600  # Log for 10 minutes
      ENV.delete('RUBY_YJIT_LOG')
      @logging_active = false
      puts "Disabled detailed YJIT logging"
    end
  end
end

# Integrate with application health checks
logger = SmartYJITLogger.new

# Call periodically from application code
def periodic_health_check
  logger.maybe_enable_logging
  # Other health checks...
end

Deployment strategies for YJIT Logging include staged rollouts with logging enabled only on specific application instances. This approach provides production insights while limiting performance impact. Ruby applications can conditionally enable logging based on environment variables, instance roles, or traffic sampling.

class ProductionYJITConfig
  def self.configure
    case ENV['DEPLOYMENT_STAGE']
    when 'canary'
      enable_full_logging
    when 'production'
      enable_minimal_logging if sample_instance?
    when 'development'
      enable_development_logging
    end
  end
  
  private
  
  def self.enable_full_logging
    ENV['RUBY_YJIT_ENABLE'] = '1'
    ENV['RUBY_YJIT_LOG'] = '1'
    ENV['RUBY_YJIT_STATS'] = '1'
  end
  
  def self.enable_minimal_logging
    ENV['RUBY_YJIT_ENABLE'] = '1'
    # No detailed logging, only stats collection
  end
  
  def self.enable_development_logging
    ENV['RUBY_YJIT_ENABLE'] = '1'
    ENV['RUBY_YJIT_LOG'] = '1'
    ENV['RUBY_YJIT_VERBOSE'] = '1'
  end
  
  def self.sample_instance?
    instance_id = ENV['INSTANCE_ID'] || Random.rand(1000)
    instance_id.to_i % 10 == 0  # 10% sampling
  end
end

# Configure logging based on deployment context
ProductionYJITConfig.configure

Reference

Environment Variables

Variable Values Description
RUBY_YJIT_ENABLE 0, 1 Enables YJIT compilation
RUBY_YJIT_LOG 0, 1 Enables compilation event logging
RUBY_YJIT_VERBOSE 0, 1 Enables verbose compilation logging
RUBY_YJIT_STATS 0, 1 Enables extended statistics collection
RUBY_YJIT_LOG_FILE file path Redirects logs to specified file
RUBY_YJIT_GREEDY_VERSIONING 0, 1 Enables aggressive method versioning
RUBY_YJIT_CALL_THRESHOLD integer Sets compilation threshold (default: 30)

Runtime Statistics Methods

Method Returns Description
RubyVM::YJIT.runtime_stats Hash or nil Returns current compilation statistics
RubyVM::YJIT.enabled? Boolean Checks if YJIT is enabled
RubyVM::YJIT.stats_enabled? Boolean Checks if statistics collection is enabled

Statistics Hash Keys

Key Type Description
:compiled_iseq_count Integer Number of compiled instruction sequences
:iseq_count Integer Total number of instruction sequences
:code_region_size Integer Total bytes allocated for JIT code
:inline_code_size Integer Bytes used for inlined code
:outlined_code_size Integer Bytes used for outlined code
:guard_failure_count Integer Number of guard failures
:compilation_failure_count Integer Number of compilation failures
:invalidation_count Integer Number of code invalidations
:constant_state_bumps Integer Constant cache invalidation events
:inline_cache_count Integer Number of inline cache entries

Log Message Patterns

Pattern Example Meaning
compile: compile: String#upcase (45 bytes) Method compilation event
inline: inline: Array#map Method inlining event
guard_failure: guard_failure: class mismatch Guard condition failure
deopt: deopt: constant invalidation Code deoptimization event
invalidate: invalidate: method redefined Code invalidation event

Error Conditions

Condition Cause Resolution
nil statistics YJIT disabled or unavailable Enable with RUBY_YJIT_ENABLE=1
Missing log output Logging disabled Set RUBY_YJIT_LOG=1
High guard failures Polymorphic call sites Refactor to reduce type variance
Frequent invalidations Dynamic code modification Minimize runtime method redefinition
Memory growth Excessive compilation Monitor code_region_size statistics

Performance Thresholds

Metric Good Acceptable Poor
Compilation ratio >30% 10-30% <10%
Guard failure rate <5% 5-15% >15%
Memory per method <500 bytes 500-1000 bytes >1000 bytes
Invalidation rate <1% 1-5% >5%