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 +4 -4
- data/exe/ips +77 -39
- 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 +47 -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: 5bb634cdd8199a1545541b115316fdb80324560b872690ad5138edda74663fb1
|
|
4
|
+
data.tar.gz: f00fa8882fb682d591c74970988ac6c371adf4174a212abc137a62e046f61fa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
results_by_ruby = rubies.map { [] }
|
|
136
|
+
quiet = runs > 1
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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 =
|
|
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,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
|
-
|
|
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.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:
|
|
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: []
|