ips 0.4.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 +176 -64
- data/lib/ips/version.rb +1 -1
- data/lib/ips/warmup.rb +4 -0
- metadata +2 -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,7 +2,9 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
versions = []
|
|
5
|
+
revs = []
|
|
5
6
|
inline_code = []
|
|
7
|
+
prelude = []
|
|
6
8
|
ruby_flags = []
|
|
7
9
|
runs = 1
|
|
8
10
|
save_path = nil
|
|
@@ -16,12 +18,21 @@ while args.first
|
|
|
16
18
|
when "--ruby"
|
|
17
19
|
args.shift
|
|
18
20
|
versions << args.shift
|
|
21
|
+
when "--sha"
|
|
22
|
+
args.shift
|
|
23
|
+
revs << args.shift
|
|
19
24
|
when "-e"
|
|
20
25
|
args.shift
|
|
21
26
|
inline_code << args.shift
|
|
22
27
|
when "-r"
|
|
23
28
|
args.shift
|
|
24
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
|
|
25
36
|
when "--disable-gems", /\A--yjit/, /\A--zjit/
|
|
26
37
|
ruby_flags << args.shift
|
|
27
38
|
when "--runs"
|
|
@@ -55,34 +66,10 @@ end
|
|
|
55
66
|
script = args.shift
|
|
56
67
|
|
|
57
68
|
if script.nil? && inline_code.empty?
|
|
58
|
-
$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>]"
|
|
59
70
|
exit 1
|
|
60
71
|
end
|
|
61
72
|
|
|
62
|
-
overrides = []
|
|
63
|
-
overrides << "save: IO.open(3, 'w')"
|
|
64
|
-
overrides << "summary: false" if versions.size > 1 || runs > 1
|
|
65
|
-
overrides << "time: #{bench_time}" if bench_time
|
|
66
|
-
overrides << "warmup: #{warmup_time}" if warmup_time
|
|
67
|
-
overrides << "debug: true" if debug
|
|
68
|
-
overrides << "frozen_string_literal: false" unless frozen_string_literal
|
|
69
|
-
|
|
70
|
-
script_source = +""
|
|
71
|
-
script_source << "# frozen_string_literal: true\n" if frozen_string_literal
|
|
72
|
-
script_source << "require 'ips'\n"
|
|
73
|
-
script_source << "IPS.overrides = { #{overrides.join(", ")} }\n"
|
|
74
|
-
|
|
75
|
-
if inline_code.any?
|
|
76
|
-
script_source << "IPS.run do |x|\n"
|
|
77
|
-
inline_code.each do |code|
|
|
78
|
-
script_source << " x.report(#{code.inspect}, #{code.inspect})\n"
|
|
79
|
-
end
|
|
80
|
-
script_source << "end\n"
|
|
81
|
-
else
|
|
82
|
-
script_source << "load(#{script.dump})\n"
|
|
83
|
-
end
|
|
84
|
-
ruby_args = ["-e", script_source]
|
|
85
|
-
|
|
86
73
|
# Shell-escape for display purposes only, not for security.
|
|
87
74
|
def shell_quote(arg)
|
|
88
75
|
return arg if arg =~ /\A[A-Za-z0-9_\-.,:+\/@]+\z/
|
|
@@ -120,6 +107,20 @@ def resolve_ruby(str)
|
|
|
120
107
|
[ruby, extra_flags]
|
|
121
108
|
end
|
|
122
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
|
+
|
|
123
124
|
if versions.empty?
|
|
124
125
|
rubies = [[nil, nil, []]]
|
|
125
126
|
else
|
|
@@ -129,62 +130,173 @@ else
|
|
|
129
130
|
}
|
|
130
131
|
end
|
|
131
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
|
+
|
|
132
206
|
$LOAD_PATH.unshift(LIB_DIR)
|
|
133
207
|
require "ips"
|
|
134
208
|
|
|
135
|
-
|
|
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
|
|
214
|
+
|
|
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
|
|
231
|
+
else
|
|
232
|
+
$stderr.puts "WARNING: failed to restore #{original_ref || original_sha}:\n#{out}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
trap("INT") do
|
|
237
|
+
restore.call
|
|
238
|
+
exit 130
|
|
239
|
+
end if original_sha
|
|
240
|
+
|
|
241
|
+
results_by_variant = variants.map { [] }
|
|
136
242
|
quiet = runs > 1
|
|
137
243
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
else
|
|
143
|
-
ruby = "ruby"
|
|
144
|
-
env = {}
|
|
145
|
-
end
|
|
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)
|
|
146
248
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
249
|
+
if ruby
|
|
250
|
+
env = { "GEM_HOME" => nil, "GEM_PATH" => nil }
|
|
251
|
+
else
|
|
252
|
+
ruby = "ruby"
|
|
253
|
+
env = {}
|
|
254
|
+
end
|
|
151
255
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
260
|
|
|
156
|
-
|
|
157
|
-
|
|
261
|
+
rd, wr = IO.pipe
|
|
262
|
+
spawn_opts = { 3 => wr }
|
|
263
|
+
spawn_opts[:out] = File::NULL if quiet
|
|
158
264
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
$stdout.print " (#{version_str})" if version_str
|
|
162
|
-
$stdout.print "..."
|
|
163
|
-
$stdout.flush
|
|
164
|
-
end
|
|
265
|
+
pid = spawn(env, *cmd, **spawn_opts)
|
|
266
|
+
wr.close
|
|
165
267
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
178
289
|
end
|
|
179
|
-
puts unless quiet
|
|
180
290
|
end
|
|
291
|
+
ensure
|
|
292
|
+
restore.call
|
|
181
293
|
end
|
|
182
294
|
$stderr.puts if quiet
|
|
183
295
|
|
|
184
|
-
results =
|
|
296
|
+
results = results_by_variant.flatten
|
|
185
297
|
|
|
186
298
|
if runs > 1 && results.any?
|
|
187
|
-
IPS::Result.compare_aggregate(
|
|
299
|
+
IPS::Result.compare_aggregate(results_by_variant, out: $stdout)
|
|
188
300
|
else
|
|
189
301
|
IPS::Result.compare(results)
|
|
190
302
|
end
|
data/lib/ips/version.rb
CHANGED
data/lib/ips/warmup.rb
CHANGED
|
@@ -40,6 +40,10 @@ module IPS
|
|
|
40
40
|
private
|
|
41
41
|
|
|
42
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
|
+
|
|
43
47
|
c = (target_ns * iters) / time_ns
|
|
44
48
|
c < 1 ? 1 : c
|
|
45
49
|
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
|
|
@@ -60,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '0'
|
|
62
62
|
requirements: []
|
|
63
|
-
rubygems_version: 4.0.
|
|
63
|
+
rubygems_version: 4.0.16
|
|
64
64
|
specification_version: 4
|
|
65
65
|
summary: Iterations per second benchmarking with statistical analysis
|
|
66
66
|
test_files: []
|