Skip to content

Commit 5ad0531

Browse files
authored
Merge pull request #436 from ruby/rmf-argument-parser
Extract parameters parser to its own object
2 parents 32f9e44 + 6afd8ff commit 5ad0531

File tree

4 files changed

+789
-160
lines changed

4 files changed

+789
-160
lines changed

lib/argument_parser.rb

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
require 'optparse'
2+
require 'shellwords'
3+
require 'rbconfig'
4+
5+
class ArgumentParser
6+
Args = Struct.new(
7+
:executables,
8+
:out_path,
9+
:out_override,
10+
:harness,
11+
:yjit_opts,
12+
:categories,
13+
:name_filters,
14+
:rss,
15+
:graph,
16+
:no_pinning,
17+
:turbo,
18+
:skip_yjit,
19+
:with_pre_init,
20+
keyword_init: true
21+
)
22+
23+
def self.parse(argv = ARGV, ruby_executable: RbConfig.ruby)
24+
new(ruby_executable: ruby_executable).parse(argv)
25+
end
26+
27+
def initialize(ruby_executable: RbConfig.ruby)
28+
@ruby_executable = ruby_executable
29+
end
30+
31+
def parse(argv)
32+
args = default_args
33+
34+
OptionParser.new do |opts|
35+
opts.on("-e=NAME::RUBY_PATH OPTIONS", "ruby executable and options to be benchmarked (default: interp, yjit)") do |v|
36+
v.split(";").each do |name_executable|
37+
name, executable = name_executable.split("::", 2)
38+
if executable.nil?
39+
executable = name # allow skipping `NAME::`
40+
end
41+
args.executables[name] = executable.shellsplit
42+
end
43+
end
44+
45+
opts.on("--chruby=NAME::VERSION OPTIONS", "ruby version under chruby and options to be benchmarked") do |v|
46+
v.split(";").each do |name_version|
47+
name, version = name_version.split("::", 2)
48+
# Convert `ruby --yjit` to `ruby::ruby --yjit`
49+
if version.nil?
50+
version = name
51+
name = name.shellsplit.first
52+
end
53+
version, *options = version.shellsplit
54+
rubies_dir = ENV["RUBIES_DIR"] || "#{ENV["HOME"]}/.rubies"
55+
unless executable = ["/opt/rubies/#{version}/bin/ruby", "#{rubies_dir}/#{version}/bin/ruby"].find { |path| File.executable?(path) }
56+
abort "Cannot find '#{version}' in /opt/rubies or #{rubies_dir}"
57+
end
58+
args.executables[name] = [executable, *options]
59+
end
60+
end
61+
62+
opts.on("--out_path=OUT_PATH", "directory where to store output data files") do |v|
63+
args.out_path = v
64+
end
65+
66+
opts.on("--out-name=OUT_FILE", "write exactly this output file plus file extension, ignoring directories, overwriting if necessary") do |v|
67+
args.out_override = v
68+
end
69+
70+
opts.on("--category=headline,other,micro,ractor", "when given, only benchmarks with specified categories will run") do |v|
71+
args.categories += v.split(",")
72+
if args.categories == ["ractor"]
73+
args.harness = "harness-ractor"
74+
end
75+
end
76+
77+
opts.on("--headline", "when given, headline benchmarks will be run") do
78+
args.categories += ["headline"]
79+
end
80+
81+
opts.on("--name_filters=x,y,z", Array, "when given, only benchmarks with names that contain one of these strings will run") do |list|
82+
args.name_filters = list
83+
end
84+
85+
opts.on("--skip-yjit", "Don't run with yjit after interpreter") do
86+
args.skip_yjit = true
87+
end
88+
89+
opts.on("--harness=HARNESS_DIR", "which harness to use") do |v|
90+
v = "harness-#{v}" unless v.start_with?('harness')
91+
args.harness = v
92+
end
93+
94+
opts.on("--warmup=N", "the number of warmup iterations for the default harness (default: 15)") do |n|
95+
ENV["WARMUP_ITRS"] = n
96+
end
97+
98+
opts.on("--bench=N", "the number of benchmark iterations for the default harness (default: 10). Also defaults MIN_BENCH_TIME to 0.") do |n|
99+
ENV["MIN_BENCH_ITRS"] = n
100+
ENV["MIN_BENCH_TIME"] ||= "0"
101+
end
102+
103+
opts.on("--once", "benchmarks only 1 iteration with no warmup for the default harness") do
104+
ENV["WARMUP_ITRS"] = "0"
105+
ENV["MIN_BENCH_ITRS"] = "1"
106+
ENV["MIN_BENCH_TIME"] = "0"
107+
end
108+
109+
opts.on("--yjit-stats=STATS", "print YJIT stats at each iteration for the default harness") do |str|
110+
ENV["YJIT_BENCH_STATS"] = str
111+
end
112+
113+
opts.on("--zjit-stats=STATS", "print ZJIT stats at each iteration for the default harness") do |str|
114+
ENV["ZJIT_BENCH_STATS"] = str
115+
end
116+
117+
opts.on("--yjit_opts=OPT_STRING", "string of command-line options to run YJIT with (ignored if you use -e)") do |str|
118+
args.yjit_opts = str
119+
end
120+
121+
opts.on("--with_pre-init=PRE_INIT_FILE",
122+
"a file to require before each benchmark run, so settings can be tuned (eg. enable/disable GC compaction)") do |str|
123+
args.with_pre_init = str
124+
end
125+
126+
opts.on("--rss", "show RSS in the output (measured after benchmark iterations)") do
127+
args.rss = true
128+
end
129+
130+
opts.on("--graph", "generate a graph image of benchmark results") do
131+
args.graph = true
132+
end
133+
134+
opts.on("--no-pinning", "don't pin ruby to a specific CPU core") do
135+
args.no_pinning = true
136+
end
137+
138+
opts.on("--turbo", "don't disable CPU turbo boost") do
139+
args.turbo = true
140+
end
141+
end.parse!(argv)
142+
143+
# Remaining arguments are treated as benchmark name filters
144+
if argv.length > 0
145+
args.name_filters += argv
146+
end
147+
148+
# If -e is not specified, benchmark the current Ruby. Compare it with YJIT if available.
149+
if args.executables.empty?
150+
if have_yjit?(@ruby_executable) && !args.skip_yjit
151+
args.executables["interp"] = [@ruby_executable]
152+
args.executables["yjit"] = [@ruby_executable, "--yjit", *args.yjit_opts.shellsplit]
153+
else
154+
args.executables["ruby"] = [@ruby_executable]
155+
end
156+
end
157+
158+
args
159+
end
160+
161+
private
162+
163+
def have_yjit?(ruby)
164+
ruby_version = `#{ruby} -v --yjit 2> #{File::NULL}`.strip
165+
ruby_version.downcase.include?("yjit")
166+
end
167+
168+
def default_args
169+
Args.new(
170+
executables: {},
171+
out_path: File.expand_path("./data"),
172+
out_override: nil,
173+
harness: "harness",
174+
yjit_opts: "",
175+
categories: [],
176+
name_filters: [],
177+
rss: false,
178+
graph: false,
179+
no_pinning: false,
180+
turbo: false,
181+
skip_yjit: false,
182+
with_pre_init: nil,
183+
)
184+
end
185+
end

run_benchmarks.rb

Lines changed: 3 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
#!/usr/bin/env ruby
22

3-
require 'optparse'
4-
require 'ostruct'
53
require 'pathname'
64
require 'fileutils'
7-
require 'shellwords'
85
require 'csv'
96
require 'json'
7+
require 'shellwords'
108
require 'rbconfig'
119
require 'etc'
1210
require 'yaml'
@@ -15,11 +13,7 @@
1513
require_relative 'lib/benchmark_runner'
1614
require_relative 'lib/table_formatter'
1715
require_relative 'lib/benchmark_filter'
18-
19-
def have_yjit?(ruby)
20-
ruby_version = `#{ruby} -v --yjit 2> #{File::NULL}`.strip
21-
ruby_version.downcase.include?("yjit")
22-
end
16+
require_relative 'lib/argument_parser'
2317

2418
def mean(values)
2519
Stats.new(values).mean
@@ -158,145 +152,7 @@ def run_benchmarks(ruby:, ruby_description:, categories:, name_filters:, out_pat
158152
[bench_data, bench_failures]
159153
end
160154

161-
# Default values for command-line arguments
162-
args = OpenStruct.new({
163-
executables: {},
164-
out_path: File.expand_path("./data"),
165-
out_override: nil,
166-
harness: "harness",
167-
yjit_opts: "",
168-
categories: [],
169-
name_filters: [],
170-
rss: false,
171-
graph: false,
172-
no_pinning: false,
173-
turbo: false,
174-
skip_yjit: false,
175-
})
176-
177-
OptionParser.new do |opts|
178-
opts.on("-e=NAME::RUBY_PATH OPTIONS", "ruby executable and options to be benchmarked (default: interp, yjit)") do |v|
179-
v.split(";").each do |name_executable|
180-
name, executable = name_executable.split("::", 2)
181-
if executable.nil?
182-
executable = name # allow skipping `NAME::`
183-
end
184-
args.executables[name] = executable.shellsplit
185-
end
186-
end
187-
188-
opts.on("--chruby=NAME::VERSION OPTIONS", "ruby version under chruby and options to be benchmarked") do |v|
189-
v.split(";").each do |name_version|
190-
name, version = name_version.split("::", 2)
191-
# Convert `ruby --yjit` to `ruby::ruby --yjit`
192-
if version.nil?
193-
version = name
194-
name = name.shellsplit.first
195-
end
196-
version, *options = version.shellsplit
197-
rubies_dir = ENV["RUBIES_DIR"] || "#{ENV["HOME"]}/.rubies"
198-
unless executable = ["/opt/rubies/#{version}/bin/ruby", "#{rubies_dir}/#{version}/bin/ruby"].find { |path| File.executable?(path) }
199-
abort "Cannot find '#{version}' in /opt/rubies or #{rubies_dir}"
200-
end
201-
args.executables[name] = [executable, *options]
202-
end
203-
end
204-
205-
opts.on("--out_path=OUT_PATH", "directory where to store output data files") do |v|
206-
args.out_path = v
207-
end
208-
209-
opts.on("--out-name=OUT_FILE", "write exactly this output file plus file extension, ignoring directories, overwriting if necessary") do |v|
210-
args.out_override = v
211-
end
212-
213-
opts.on("--category=headline,other,micro,ractor", "when given, only benchmarks with specified categories will run") do |v|
214-
args.categories += v.split(",")
215-
if args.categories == ["ractor"]
216-
args.harness = "harness-ractor"
217-
end
218-
end
219-
220-
opts.on("--headline", "when given, headline benchmarks will be run") do
221-
args.categories += ["headline"]
222-
end
223-
224-
opts.on("--name_filters=x,y,z", Array, "when given, only benchmarks with names that contain one of these strings will run") do |list|
225-
args.name_filters = list
226-
end
227-
228-
opts.on("--skip-yjit", "Don't run with yjit after interpreter") do
229-
args.skip_yjit = true
230-
end
231-
232-
opts.on("--harness=HARNESS_DIR", "which harness to use") do |v|
233-
v = "harness-#{v}" unless v.start_with?('harness')
234-
args.harness = v
235-
end
236-
237-
opts.on("--warmup=N", "the number of warmup iterations for the default harness (default: 15)") do |n|
238-
ENV["WARMUP_ITRS"] = n
239-
end
240-
241-
opts.on("--bench=N", "the number of benchmark iterations for the default harness (default: 10). Also defaults MIN_BENCH_TIME to 0.") do |n|
242-
ENV["MIN_BENCH_ITRS"] = n
243-
ENV["MIN_BENCH_TIME"] ||= "0"
244-
end
245-
246-
opts.on("--once", "benchmarks only 1 iteration with no warmup for the default harness") do
247-
ENV["WARMUP_ITRS"] = "0"
248-
ENV["MIN_BENCH_ITRS"] = "1"
249-
ENV["MIN_BENCH_TIME"] = "0"
250-
end
251-
252-
opts.on("--yjit-stats=STATS", "print YJIT stats at each iteration for the default harness") do |str|
253-
ENV["YJIT_BENCH_STATS"] = str
254-
end
255-
256-
opts.on("--zjit-stats=STATS", "print ZJIT stats at each iteration for the default harness") do |str|
257-
ENV["ZJIT_BENCH_STATS"] = str
258-
end
259-
260-
opts.on("--yjit_opts=OPT_STRING", "string of command-line options to run YJIT with (ignored if you use -e)") do |str|
261-
args.yjit_opts=str
262-
end
263-
264-
opts.on("--with_pre-init=PRE_INIT_FILE",
265-
"a file to require before each benchmark run, so settings can be tuned (eg. enable/disable GC compaction)") do |str|
266-
args.with_pre_init = str
267-
end
268-
269-
opts.on("--rss", "show RSS in the output (measured after benchmark iterations)") do
270-
args.rss = true
271-
end
272-
273-
opts.on("--graph", "generate a graph image of benchmark results") do
274-
args.graph = true
275-
end
276-
277-
opts.on("--no-pinning", "don't pin ruby to a specific CPU core") do
278-
args.no_pinning = true
279-
end
280-
281-
opts.on("--turbo", "don't disable CPU turbo boost") do
282-
args.turbo = true
283-
end
284-
end.parse!
285-
286-
# Remaining arguments are treated as benchmark name filters
287-
if ARGV.length > 0
288-
args.name_filters += ARGV
289-
end
290-
291-
# If -e is not specified, benchmark the current Ruby. Compare it with YJIT if available.
292-
if args.executables.empty?
293-
if have_yjit?(RbConfig.ruby) && !args.skip_yjit
294-
args.executables["interp"] = [RbConfig.ruby]
295-
args.executables["yjit"] = [RbConfig.ruby, "--yjit", *args.yjit_opts.shellsplit]
296-
else
297-
args.executables["ruby"] = [RbConfig.ruby]
298-
end
299-
end
155+
args = ArgumentParser.parse(ARGV)
300156

301157
CPUConfig.configure_for_benchmarking(turbo: args.turbo)
302158

0 commit comments

Comments
 (0)