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