Skip to content

Commit b233b7d

Browse files
committed
feat(bench): allow running benchmarks via glob (#41)
- allow specifying a glob of benchmarks to run instead of a directory - record results for plain Ruby execution, native tracer and pure Ruby tracer - compare traces structurally via JSON and print consolidated table - update just bench recipe to pass a glob
1 parent 798f1a0 commit b233b7d

File tree

2 files changed

+71
-45
lines changed

2 files changed

+71
-45
lines changed

Justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ alias t := test
33
test:
44
ruby -Itest test/test_tracer.rb
55

6-
bench name="heavy_work" write_report="console":
7-
ruby test/benchmarks/run_benchmarks.rb test/benchmarks/programs --write-report={{write_report}}
6+
bench pattern="*" write_report="console":
7+
ruby test/benchmarks/run_benchmarks.rb '{{pattern}}' --write-report={{write_report}}
88

99
build-extension:
1010
cargo build --release --manifest-path gems/native-tracer/ext/native_tracer/Cargo.toml

test/benchmarks/run_benchmarks.rb

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
require 'json'
55
require 'fileutils'
6-
require 'digest'
76
require 'benchmark'
87
require 'optparse'
98

@@ -14,82 +13,108 @@
1413

1514
options = { write_report: WRITE_REPORT_DEFAULT }
1615
OptionParser.new do |opts|
17-
opts.banner = 'Usage: ruby run_benchmarks.rb BENCHMARK_DIR [options]'
16+
opts.banner = 'Usage: ruby run_benchmarks.rb GLOB [options]'
1817
opts.on('--write-report=DEST', 'console or path to .json/.svg report') do |dest|
1918
options[:write_report] = dest
2019
end
2120
end.parse!
21+
pattern = ARGV.shift || abort('Usage: ruby run_benchmarks.rb GLOB [options]')
2222

23-
benchmark_dir = ARGV.shift || abort('Usage: ruby run_benchmarks.rb BENCHMARK_DIR [options]')
24-
unless Dir.exist?(benchmark_dir)
25-
abort("Benchmark directory not found: #{benchmark_dir}")
26-
end
27-
28-
# Collect benchmark names (file basenames without extension)
29-
benchmarks = Dir.glob(File.join(benchmark_dir, '*.rb')).map { |f| File.basename(f, '.rb') }
23+
# Collect benchmark names and match against the provided glob
24+
all_programs = Dir.glob(File.join(PROGRAMS_DIR, '*.rb')).map { |f| File.basename(f, '.rb') }
25+
benchmarks = all_programs.select { |name| File.fnmatch?(pattern, name) }
3026
if benchmarks.empty?
31-
abort("No benchmark files (*.rb) found in directory: #{benchmark_dir}")
27+
abort("No benchmarks match pattern: #{pattern}")
3228
end
3329

34-
# Compare two files for identical content
35-
def files_identical?(a, b)
36-
cmp_result = system('cmp', '-s', a, b)
37-
return $?.success? if !cmp_result.nil?
38-
File.binread(a) == File.binread(b)
30+
# Compare two trace files structurally
31+
def traces_equal?(a, b)
32+
JSON.parse(File.read(a)) == JSON.parse(File.read(b))
3933
end
4034

4135
# Run a single benchmark by name
42-
def run_benchmark(name, benchmark_dir)
43-
program = File.expand_path(File.join(benchmark_dir, "#{name}.rb"))
44-
fixture = File.join(FIXTURES_DIR, "#{name}_trace.json")
45-
output_dir = File.join(TMP_DIR, name)
46-
47-
FileUtils.mkdir_p(output_dir)
36+
def run_benchmark(name)
37+
program = File.join(PROGRAMS_DIR, "#{name}.rb")
38+
fixture = File.join(FIXTURES_DIR, "#{name}_trace.json")
4839
raise 'Reference trace unavailable' unless File.exist?(fixture)
4940

41+
base_dir = File.join(TMP_DIR, name)
42+
FileUtils.rm_rf(base_dir)
43+
44+
results = { name: name }
45+
46+
elapsed = Benchmark.realtime do
47+
system('ruby', program)
48+
raise 'Program failed' unless $?.success?
49+
end
50+
results[:ruby_ms] = (elapsed * 1000).round
51+
52+
native_dir = File.join(TMP_DIR, name, 'native')
53+
FileUtils.mkdir_p(native_dir)
54+
elapsed = Benchmark.realtime do
55+
system('ruby', File.expand_path('../../gems/native-tracer/lib/native_trace.rb', __dir__),
56+
'--out-dir', native_dir, program)
57+
raise 'Native trace failed' unless $?.success?
58+
end
59+
results[:native_ms] = (elapsed * 1000).round
60+
native_trace = File.join(native_dir, 'trace.json')
61+
results[:native_ok] = traces_equal?(fixture, native_trace)
62+
63+
pure_dir = File.join(TMP_DIR, name, 'pure')
64+
FileUtils.mkdir_p(pure_dir)
5065
elapsed = Benchmark.realtime do
5166
system('ruby', File.expand_path('../../gems/pure-ruby-tracer/lib/trace.rb', __dir__),
52-
'--out-dir', output_dir,
53-
program)
54-
raise 'Trace failed' unless $?.success?
67+
'--out-dir', pure_dir, program)
68+
raise 'Pure trace failed' unless $?.success?
5569
end
56-
runtime_ms = (elapsed * 1000).round
57-
output_trace = File.join(output_dir, 'trace.json')
58-
success = files_identical?(fixture, output_trace)
59-
size_bytes = File.size(output_trace)
70+
results[:pure_ms] = (elapsed * 1000).round
71+
pure_trace = File.join(pure_dir, 'trace.json')
72+
results[:pure_ok] = traces_equal?(fixture, pure_trace)
6073

61-
{ name: name, runtime_ms: runtime_ms, trace_size: size_bytes, success: success }
74+
results
6275
end
6376

6477
# Execute all benchmarks
65-
results = benchmarks.map { |b| run_benchmark(b, benchmark_dir) }
78+
results = benchmarks.map { |b| run_benchmark(b) }
6679

6780
# Reporting
6881
if options[:write_report] == 'console'
6982
# Determine column widths
70-
name_w = [ 'Benchmark'.length, *results.map { |r| r[:name].length } ].max
71-
rt_w = [ 'Runtime'.length, *results.map { |r| r[:runtime_ms].to_s.length } ].max
72-
ts_w = [ 'Trace Size'.length, *results.map { |r| r[:trace_size].to_s.length } ].max
83+
name_w = ['Benchmark'.length, *results.map { |r| r[:name].length }].max
84+
ruby_w = ['Ruby'.length, *results.map { |r| "#{r[:ruby_ms]}ms".length }].max
85+
native_w = ['Native'.length, *results.map { |r| "#{r[:native_ok] ? 'OK' : 'FAIL'} #{r[:native_ms]}ms".length }].max
86+
pure_w = ['Pure'.length, *results.map { |r| "#{r[:pure_ok] ? 'OK' : 'FAIL'} #{r[:pure_ms]}ms".length }].max
7387

7488
# Header
75-
printf "%-#{name_w}s %#{rt_w}s %#{ts_w}s %s\n", 'Benchmark', 'Runtime', 'Trace Size', 'Status'
76-
puts '-' * (name_w + rt_w + ts_w + 10)
89+
printf "%-#{name_w}s %-#{ruby_w}s %-#{native_w}s %-#{pure_w}s\n", 'Benchmark', 'Ruby', 'Native', 'Pure'
90+
puts '-' * (name_w + ruby_w + native_w + pure_w + 6)
7791

7892
# Rows
7993
results.each do |r|
80-
status = r[:success] ? 'OK' : 'FAIL'
81-
printf "%-#{name_w}s %#{rt_w}d ms %#{ts_w}d %s\n", r[:name], r[:runtime_ms], r[:trace_size], status
94+
ruby_s = "#{r[:ruby_ms]}ms"
95+
native_s = "#{r[:native_ok] ? 'OK' : 'FAIL'} #{r[:native_ms]}ms"
96+
pure_s = "#{r[:pure_ok] ? 'OK' : 'FAIL'} #{r[:pure_ms]}ms"
97+
printf "%-#{name_w}s %-#{ruby_w}s %-#{native_w}s %-#{pure_w}s\n", r[:name], ruby_s, native_s, pure_s
8298
end
8399

84100
# Exit with non-zero if any failed
85-
exit 1 unless results.all? { |r| r[:success] }
101+
exit 1 unless results.all? { |r| r[:native_ok] && r[:pure_ok] }
86102
else
87103
dest = options[:write_report]
88104
FileUtils.mkdir_p(File.dirname(dest))
89105

90106
case File.extname(dest)
91107
when '.json'
92-
data = results.map { |r| { benchmark: r[:name], runtime_ms: r[:runtime_ms], trace_bytes: r[:trace_size] } }
108+
data = results.map do |r|
109+
{
110+
benchmark: r[:name],
111+
ruby_ms: r[:ruby_ms],
112+
native_ms: r[:native_ms],
113+
native_ok: r[:native_ok],
114+
pure_ms: r[:pure_ms],
115+
pure_ok: r[:pure_ok]
116+
}
117+
end
93118
File.write(dest, JSON.pretty_generate(data))
94119
when '.svg'
95120
row_height = 25
@@ -98,11 +123,12 @@ def run_benchmark(name, benchmark_dir)
98123
svg << " <foreignObject width='100%' height='100%'>\n"
99124
svg << " <style>table{border-collapse:collapse;font-family:sans-serif;}td,th{border:1px solid #999;padding:4px;}</style>\n"
100125
svg << " <table>\n"
101-
svg << " <thead><tr><th>Benchmark</th><th>Runtime (ms)</th><th>Trace size (bytes)</th><th>Status</th></tr></thead>\n"
126+
svg << " <thead><tr><th>Benchmark</th><th>Ruby (ms)</th><th>Native</th><th>Pure</th></tr></thead>\n"
102127
svg << " <tbody>\n"
103128
results.each do |r|
104-
status = r[:success] ? 'OK' : 'FAIL'
105-
svg << " <tr><td>#{r[:name]}</td><td>#{r[:runtime_ms]}</td><td>#{r[:trace_size]}</td><td>#{status}</td></tr>\n"
129+
native_s = r[:native_ok] ? 'OK' : 'FAIL'
130+
pure_s = r[:pure_ok] ? 'OK' : 'FAIL'
131+
svg << " <tr><td>#{r[:name]}</td><td>#{r[:ruby_ms]}</td><td>#{native_s} #{r[:native_ms]}</td><td>#{pure_s} #{r[:pure_ms]}</td></tr>\n"
106132
end
107133
svg << " </tbody>\n"
108134
svg << " </table>\n"
@@ -114,7 +140,7 @@ def run_benchmark(name, benchmark_dir)
114140
end
115141

116142
# Warn and exit if any failures
117-
unless results.all? { |r| r[:success] }
143+
unless results.all? { |r| r[:native_ok] && r[:pure_ok] }
118144
warn 'One or more traces differ from reference!'
119145
exit 1
120146
end

0 commit comments

Comments
 (0)