motion-benchmark-ips 1.0 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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