ips 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -0
- data/exe/ips +196 -46
- data/lib/ips/job/entry.rb +3 -2
- data/lib/ips/job.rb +16 -33
- data/lib/ips/result.rb +56 -0
- data/lib/ips/version.rb +1 -1
- data/lib/ips/warmup.rb +51 -0
- data/lib/ips.rb +12 -3
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd8eb49702573799e4d4754155e2d4c76713e1363f8dc2659ffbc99f96d21fe0
|
|
4
|
+
data.tar.gz: 7936ccc9a3896c754fd5fa2cd719e668df7317c6c92bc35e8d70fe301d1970e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac0010e763e43f03af94479fe5a9098a1d8b1caf0b58898f5248035e7a11e9cd946dbc195549f06c161104013c338a59f96440c5f630393be8da99b6728b2f0e
|
|
7
|
+
data.tar.gz: cf9749c62b609463ab3214ddf158b213ffcd4bc16f456cca71b7a3cbfb29c11dc8a93499271a56c7ced0078120b7ffcf12d33fb41d162fb259f8c469f5e710d4
|
data/README.md
CHANGED
|
@@ -39,10 +39,24 @@ Compare across Ruby versions, arguments, and commands:
|
|
|
39
39
|
$ ips --ruby '3.4' --ruby '3.4 --yjit' --ruby '4.0' --ruby '4.0 --yjit' -e 'Object.new' -e 'Object.allocate'
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Compare the same benchmark across commits of the current repository:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
$ ips --sha HEAD~3 --sha HEAD~10..HEAD~5 bench.rb
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Each commit is checked out in place with `git checkout --detach`, so the Ruby,
|
|
49
|
+
the bundle, and any per-directory setup stay identical across every commit. The
|
|
50
|
+
working tree must be clean, and the branch you started on is restored when the
|
|
51
|
+
run finishes (or is interrupted).
|
|
52
|
+
|
|
42
53
|
Options:
|
|
43
54
|
|
|
44
55
|
- `--ruby VERSION` — run against a specific Ruby (repeatable)
|
|
56
|
+
- `--sha REV` — also run against a commit of the current repo (repeatable; a `A..B` range expands to its commits, oldest first)
|
|
45
57
|
- `-e CODE` — inline benchmark expression (repeatable)
|
|
58
|
+
- `--prelude CODE` — run setup code before the benchmark (repeatable)
|
|
59
|
+
- `-I DIR` — add a directory to the load path (repeatable)
|
|
46
60
|
- `-t SECONDS` — benchmark time (default: 5)
|
|
47
61
|
- `-w SECONDS` — warmup time (default: 20% of benchmark time)
|
|
48
62
|
- `--save PATH` — save results to JSON (appends to existing file)
|
data/exe/ips
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
versions = []
|
|
5
|
+
revs = []
|
|
5
6
|
inline_code = []
|
|
7
|
+
prelude = []
|
|
6
8
|
ruby_flags = []
|
|
9
|
+
runs = 1
|
|
7
10
|
save_path = nil
|
|
8
11
|
bench_time = nil
|
|
9
12
|
warmup_time = nil
|
|
@@ -15,14 +18,26 @@ while args.first
|
|
|
15
18
|
when "--ruby"
|
|
16
19
|
args.shift
|
|
17
20
|
versions << args.shift
|
|
21
|
+
when "--sha"
|
|
22
|
+
args.shift
|
|
23
|
+
revs << args.shift
|
|
18
24
|
when "-e"
|
|
19
25
|
args.shift
|
|
20
26
|
inline_code << args.shift
|
|
21
27
|
when "-r"
|
|
22
28
|
args.shift
|
|
23
29
|
ruby_flags.push("-r", args.shift)
|
|
30
|
+
when "-I"
|
|
31
|
+
args.shift
|
|
32
|
+
ruby_flags.push("-I", args.shift)
|
|
33
|
+
when "--prelude"
|
|
34
|
+
args.shift
|
|
35
|
+
prelude << args.shift
|
|
24
36
|
when "--disable-gems", /\A--yjit/, /\A--zjit/
|
|
25
37
|
ruby_flags << args.shift
|
|
38
|
+
when "--runs"
|
|
39
|
+
args.shift
|
|
40
|
+
runs = args.shift.to_i
|
|
26
41
|
when "--save"
|
|
27
42
|
args.shift
|
|
28
43
|
save_path = args.shift
|
|
@@ -51,31 +66,10 @@ end
|
|
|
51
66
|
script = args.shift
|
|
52
67
|
|
|
53
68
|
if script.nil? && inline_code.empty?
|
|
54
|
-
$stderr.puts "Usage: ips [--ruby VERSION ...] [-e CODE ...] [<script>]"
|
|
69
|
+
$stderr.puts "Usage: ips [--ruby VERSION ...] [--sha REV ...] [-r LIB ...] [--prelude CODE ...] [-e CODE ...] [<script>]"
|
|
55
70
|
exit 1
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
if inline_code.any?
|
|
59
|
-
run_opts = []
|
|
60
|
-
run_opts << "summary: false" if versions.size > 1
|
|
61
|
-
run_opts << "time: #{bench_time}" if bench_time
|
|
62
|
-
run_opts << "warmup: #{warmup_time}" if warmup_time
|
|
63
|
-
run_opts << "debug: true" if debug
|
|
64
|
-
run_opts << "frozen_string_literal: false" unless frozen_string_literal
|
|
65
|
-
run_opts_str = run_opts.any? ? "(#{run_opts.join(", ")})" : ""
|
|
66
|
-
script_source = +""
|
|
67
|
-
script_source << "# frozen_string_literal: true\n" if frozen_string_literal
|
|
68
|
-
script_source << "require 'ips'\nresult = IPS.run#{run_opts_str} do |x|\n"
|
|
69
|
-
inline_code.each do |code|
|
|
70
|
-
script_source << " x.report(#{code.inspect}, #{code.inspect})\n"
|
|
71
|
-
end
|
|
72
|
-
script_source << "end\n"
|
|
73
|
-
script_source << "IO.open(3, 'w') { |f| Marshal.dump(result, f) }\n"
|
|
74
|
-
ruby_args = ["-e", script_source]
|
|
75
|
-
else
|
|
76
|
-
ruby_args = [script]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
73
|
# Shell-escape for display purposes only, not for security.
|
|
80
74
|
def shell_quote(arg)
|
|
81
75
|
return arg if arg =~ /\A[A-Za-z0-9_\-.,:+\/@]+\z/
|
|
@@ -86,17 +80,21 @@ RUBY_DIRS = [File.expand_path("~/.rubies"), "/opt/rubies"]
|
|
|
86
80
|
LIB_DIR = File.expand_path("../lib", __dir__)
|
|
87
81
|
|
|
88
82
|
def find_ruby(version)
|
|
83
|
+
match = nil
|
|
89
84
|
RUBY_DIRS.each do |dir|
|
|
90
85
|
next unless Dir.exist?(dir)
|
|
91
86
|
Dir.children(dir).sort.each do |name|
|
|
92
87
|
bare = name.delete_prefix("ruby-")
|
|
93
|
-
if bare
|
|
88
|
+
if bare == version || name == version
|
|
94
89
|
ruby = File.join(dir, name, "bin", "ruby")
|
|
95
90
|
return ruby if File.executable?(ruby)
|
|
91
|
+
elsif bare.include?(version) || name.include?(version)
|
|
92
|
+
ruby = File.join(dir, name, "bin", "ruby")
|
|
93
|
+
match = ruby if File.executable?(ruby)
|
|
96
94
|
end
|
|
97
95
|
end
|
|
98
96
|
end
|
|
99
|
-
|
|
97
|
+
match
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
def resolve_ruby(str)
|
|
@@ -109,6 +107,20 @@ def resolve_ruby(str)
|
|
|
109
107
|
[ruby, extra_flags]
|
|
110
108
|
end
|
|
111
109
|
|
|
110
|
+
# Runs git in the current directory, capturing stdout and stderr together.
|
|
111
|
+
def git(*git_args)
|
|
112
|
+
out = IO.popen(["git", *git_args], err: [:child, :out], &:read)
|
|
113
|
+
[out, $?]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def git!(*git_args)
|
|
117
|
+
out, status = git(*git_args)
|
|
118
|
+
unless status.success?
|
|
119
|
+
abort "git #{git_args.join(" ")} failed:\n#{out}"
|
|
120
|
+
end
|
|
121
|
+
out
|
|
122
|
+
end
|
|
123
|
+
|
|
112
124
|
if versions.empty?
|
|
113
125
|
rubies = [[nil, nil, []]]
|
|
114
126
|
else
|
|
@@ -118,38 +130,176 @@ else
|
|
|
118
130
|
}
|
|
119
131
|
end
|
|
120
132
|
|
|
133
|
+
# Each checkout is [label, sha]. Without --sha we stay where we are and label
|
|
134
|
+
# nothing. With --sha the current checkout is always the first variant, so
|
|
135
|
+
# there's a baseline to compare against.
|
|
136
|
+
original_ref = nil
|
|
137
|
+
original_sha = nil
|
|
138
|
+
|
|
139
|
+
if revs.empty?
|
|
140
|
+
checkouts = [[nil, nil]]
|
|
141
|
+
else
|
|
142
|
+
dirty = git!("status", "--porcelain", "-uno")
|
|
143
|
+
unless dirty.empty?
|
|
144
|
+
abort "Working tree has uncommitted changes to tracked files; commit or stash them before using --sha:\n#{dirty}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
original_sha = git!("rev-parse", "HEAD").strip
|
|
148
|
+
branch, branch_status = git("symbolic-ref", "--short", "HEAD")
|
|
149
|
+
original_ref = branch_status.success? ? branch.strip : nil
|
|
150
|
+
|
|
151
|
+
full_shas = [original_sha]
|
|
152
|
+
revs.each do |rev|
|
|
153
|
+
if rev.include?("..")
|
|
154
|
+
full_shas.concat(git!("rev-list", "--reverse", rev).split("\n"))
|
|
155
|
+
else
|
|
156
|
+
full_shas << git!("rev-parse", "--verify", "#{rev}^{commit}").strip
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
full_shas.uniq!
|
|
160
|
+
|
|
161
|
+
checkouts = full_shas.map { |sha| [git!("rev-parse", "--short", sha).strip, sha] }
|
|
162
|
+
# Prefer the branch name for the baseline when we have one.
|
|
163
|
+
checkouts[0][0] = original_ref if original_ref
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Variants are the product of checkouts and rubies.
|
|
167
|
+
variants = checkouts.flat_map { |sha_label, sha|
|
|
168
|
+
rubies.map { |version_str, ruby, extra_flags|
|
|
169
|
+
label =
|
|
170
|
+
if version_str && sha_label
|
|
171
|
+
"ruby #{version_str} @ #{sha_label}"
|
|
172
|
+
elsif version_str
|
|
173
|
+
"ruby #{version_str}"
|
|
174
|
+
else
|
|
175
|
+
sha_label
|
|
176
|
+
end
|
|
177
|
+
[label, ruby, extra_flags, sha]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
overrides = []
|
|
182
|
+
overrides << "save: IO.open(3, 'w')"
|
|
183
|
+
overrides << "summary: false" if variants.size > 1 || runs > 1
|
|
184
|
+
overrides << "time: #{bench_time}" if bench_time
|
|
185
|
+
overrides << "warmup: #{warmup_time}" if warmup_time
|
|
186
|
+
overrides << "debug: true" if debug
|
|
187
|
+
overrides << "frozen_string_literal: false" unless frozen_string_literal
|
|
188
|
+
|
|
189
|
+
script_source = +""
|
|
190
|
+
script_source << "# frozen_string_literal: true\n" if frozen_string_literal
|
|
191
|
+
script_source << "require 'ips'\n"
|
|
192
|
+
script_source << "IPS.overrides = { #{overrides.join(", ")} }\n"
|
|
193
|
+
prelude.each { |code| script_source << code << "\n" }
|
|
194
|
+
|
|
195
|
+
if inline_code.any?
|
|
196
|
+
script_source << "IPS.run do |x|\n"
|
|
197
|
+
inline_code.each do |code|
|
|
198
|
+
script_source << " x.report(#{code.inspect}, #{code.inspect})\n"
|
|
199
|
+
end
|
|
200
|
+
script_source << "end\n"
|
|
201
|
+
else
|
|
202
|
+
script_source << "load(#{script.dump})\n"
|
|
203
|
+
end
|
|
204
|
+
ruby_args = ["-e", script_source]
|
|
205
|
+
|
|
121
206
|
$LOAD_PATH.unshift(LIB_DIR)
|
|
122
207
|
require "ips"
|
|
123
208
|
|
|
124
|
-
|
|
209
|
+
# Check out commits in place, never changing directory and never creating a
|
|
210
|
+
# worktree, so the ruby, the bundle, and any per-directory setup stay identical
|
|
211
|
+
# across every variant.
|
|
212
|
+
current_sha = original_sha
|
|
213
|
+
moved = false
|
|
125
214
|
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
|
|
215
|
+
checkout = lambda do |sha|
|
|
216
|
+
next if sha.nil? || sha == current_sha
|
|
217
|
+
$stdout.puts "git checkout --detach #{sha}" if verbose
|
|
218
|
+
out, status = git("checkout", "--detach", sha)
|
|
219
|
+
abort "git checkout --detach #{sha} failed:\n#{out}" unless status.success?
|
|
220
|
+
moved = true
|
|
221
|
+
current_sha = sha
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
restore = lambda do
|
|
225
|
+
next unless moved
|
|
226
|
+
moved = false
|
|
227
|
+
git_args = original_ref ? ["checkout", original_ref] : ["checkout", "--detach", original_sha]
|
|
228
|
+
out, status = git(*git_args)
|
|
229
|
+
if status.success?
|
|
230
|
+
current_sha = original_sha
|
|
129
231
|
else
|
|
130
|
-
|
|
131
|
-
env = {}
|
|
232
|
+
$stderr.puts "WARNING: failed to restore #{original_ref || original_sha}:\n#{out}"
|
|
132
233
|
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
trap("INT") do
|
|
237
|
+
restore.call
|
|
238
|
+
exit 130
|
|
239
|
+
end if original_sha
|
|
133
240
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
241
|
+
results_by_variant = variants.map { [] }
|
|
242
|
+
quiet = runs > 1
|
|
243
|
+
|
|
244
|
+
begin
|
|
245
|
+
runs.times do |run_index|
|
|
246
|
+
variants.each_with_index do |(label, ruby, extra_flags, sha), variant_index|
|
|
247
|
+
checkout.call(sha)
|
|
248
|
+
|
|
249
|
+
if ruby
|
|
250
|
+
env = { "GEM_HOME" => nil, "GEM_PATH" => nil }
|
|
251
|
+
else
|
|
252
|
+
ruby = "ruby"
|
|
253
|
+
env = {}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
cmd = [ruby, *extra_flags, *ruby_flags]
|
|
257
|
+
cmd << "-v" unless quiet
|
|
258
|
+
cmd.push("-I", LIB_DIR, *ruby_args)
|
|
259
|
+
$stdout.puts cmd.map { |a| shell_quote(a) }.join(" ") if verbose
|
|
260
|
+
|
|
261
|
+
rd, wr = IO.pipe
|
|
262
|
+
spawn_opts = { 3 => wr }
|
|
263
|
+
spawn_opts[:out] = File::NULL if quiet
|
|
264
|
+
|
|
265
|
+
pid = spawn(env, *cmd, **spawn_opts)
|
|
266
|
+
wr.close
|
|
267
|
+
|
|
268
|
+
if quiet
|
|
269
|
+
$stdout.print "\rRun #{run_index + 1}/#{runs}"
|
|
270
|
+
$stdout.print " (#{label})" if label
|
|
271
|
+
$stdout.print "..."
|
|
272
|
+
$stdout.flush
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
until rd.eof?
|
|
276
|
+
result = Marshal.load(rd)
|
|
277
|
+
result.ruby_executable = ruby
|
|
278
|
+
result.run_label = label if label
|
|
279
|
+
results_by_variant[variant_index] << result
|
|
280
|
+
end
|
|
281
|
+
rd.close
|
|
282
|
+
_, status = Process.wait2(pid)
|
|
283
|
+
unless status.success?
|
|
284
|
+
$stderr.puts if quiet
|
|
285
|
+
cmd_str = cmd.map { |a| shell_quote(a) }.join(" ")
|
|
286
|
+
abort "Command failed (exit #{status.exitstatus}): #{cmd_str}"
|
|
287
|
+
end
|
|
288
|
+
puts unless quiet
|
|
289
|
+
end
|
|
148
290
|
end
|
|
149
|
-
|
|
291
|
+
ensure
|
|
292
|
+
restore.call
|
|
150
293
|
end
|
|
294
|
+
$stderr.puts if quiet
|
|
151
295
|
|
|
152
|
-
|
|
296
|
+
results = results_by_variant.flatten
|
|
297
|
+
|
|
298
|
+
if runs > 1 && results.any?
|
|
299
|
+
IPS::Result.compare_aggregate(results_by_variant, out: $stdout)
|
|
300
|
+
else
|
|
301
|
+
IPS::Result.compare(results)
|
|
302
|
+
end
|
|
153
303
|
|
|
154
304
|
if save_path && results.any?
|
|
155
305
|
IPS::Result.save!(save_path, results)
|
data/lib/ips/job/entry.rb
CHANGED
|
@@ -32,8 +32,9 @@ module IPS
|
|
|
32
32
|
end
|
|
33
33
|
RUBY
|
|
34
34
|
eval_source = "# frozen_string_literal: #{@frozen_string_literal}\n#{@source}"
|
|
35
|
-
m =
|
|
36
|
-
m.
|
|
35
|
+
m = Module.new
|
|
36
|
+
m.module_eval(eval_source)
|
|
37
|
+
extend(m)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def define_call_times_block(act)
|
data/lib/ips/job.rb
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
require "ips/job/entry"
|
|
4
4
|
require "ips/result"
|
|
5
5
|
require "ips/display"
|
|
6
|
+
require "ips/warmup"
|
|
6
7
|
|
|
7
8
|
module IPS
|
|
8
9
|
class Job
|
|
9
|
-
MAX_ITERATIONS = 1 << 30
|
|
10
|
-
|
|
11
10
|
attr_accessor :warmup, :time
|
|
12
11
|
|
|
13
12
|
def initialize(time: 5, warmup: time * 0.2, summary: true, debug: false, frozen_string_literal: true, out: $stdout)
|
|
@@ -52,45 +51,29 @@ module IPS
|
|
|
52
51
|
|
|
53
52
|
private
|
|
54
53
|
|
|
55
|
-
def cycles_per_100ms(time_ns, iters)
|
|
56
|
-
cycles = ((Timing::NANOSECONDS_PER_100MS.to_f / time_ns) * iters).to_i
|
|
57
|
-
cycles <= 0 ? 1 : cycles
|
|
58
|
-
end
|
|
59
|
-
|
|
60
54
|
def warmup_item(item, display)
|
|
61
55
|
Timing.clean_env
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cycles =
|
|
67
|
-
begin
|
|
57
|
+
budget_ns = (@warmup * Timing::NANOSECONDS_PER_SECOND).to_i
|
|
58
|
+
warmup = Warmup.new
|
|
59
|
+
last_progress = 0
|
|
60
|
+
cycles = warmup.run(budget_ns) do |iters|
|
|
68
61
|
t0 = Timing.now
|
|
69
|
-
item.call_times(
|
|
62
|
+
item.call_times(iters)
|
|
70
63
|
t1 = Timing.now
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
elapsed_ns = t1 - t0
|
|
65
|
+
if t1 - last_progress > Timing::NANOSECONDS_PER_100MS
|
|
66
|
+
estimate = Timing::NANOSECONDS_PER_SECOND * (iters.to_f / elapsed_ns)
|
|
67
|
+
display.progress(estimate: estimate)
|
|
68
|
+
last_progress = t1
|
|
69
|
+
end
|
|
70
|
+
@out.printf " warmup: %d cycles in %dns (%s i/s)\n",
|
|
71
|
+
iters, elapsed_ns, Display.format_ips(Timing::NANOSECONDS_PER_SECOND * (iters.to_f / elapsed_ns)) if @debug
|
|
72
|
+
elapsed_ns
|
|
73
|
+
end
|
|
80
74
|
|
|
81
|
-
per_100ms = cycles_per_100ms(warmup_ns, warmup_iter)
|
|
82
|
-
cycles = per_100ms > MAX_ITERATIONS ? MAX_ITERATIONS : per_100ms
|
|
83
75
|
@timing[item] = cycles
|
|
84
76
|
@out.puts " cycles: #{cycles}" if @debug
|
|
85
|
-
|
|
86
|
-
target = Timing.add_second(before, @warmup)
|
|
87
|
-
while Timing.now + Timing::NANOSECONDS_PER_100MS < target
|
|
88
|
-
t0 = Timing.now
|
|
89
|
-
item.call_times(cycles)
|
|
90
|
-
t1 = Timing.now
|
|
91
|
-
estimate = Timing::NANOSECONDS_PER_SECOND * (cycles.to_f / (t1 - t0))
|
|
92
|
-
display.progress(estimate: estimate)
|
|
93
|
-
end
|
|
94
77
|
end
|
|
95
78
|
|
|
96
79
|
def measure_item(item, display)
|
data/lib/ips/result.rb
CHANGED
|
@@ -77,6 +77,39 @@ module IPS
|
|
|
77
77
|
data["runs"].map { |h| from_h(h) }
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
def self.compare_aggregate(results_by_group, out: $stdout)
|
|
81
|
+
run_count = results_by_group.sum(&:size)
|
|
82
|
+
multiple_groups = results_by_group.size > 1
|
|
83
|
+
|
|
84
|
+
pairs = results_by_group.flat_map do |results|
|
|
85
|
+
next [] if results.empty?
|
|
86
|
+
run_label = results.first.run_label
|
|
87
|
+
group_entries(results).map do |entry_label, entries|
|
|
88
|
+
label = multiple_groups && run_label ? "#{entry_label} (#{run_label})" : entry_label
|
|
89
|
+
[label, Entry::Aggregate.new(entries)]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
max_label = pairs.map { |l, _| l.size }.max
|
|
94
|
+
max_label = 20 if max_label < 20
|
|
95
|
+
out.puts "\nAggregate (#{run_count} runs)"
|
|
96
|
+
pairs.each do |label, agg|
|
|
97
|
+
error = agg.error_pct > 100 ? ">100" : "%4.1f" % agg.error_pct
|
|
98
|
+
out.printf "%#{max_label}s: %10s i/s (±%s%%)\n",
|
|
99
|
+
label, Display.format_ips(agg.ips), error
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Display.compare(pairs, out: out)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.group_entries(results)
|
|
106
|
+
num_entries = results.first.entries.size
|
|
107
|
+
num_entries.times.map do |i|
|
|
108
|
+
entries = results.map { |r| r.entries[i] }
|
|
109
|
+
[entries.first.label, entries]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
80
113
|
def self.compare(results, out: $stdout)
|
|
81
114
|
return if results.size < 2
|
|
82
115
|
|
|
@@ -202,6 +235,29 @@ module IPS
|
|
|
202
235
|
def self.from_h(hash)
|
|
203
236
|
new(hash["label"], hash["cycles"], hash["times"], hash["gc_times"])
|
|
204
237
|
end
|
|
238
|
+
|
|
239
|
+
class Aggregate
|
|
240
|
+
attr_reader :entries
|
|
241
|
+
|
|
242
|
+
def initialize(entries)
|
|
243
|
+
@entries = entries
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def ips
|
|
247
|
+
ips_values = @entries.map(&:ips)
|
|
248
|
+
ips_values.sum / ips_values.size
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def stddev
|
|
252
|
+
mean = ips
|
|
253
|
+
variance = @entries.map(&:ips).sum { |v| (v - mean) ** 2 } / @entries.size
|
|
254
|
+
Math.sqrt(variance)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def error_pct
|
|
258
|
+
(stddev / ips) * 100.0
|
|
259
|
+
end
|
|
260
|
+
end
|
|
205
261
|
end
|
|
206
262
|
end
|
|
207
263
|
end
|
data/lib/ips/version.rb
CHANGED
data/lib/ips/warmup.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IPS
|
|
4
|
+
class Warmup
|
|
5
|
+
MAX_ITERATIONS = 1 << 30
|
|
6
|
+
TARGET_BATCH_NS = 100_000_000
|
|
7
|
+
MAX_WARMUP_CYCLES = 1000
|
|
8
|
+
TARGET_WARMUP_CALLS = 1000
|
|
9
|
+
|
|
10
|
+
def run(budget_ns)
|
|
11
|
+
elapsed = 0
|
|
12
|
+
|
|
13
|
+
# 1. Rough calibrate
|
|
14
|
+
last_ns = yield 1
|
|
15
|
+
elapsed += last_ns
|
|
16
|
+
|
|
17
|
+
# 2. JIT warmup
|
|
18
|
+
calls_remaining = TARGET_WARMUP_CALLS
|
|
19
|
+
cycles = 1
|
|
20
|
+
while elapsed < budget_ns && calls_remaining > 0
|
|
21
|
+
remaining_ns = budget_ns - elapsed
|
|
22
|
+
cycles = cycles_for(last_ns, cycles, remaining_ns / calls_remaining)
|
|
23
|
+
cycles = MAX_WARMUP_CYCLES if cycles > MAX_WARMUP_CYCLES
|
|
24
|
+
last_ns = yield cycles
|
|
25
|
+
elapsed += last_ns
|
|
26
|
+
calls_remaining -= 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 3. Calibrate to 100ms batches and run out the budget
|
|
30
|
+
cycles = cycles_for(last_ns, cycles, TARGET_BATCH_NS)
|
|
31
|
+
cycles = MAX_ITERATIONS if cycles > MAX_ITERATIONS
|
|
32
|
+
while elapsed < budget_ns
|
|
33
|
+
last_ns = yield cycles
|
|
34
|
+
elapsed += last_ns
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
cycles
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def cycles_for(time_ns, iters, target_ns)
|
|
43
|
+
# Below the clock's resolution, so there's nothing to extrapolate from.
|
|
44
|
+
# Grow the batch and let the caller measure again.
|
|
45
|
+
return iters * 2 if time_ns <= 0
|
|
46
|
+
|
|
47
|
+
c = (target_ns * iters) / time_ns
|
|
48
|
+
c < 1 ? 1 : c
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/ips.rb
CHANGED
|
@@ -6,10 +6,18 @@ require "ips/display"
|
|
|
6
6
|
require "ips/job"
|
|
7
7
|
|
|
8
8
|
module IPS
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
@overrides = {}
|
|
10
|
+
|
|
11
|
+
def self.run(time: 5, warmup: time * 0.2, summary: true, debug: false, frozen_string_literal: true, save: nil, out: $stdout)
|
|
12
|
+
opts = { time: time, warmup: warmup, summary: summary, debug: debug, frozen_string_literal: frozen_string_literal, out: out }
|
|
13
|
+
opts.merge!(@overrides)
|
|
14
|
+
save = opts.delete(:save)
|
|
15
|
+
|
|
16
|
+
job = Job.new(**opts)
|
|
11
17
|
yield job
|
|
12
|
-
job.run
|
|
18
|
+
result = job.run
|
|
19
|
+
Marshal.dump(result, save) if save
|
|
20
|
+
result
|
|
13
21
|
end
|
|
14
22
|
|
|
15
23
|
def self.report(label = nil, action = nil, &block)
|
|
@@ -21,6 +29,7 @@ module IPS
|
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
class << self
|
|
32
|
+
attr_accessor :overrides
|
|
24
33
|
alias_method :ips, :run
|
|
25
34
|
end
|
|
26
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ips
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Hawthorn
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- lib/ips/result.rb
|
|
40
40
|
- lib/ips/timing.rb
|
|
41
41
|
- lib/ips/version.rb
|
|
42
|
+
- lib/ips/warmup.rb
|
|
42
43
|
homepage: https://github.com/jhawthorn/ips
|
|
43
44
|
licenses:
|
|
44
45
|
- MIT
|
|
@@ -59,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
59
60
|
- !ruby/object:Gem::Version
|
|
60
61
|
version: '0'
|
|
61
62
|
requirements: []
|
|
62
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 4.0.16
|
|
63
64
|
specification_version: 4
|
|
64
65
|
summary: Iterations per second benchmarking with statistical analysis
|
|
65
66
|
test_files: []
|