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 |