|
| 1 | +#!/usr/bin/env ruby |
| 2 | +# frozen_string_literal: true |
| 3 | + |
1 | 4 | require 'json' |
2 | 5 | require 'fileutils' |
3 | 6 | require 'digest' |
4 | 7 | require 'benchmark' |
| 8 | +require 'optparse' |
5 | 9 |
|
6 | | -USAGE = "Usage: ruby run_benchmark.rb BENCHMARK_NAME" |
7 | | - |
8 | | -BENCHMARK = ARGV.shift || abort(USAGE) |
9 | 10 | HASHES = { |
10 | 11 | 'heavy_work' => '912fc0347cb8a57abd94a7defd76b147f3a79e556745e45207b89529f8a59d8b' |
11 | | -} |
| 12 | +}.freeze |
12 | 13 |
|
13 | | -unless HASHES.key?(BENCHMARK) |
14 | | - abort("Unknown benchmark '#{BENCHMARK}'") |
15 | | -end |
16 | | - |
17 | | -PROGRAM = File.join('test', 'benchmarks', 'programs', "#{BENCHMARK}.rb") |
18 | | -FIXTURE = File.expand_path("fixtures/#{BENCHMARK}_trace.json", __dir__) |
| 14 | +PROGRAMS_DIR = File.expand_path('programs', __dir__) |
| 15 | +FIXTURES_DIR = File.expand_path('fixtures', __dir__) |
19 | 16 | TMP_DIR = File.expand_path('tmp', __dir__) |
20 | | -OUTPUT_DIR = File.join(TMP_DIR, BENCHMARK) |
21 | | -EXPECTED_HASH = HASHES[BENCHMARK] |
22 | | - |
23 | | -FileUtils.mkdir_p(TMP_DIR) |
24 | | -FileUtils.mkdir_p(OUTPUT_DIR) |
25 | 17 |
|
26 | | -unless File.exist?(FIXTURE) && Digest::SHA256.file(FIXTURE).hexdigest == EXPECTED_HASH |
27 | | - warn "Reference trace missing or corrupt. Attempting to fetch via git lfs..." |
28 | | - system('git', 'lfs', 'pull', '--include', FIXTURE) |
29 | | -end |
30 | | - |
31 | | -raise 'reference trace unavailable' unless File.exist?(FIXTURE) |
32 | | -raise 'reference trace hash mismatch' unless Digest::SHA256.file(FIXTURE).hexdigest == EXPECTED_HASH |
| 18 | +WRITE_REPORT_DEFAULT = 'console' |
33 | 19 |
|
34 | | -elapsed = Benchmark.realtime do |
35 | | - system('ruby', File.expand_path('../../gems/pure-ruby-tracer/lib/trace.rb', __dir__), '--out-dir', OUTPUT_DIR, PROGRAM) |
36 | | - raise 'trace failed' unless $?.success? |
37 | | -end |
38 | | -puts "Benchmark runtime: #{(elapsed * 1000).round} ms" |
| 20 | +options = { write_report: WRITE_REPORT_DEFAULT } |
| 21 | +OptionParser.new do |opts| |
| 22 | + opts.banner = 'Usage: ruby run_benchmark.rb BENCHMARK_NAME [options]' |
| 23 | + opts.on('--write-report=DEST', 'console or path to .json/.svg report') do |dest| |
| 24 | + options[:write_report] = dest |
| 25 | + end |
| 26 | +end.parse! |
39 | 27 |
|
40 | 28 | def files_identical?(a, b) |
41 | 29 | cmp_result = system('cmp', '-s', a, b) |
42 | 30 | return $?.success? if !cmp_result.nil? |
43 | 31 | File.binread(a) == File.binread(b) |
44 | 32 | end |
45 | 33 |
|
46 | | -OUTPUT_TRACE = File.join(OUTPUT_DIR, 'trace.json') |
47 | | -if files_identical?(FIXTURE, OUTPUT_TRACE) |
48 | | - puts 'Trace matches reference.' |
| 34 | +def run_benchmark(name) |
| 35 | + program = File.join('test', 'benchmarks', 'programs', "#{name}.rb") |
| 36 | + fixture = File.expand_path("fixtures/#{name}_trace.json", __dir__) |
| 37 | + output_dir = File.join(TMP_DIR, name) |
| 38 | + expected_hash = HASHES[name] |
| 39 | + |
| 40 | + FileUtils.mkdir_p(TMP_DIR) |
| 41 | + FileUtils.mkdir_p(output_dir) |
| 42 | + |
| 43 | + unless File.exist?(fixture) && Digest::SHA256.file(fixture).hexdigest == expected_hash |
| 44 | + warn 'Reference trace missing or corrupt. Attempting to fetch via git lfs...' |
| 45 | + system('git', 'lfs', 'pull', '--include', fixture) |
| 46 | + end |
| 47 | + |
| 48 | + raise 'reference trace unavailable' unless File.exist?(fixture) |
| 49 | + raise 'reference trace hash mismatch' unless Digest::SHA256.file(fixture).hexdigest == expected_hash |
| 50 | + |
| 51 | + elapsed = Benchmark.realtime do |
| 52 | + system('ruby', File.expand_path('../../gems/pure-ruby-tracer/lib/trace.rb', __dir__), '--out-dir', output_dir, program) |
| 53 | + raise 'trace failed' unless $?.success? |
| 54 | + end |
| 55 | + runtime_ms = (elapsed * 1000).round |
| 56 | + |
| 57 | + output_trace = File.join(output_dir, 'trace.json') |
| 58 | + success = files_identical?(fixture, output_trace) |
| 59 | + size_bytes = File.size(output_trace) |
| 60 | + |
| 61 | + { name: name, runtime_ms: runtime_ms, trace_size: size_bytes, success: success } |
| 62 | +end |
| 63 | + |
| 64 | +if options[:write_report] == 'console' |
| 65 | + bench = ARGV.shift || abort('Usage: ruby run_benchmark.rb BENCHMARK_NAME [options]') |
| 66 | + abort("Unknown benchmark '#{bench}'") unless HASHES.key?(bench) |
| 67 | + result = run_benchmark(bench) |
| 68 | + puts "Benchmark runtime: #{result[:runtime_ms]} ms" |
| 69 | + if result[:success] |
| 70 | + puts 'Trace matches reference.' |
| 71 | + else |
| 72 | + warn 'Trace differs from reference!' |
| 73 | + exit 1 |
| 74 | + end |
49 | 75 | else |
50 | | - warn 'Trace differs from reference!' |
51 | | - exit 1 |
| 76 | + benches = ARGV.empty? ? HASHES.keys.sort : ARGV |
| 77 | + benches.each { |b| abort("Unknown benchmark '#{b}'") unless HASHES.key?(b) } |
| 78 | + results = benches.map { |b| run_benchmark(b) } |
| 79 | + |
| 80 | + dest = options[:write_report] |
| 81 | + FileUtils.mkdir_p(File.dirname(dest)) |
| 82 | + case File.extname(dest) |
| 83 | + when '.json' |
| 84 | + data = results.map { |r| { benchmark: r[:name], runtime_ms: r[:runtime_ms], trace_bytes: r[:trace_size] } } |
| 85 | + File.write(dest, JSON.pretty_generate(data)) |
| 86 | + when '.svg' |
| 87 | + row_height = 25 |
| 88 | + height = 40 + row_height * results.size |
| 89 | + svg = +"<svg xmlns='http://www.w3.org/2000/svg' width='500' height='#{height}'>\n" |
| 90 | + svg << " <foreignObject width='100%' height='100%'>\n" |
| 91 | + svg << " <style>table{border-collapse:collapse;font-family:sans-serif;}td,th{border:1px solid #999;padding:4px;}</style>\n" |
| 92 | + svg << " <table>\n" |
| 93 | + svg << " <thead><tr><th>Benchmark</th><th>Runtime (ms)</th><th>Trace size (bytes)</th></tr></thead>\n" |
| 94 | + svg << " <tbody>\n" |
| 95 | + results.each do |r| |
| 96 | + svg << " <tr><td>#{r[:name]}</td><td>#{r[:runtime_ms]}</td><td>#{r[:trace_size]}</td></tr>\n" |
| 97 | + end |
| 98 | + svg << " </tbody>\n" |
| 99 | + svg << " </table>\n" |
| 100 | + svg << " </foreignObject>\n" |
| 101 | + svg << "</svg>\n" |
| 102 | + File.write(dest, svg) |
| 103 | + else |
| 104 | + abort "Unknown report format '#{dest}'" |
| 105 | + end |
| 106 | + |
| 107 | + unless results.all? { |r| r[:success] } |
| 108 | + warn 'One or more traces differ from reference!' |
| 109 | + exit 1 |
| 110 | + end |
52 | 111 | end |
| 112 | + |
0 commit comments