|
| 1 | +# Loaded with `ruby -r` (through RUBYOPT) when RESULTS_DIR is set. Every |
| 2 | +# Benchmark.ips call in a benchmark file also writes its report as JSON to |
| 3 | +# RESULTS_DIR/<label>/<benchmark>.json, so the benchmark files stay untouched. |
| 4 | +# Each result also records where it ran, so only comparable numbers get |
| 5 | +# compared. |
| 6 | +# |
| 7 | +# Keep this file compatible with the oldest Ruby in the CI matrix (2.1). |
| 8 | +# |
| 9 | +# JRuby 9.1 can load this file before bundler/setup from RUBYOPT, so set up |
| 10 | +# the bundle here to find benchmark-ips either way. |
| 11 | +require "bundler/setup" |
| 12 | +require "benchmark/ips" |
| 13 | +require "fileutils" |
| 14 | +require "json" |
| 15 | +require "rbconfig" |
| 16 | + |
| 17 | +module CollectResults |
| 18 | + @calls = 0 |
| 19 | + |
| 20 | + class << self |
| 21 | + attr_accessor :calls |
| 22 | + |
| 23 | + def label |
| 24 | + env("RESULTS_LABEL") || ["#{RUBY_ENGINE}-#{engine_version}", env("RUBY_VARIANT")].compact.join("+") |
| 25 | + end |
| 26 | + |
| 27 | + def engine_version |
| 28 | + defined?(RUBY_ENGINE_VERSION) ? RUBY_ENGINE_VERSION : RUBY_VERSION |
| 29 | + end |
| 30 | + |
| 31 | + def write(report) |
| 32 | + self.calls += 1 |
| 33 | + benchmark = $0.sub(%r{\A\./}, "") |
| 34 | + name = benchmark.sub(%r{\Acode/}, "").sub(/\.rb\z/, "").gsub("/", "--") |
| 35 | + name += "--#{calls}" if calls > 1 |
| 36 | + |
| 37 | + path = File.join(ENV["RESULTS_DIR"], label, "#{name}.json") |
| 38 | + FileUtils.mkdir_p(File.dirname(path)) |
| 39 | + File.write(path, JSON.pretty_generate({ |
| 40 | + "benchmark" => benchmark, |
| 41 | + "part" => calls, |
| 42 | + "label" => label, |
| 43 | + "ruby_description" => RUBY_DESCRIPTION, |
| 44 | + "engine" => RUBY_ENGINE, |
| 45 | + "engine_version" => engine_version, |
| 46 | + "ruby_version" => RUBY_VERSION, |
| 47 | + "variant" => env("RUBY_VARIANT"), |
| 48 | + "flags" => env("RUBY_VARIANT_FLAGS"), |
| 49 | + "jit" => jit, |
| 50 | + "commit" => env("RESULTS_COMMIT"), |
| 51 | + "pr" => env("RESULTS_PR") && env("RESULTS_PR").to_i, |
| 52 | + "run_id" => env("GITHUB_RUN_ID"), |
| 53 | + "run_attempt" => env("GITHUB_RUN_ATTEMPT"), |
| 54 | + "runner" => env("RUNNER_NAME"), |
| 55 | + "environment" => environment, |
| 56 | + "entries" => report.data |
| 57 | + })) |
| 58 | + end |
| 59 | + |
| 60 | + # An empty value counts as unset: CI passes an empty matrix field (no |
| 61 | + # variant) as an empty string. |
| 62 | + def env(name) |
| 63 | + value = ENV[name] |
| 64 | + value unless value.nil? || value.empty? |
| 65 | + end |
| 66 | + |
| 67 | + def jit |
| 68 | + if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? |
| 69 | + "yjit" |
| 70 | + elsif defined?(RubyVM::ZJIT) && RubyVM::ZJIT.respond_to?(:enabled?) && RubyVM::ZJIT.enabled? |
| 71 | + "zjit" |
| 72 | + end |
| 73 | + end |
| 74 | + |
| 75 | + # The same for every benchmark in a run, so work it out once. A field that |
| 76 | + # cannot be read (no /etc/os-release or ldd outside Docker) is nil; the |
| 77 | + # others are kept. |
| 78 | + def environment |
| 79 | + @environment ||= { |
| 80 | + "arch" => RbConfig::CONFIG["host_cpu"], |
| 81 | + "cpu" => safely { cpu }, |
| 82 | + "nproc" => safely { nproc }, |
| 83 | + "os" => safely { File.read("/etc/os-release")[/^PRETTY_NAME="?([^"\n]+)/, 1] }, |
| 84 | + "glibc" => safely { `ldd --version 2>&1`[/\d+\.\d+/] }, |
| 85 | + "cc" => cc_version, |
| 86 | + "optflags" => RbConfig::CONFIG["optflags"], |
| 87 | + "configure_args" => RbConfig::CONFIG["configure_args"] |
| 88 | + } |
| 89 | + end |
| 90 | + |
| 91 | + def safely |
| 92 | + yield |
| 93 | + rescue SystemCallError |
| 94 | + nil |
| 95 | + end |
| 96 | + |
| 97 | + # x86 lists a model name. arm64 only lists implementer and part codes, |
| 98 | + # which name the CPU design on servers; inside Docker Desktop's VM on a |
| 99 | + # Mac they only say "Apple", so different Macs look the same. |
| 100 | + def cpu |
| 101 | + model = cpuinfo[/^model name\s*:\s*(.+)$/, 1] |
| 102 | + return model if model |
| 103 | + |
| 104 | + implementer = cpuinfo[/^CPU implementer\s*:\s*(\S+)/, 1] |
| 105 | + part = cpuinfo[/^CPU part\s*:\s*(\S+)/, 1] |
| 106 | + "CPU implementer #{implementer}, part #{part}" if implementer && part |
| 107 | + end |
| 108 | + |
| 109 | + def cpuinfo |
| 110 | + File.exist?("/proc/cpuinfo") ? File.read("/proc/cpuinfo") : "" |
| 111 | + end |
| 112 | + |
| 113 | + # Etc.nprocessors is Ruby 2.2+. |
| 114 | + def nproc |
| 115 | + require "etc" |
| 116 | + Etc.respond_to?(:nprocessors) ? Etc.nprocessors : cpuinfo.scan(/^processor\s*:/).size |
| 117 | + end |
| 118 | + |
| 119 | + # CC_VERSION_MESSAGE is only set by newer CRubies; nil elsewhere. |
| 120 | + def cc_version |
| 121 | + message = RbConfig::CONFIG["CC_VERSION_MESSAGE"] |
| 122 | + message && message.lines.first.strip |
| 123 | + end |
| 124 | + end |
| 125 | + |
| 126 | + def ips(*args, &block) |
| 127 | + report = super |
| 128 | + CollectResults.write(report) if report.respond_to?(:data) |
| 129 | + report |
| 130 | + end |
| 131 | +end |
| 132 | + |
| 133 | +# Benchmark extends Benchmark::IPS, so the hook goes on Benchmark's singleton |
| 134 | +# class. Prepending to Benchmark::IPS would not reach it before Ruby 3.0. |
| 135 | +Benchmark.singleton_class.send(:prepend, CollectResults) |
0 commit comments