|
# 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)
|