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% |