ips 0.3.0 → 0.4.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: 5bb634cdd8199a1545541b115316fdb80324560b872690ad5138edda74663fb1
4
+ data.tar.gz: f00fa8882fb682d591c74970988ac6c371adf4174a212abc137a62e046f61fa6
5
5
  SHA512:
6
- metadata.gz: d0178e0e87a6913dc3134f4e748bdb795f16e4fb2a2cca10c3f1fc7e7dc9a4615e5bb3d45c12aff7adb6a0a12afd644392b77f07bd080ee91e382734b0ea1b8e
7
- data.tar.gz: 199d08e24ae772bc5030a9f8daf63b4b453cfef6e8623110fd292ae70d5d1ca1f806bcaae7b86e364dae1850deea6a398b4b01426166c153cbc45917ec823644
6
+ metadata.gz: c3bd6bf124e9a17ac2e1f02d8d68166f512be659f1fc488ac063213589762fd0f9d81aade8825dc56fa4da4162812cf68640403829a73b907da0ac735bf3f190
7
+ data.tar.gz: 1858e8651c5472a4c032dc7e2f4d5a4a758d20fad9b179ba55c5e2af05518f93609c2a6c961594798f2d8b639413b82980c038352071182d7d42dd4eee032f13
data/exe/ips CHANGED
@@ -4,6 +4,7 @@
4
4
  versions = []
5
5
  inline_code = []
6
6
  ruby_flags = []
7
+ runs = 1
7
8
  save_path = nil
8
9
  bench_time = nil
9
10
  warmup_time = nil
@@ -23,6 +24,9 @@ while args.first
23
24
  ruby_flags.push("-r", args.shift)
24
25
  when "--disable-gems", /\A--yjit/, /\A--zjit/
25
26
  ruby_flags << args.shift
27
+ when "--runs"
28
+ args.shift
29
+ runs = args.shift.to_i
26
30
  when "--save"
27
31
  args.shift
28
32
  save_path = args.shift
@@ -55,26 +59,29 @@ if script.nil? && inline_code.empty?
55
59
  exit 1
56
60
  end
57
61
 
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
+
58
75
  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"
76
+ script_source << "IPS.run do |x|\n"
69
77
  inline_code.each do |code|
70
78
  script_source << " x.report(#{code.inspect}, #{code.inspect})\n"
71
79
  end
72
80
  script_source << "end\n"
73
- script_source << "IO.open(3, 'w') { |f| Marshal.dump(result, f) }\n"
74
- ruby_args = ["-e", script_source]
75
81
  else
76
- ruby_args = [script]
82
+ script_source << "load(#{script.dump})\n"
77
83
  end
84
+ ruby_args = ["-e", script_source]
78
85
 
79
86
  # Shell-escape for display purposes only, not for security.
80
87
  def shell_quote(arg)
@@ -86,17 +93,21 @@ RUBY_DIRS = [File.expand_path("~/.rubies"), "/opt/rubies"]
86
93
  LIB_DIR = File.expand_path("../lib", __dir__)
87
94
 
88
95
  def find_ruby(version)
96
+ match = nil
89
97
  RUBY_DIRS.each do |dir|
90
98
  next unless Dir.exist?(dir)
91
99
  Dir.children(dir).sort.each do |name|
92
100
  bare = name.delete_prefix("ruby-")
93
- if bare.start_with?(version)
101
+ if bare == version || name == version
94
102
  ruby = File.join(dir, name, "bin", "ruby")
95
103
  return ruby if File.executable?(ruby)
104
+ elsif bare.include?(version) || name.include?(version)
105
+ ruby = File.join(dir, name, "bin", "ruby")
106
+ match = ruby if File.executable?(ruby)
96
107
  end
97
108
  end
98
109
  end
99
- nil
110
+ match
100
111
  end
101
112
 
102
113
  def resolve_ruby(str)
@@ -121,35 +132,62 @@ end
121
132
  $LOAD_PATH.unshift(LIB_DIR)
122
133
  require "ips"
123
134
 
124
- results = []
135
+ results_by_ruby = rubies.map { [] }
136
+ quiet = runs > 1
125
137
 
126
- rubies.each do |version_str, ruby, extra_flags|
127
- if ruby
128
- env = { "GEM_HOME" => nil, "GEM_PATH" => nil }
129
- else
130
- ruby = "ruby"
131
- env = {}
132
- end
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
146
+
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
151
+
152
+ rd, wr = IO.pipe
153
+ spawn_opts = { 3 => wr }
154
+ spawn_opts[:out] = File::NULL if quiet
133
155
 
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
156
+ pid = spawn(env, *cmd, **spawn_opts)
157
+ wr.close
158
+
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
165
+
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}"
178
+ end
179
+ puts unless quiet
148
180
  end
149
- puts
150
181
  end
182
+ $stderr.puts if quiet
183
+
184
+ results = results_by_ruby.flatten
151
185
 
152
- IPS::Result.compare(results)
186
+ if runs > 1 && results.any?
187
+ IPS::Result.compare_aggregate(results_by_ruby, out: $stdout)
188
+ else
189
+ IPS::Result.compare(results)
190
+ end
153
191
 
154
192
  if save_path && results.any?
155
193
  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.4.0"
5
5
  end
data/lib/ips/warmup.rb ADDED
@@ -0,0 +1,47 @@
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
+ c = (target_ns * iters) / time_ns
44
+ c < 1 ? 1 : c
45
+ end
46
+ end
47
+ 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.4.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.3
63
64
  specification_version: 4
64
65
  summary: Iterations per second benchmarking with statistical analysis
65
66
  test_files: []