|
| 1 | +# A harness for Ruby implementations that compile the whole program ahead of |
| 2 | +# time, such as Spinel (https://github.com/matz/spinel). harness/harness.rb |
| 3 | +# relies on CRuby-only libraries and reflection (RubyVM, RbConfig, Fiddle, |
| 4 | +# Bundler, $LOAD_PATH) that a whole-program compiler refuses to compile, so |
| 5 | +# this file reimplements the same protocol in a portable subset of Ruby: |
| 6 | +# |
| 7 | +# * WARMUP_ITRS, MIN_BENCH_ITRS and MIN_BENCH_TIME select the iteration count. |
| 8 | +# * RESULT_JSON_PATH receives the JSON that run_benchmarks.rb reads. |
| 9 | +# |
| 10 | +# harness/loader.rb selects this harness automatically when RUBY_ENGINE is |
| 11 | +# "spinel", so `spinel -E benchmarks/fib.rb` works without any -I option. It |
| 12 | +# also runs on CRuby (`ruby -Iharness-spinel benchmarks/fib.rb`), which is how |
| 13 | +# its output is kept compatible with the default harness. |
| 14 | +require "json" |
| 15 | + |
| 16 | +# Warmup iterations |
| 17 | +WARMUP_ITRS = Integer(ENV.fetch('WARMUP_ITRS', '15')) |
| 18 | + |
| 19 | +# Minimum number of benchmarking iterations |
| 20 | +MIN_BENCH_ITRS = Integer(ENV.fetch('MIN_BENCH_ITRS', '10')) |
| 21 | + |
| 22 | +# Minimum benchmarking time in seconds |
| 23 | +MIN_BENCH_TIME = Integer(ENV.fetch('MIN_BENCH_TIME', '10')) |
| 24 | + |
| 25 | +# Do expand_path at require-time, not when returning results, before the benchmark is likely to chdir |
| 26 | +default_path = File.expand_path("../data/results-#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}-#{Time.now.strftime('%F-%H%M%S')}.json", __dir__) |
| 27 | +YB_OUTPUT_FILE = File.expand_path(ENV.fetch("RESULT_JSON_PATH", default_path)) |
| 28 | + |
| 29 | +puts RUBY_DESCRIPTION |
| 30 | + |
| 31 | +# Ractor.make_shareable is unavailable; benchmarks call this to share constants with Ractors. |
| 32 | +def make_shareable(obj, copy: false) |
| 33 | + obj |
| 34 | +end |
| 35 | + |
| 36 | +def realtime |
| 37 | + r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) |
| 38 | + yield |
| 39 | + Process.clock_gettime(Process::CLOCK_MONOTONIC) - r0 |
| 40 | +end |
| 41 | + |
| 42 | +# This returns its best estimate of the Resident Set Size in bytes. |
| 43 | +def get_rss |
| 44 | + mem_rollup_file = "/proc/#{Process.pid}/smaps_rollup" |
| 45 | + if File.exist?(mem_rollup_file) |
| 46 | + # Rss is always reported in kB by the Linux kernel, e.g. "Rss: 62796 kB" |
| 47 | + rss_line = File.read(mem_rollup_file).lines.find { |line| line.start_with?("Rss") } |
| 48 | + 1024 * rss_line.split(":")[1].to_i |
| 49 | + else |
| 50 | + 1024 * `ps -o rss= -p #{Process.pid}`.to_i |
| 51 | + end |
| 52 | +end |
| 53 | + |
| 54 | +# Takes a block as input |
| 55 | +def run_benchmark(_num_itrs_hint, &block) |
| 56 | + times = [] |
| 57 | + rss_samples = [] |
| 58 | + total_time = 0.0 |
| 59 | + num_itrs = 0 |
| 60 | + |
| 61 | + puts "itr: time" |
| 62 | + begin |
| 63 | + time = realtime(&block) |
| 64 | + num_itrs += 1 |
| 65 | + |
| 66 | + time_ms = (1000 * time).to_i |
| 67 | + puts "%4s %6s" % ["##{num_itrs}:", "#{time_ms}ms"] |
| 68 | + |
| 69 | + # We internally save the time in seconds to avoid loss of precision |
| 70 | + times << time |
| 71 | + total_time += time |
| 72 | + # Sample current RSS between iterations (outside the timed block) |
| 73 | + rss_samples << get_rss |
| 74 | + end until num_itrs >= WARMUP_ITRS + MIN_BENCH_ITRS && total_time >= MIN_BENCH_TIME |
| 75 | + |
| 76 | + warmup = times[0...WARMUP_ITRS] |
| 77 | + bench = times[WARMUP_ITRS..-1] |
| 78 | + return_results(warmup, bench, rss_samples[WARMUP_ITRS..-1]) |
| 79 | + |
| 80 | + if bench.size > 1 |
| 81 | + bench_ms = ((bench.sum / bench.size) * 1000.0).to_i |
| 82 | + puts "Average of last #{bench.size}, non-warmup iters: #{bench_ms}ms" |
| 83 | + end |
| 84 | +end |
| 85 | + |
| 86 | +def return_results(warmup_iterations, bench_iterations, rss_samples) |
| 87 | + # Full GC before measuring RSS to lower GC variance. |
| 88 | + GC.start |
| 89 | + |
| 90 | + rss = get_rss |
| 91 | + ruby_bench_results = { |
| 92 | + "RUBY_DESCRIPTION" => RUBY_DESCRIPTION, |
| 93 | + "warmup" => warmup_iterations, |
| 94 | + "bench" => bench_iterations, |
| 95 | + "rss_samples" => rss_samples, |
| 96 | + "rss" => rss, |
| 97 | + } |
| 98 | + |
| 99 | + puts "RSS: %.1fMiB" % (rss / 1024.0 / 1024.0) |
| 100 | + |
| 101 | + out_path = YB_OUTPUT_FILE |
| 102 | + system('mkdir', '-p', File.dirname(out_path)) |
| 103 | + |
| 104 | + # Using default path? Print where we put it. |
| 105 | + puts "Writing file #{out_path}" unless ENV["RESULT_JSON_PATH"] |
| 106 | + |
| 107 | + File.write(out_path, JSON.generate(ruby_bench_results)) |
| 108 | +end |
0 commit comments