motion-benchmark-ips 1.0 → 1.1
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 +27 -0
- data/lib/project/compare.rb +65 -30
- data/lib/project/ips.rb +67 -236
- data/lib/project/ips/job.rb +351 -0
- data/lib/project/ips/job/entry.rb +77 -0
- data/lib/project/ips/job/stdout_report.rb +64 -0
- data/lib/project/ips/report.rb +189 -0
- data/lib/project/ips/stats/bootstrap.rb +51 -0
- data/lib/project/ips/stats/sd.rb +33 -0
- data/lib/project/timing.rb +52 -12
- metadata +10 -4
@@ -0,0 +1,351 @@
|
|
1
|
+
module Benchmark
|
2
|
+
module IPS
|
3
|
+
# Benchmark jobs.
|
4
|
+
class Job
|
5
|
+
# Microseconds per 100 millisecond.
|
6
|
+
MICROSECONDS_PER_100MS = 100_000
|
7
|
+
# Microseconds per second.
|
8
|
+
MICROSECONDS_PER_SECOND = Timing::MICROSECONDS_PER_SECOND
|
9
|
+
# The percentage of the expected runtime to allow
|
10
|
+
# before reporting a weird runtime
|
11
|
+
MAX_TIME_SKEW = 0.05
|
12
|
+
|
13
|
+
# Two-element arrays, consisting of label and block pairs.
|
14
|
+
# @return [Array<Entry>] list of entries
|
15
|
+
attr_reader :list
|
16
|
+
|
17
|
+
# Determining whether to run comparison utility.
|
18
|
+
# @return [Boolean] true if needs to run compare.
|
19
|
+
attr_reader :compare
|
20
|
+
|
21
|
+
# Determining whether to hold results between Ruby invocations
|
22
|
+
# @return [Boolean]
|
23
|
+
attr_accessor :hold
|
24
|
+
|
25
|
+
# Report object containing information about the run.
|
26
|
+
# @return [Report] the report object.
|
27
|
+
attr_reader :full_report
|
28
|
+
|
29
|
+
# Storing Iterations in time period.
|
30
|
+
# @return [Hash]
|
31
|
+
attr_reader :timing
|
32
|
+
|
33
|
+
# Warmup time setter and getter (in seconds).
|
34
|
+
# @return [Integer]
|
35
|
+
attr_accessor :warmup
|
36
|
+
|
37
|
+
# Calculation time setter and getter (in seconds).
|
38
|
+
# @return [Integer]
|
39
|
+
attr_accessor :time
|
40
|
+
|
41
|
+
# Warmup and calculation iterations.
|
42
|
+
# @return [Integer]
|
43
|
+
attr_accessor :iterations
|
44
|
+
|
45
|
+
# Statistics model.
|
46
|
+
# @return [Object]
|
47
|
+
attr_accessor :stats
|
48
|
+
|
49
|
+
# Confidence.
|
50
|
+
# @return [Integer]
|
51
|
+
attr_accessor :confidence
|
52
|
+
|
53
|
+
# Instantiate the Benchmark::IPS::Job.
|
54
|
+
# @option opts [Benchmark::Suite] (nil) :suite Specify Benchmark::Suite.
|
55
|
+
# @option opts [Boolean] (false) :quiet Suppress the printing of information.
|
56
|
+
def initialize opts={}
|
57
|
+
@suite = opts[:suite] || nil
|
58
|
+
@stdout = opts[:quiet] ? nil : StdoutReport.new
|
59
|
+
@list = []
|
60
|
+
@compare = false
|
61
|
+
@json_path = false
|
62
|
+
@held_path = nil
|
63
|
+
@held_results = nil
|
64
|
+
|
65
|
+
@timing = Hash.new 1 # default to 1 in case warmup isn't run
|
66
|
+
@full_report = Report.new
|
67
|
+
|
68
|
+
# Default warmup and calculation time in seconds.
|
69
|
+
@warmup = 2
|
70
|
+
@time = 5
|
71
|
+
@iterations = 1
|
72
|
+
|
73
|
+
# Default statistical model
|
74
|
+
@stats = :sd
|
75
|
+
@confidence = 95
|
76
|
+
end
|
77
|
+
|
78
|
+
# Job configuration options, set +@warmup+ and +@time+.
|
79
|
+
# @option opts [Integer] :warmup Warmup time.
|
80
|
+
# @option opts [Integer] :time Calculation time.
|
81
|
+
# @option iterations [Integer] :time Warmup and calculation iterations.
|
82
|
+
def config opts
|
83
|
+
@warmup = opts[:warmup] if opts[:warmup]
|
84
|
+
@time = opts[:time] if opts[:time]
|
85
|
+
@suite = opts[:suite] if opts[:suite]
|
86
|
+
@iterations = opts[:iterations] if opts[:iterations]
|
87
|
+
@stats = opts[:stats] if opts[:stats]
|
88
|
+
@confidence = opts[:confidence] if opts[:confidence]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return true if job needs to be compared.
|
92
|
+
# @return [Boolean] Need to compare?
|
93
|
+
def compare?
|
94
|
+
@compare
|
95
|
+
end
|
96
|
+
|
97
|
+
# Set @compare to true.
|
98
|
+
def compare!
|
99
|
+
@compare = true
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return true if results are held while multiple Ruby invocations
|
103
|
+
# @return [Boolean] Need to hold results between multiple Ruby invocations?
|
104
|
+
def hold?
|
105
|
+
!!@held_path
|
106
|
+
end
|
107
|
+
|
108
|
+
# Set @hold to true.
|
109
|
+
def hold!(held_path)
|
110
|
+
@held_path = held_path
|
111
|
+
end
|
112
|
+
|
113
|
+
# Return true if job needs to generate json.
|
114
|
+
# @return [Boolean] Need to generate json?
|
115
|
+
def json?
|
116
|
+
!!@json_path
|
117
|
+
end
|
118
|
+
|
119
|
+
# Set @json_path to given path, defaults to "data.json".
|
120
|
+
def json!(path="data.json")
|
121
|
+
@json_path = path
|
122
|
+
end
|
123
|
+
|
124
|
+
# Registers the given label and block pair in the job list.
|
125
|
+
# @param label [String] Label of benchmarked code.
|
126
|
+
# @param str [String] Code to be benchmarked.
|
127
|
+
# @param blk [Proc] Code to be benchmarked.
|
128
|
+
# @raise [ArgumentError] Raises if str and blk are both present.
|
129
|
+
# @raise [ArgumentError] Raises if str and blk are both absent.
|
130
|
+
def item(label="", str=nil, &blk) # :yield:
|
131
|
+
if blk and str
|
132
|
+
raise ArgumentError, "specify a block and a str, but not both"
|
133
|
+
end
|
134
|
+
|
135
|
+
action = str || blk
|
136
|
+
raise ArgumentError, "no block or string" unless action
|
137
|
+
|
138
|
+
@list.push Entry.new(label, action)
|
139
|
+
self
|
140
|
+
end
|
141
|
+
alias_method :report, :item
|
142
|
+
|
143
|
+
# Calculate the cycles needed to run for approx 100ms,
|
144
|
+
# given the number of iterations to run the given time.
|
145
|
+
# @param [Float] time_msec Each iteration's time in ms.
|
146
|
+
# @param [Integer] iters Iterations.
|
147
|
+
# @return [Integer] Cycles per 100ms.
|
148
|
+
def cycles_per_100ms time_msec, iters
|
149
|
+
cycles = ((MICROSECONDS_PER_100MS / time_msec) * iters).to_i
|
150
|
+
cycles <= 0 ? 1 : cycles
|
151
|
+
end
|
152
|
+
|
153
|
+
# Calculate the time difference of before and after in microseconds.
|
154
|
+
# @param [Time] before time.
|
155
|
+
# @param [Time] after time.
|
156
|
+
# @return [Float] Time difference of before and after.
|
157
|
+
def time_us before, after
|
158
|
+
(after.to_f - before.to_f) * MICROSECONDS_PER_SECOND
|
159
|
+
end
|
160
|
+
|
161
|
+
# Calculate the interations per second given the number
|
162
|
+
# of cycles run and the time in microseconds that elapsed.
|
163
|
+
# @param [Integer] cycles Cycles.
|
164
|
+
# @param [Integer] time_us Time in microsecond.
|
165
|
+
# @return [Float] Iteration per second.
|
166
|
+
def iterations_per_sec cycles, time_us
|
167
|
+
MICROSECONDS_PER_SECOND * (cycles.to_f / time_us.to_f)
|
168
|
+
end
|
169
|
+
|
170
|
+
def held_results?
|
171
|
+
File.exist?(@held_path)
|
172
|
+
end
|
173
|
+
|
174
|
+
def load_held_results
|
175
|
+
require "json"
|
176
|
+
@held_results = Hash[File.open(@held_path).map { |line|
|
177
|
+
result = JSON.parse(line)
|
178
|
+
[result['item'], result]
|
179
|
+
}]
|
180
|
+
end
|
181
|
+
|
182
|
+
def run
|
183
|
+
if @warmup && @warmup != 0 then
|
184
|
+
@stdout.start_warming if @stdout
|
185
|
+
@iterations.times do
|
186
|
+
run_warmup
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
@stdout.start_running if @stdout
|
191
|
+
|
192
|
+
held = nil
|
193
|
+
|
194
|
+
@iterations.times do |n|
|
195
|
+
held = run_benchmark
|
196
|
+
end
|
197
|
+
|
198
|
+
@stdout.footer if @stdout
|
199
|
+
|
200
|
+
if held
|
201
|
+
puts
|
202
|
+
puts 'Pausing here -- run Ruby again to measure the next benchmark...'
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Run warmup.
|
207
|
+
def run_warmup
|
208
|
+
@list.each do |item|
|
209
|
+
next if hold? && @held_results && @held_results.key?(item.label)
|
210
|
+
|
211
|
+
@suite.warming item.label, @warmup if @suite
|
212
|
+
@stdout.warming item.label, @warmup if @stdout
|
213
|
+
|
214
|
+
Timing.clean_env
|
215
|
+
|
216
|
+
before = Timing.now
|
217
|
+
target = Timing.add_second before, @warmup
|
218
|
+
|
219
|
+
warmup_iter = 0
|
220
|
+
|
221
|
+
while Timing.now < target
|
222
|
+
item.call_times(1)
|
223
|
+
warmup_iter += 1
|
224
|
+
end
|
225
|
+
|
226
|
+
after = Timing.now
|
227
|
+
|
228
|
+
warmup_time_us = Timing.time_us(before, after)
|
229
|
+
|
230
|
+
@timing[item] = cycles_per_100ms warmup_time_us, warmup_iter
|
231
|
+
|
232
|
+
@stdout.warmup_stats warmup_time_us, @timing[item] if @stdout
|
233
|
+
@suite.warmup_stats warmup_time_us, @timing[item] if @suite
|
234
|
+
|
235
|
+
break if hold?
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Run calculation.
|
240
|
+
def run_benchmark
|
241
|
+
@list.each do |item|
|
242
|
+
if hold? && @held_results && @held_results.key?(item.label)
|
243
|
+
result = @held_results[item.label]
|
244
|
+
create_report(item.label, result['measured_us'], result['iter'],
|
245
|
+
create_stats(result['samples']), result['cycles'])
|
246
|
+
next
|
247
|
+
end
|
248
|
+
|
249
|
+
@suite.running item.label, @time if @suite
|
250
|
+
@stdout.running item.label, @time if @stdout
|
251
|
+
|
252
|
+
Timing.clean_env
|
253
|
+
|
254
|
+
iter = 0
|
255
|
+
|
256
|
+
measurements_us = []
|
257
|
+
|
258
|
+
# Running this number of cycles should take around 100ms.
|
259
|
+
cycles = @timing[item]
|
260
|
+
|
261
|
+
target = Timing.add_second Timing.now, @time
|
262
|
+
|
263
|
+
while (before = Timing.now) < target
|
264
|
+
item.call_times cycles
|
265
|
+
after = Timing.now
|
266
|
+
|
267
|
+
# If for some reason the timing said this took no time (O_o)
|
268
|
+
# then ignore the iteration entirely and start another.
|
269
|
+
iter_us = Timing.time_us before, after
|
270
|
+
next if iter_us <= 0.0
|
271
|
+
|
272
|
+
iter += cycles
|
273
|
+
|
274
|
+
measurements_us << iter_us
|
275
|
+
end
|
276
|
+
|
277
|
+
final_time = before
|
278
|
+
|
279
|
+
measured_us = measurements_us.inject(:+)
|
280
|
+
|
281
|
+
samples = measurements_us.map { |time_us|
|
282
|
+
iterations_per_sec cycles, time_us
|
283
|
+
}
|
284
|
+
|
285
|
+
rep = create_report(item.label, measured_us, iter, create_stats(samples), cycles)
|
286
|
+
|
287
|
+
if (final_time - target).abs >= (@time.to_f * MAX_TIME_SKEW)
|
288
|
+
rep.show_total_time!
|
289
|
+
end
|
290
|
+
|
291
|
+
@stdout.add_report rep, caller(1).first if @stdout
|
292
|
+
@suite.add_report rep, caller(1).first if @suite
|
293
|
+
|
294
|
+
if hold? && item != @list.last
|
295
|
+
File.open @held_path, "a" do |f|
|
296
|
+
require "json"
|
297
|
+
f.write JSON.generate({
|
298
|
+
:item => item.label,
|
299
|
+
:measured_us => measured_us,
|
300
|
+
:iter => iter,
|
301
|
+
:samples => samples,
|
302
|
+
:cycles => cycles
|
303
|
+
})
|
304
|
+
f.write "\n"
|
305
|
+
end
|
306
|
+
|
307
|
+
return true
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
if hold? && @full_report.entries.size == @list.size
|
312
|
+
File.delete @held_path if File.exist?(@held_path)
|
313
|
+
end
|
314
|
+
|
315
|
+
false
|
316
|
+
end
|
317
|
+
|
318
|
+
def create_stats(samples)
|
319
|
+
case @stats
|
320
|
+
when :sd
|
321
|
+
Stats::SD.new(samples)
|
322
|
+
when :bootstrap
|
323
|
+
Stats::Bootstrap.new(samples, @confidence)
|
324
|
+
else
|
325
|
+
raise "unknown stats #{@stats}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Run comparison of entries in +@full_report+.
|
330
|
+
def run_comparison
|
331
|
+
@full_report.run_comparison if compare?
|
332
|
+
end
|
333
|
+
|
334
|
+
# Generate json from +@full_report+.
|
335
|
+
def generate_json
|
336
|
+
@full_report.generate_json @json_path if json?
|
337
|
+
end
|
338
|
+
|
339
|
+
# Create report by add entry to +@full_report+.
|
340
|
+
# @param label [String] Report item label.
|
341
|
+
# @param measured_us [Integer] Measured time in microsecond.
|
342
|
+
# @param iter [Integer] Iterations.
|
343
|
+
# @param samples [Array<Float>] Sampled iterations per second.
|
344
|
+
# @param cycles [Integer] Number of Cycles.
|
345
|
+
# @return [Report::Entry] Entry with data.
|
346
|
+
def create_report(label, measured_us, iter, samples, cycles)
|
347
|
+
@full_report.add_entry label, measured_us, iter, samples, cycles
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Benchmark
|
2
|
+
module IPS
|
3
|
+
# Benchmark jobs.
|
4
|
+
class Job
|
5
|
+
# Entries in Benchmark Jobs.
|
6
|
+
class Entry
|
7
|
+
# Instantiate the Benchmark::IPS::Job::Entry.
|
8
|
+
# @param label [#to_s] Label of Benchmarked code.
|
9
|
+
# @param action [String, Proc] Code to be benchmarked.
|
10
|
+
# @raise [ArgumentError] Raises when action is not String or not responding to +call+.
|
11
|
+
def initialize(label, action)
|
12
|
+
@label = label
|
13
|
+
|
14
|
+
if action.kind_of? String
|
15
|
+
compile action
|
16
|
+
@action = self
|
17
|
+
@as_action = true
|
18
|
+
else
|
19
|
+
unless action.respond_to? :call
|
20
|
+
raise ArgumentError, "invalid action, must respond to #call"
|
21
|
+
end
|
22
|
+
|
23
|
+
@action = action
|
24
|
+
|
25
|
+
if action.respond_to? :arity and action.arity > 0
|
26
|
+
@call_loop = true
|
27
|
+
else
|
28
|
+
@call_loop = false
|
29
|
+
end
|
30
|
+
|
31
|
+
@as_action = false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# The label of benchmarking action.
|
36
|
+
# @return [#to_s] Label of action.
|
37
|
+
attr_reader :label
|
38
|
+
|
39
|
+
# The benchmarking action.
|
40
|
+
# @return [String, Proc] Code to be called, could be String / Proc.
|
41
|
+
attr_reader :action
|
42
|
+
|
43
|
+
# Call action by given times, return if +@call_loop+ is present.
|
44
|
+
# @param times [Integer] Times to call +@action+.
|
45
|
+
# @return [Integer] Number of times the +@action+ has been called.
|
46
|
+
def call_times(times)
|
47
|
+
return @action.call(times) if @call_loop
|
48
|
+
|
49
|
+
act = @action
|
50
|
+
|
51
|
+
i = 0
|
52
|
+
while i < times
|
53
|
+
act.call
|
54
|
+
i += 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Compile code into +call_times+ method.
|
59
|
+
# @param str [String] Code to be compiled.
|
60
|
+
# @return [Symbol] :call_times.
|
61
|
+
def compile(str)
|
62
|
+
m = (class << self; self; end)
|
63
|
+
code = <<-CODE
|
64
|
+
def call_times(__total);
|
65
|
+
__i = 0
|
66
|
+
while __i < __total
|
67
|
+
#{str};
|
68
|
+
__i += 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
CODE
|
72
|
+
m.class_eval code
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Benchmark
|
2
|
+
module IPS
|
3
|
+
class Job
|
4
|
+
class StdoutReport
|
5
|
+
def initialize
|
6
|
+
@last_item = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def start_warming
|
10
|
+
$stdout.puts "Warming up --------------------------------------"
|
11
|
+
end
|
12
|
+
|
13
|
+
def start_running
|
14
|
+
$stdout.puts "Calculating -------------------------------------"
|
15
|
+
end
|
16
|
+
|
17
|
+
def warming(label, _warmup)
|
18
|
+
$stdout.print rjust(label)
|
19
|
+
end
|
20
|
+
|
21
|
+
def warmup_stats(_warmup_time_us, timing)
|
22
|
+
case format
|
23
|
+
when :human
|
24
|
+
$stdout.printf "%s i/100ms\n", Helpers.scale(timing)
|
25
|
+
else
|
26
|
+
$stdout.printf "%10d i/100ms\n", timing
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :running, :warming
|
31
|
+
|
32
|
+
def add_report(item, caller)
|
33
|
+
$stdout.puts " #{item.body}"
|
34
|
+
@last_item = item
|
35
|
+
end
|
36
|
+
|
37
|
+
def footer
|
38
|
+
return unless @last_item
|
39
|
+
footer = @last_item.stats.footer
|
40
|
+
$stdout.puts footer.rjust(40) if footer
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @return [Symbol] format used for benchmarking
|
46
|
+
def format
|
47
|
+
Benchmark::IPS.options[:format]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add padding to label's right if label's length < 20,
|
51
|
+
# Otherwise add a new line and 20 whitespaces.
|
52
|
+
# @return [String] Right justified label.
|
53
|
+
def rjust(label)
|
54
|
+
label = label.to_s
|
55
|
+
if label.size > 20
|
56
|
+
"#{label}\n#{' ' * 20}"
|
57
|
+
else
|
58
|
+
label.rjust(20)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|