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.
@@ -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