Skip to content

Commit 33a0f9c

Browse files
author
Yohan Robert
committed
Merge pull request #1588 from bf4/benchmark_revision_runner
Add benchmark regression runner
2 parents 17711a8 + 146968d commit 33a0f9c

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed

bin/bench_regression

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
#!/usr/bin/env ruby
2+
require 'fileutils'
3+
require 'pathname'
4+
require 'shellwords'
5+
require 'English'
6+
7+
############################
8+
# USAGE
9+
#
10+
# bundle exec bin/bench_regression <ref1> <ref2>
11+
# <ref1> defaults to the current branch
12+
# <ref2> defaults to the master branch
13+
# bundle exec bin/bench_regression current # will run on the current branch
14+
# bundle exec bin/bench_regression revisions 792fb8a90 master # every revision inclusive
15+
# bundle exec bin/bench_regression 792fb8a90 master --repeat-count 2 --env CACHE_ON=off
16+
# bundle exec bin/bench_regression vendor
17+
###########################
18+
19+
class BenchRegression
20+
ROOT = Pathname File.expand_path(File.join(*['..', '..']), __FILE__)
21+
TMP_DIR_NAME = File.join('tmp', 'bench')
22+
TMP_DIR = File.join(ROOT, TMP_DIR_NAME)
23+
E_TMP_DIR = Shellwords.shellescape(TMP_DIR)
24+
load ROOT.join('bin', 'bench')
25+
26+
attr_reader :source_stasher
27+
28+
def initialize
29+
@source_stasher = SourceStasher.new
30+
end
31+
32+
class SourceStasher
33+
attr_reader :gem_require_paths, :gem_paths
34+
attr_writer :vendor
35+
36+
def initialize
37+
@gem_require_paths = []
38+
@gem_paths = []
39+
refresh_temp_dir
40+
@vendor = false
41+
end
42+
43+
def temp_dir_empty?
44+
File.directory?(TMP_DIR) &&
45+
Dir[File.join(TMP_DIR, '*')].none?
46+
end
47+
48+
def empty_temp_dir
49+
return if @vendor
50+
return if temp_dir_empty?
51+
FileUtils.mkdir_p(TMP_DIR)
52+
Dir[File.join(TMP_DIR, '*')].each do |file|
53+
if File.directory?(file)
54+
FileUtils.rm_rf(file)
55+
else
56+
FileUtils.rm(file)
57+
end
58+
end
59+
end
60+
61+
def fill_temp_dir
62+
vendor_files(Dir[File.join(ROOT, 'test', 'benchmark', '*.{rb,ru}')])
63+
# vendor_file(File.join('bin', 'bench'))
64+
housekeeping { empty_temp_dir }
65+
vendor_gem('benchmark-ips')
66+
end
67+
68+
def vendor_files(files)
69+
files.each do |file|
70+
vendor_file(file)
71+
end
72+
end
73+
74+
def vendor_file(file)
75+
FileUtils.cp(file, File.join(TMP_DIR, File.basename(file)))
76+
end
77+
78+
def vendor_gem(gem_name)
79+
directory_name = `bundle exec gem unpack benchmark-ips --target=#{E_TMP_DIR}`[/benchmark-ips.+\d/]
80+
gem_paths << File.join(TMP_DIR, directory_name)
81+
gem_require_paths << File.join(TMP_DIR_NAME, directory_name, 'lib')
82+
housekeeping { remove_vendored_gems }
83+
end
84+
85+
def remove_vendored_gems
86+
return if @vendor
87+
FileUtils.rm_rf(*gem_paths)
88+
end
89+
90+
def refresh_temp_dir
91+
empty_temp_dir
92+
fill_temp_dir
93+
end
94+
95+
def housekeeping
96+
at_exit { yield }
97+
end
98+
end
99+
100+
module RevisionMethods
101+
module_function
102+
def current_branch
103+
@current_branch ||= `cat .git/HEAD | cut -d/ -f3,4,5`.chomp
104+
end
105+
106+
def current_revision
107+
`git rev-parse --short HEAD`.chomp
108+
end
109+
110+
def revision_description(rev)
111+
`git log --oneline -1 #{rev}`.chomp
112+
end
113+
114+
def revisions(start_ref, end_ref)
115+
cmd = "git rev-list --reverse #{start_ref}..#{end_ref}"
116+
`#{cmd}`.chomp.split("\n")
117+
end
118+
119+
def checkout_ref(ref)
120+
`git checkout #{ref}`.chomp
121+
if $CHILD_STATUS
122+
STDERR.puts "Checkout failed: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
123+
$CHILD_STATUS.success?
124+
else
125+
true
126+
end
127+
end
128+
129+
def clean_head
130+
system('git reset --hard --quiet')
131+
end
132+
end
133+
module ShellMethods
134+
135+
def sh(cmd)
136+
puts cmd
137+
# system(cmd)
138+
run(cmd)
139+
# env = {}
140+
# # out = STDOUT
141+
# pid = spawn(env, cmd)
142+
# Process.wait(pid)
143+
# pid = fork do
144+
# exec cmd
145+
# end
146+
# Process.waitpid2(pid)
147+
# puts $CHILD_STATUS.exitstatus
148+
end
149+
150+
require 'pty'
151+
# should consider trapping SIGINT in here
152+
def run(cmd)
153+
puts cmd
154+
child_process = ''
155+
result = ''
156+
# http://stackoverflow.com/a/1162850
157+
# stream output of subprocess
158+
begin
159+
PTY.spawn(cmd) do |stdin, _stdout, pid|
160+
begin
161+
# Do stuff with the output here. Just printing to show it works
162+
stdin.each do |line|
163+
print line
164+
result << line
165+
end
166+
child_process = PTY.check(pid)
167+
rescue Errno::EIO
168+
puts 'Errno:EIO error, but this probably just means ' \
169+
'that the process has finished giving output'
170+
end
171+
end
172+
rescue PTY::ChildExited
173+
puts 'The child process exited!'
174+
end
175+
unless (child_process && child_process.success?)
176+
exitstatus = child_process.exitstatus
177+
puts "FAILED: #{child_process.pid} exited with status #{exitstatus.inspect} due to failed command #{cmd}"
178+
exit exitstatus || 1
179+
end
180+
result
181+
end
182+
183+
def bundle(ref)
184+
system("rm -f Gemfile.lock")
185+
# This is absolutely critical for bundling to work
186+
Bundler.with_clean_env do
187+
system("bundle check ||
188+
bundle install --local ||
189+
bundle install ||
190+
bundle update")
191+
end
192+
193+
# if $CHILD_STATUS
194+
# STDERR.puts "Bundle failed at: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
195+
# $CHILD_STATUS.success?
196+
# else
197+
# false
198+
# end
199+
end
200+
end
201+
include ShellMethods
202+
include RevisionMethods
203+
204+
def benchmark_refs(ref1: nil, ref2: nil, cmd:)
205+
checking_out = false
206+
ref0 = current_branch
207+
ref1 ||= current_branch
208+
ref2 ||= 'master'
209+
p [ref0, ref1, ref2, current_revision]
210+
211+
run_benchmark_at_ref(cmd, ref1)
212+
p [ref0, ref1, ref2, current_revision]
213+
run_benchmark_at_ref(cmd, ref2)
214+
p [ref0, ref1, ref2, current_revision]
215+
216+
checking_out = true
217+
checkout_ref(ref0)
218+
rescue Exception # rubocop:disable Lint/RescueException
219+
STDERR.puts "[ERROR] #{$!.message}"
220+
checkout_ref(ref0) unless checking_out
221+
raise
222+
end
223+
224+
def benchmark_revisions(ref1: nil, ref2: nil, cmd:)
225+
checking_out = false
226+
ref0 = current_branch
227+
ref1 ||= current_branch
228+
ref2 ||= 'master'
229+
230+
revisions(ref1, ref2).each do |rev|
231+
STDERR.puts "Checking out: #{revision_description(rev)}"
232+
233+
run_benchmark_at_ref(cmd, rev)
234+
clean_head
235+
end
236+
checking_out = true
237+
checkout_ref(ref0)
238+
rescue Exception # rubocop:disable Lint/RescueException
239+
STDERR.puts "[ERROR]: #{$!.message}"
240+
checkout_ref(ref0) unless checking_out
241+
raise
242+
end
243+
244+
def run_benchmark_at_ref(cmd, ref)
245+
checkout_ref(ref)
246+
run_benchmark(cmd, ref)
247+
end
248+
249+
def run_benchmark(cmd, ref = nil)
250+
ref ||= current_revision
251+
bundle(ref) &&
252+
benchmark_tests(cmd, ref)
253+
end
254+
255+
def benchmark_tests(cmd, ref)
256+
base = E_TMP_DIR
257+
# cmd.sub('bin/bench', 'tmp/revision_runner/bench')
258+
# bundle = Gem.bin('bunle'
259+
# Bundler.with_clean_env(&block)
260+
261+
# cmd = Shellwords.shelljoin(cmd)
262+
# cmd = "COMMIT_HASH=#{ref} BASE=#{base} bundle exec ruby -rbenchmark/ips #{cmd}"
263+
# Add vendoring benchmark/ips to load path
264+
265+
# CURRENT THINKING: IMPORTANT
266+
# Pass into require statement as RUBYOPTS i.e. via env rather than command line argument
267+
# otherwise, have a 'fast ams benchmarking' module that extends benchmarkings to add the 'ams'
268+
# method but doesn't depend on benchmark-ips
269+
options = {
270+
commit_hash: ref,
271+
base: base,
272+
rubyopt: Shellwords.shellescape("-Ilib:#{source_stasher.gem_require_paths.join(':')}")
273+
}
274+
BenchmarkDriver.parse_argv_and_run(ARGV.dup, options)
275+
end
276+
end
277+
278+
if $PROGRAM_NAME == __FILE__
279+
benchmarking = BenchRegression.new
280+
281+
case ARGV[0]
282+
when 'current'
283+
# Run current branch only
284+
285+
# super simple command line parsing
286+
args = ARGV.dup
287+
_ = args.shift # remove 'current' from args
288+
cmd = args
289+
benchmarking.run_benchmark(cmd)
290+
when 'revisions'
291+
# Runs on every revision
292+
293+
# super simple command line parsing
294+
args = ARGV.dup
295+
_ = args.shift
296+
ref1 = args.shift # remove 'revisions' from args
297+
ref2 = args.shift
298+
cmd = args
299+
benchmarking.benchmark_revisions(ref1: ref1, ref2: ref2, cmd: cmd)
300+
when 'vendor'
301+
# Just prevents vendored files from being cleaned up
302+
# at exit. (They are vendored at initialize.)
303+
benchmarking.source_stasher.vendor = true
304+
else
305+
# Default: Compare current_branch to master
306+
# Optionally: pass in two refs as args to `bin/bench_regression`
307+
# TODO: Consider checking across more revisions, to automatically find problems.
308+
309+
# super simple command line parsing
310+
args = ARGV.dup
311+
ref1 = args.shift
312+
ref2 = args.shift
313+
cmd = args
314+
benchmarking.benchmark_refs(ref1: ref1, ref2: ref2, cmd: cmd)
315+
end
316+
end

0 commit comments

Comments
 (0)