Project

General

Profile

Bug #22396 ยป profiling_jit.rb

ivoanjo (Ivo Anjo), 09/30/2026 09:53 AM

 
# Reproduces a Ruby profiler/JIT performance issue: postponed jobs triggered by
# sampling can force YJIT and ZJIT out of compiled code, slowing a CPU-bound loop.
# Compares profiling off, on, then off again to show the slowdown and recovery.
#
# Uses benchmark-ips + datadog or rperf or stackperf
#
# 1. With interpreter no issue:
# PROFILER=datadog ruby --disable-yjit --disable-zjit profiling_jit.rb
# 2. With YJIT shows issue:
# PROFILER=datadog ruby --yjit --yjit-call-threshold=1 profiling_jit.rb
# 3. With ZJIT as well:
# PROFILER=datadog ruby --zjit --zjit-call-threshold=2 profiling_jit.rb
#
# To inspect side exits, add --yjit-stats=quiet or --zjit-stats-quiet
# (Note: the yjit/zjit stats add a lot of overhead, so that's why you want to test without them when looking at perf numbers)

require "benchmark/ips"

module DatadogProfiler
def self.start
require "datadog"
Datadog.configure do |c|
c.diagnostics.startup_logs.enabled = false
c.tracing.enabled = false
c.telemetry.enabled = false
c.remote.enabled = false
c.profiling.enabled = true
c.profiling.advanced.gc_enabled = false
c.profiling.exporter.transport = Object.new.tap { def it.export(_flush) = true }
end
Datadog::Profiling.wait_until_running
end

def self.stop = Datadog.shutdown!
end

module StackProfProfiler
def self.start
require "stackprof"
StackProf.start(mode: :wall, interval: 1000, raw: false, ignore_gc: true)
end

def self.stop = StackProf.stop
end

module RperfProfiler
def self.start
require "rperf"
Rperf.start(mode: :wall, frequency: 1000, inherit: false)
end

def self.stop = Rperf.stop
end

def cpu_loop
i = 0
i += 1 while i < 10_000_000
end

def side_exits
return RubyVM::YJIT.runtime_stats[:side_exit_count] if defined?(RubyVM::YJIT) && RubyVM::YJIT.stats_enabled?
RubyVM::ZJIT.stats[:side_exit_count] if defined?(RubyVM::ZJIT) && RubyVM::ZJIT.stats_enabled?
end

def measure(label)
before = side_exits
report = Benchmark.ips(time: 5, warmup: 0) do |x|
x.report(label) { cpu_loop }\
end
puts "Side exits: #{side_exits - before}" if before
report.entries.first
end

profiler = {
"datadog" => DatadogProfiler,
"stackprof" => StackProfProfiler,
"rperf" => RperfProfiler,
"none" => nil,
}.fetch(ENV.fetch("PROFILER"))

puts "Profiler: #{ENV.fetch("PROFILER")}"
puts

# Warm up the profiler...
profiler&.start
sleep 1
profiler&.stop

puts RUBY_DESCRIPTION

measure("Warmup")
off_before = measure("Profiler off")
profiler&.start
on = measure("Profiler on")
profiler&.stop
off_after = measure("Profiler off again")

Benchmark.compare(off_before, on, off_after)
    (1-1/1)