mirror of
https://github.com/ruby/ruby.git
synced 2026-10-29 00:46:26 +08:00
When running benchmarks, sometimes there is a overhead in the statistics collection, so it can be useful to disable `zjit-stats`. In my case I was investigating https://github.com/Shopify/ruby/issues/1051 and was confused why I was not seeing any new instruction being generated in the LIR but there was still performance difference when running zjit-diff. The cause for the performance difference in that PR was new instructions to increment stats counters. So this PR allow running the tool without stats enabled. I'm even wondering if the default should not be with it disabled.
278 lines
7.3 KiB
Ruby
Executable File
278 lines
7.3 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require 'fileutils'
|
|
require 'optparse'
|
|
require 'tmpdir'
|
|
require 'logger'
|
|
require 'digest'
|
|
require 'shellwords'
|
|
|
|
GitRef = Struct.new(:ref, :commit_hash)
|
|
|
|
RUBIES_DIR = File.join(Dir.home, '.zjit-diff')
|
|
BEFORE_NAME = 'ruby-zjit-before'
|
|
AFTER_NAME = 'ruby-zjit-after'
|
|
|
|
LOG = Logger.new($stderr)
|
|
|
|
def macos?
|
|
Gem::Platform.local == 'darwin'
|
|
end
|
|
|
|
class CommandRunner
|
|
def initialize(quiet: false)
|
|
@quiet = quiet
|
|
end
|
|
|
|
def cmd(*args, **options)
|
|
options[:out] ||= @quiet ? File::NULL : $stderr
|
|
options = options.merge(exception: true)
|
|
system(*args, **options)
|
|
end
|
|
end
|
|
|
|
class ZJITDiff
|
|
DATA_FILENAME = File.join('data', 'zjit_diff')
|
|
RUBY_BENCH_REPO_URL = 'https://github.com/ruby/ruby-bench.git'
|
|
|
|
def initialize(before_hash:, after_hash:, runner:, options:)
|
|
@before_hash = before_hash
|
|
@after_hash = after_hash
|
|
@runner = runner
|
|
@options = options
|
|
end
|
|
|
|
def bench!
|
|
LOG.info('Running benchmarks')
|
|
ruby_bench_path = @options[:bench_path] || setup_ruby_bench
|
|
run_benchmarks(ruby_bench_path)
|
|
end
|
|
|
|
private
|
|
|
|
def run_benchmarks(ruby_bench_path)
|
|
Dir.chdir(ruby_bench_path) do
|
|
zjit_stats = @options[:zjit_stats] ? ' --zjit-stats' : ''
|
|
@runner.cmd({ 'RUBIES_DIR' => RUBIES_DIR },
|
|
'./run_benchmarks.rb',
|
|
'--chruby',
|
|
"before::#{@before_hash}#{zjit_stats};after::#{@after_hash}#{zjit_stats}",
|
|
'--out-name',
|
|
DATA_FILENAME,
|
|
*@options[:bench_args],
|
|
*@options[:name_filters])
|
|
|
|
@runner.cmd('./misc/zjit_diff.rb', "#{DATA_FILENAME}.json", out: $stdout)
|
|
end
|
|
end
|
|
|
|
def setup_ruby_bench
|
|
path = File.join(Dir.tmpdir, 'ruby-bench')
|
|
if Dir.exist?(path)
|
|
LOG.info('ruby-bench already cloned, pulling from upstream')
|
|
Dir.chdir(path) do
|
|
@runner.cmd('git', 'pull')
|
|
end
|
|
else
|
|
LOG.info("ruby-bench not cloned yet, cloning repository to #{path}")
|
|
@runner.cmd('git', 'clone', RUBY_BENCH_REPO_URL, path)
|
|
end
|
|
path
|
|
end
|
|
end
|
|
|
|
class RubyWorktree
|
|
attr_reader :hash
|
|
|
|
BREW_REQUIRED_PACKAGES = %w[openssl readline libyaml].freeze
|
|
|
|
def initialize(name:, ref:, runner:, force_rebuild: false)
|
|
@path = File.join(Dir.tmpdir, name)
|
|
@ref = ref
|
|
@force_rebuild = force_rebuild
|
|
@runner = runner
|
|
@hash = nil
|
|
|
|
setup_worktree
|
|
end
|
|
|
|
def build!
|
|
Dir.chdir(@path) do
|
|
configure_cmd_args = ['--enable-zjit=dev', '--disable-install-doc']
|
|
if macos?
|
|
brew_prefixes = BREW_REQUIRED_PACKAGES.map do |pkg|
|
|
`brew --prefix #{pkg}`.strip
|
|
end
|
|
configure_cmd_args << "--with-opt-dir=#{brew_prefixes.join(':')}"
|
|
end
|
|
configure_cmd_hash = Digest::MD5.hexdigest(configure_cmd_args.join(''))
|
|
|
|
build_cmd_args = ['-j', 'miniruby']
|
|
build_cmd_hash = Digest::MD5.hexdigest(build_cmd_args.join(''))
|
|
|
|
@hash = "#{configure_cmd_hash}-#{build_cmd_hash}-#{@ref.commit_hash}"
|
|
prefix = File.join(RUBIES_DIR, @hash)
|
|
|
|
if Dir.exist?(prefix) && !@force_rebuild
|
|
LOG.info("Found existing build for #{@ref.ref}, skipping build")
|
|
return
|
|
end
|
|
|
|
@runner.cmd('./autogen.sh')
|
|
|
|
cmd = [
|
|
'./configure',
|
|
*configure_cmd_args,
|
|
"--prefix=#{prefix}"
|
|
]
|
|
|
|
@runner.cmd(*cmd)
|
|
@runner.cmd('make', *build_cmd_args)
|
|
@runner.cmd('make', 'install')
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def setup_worktree
|
|
if Dir.exist?(@path)
|
|
LOG.info("Existing worktree found at #{@path}")
|
|
Dir.chdir(@path) do
|
|
@runner.cmd('git', 'checkout', @ref.commit_hash)
|
|
end
|
|
else
|
|
LOG.info("Creating worktree for ref '#{@ref.ref}' at #{@path}")
|
|
@runner.cmd('git', 'worktree', 'add', '--detach', @path, @ref.commit_hash)
|
|
end
|
|
end
|
|
end
|
|
|
|
def clean!
|
|
[BEFORE_NAME, AFTER_NAME].each do |name|
|
|
path = File.join(Dir.tmpdir, name)
|
|
if Dir.exist?(path)
|
|
LOG.info("Removing worktree at #{path}")
|
|
system('git', 'worktree', 'remove', '--force', path)
|
|
end
|
|
end
|
|
|
|
if Dir.exist?(RUBIES_DIR)
|
|
LOG.info("Removing ruby installations from #{RUBIES_DIR}")
|
|
FileUtils.rm_rf(RUBIES_DIR)
|
|
end
|
|
|
|
bench_path = File.join(Dir.tmpdir, 'ruby-bench')
|
|
return unless Dir.exist?(bench_path)
|
|
|
|
LOG.info("Removing ruby-bench clone at #{bench_path}")
|
|
FileUtils.rm_rf(bench_path)
|
|
end
|
|
|
|
def parse_ref(ref)
|
|
out = `git rev-parse --verify #{ref}`
|
|
return nil unless $?.success?
|
|
|
|
GitRef.new(ref: ref, commit_hash: out.strip)
|
|
end
|
|
|
|
DEFAULT_BENCHMARKS = %w[lobsters railsbench].freeze
|
|
|
|
options = { zjit_stats: true }
|
|
|
|
subtext = <<~HELP
|
|
Subcommands:
|
|
bench : Run benchmarks
|
|
clean : Clean temporary files created by benchmarks
|
|
See '#{$PROGRAM_NAME} COMMAND --help' for more information on a specific command.
|
|
HELP
|
|
|
|
top_level = OptionParser.new do |opts|
|
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
opts.separator('')
|
|
opts.separator(subtext)
|
|
end
|
|
|
|
subcommands = {
|
|
'bench' => OptionParser.new do |opts|
|
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options] <benchmarks to run>"
|
|
|
|
opts.on('--before REF', 'Git ref for ruby (before)') do |ref|
|
|
git_ref = parse_ref ref
|
|
if git_ref.nil?
|
|
warn "Error: '#{ref}' is not a valid git ref"
|
|
exit 1
|
|
end
|
|
|
|
options[:before] = git_ref
|
|
end
|
|
|
|
opts.on('--after REF', 'Git ref for ruby (after)') do |ref|
|
|
git_ref = parse_ref ref
|
|
if git_ref.nil?
|
|
warn "Error: '#{ref}' is not a valid git ref"
|
|
exit 1
|
|
end
|
|
|
|
options[:after] = git_ref
|
|
end
|
|
|
|
opts.on('--bench-path PATH',
|
|
'Path to an existing ruby-bench repository clone ' \
|
|
'(if not specified, ruby-bench will be cloned automatically to a temporary directory)') do |path|
|
|
options[:bench_path] = path
|
|
end
|
|
|
|
opts.on('--bench-args ARGS', 'Args to pass to ruby-bench') do |bench_args|
|
|
options[:bench_args] = bench_args.shellsplit
|
|
end
|
|
|
|
opts.on('--force-rebuild',
|
|
'Force building ruby again instead of using even if existing builds exist in the cache at ~/.diffs') do
|
|
options[:force_rebuild] = true
|
|
end
|
|
|
|
opts.on('--[no-]zjit-stats', 'Pass --zjit-stats to each Ruby command') do |zjit_stats|
|
|
options[:zjit_stats] = zjit_stats
|
|
end
|
|
|
|
opts.on('--quiet', 'Silence output of commands except for benchmark result') do
|
|
options[:quiet] = true
|
|
end
|
|
|
|
opts.separator('')
|
|
opts.separator('If no benchmarks are specified, the benchmarks that will be run are:')
|
|
opts.separator(DEFAULT_BENCHMARKS.join(', '))
|
|
end,
|
|
'clean' => OptionParser.new do |opts|
|
|
end
|
|
}
|
|
|
|
top_level.order!
|
|
command = ARGV.shift
|
|
subcommands[command].order!
|
|
|
|
case command
|
|
when 'bench'
|
|
options[:name_filters] = ARGV.empty? ? DEFAULT_BENCHMARKS : ARGV
|
|
options[:after] ||= parse_ref('HEAD')
|
|
|
|
runner = CommandRunner.new(quiet: options[:quiet])
|
|
|
|
before = RubyWorktree.new(name: BEFORE_NAME,
|
|
ref: options[:before],
|
|
runner: runner,
|
|
force_rebuild: options[:force_rebuild])
|
|
before.build!
|
|
after = RubyWorktree.new(name: AFTER_NAME,
|
|
ref: options[:after],
|
|
runner: runner,
|
|
force_rebuild: options[:force_rebuild])
|
|
after.build!
|
|
|
|
zjit_diff = ZJITDiff.new(runner: runner, before_hash: before.hash, after_hash: after.hash, options: options)
|
|
zjit_diff.bench!
|
|
when 'clean'
|
|
clean!
|
|
end
|