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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c8dde979505d34763745a2f6dcf6f9df0a8e6396fdd2351b769282393839417
4
- data.tar.gz: 4225c76063e8a905bbf14efb5eb8ee000a030b4f674d6c2c9cef9571f007d4b6
3
+ metadata.gz: dd8eb49702573799e4d4754155e2d4c76713e1363f8dc2659ffbc99f96d21fe0
4
+ data.tar.gz: 7936ccc9a3896c754fd5fa2cd719e668df7317c6c92bc35e8d70fe301d1970e7
5
5
  SHA512:
6
- metadata.gz: d0178e0e87a6913dc3134f4e748bdb795f16e4fb2a2cca10c3f1fc7e7dc9a4615e5bb3d45c12aff7adb6a0a12afd644392b77f07bd080ee91e382734b0ea1b8e
7
- data.tar.gz: 199d08e24ae772bc5030a9f8daf63b4b453cfef6e8623110fd292ae70d5d1ca1f806bcaae7b86e364dae1850deea6a398b4b01426166c153cbc45917ec823644
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.start_with?(version)
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
- nil
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
- results = []
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
- rubies.each do |version_str, ruby, extra_flags|
127
- if ruby
128
- env = { "GEM_HOME" => nil, "GEM_PATH" => nil }
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
- ruby = "ruby"
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
- cmd = [ruby, *extra_flags, *ruby_flags, "-v", "-I", LIB_DIR, *ruby_args]
135
- $stderr.puts cmd.map { |a| shell_quote(a) }.join(" ") if verbose
136
- rd, wr = IO.pipe
137
- pid = spawn(env, *cmd, 3 => wr)
138
- wr.close
139
- data = rd.read
140
- rd.close
141
- Process.wait(pid)
142
- raise "Child process failed" unless $?.success?
143
- unless data.empty?
144
- result = Marshal.load(data)
145
- result.ruby_executable = ruby
146
- result.run_label = "ruby #{version_str}" if version_str
147
- results << result
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
- puts
291
+ ensure
292
+ restore.call
150
293
  end
294
+ $stderr.puts if quiet
151
295
 
152
- IPS::Result.compare(results)
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 = (class << self; self; end)
36
- m.class_eval(eval_source)
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
- before = Timing.now
64
- target = Timing.add_second(before, @warmup / 2.0)
65
-
66
- cycles = 1
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(cycles)
62
+ item.call_times(iters)
70
63
  t1 = Timing.now
71
- warmup_iter = cycles
72
- warmup_ns = t1 - t0
73
-
74
- estimate = Timing::NANOSECONDS_PER_SECOND * (warmup_iter.to_f / warmup_ns)
75
- display.progress(estimate: estimate)
76
-
77
- break if cycles >= MAX_ITERATIONS
78
- cycles *= 2
79
- end while Timing.now + warmup_ns * 2 < target
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IPS
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
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
- def self.run(time: 5, warmup: time * 0.2, summary: true, debug: false, frozen_string_literal: true, out: $stdout)
10
- job = Job.new(time: time, warmup: warmup, summary: summary, debug: debug, frozen_string_literal: frozen_string_literal, out: out)
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.3.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: 3.6.9
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: []