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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bb634cdd8199a1545541b115316fdb80324560b872690ad5138edda74663fb1
4
- data.tar.gz: f00fa8882fb682d591c74970988ac6c371adf4174a212abc137a62e046f61fa6
3
+ metadata.gz: dd8eb49702573799e4d4754155e2d4c76713e1363f8dc2659ffbc99f96d21fe0
4
+ data.tar.gz: 7936ccc9a3896c754fd5fa2cd719e668df7317c6c92bc35e8d70fe301d1970e7
5
5
  SHA512:
6
- metadata.gz: c3bd6bf124e9a17ac2e1f02d8d68166f512be659f1fc488ac063213589762fd0f9d81aade8825dc56fa4da4162812cf68640403829a73b907da0ac735bf3f190
7
- data.tar.gz: 1858e8651c5472a4c032dc7e2f4d5a4a758d20fad9b179ba55c5e2af05518f93609c2a6c961594798f2d8b639413b82980c038352071182d7d42dd4eee032f13
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
- results_by_ruby = rubies.map { [] }
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
- runs.times do |run_index|
139
- rubies.each_with_index do |(version_str, ruby, extra_flags), ruby_index|
140
- if ruby
141
- env = { "GEM_HOME" => nil, "GEM_PATH" => nil }
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
- cmd = [ruby, *extra_flags, *ruby_flags]
148
- cmd << "-v" unless quiet
149
- cmd.push("-I", LIB_DIR, *ruby_args)
150
- $stdout.puts cmd.map { |a| shell_quote(a) }.join(" ") if verbose
249
+ if ruby
250
+ env = { "GEM_HOME" => nil, "GEM_PATH" => nil }
251
+ else
252
+ ruby = "ruby"
253
+ env = {}
254
+ end
151
255
 
152
- rd, wr = IO.pipe
153
- spawn_opts = { 3 => wr }
154
- spawn_opts[:out] = File::NULL if quiet
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
- pid = spawn(env, *cmd, **spawn_opts)
157
- wr.close
261
+ rd, wr = IO.pipe
262
+ spawn_opts = { 3 => wr }
263
+ spawn_opts[:out] = File::NULL if quiet
158
264
 
159
- if quiet
160
- $stdout.print "\rRun #{run_index + 1}/#{runs}"
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
- until rd.eof?
167
- result = Marshal.load(rd)
168
- result.ruby_executable = ruby
169
- result.run_label = "ruby #{version_str}" if version_str
170
- results_by_ruby[ruby_index] << result
171
- end
172
- rd.close
173
- _, status = Process.wait2(pid)
174
- unless status.success?
175
- $stderr.puts if quiet
176
- cmd_str = cmd.map { |a| shell_quote(a) }.join(" ")
177
- abort "Command failed (exit #{status.exitstatus}): #{cmd_str}"
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 = results_by_ruby.flatten
296
+ results = results_by_variant.flatten
185
297
 
186
298
  if runs > 1 && results.any?
187
- IPS::Result.compare_aggregate(results_by_ruby, out: $stdout)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IPS
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.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.3
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: []