Skip to content

Commit b1a7224

Browse files
authored
Merge pull request #438 from ruby/rmf-benchmark-suite
Extract `run_benchmarks` to its own object, `BenchmarkSuite`
2 parents a088cc8 + fe8e2eb commit b1a7224

File tree

5 files changed

+774
-292
lines changed

5 files changed

+774
-292
lines changed

lib/benchmark_runner.rb

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,6 @@ def free_file_no(directory)
1616
end
1717
end
1818

19-
# Resolve the pre_init file path into a form that can be required
20-
def expand_pre_init(path)
21-
require 'pathname'
22-
23-
path = Pathname.new(path)
24-
25-
unless path.exist?
26-
puts "--with-pre-init called with non-existent file!"
27-
exit(-1)
28-
end
29-
30-
if path.directory?
31-
puts "--with-pre-init called with a directory, please pass a .rb file"
32-
exit(-1)
33-
end
34-
35-
library_name = path.basename(path.extname)
36-
load_path = path.parent.expand_path
37-
38-
[
39-
"-I", load_path,
40-
"-r", library_name
41-
]
42-
end
43-
4419
# Sort benchmarks with headlines first, then others, then micro
4520
def sort_benchmarks(bench_names, metadata)
4621
headline_benchmarks = metadata.select { |_, meta| meta['category'] == 'headline' }.keys
@@ -51,36 +26,6 @@ def sort_benchmarks(bench_names, metadata)
5126
headline_names.sort + other_names.sort + micro_names.sort
5227
end
5328

54-
# Check which OS we are running
55-
def os
56-
@os ||= (
57-
host_os = RbConfig::CONFIG['host_os']
58-
case host_os
59-
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
60-
:windows
61-
when /darwin|mac os/
62-
:macosx
63-
when /linux/
64-
:linux
65-
when /solaris|bsd/
66-
:unix
67-
else
68-
raise "unknown os: #{host_os.inspect}"
69-
end
70-
)
71-
end
72-
73-
# Generate setarch prefix for Linux
74-
def setarch_prefix
75-
# Disable address space randomization (for determinism)
76-
prefix = ["setarch", `uname -m`.strip, "-R"]
77-
78-
# Abort if we don't have permission (perhaps in a docker container).
79-
return [] unless system(*prefix, "true", out: File::NULL, err: File::NULL)
80-
81-
prefix
82-
end
83-
8429
# Checked system - error or return info if the command fails
8530
def check_call(command, env: {}, raise_error: true, quiet: false)
8631
puts("+ #{command}") unless quiet

lib/benchmark_suite.rb

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
require 'pathname'
5+
require 'fileutils'
6+
require 'shellwords'
7+
require 'etc'
8+
require 'yaml'
9+
require 'rbconfig'
10+
require_relative 'benchmark_filter'
11+
require_relative 'benchmark_runner'
12+
13+
# BenchmarkSuite runs a collection of benchmarks and collects their results
14+
class BenchmarkSuite
15+
BENCHMARKS_DIR = "benchmarks"
16+
RACTOR_BENCHMARKS_DIR = "benchmarks-ractor"
17+
RACTOR_ONLY_CATEGORY = ["ractor-only"].freeze
18+
RACTOR_CATEGORY = ["ractor"].freeze
19+
RACTOR_HARNESS = "harness-ractor"
20+
21+
attr_reader :ruby, :ruby_description, :categories, :name_filters, :out_path, :harness, :pre_init, :no_pinning, :bench_dir, :ractor_bench_dir
22+
23+
def initialize(ruby:, ruby_description:, categories:, name_filters:, out_path:, harness:, pre_init: nil, no_pinning: false)
24+
@ruby = ruby
25+
@ruby_description = ruby_description
26+
@categories = categories
27+
@name_filters = name_filters
28+
@out_path = out_path
29+
@harness = harness
30+
@pre_init = pre_init ? expand_pre_init(pre_init) : nil
31+
@no_pinning = no_pinning
32+
@ractor_only = (categories == RACTOR_ONLY_CATEGORY)
33+
34+
setup_benchmark_directories
35+
end
36+
37+
# Run all the benchmarks and record execution times
38+
# Returns [bench_data, bench_failures]
39+
def run
40+
bench_data = {}
41+
bench_failures = {}
42+
43+
bench_file_grouping.each do |bench_dir, bench_files|
44+
bench_files.each_with_index do |entry, idx|
45+
bench_name = entry.delete_suffix('.rb')
46+
47+
puts("Running benchmark \"#{bench_name}\" (#{idx+1}/#{bench_files.length})")
48+
49+
result_json_path = File.join(out_path, "temp#{Process.pid}.json")
50+
result = run_single_benchmark(bench_dir, entry, result_json_path)
51+
52+
if result[:success]
53+
bench_data[bench_name] = process_benchmark_result(result_json_path, result[:command])
54+
else
55+
bench_failures[bench_name] = result[:status].exitstatus
56+
end
57+
end
58+
end
59+
60+
[bench_data, bench_failures]
61+
end
62+
63+
private
64+
65+
def setup_benchmark_directories
66+
if @ractor_only
67+
@bench_dir = RACTOR_BENCHMARKS_DIR
68+
@ractor_bench_dir = RACTOR_BENCHMARKS_DIR
69+
@harness = RACTOR_HARNESS
70+
@categories = []
71+
else
72+
@bench_dir = BENCHMARKS_DIR
73+
@ractor_bench_dir = RACTOR_BENCHMARKS_DIR
74+
end
75+
end
76+
77+
def process_benchmark_result(result_json_path, command)
78+
JSON.parse(File.read(result_json_path)).tap do |json|
79+
json["command_line"] = command
80+
File.unlink(result_json_path)
81+
end
82+
end
83+
84+
def run_single_benchmark(bench_dir, entry, result_json_path)
85+
# Path to the benchmark runner script
86+
script_path = File.join(bench_dir, entry)
87+
88+
unless script_path.end_with?('.rb')
89+
script_path = File.join(script_path, 'benchmark.rb')
90+
end
91+
92+
# Fix for jruby/jruby#7394 in JRuby 9.4.2.0
93+
script_path = File.expand_path(script_path)
94+
95+
# Set up the environment for the benchmarking command
96+
ENV["RESULT_JSON_PATH"] = result_json_path
97+
98+
# Set up the benchmarking command
99+
cmd = base_cmd + [
100+
*ruby,
101+
"-I", harness,
102+
*pre_init,
103+
script_path,
104+
].compact
105+
106+
# Do the benchmarking
107+
result = BenchmarkRunner.check_call(cmd.shelljoin, env: benchmark_env, raise_error: false)
108+
result[:command] = cmd.shelljoin
109+
result
110+
end
111+
112+
def benchmark_env
113+
@benchmark_env ||= begin
114+
# When the Ruby running this script is not the first Ruby in PATH, shell commands
115+
# like `bundle install` in a child process will not use the Ruby being benchmarked.
116+
# It overrides PATH to guarantee the commands of the benchmarked Ruby will be used.
117+
env = {}
118+
ruby_path = `#{ruby.shelljoin} -e 'print RbConfig.ruby' 2> #{File::NULL}`
119+
120+
if ruby_path != RbConfig.ruby
121+
env["PATH"] = "#{File.dirname(ruby_path)}:#{ENV["PATH"]}"
122+
123+
# chruby sets GEM_HOME and GEM_PATH in your shell. We have to unset it in the child
124+
# process to avoid installing gems to the version that is running run_benchmarks.rb.
125+
["GEM_HOME", "GEM_PATH"].each do |var|
126+
env[var] = nil if ENV.key?(var)
127+
end
128+
end
129+
130+
env
131+
end
132+
end
133+
134+
def bench_file_grouping
135+
grouping = { bench_dir => filtered_bench_entries(bench_dir, main_benchmark_filter) }
136+
137+
if benchmark_ractor_directory?
138+
# We ignore the category filter here because everything in the
139+
# benchmarks-ractor directory should be included when we're benchmarking the
140+
# Ractor category
141+
grouping[ractor_bench_dir] = filtered_bench_entries(ractor_bench_dir, ractor_benchmark_filter)
142+
end
143+
144+
grouping
145+
end
146+
147+
def main_benchmark_filter
148+
@main_benchmark_filter ||= BenchmarkFilter.new(
149+
categories: categories,
150+
name_filters: name_filters,
151+
metadata: benchmarks_metadata
152+
)
153+
end
154+
155+
def ractor_benchmark_filter
156+
@ractor_benchmark_filter ||= BenchmarkFilter.new(
157+
categories: [],
158+
name_filters: name_filters,
159+
metadata: benchmarks_metadata
160+
)
161+
end
162+
163+
def benchmarks_metadata
164+
@benchmarks_metadata ||= YAML.load_file('benchmarks.yml')
165+
end
166+
167+
def filtered_bench_entries(dir, filter)
168+
Dir.children(dir).sort.filter do |entry|
169+
filter.match?(entry)
170+
end
171+
end
172+
173+
def benchmark_ractor_directory?
174+
categories == RACTOR_CATEGORY
175+
end
176+
177+
# Check if running on Linux
178+
def linux?
179+
@linux ||= RbConfig::CONFIG['host_os'] =~ /linux/
180+
end
181+
182+
# Set up the base command with CPU pinning if needed
183+
def base_cmd
184+
@base_cmd ||= if linux?
185+
cmd = setarch_prefix
186+
187+
# Pin the process to one given core to improve caching and reduce variance on CRuby
188+
# Other Rubies need to use multiple cores, e.g., for JIT threads
189+
if ruby_description.start_with?('ruby ') && !no_pinning
190+
# The last few cores of Intel CPU may be slow E-Cores, so avoid using the last one.
191+
cpu = [(Etc.nprocessors / 2) - 1, 0].max
192+
cmd.concat(["taskset", "-c", "#{cpu}"])
193+
end
194+
195+
cmd
196+
else
197+
[]
198+
end
199+
end
200+
201+
# Generate setarch prefix for Linux
202+
def setarch_prefix
203+
# Disable address space randomization (for determinism)
204+
prefix = ["setarch", `uname -m`.strip, "-R"]
205+
206+
# Abort if we don't have permission (perhaps in a docker container).
207+
return [] unless system(*prefix, "true", out: File::NULL, err: File::NULL)
208+
209+
prefix
210+
end
211+
212+
# Resolve the pre_init file path into a form that can be required
213+
def expand_pre_init(path)
214+
path = Pathname.new(path)
215+
216+
unless path.exist?
217+
puts "--with-pre-init called with non-existent file!"
218+
exit(-1)
219+
end
220+
221+
if path.directory?
222+
puts "--with-pre-init called with a directory, please pass a .rb file"
223+
exit(-1)
224+
end
225+
226+
library_name = path.basename(path.extname)
227+
load_path = path.parent.expand_path
228+
229+
[
230+
"-I", load_path,
231+
"-r", library_name
232+
]
233+
end
234+
end

0 commit comments

Comments
 (0)