-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add benchmark regression runner #1588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
#!/usr/bin/env ruby | ||
require 'fileutils' | ||
require 'pathname' | ||
require 'shellwords' | ||
require 'English' | ||
|
||
############################ | ||
# USAGE | ||
# | ||
# bundle exec bin/bench_regression <ref1> <ref2> | ||
# <ref1> defaults to the current branch | ||
# <ref2> defaults to the master branch | ||
# bundle exec bin/bench_regression current # will run on the current branch | ||
# bundle exec bin/bench_regression revisions 792fb8a90 master # every revision inclusive | ||
# bundle exec bin/bench_regression 792fb8a90 master --repeat-count 2 --env CACHE_ON=off | ||
# bundle exec bin/bench_regression vendor | ||
########################### | ||
|
||
class BenchRegression | ||
ROOT = Pathname File.expand_path(File.join(*['..', '..']), __FILE__) | ||
TMP_DIR_NAME = File.join('tmp', 'bench') | ||
TMP_DIR = File.join(ROOT, TMP_DIR_NAME) | ||
E_TMP_DIR = Shellwords.shellescape(TMP_DIR) | ||
load ROOT.join('bin', 'bench') | ||
|
||
attr_reader :source_stasher | ||
|
||
def initialize | ||
@source_stasher = SourceStasher.new | ||
end | ||
|
||
class SourceStasher | ||
attr_reader :gem_require_paths, :gem_paths | ||
attr_writer :vendor | ||
|
||
def initialize | ||
@gem_require_paths = [] | ||
@gem_paths = [] | ||
refresh_temp_dir | ||
@vendor = false | ||
end | ||
|
||
def temp_dir_empty? | ||
File.directory?(TMP_DIR) && | ||
Dir[File.join(TMP_DIR, '*')].none? | ||
end | ||
|
||
def empty_temp_dir | ||
return if @vendor | ||
return if temp_dir_empty? | ||
FileUtils.mkdir_p(TMP_DIR) | ||
Dir[File.join(TMP_DIR, '*')].each do |file| | ||
if File.directory?(file) | ||
FileUtils.rm_rf(file) | ||
else | ||
FileUtils.rm(file) | ||
end | ||
end | ||
end | ||
|
||
def fill_temp_dir | ||
vendor_files(Dir[File.join(ROOT, 'test', 'benchmark', '*.{rb,ru}')]) | ||
# vendor_file(File.join('bin', 'bench')) | ||
housekeeping { empty_temp_dir } | ||
vendor_gem('benchmark-ips') | ||
end | ||
|
||
def vendor_files(files) | ||
files.each do |file| | ||
vendor_file(file) | ||
end | ||
end | ||
|
||
def vendor_file(file) | ||
FileUtils.cp(file, File.join(TMP_DIR, File.basename(file))) | ||
end | ||
|
||
def vendor_gem(gem_name) | ||
directory_name = `bundle exec gem unpack benchmark-ips --target=#{E_TMP_DIR}`[/benchmark-ips.+\d/] | ||
gem_paths << File.join(TMP_DIR, directory_name) | ||
gem_require_paths << File.join(TMP_DIR_NAME, directory_name, 'lib') | ||
housekeeping { remove_vendored_gems } | ||
end | ||
|
||
def remove_vendored_gems | ||
return if @vendor | ||
FileUtils.rm_rf(*gem_paths) | ||
end | ||
|
||
def refresh_temp_dir | ||
empty_temp_dir | ||
fill_temp_dir | ||
end | ||
|
||
def housekeeping | ||
at_exit { yield } | ||
end | ||
end | ||
|
||
module RevisionMethods | ||
module_function | ||
def current_branch | ||
@current_branch ||= `cat .git/HEAD | cut -d/ -f3,4,5`.chomp | ||
end | ||
|
||
def current_revision | ||
`git rev-parse --short HEAD`.chomp | ||
end | ||
|
||
def revision_description(rev) | ||
`git log --oneline -1 #{rev}`.chomp | ||
end | ||
|
||
def revisions(start_ref, end_ref) | ||
cmd = "git rev-list --reverse #{start_ref}..#{end_ref}" | ||
`#{cmd}`.chomp.split("\n") | ||
end | ||
|
||
def checkout_ref(ref) | ||
`git checkout #{ref}`.chomp | ||
if $CHILD_STATUS | ||
STDERR.puts "Checkout failed: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success? | ||
$CHILD_STATUS.success? | ||
else | ||
true | ||
end | ||
end | ||
|
||
def clean_head | ||
system('git reset --hard --quiet') | ||
end | ||
end | ||
module ShellMethods | ||
|
||
def sh(cmd) | ||
puts cmd | ||
# system(cmd) | ||
run(cmd) | ||
# env = {} | ||
# # out = STDOUT | ||
# pid = spawn(env, cmd) | ||
# Process.wait(pid) | ||
# pid = fork do | ||
# exec cmd | ||
# end | ||
# Process.waitpid2(pid) | ||
# puts $CHILD_STATUS.exitstatus | ||
end | ||
|
||
require 'pty' | ||
# should consider trapping SIGINT in here | ||
def run(cmd) | ||
puts cmd | ||
child_process = '' | ||
result = '' | ||
# http://stackoverflow.com/a/1162850 | ||
# stream output of subprocess | ||
begin | ||
PTY.spawn(cmd) do |stdin, _stdout, pid| | ||
begin | ||
# Do stuff with the output here. Just printing to show it works | ||
stdin.each do |line| | ||
print line | ||
result << line | ||
end | ||
child_process = PTY.check(pid) | ||
rescue Errno::EIO | ||
puts 'Errno:EIO error, but this probably just means ' \ | ||
'that the process has finished giving output' | ||
end | ||
end | ||
rescue PTY::ChildExited | ||
puts 'The child process exited!' | ||
end | ||
unless (child_process && child_process.success?) | ||
exitstatus = child_process.exitstatus | ||
puts "FAILED: #{child_process.pid} exited with status #{exitstatus.inspect} due to failed command #{cmd}" | ||
exit exitstatus || 1 | ||
end | ||
result | ||
end | ||
|
||
def bundle(ref) | ||
system("rm -f Gemfile.lock") | ||
# This is absolutely critical for bundling to work | ||
Bundler.with_clean_env do | ||
system("bundle check || | ||
bundle install --local || | ||
bundle install || | ||
bundle update") | ||
end | ||
|
||
# if $CHILD_STATUS | ||
# STDERR.puts "Bundle failed at: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success? | ||
# $CHILD_STATUS.success? | ||
# else | ||
# false | ||
# end | ||
end | ||
end | ||
include ShellMethods | ||
include RevisionMethods | ||
|
||
def benchmark_refs(ref1: nil, ref2: nil, cmd:) | ||
checking_out = false | ||
ref0 = current_branch | ||
ref1 ||= current_branch | ||
ref2 ||= 'master' | ||
p [ref0, ref1, ref2, current_revision] | ||
|
||
run_benchmark_at_ref(cmd, ref1) | ||
p [ref0, ref1, ref2, current_revision] | ||
run_benchmark_at_ref(cmd, ref2) | ||
p [ref0, ref1, ref2, current_revision] | ||
|
||
checking_out = true | ||
checkout_ref(ref0) | ||
rescue Exception # rubocop:disable Lint/RescueException | ||
STDERR.puts "[ERROR] #{$!.message}" | ||
checkout_ref(ref0) unless checking_out | ||
raise | ||
end | ||
|
||
def benchmark_revisions(ref1: nil, ref2: nil, cmd:) | ||
checking_out = false | ||
ref0 = current_branch | ||
ref1 ||= current_branch | ||
ref2 ||= 'master' | ||
|
||
revisions(ref1, ref2).each do |rev| | ||
STDERR.puts "Checking out: #{revision_description(rev)}" | ||
|
||
run_benchmark_at_ref(cmd, rev) | ||
clean_head | ||
end | ||
checking_out = true | ||
checkout_ref(ref0) | ||
rescue Exception # rubocop:disable Lint/RescueException | ||
STDERR.puts "[ERROR]: #{$!.message}" | ||
checkout_ref(ref0) unless checking_out | ||
raise | ||
end | ||
|
||
def run_benchmark_at_ref(cmd, ref) | ||
checkout_ref(ref) | ||
run_benchmark(cmd, ref) | ||
end | ||
|
||
def run_benchmark(cmd, ref = nil) | ||
ref ||= current_revision | ||
bundle(ref) && | ||
benchmark_tests(cmd, ref) | ||
end | ||
|
||
def benchmark_tests(cmd, ref) | ||
base = E_TMP_DIR | ||
# cmd.sub('bin/bench', 'tmp/revision_runner/bench') | ||
# bundle = Gem.bin('bunle' | ||
# Bundler.with_clean_env(&block) | ||
|
||
# cmd = Shellwords.shelljoin(cmd) | ||
# cmd = "COMMIT_HASH=#{ref} BASE=#{base} bundle exec ruby -rbenchmark/ips #{cmd}" | ||
# Add vendoring benchmark/ips to load path | ||
|
||
# CURRENT THINKING: IMPORTANT | ||
# Pass into require statement as RUBYOPTS i.e. via env rather than command line argument | ||
# otherwise, have a 'fast ams benchmarking' module that extends benchmarkings to add the 'ams' | ||
# method but doesn't depend on benchmark-ips | ||
options = { | ||
commit_hash: ref, | ||
base: base, | ||
rubyopt: Shellwords.shellescape("-Ilib:#{source_stasher.gem_require_paths.join(':')}") | ||
} | ||
BenchmarkDriver.parse_argv_and_run(ARGV.dup, options) | ||
end | ||
end | ||
|
||
if $PROGRAM_NAME == __FILE__ | ||
benchmarking = BenchRegression.new | ||
|
||
case ARGV[0] | ||
when 'current' | ||
# Run current branch only | ||
|
||
# super simple command line parsing | ||
args = ARGV.dup | ||
_ = args.shift # remove 'current' from args | ||
cmd = args | ||
benchmarking.run_benchmark(cmd) | ||
when 'revisions' | ||
# Runs on every revision | ||
|
||
# super simple command line parsing | ||
args = ARGV.dup | ||
_ = args.shift | ||
ref1 = args.shift # remove 'revisions' from args | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't the comment be on the line above? |
||
ref2 = args.shift | ||
cmd = args | ||
benchmarking.benchmark_revisions(ref1: ref1, ref2: ref2, cmd: cmd) | ||
when 'vendor' | ||
# Just prevents vendored files from being cleaned up | ||
# at exit. (They are vendored at initialize.) | ||
benchmarking.source_stasher.vendor = true | ||
else | ||
# Default: Compare current_branch to master | ||
# Optionally: pass in two refs as args to `bin/bench_regression` | ||
# TODO: Consider checking across more revisions, to automatically find problems. | ||
|
||
# super simple command line parsing | ||
args = ARGV.dup | ||
ref1 = args.shift | ||
ref2 = args.shift | ||
cmd = args | ||
benchmarking.benchmark_refs(ref1: ref1, ref2: ref2, cmd: cmd) | ||
end | ||
end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be muted. Here is what I get as output: