Skip to content

Commit

Permalink
Collect ratio_in_yjit from YJIT stats if avalible (#54)
Browse files Browse the repository at this point in the history
* Collect ratio_in_yjit from YJIT stats if avalible

* This is only collected if --yjit-stats is set in RUBYOPT
* Since setting --yjit-stats has some overhead, we don't want to require it
* I added a bin that we can shell out to with various values of RUBYOPT
  to intergration test this stuff, rather than trying to enable /
disable yjit within the test suite process

* fmt

* Move test to run on all rubies
  • Loading branch information
errm authored Feb 27, 2024
1 parent 531f7fd commit 91ccf91
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 22 deletions.
11 changes: 11 additions & 0 deletions bin/yjit_intergration_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env ruby

require "bundler/setup"
require "promenade"
require "promenade/yjit/stats"
require "prometheus/client"
require "prometheus/client/formats/text"

Promenade.setup
Promenade::YJIT::Stats.instrument
puts Prometheus::Client::Formats::Text.marshal_multiprocess
13 changes: 12 additions & 1 deletion lib/promenade/yjit/stats.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
module Promenade
module YJIT
class Stats
RUNTIME_STATS = %i(
code_region_size
ratio_in_yjit
).freeze

Promenade.gauge :ruby_yjit_code_region_size do
doc "Ruby YJIT code size"
end

Promenade.gauge :ruby_yjit_ratio_in_yjit do
doc "Shows the ratio of YJIT-executed instructions in %"
end

def self.instrument
return unless defined?(::RubyVM::YJIT) && ::RubyVM::YJIT.enabled?

Promenade.metric(:ruby_yjit_code_region_size).set({}, ::RubyVM::YJIT.runtime_stats[:code_region_size])
::RubyVM::YJIT.runtime_stats.select { |stat, _| RUNTIME_STATS.include? stat }.each do |stat, value|
Promenade.metric(:"ruby_yjit_#{stat}").set({}, value)
end
end
end
end
Expand Down
69 changes: 48 additions & 21 deletions spec/yjit_spec.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
require "promenade/yjit/stats"
require "open3"

RSpec.describe Promenade::YJIT::Stats do
describe "recording yjit stats" do
it "records code_region_size" do
# This method should not blow up in any case
it "doesn't explode" do
# This method should not blow up in any case, on any version of ruby
expect { described_class.instrument }.not_to raise_error

if defined?(RubyVM::YJIT) && defined?(RubyVM::YJIT.enable)

# We want to test that this doesn't blow up when yjit is present but isn't enabled yet
# you need to run the testsuite with yjit disabled for this to work
expect(RubyVM::YJIT.enabled?).to be_falsey
expect { described_class.instrument }.not_to raise_error

# Then we enable yjit to test the instrumentation
RubyVM::YJIT.enable
described_class.instrument

expect(Promenade.metric(:ruby_yjit_code_region_size).get).to eq RubyVM::YJIT.runtime_stats[:code_region_size]
else
version = RUBY_VERSION.match(/(\d).(\d).\d/)
major = version[1].to_i
minor = version[2].to_i
if major >= 3 && minor >= 3
flunk "YJIT must be avalibe to test properly in ruby 3.3+"
end
metrics = run_yjit_metrics("")
expect(metrics).to be_empty
end

it "records yjit stats" do
version = RUBY_VERSION.match(/(\d).(\d).\d/)
major = version[1].to_i
minor = version[2].to_i
unless major >= 3 && minor >= 3
pending "YJIT metrics are only expected to work in ruby 3.3.0+"
end

metrics = run_yjit_metrics("--yjit")
expect(metrics[:ruby_yjit_code_region_size]).to satisfy("be nonzero") { |n| n > 0 }
# ratio_in_yjit is only set when --yjit-stats is enabled
expect(metrics[:ruby_yjit_ratio_in_yjit]).to be_nil

metrics = run_yjit_metrics("--yjit --yjit-stats=quiet")
expect(metrics[:ruby_yjit_code_region_size]).to satisfy("be nonzero") { |n| n > 0 }
expect(metrics[:ruby_yjit_ratio_in_yjit]).to satisfy("be nonzero") { |n| n > 0 }
end
end

def run_yjit_metrics(rubyopt)
dir = Dir.mktmpdir
begin
output, status = Open3.capture2e({ "PROMETHEUS_MULTIPROC_DIR" => dir, "RUBYOPT" => rubyopt }, "bin/yjit_intergration_test")
expect(status).to eq 0
parse_metrics(output)
ensure
FileUtils.remove_entry dir
end
end

def parse_metrics(output)
output.lines.reject { |line| line.match("#") }.filter_map do |line|
match = line.match(/([a-z_]+)\{.+\} (\d+\.?\d*)/)
next unless match

[match[1].to_sym, parse_number(match[2])]
end.to_h
end

def parse_number(string)
Integer(string)
rescue StandardError
string.to_f
end
end

0 comments on commit 91ccf91

Please sign in to comment.