benchmarker 0.1.0 → 1.0.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 +7 -0
- data/CHANGES.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +26 -0
- data/Rakefile +107 -0
- data/benchmarker.gemspec +20 -34
- data/lib/benchmarker.rb +1041 -0
- data/test/benchmarker_test.rb +1532 -560
- metadata +53 -79
- data/CHANGES.txt +0 -6
- data/examples/bench_loop.rb +0 -51
- data/examples/bench_require.rb +0 -34
- data/test/oktest.rb +0 -825
data/lib/benchmarker.rb
ADDED
@@ -0,0 +1,1041 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
###
|
5
|
+
### $Release: 1.0.0 $
|
6
|
+
### $Copyright: copyright(c) 2010-2021 kuwata-lab.com all rights reserved $
|
7
|
+
### $License: MIT License $
|
8
|
+
###
|
9
|
+
|
10
|
+
|
11
|
+
module Benchmarker
|
12
|
+
|
13
|
+
|
14
|
+
VERSION = "$Release: 1.0.0 $".split()[1]
|
15
|
+
|
16
|
+
N_REPEAT = 100 # number of repeat (100 times)
|
17
|
+
|
18
|
+
OPTIONS = {} # ex: {loop: 1000, iter: 10, extra: 2, inverse: true}
|
19
|
+
|
20
|
+
def self.new(title=nil, **kwargs, &b)
|
21
|
+
#; [!s7y6x] overwrites existing options by command-line options.
|
22
|
+
kwargs.update(OPTIONS)
|
23
|
+
#; [!2zh7w] creates new Benchmark object wit options.
|
24
|
+
bm = Benchmark.new(title: title, **kwargs)
|
25
|
+
return bm
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.scope(title=nil, **kwargs, &block)
|
29
|
+
#; [!4f695] creates Benchmark object, define tasks, and run them.
|
30
|
+
bm = self.new(title, **kwargs)
|
31
|
+
bm.scope(&block)
|
32
|
+
bm.run()
|
33
|
+
return bm
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class Benchmark
|
38
|
+
|
39
|
+
def initialize(title: nil, width: 30, loop: 1, iter: 1, extra: 0, inverse: false, outfile: nil, quiet: false, colorize: nil, sleep: nil, filter: nil)
|
40
|
+
@title = title
|
41
|
+
@width = width || 30
|
42
|
+
@loop = loop || 1
|
43
|
+
@iter = iter || 1
|
44
|
+
@extra = extra || 0
|
45
|
+
@inverse = inverse || false
|
46
|
+
@outfile = outfile
|
47
|
+
@quiet = quiet || false
|
48
|
+
@colorize = colorize
|
49
|
+
@sleep = sleep
|
50
|
+
@filter = filter
|
51
|
+
if filter
|
52
|
+
#; [!0mz0f] error when filter string is invalid format.
|
53
|
+
filter =~ /^(task|tag)(!?=+)(.*)/ or
|
54
|
+
raise ArgumentError.new("#{filter}: invalid filter.")
|
55
|
+
#; [!xo7bq] error when filter operator is invalid.
|
56
|
+
$2 == '=' || $2 == '!=' or
|
57
|
+
raise ArgumentError.new("#{filter}: expected operator is '=' or '!='.")
|
58
|
+
end
|
59
|
+
@entries = [] # [[Task, Resutl]]
|
60
|
+
@jdata = {}
|
61
|
+
@hooks = {} # {before: Proc, after: Proc, ...}
|
62
|
+
@empty_task = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :title, :width, :loop, :iter, :extra, :inverse, :outfile, :quiet, :colorize, :sleep, :filter
|
66
|
+
|
67
|
+
def clear()
|
68
|
+
#; [!phqdn] clears benchmark result and JSON data.
|
69
|
+
@entries.each {|_, result| result.clear() }
|
70
|
+
@jdata = {}
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def scope(&block)
|
75
|
+
#; [!wrjy0] creates wrapper object and yields block with it as self.
|
76
|
+
#; [!6h24d] passes benchmark object as argument of block.
|
77
|
+
scope = Scope.new(self)
|
78
|
+
scope.instance_exec(self, &block)
|
79
|
+
#; [!y0uwr] returns self.
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def define_empty_task(code=nil, tag: nil, skip: nil, &block) # :nodoc:
|
84
|
+
#; [!qzr1s] error when called more than once.
|
85
|
+
@empty_task.nil? or
|
86
|
+
raise "cannot define empty task more than once."
|
87
|
+
#; [!w66xp] creates empty task.
|
88
|
+
@empty_task = TASK.new(nil, code, tag: tag, skip: skip, &block)
|
89
|
+
return @empty_task
|
90
|
+
end
|
91
|
+
|
92
|
+
def define_task(name, code=nil, tag: nil, skip: nil, &block) # :nodoc:
|
93
|
+
#; [!re6b8] creates new task.
|
94
|
+
#; [!r8o0p] can take a tag.
|
95
|
+
task = TASK.new(name, code, tag: tag, skip: skip, &block)
|
96
|
+
@entries << [task, Result.new]
|
97
|
+
return task
|
98
|
+
end
|
99
|
+
|
100
|
+
def define_hook(key, &block)
|
101
|
+
#; [!2u53t] register proc object with symbol key.
|
102
|
+
@hooks[key] = block
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def call_hook(key, *args)
|
107
|
+
#; [!0to2s] calls hook with arguments.
|
108
|
+
fn = @hooks[key]
|
109
|
+
fn.call(*args) if fn
|
110
|
+
end
|
111
|
+
private :call_hook
|
112
|
+
|
113
|
+
def run(warmup: false)
|
114
|
+
#; [!0fo0l] runs benchmark tasks and reports result.
|
115
|
+
report_environment()
|
116
|
+
filter_tasks()
|
117
|
+
#; [!2j4ks] calls 'before_all' hook.
|
118
|
+
call_hook(:before_all)
|
119
|
+
begin
|
120
|
+
if warmup
|
121
|
+
#; [!6h26u] runs preriminary round when `warmup: true` provided.
|
122
|
+
_ignore_output { invoke_tasks() }
|
123
|
+
clear()
|
124
|
+
end
|
125
|
+
invoke_tasks()
|
126
|
+
#; [!w1rq7] calls 'after_all' hook even if error raised.
|
127
|
+
ensure
|
128
|
+
call_hook(:after_all)
|
129
|
+
end
|
130
|
+
ignore_skipped_tasks()
|
131
|
+
report_minmax()
|
132
|
+
report_average()
|
133
|
+
report_stats()
|
134
|
+
write_outfile()
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def _ignore_output(&b)
|
141
|
+
#; [!wazs7] ignores output in block argument.
|
142
|
+
require 'stringio'
|
143
|
+
bkup, $stdout = $stdout, StringIO.new
|
144
|
+
begin
|
145
|
+
yield
|
146
|
+
ensure
|
147
|
+
$stdout = bkup
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def filter_tasks()
|
152
|
+
#; [!g207d] do nothing when filter string is not provided.
|
153
|
+
if @filter
|
154
|
+
#; [!f1n1v] filters tasks by task name when filer string is 'task=...'.
|
155
|
+
#; [!m79cf] filters tasks by tag value when filer string is 'tag=...'.
|
156
|
+
@filter =~ /^(task|tag)(=|!=)(.*)/ or raise "** internal error"
|
157
|
+
key = $1; op = $2; pattern = $3
|
158
|
+
@entries = @entries.select {|task, _|
|
159
|
+
val = key == 'tag' ? task.tag : task.name
|
160
|
+
if val
|
161
|
+
bool = [val].flatten.any? {|v| File.fnmatch(pattern, v, File::FNM_EXTGLOB) }
|
162
|
+
else
|
163
|
+
bool = false
|
164
|
+
end
|
165
|
+
#; [!0in0q] supports negative filter by '!=' operator.
|
166
|
+
op == '!=' ? !bool : bool
|
167
|
+
}
|
168
|
+
end
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
|
172
|
+
def invoke_tasks()
|
173
|
+
@jdata[:Results] = []
|
174
|
+
#; [!c8yak] invokes tasks once if 'iter' option not specified.
|
175
|
+
#; [!unond] invokes tasks multiple times if 'iter' option specified.
|
176
|
+
#; [!wzvdb] invokes tasks 16 times if 'iter' is 10 and 'extra' is 3.
|
177
|
+
n = @iter + 2 * @extra
|
178
|
+
(1..n).each do |i|
|
179
|
+
@jdata[:Results] << (rows = [])
|
180
|
+
#; [!5axhl] prints result even on quiet mode if no 'iter' nor 'extra'.
|
181
|
+
quiet = @quiet && n != 1
|
182
|
+
#; [!yg9i7] prints result unless quiet mode.
|
183
|
+
#; [!94916] suppresses result if quiet mode.
|
184
|
+
#heading = n == 1 ? "##" : "## (##{i})"
|
185
|
+
if n == 1
|
186
|
+
heading = "##"
|
187
|
+
space = " " * (@width - heading.length)
|
188
|
+
else
|
189
|
+
heading = "## " + colorize_iter("(##{i})")
|
190
|
+
space = " " * (@width - "## (##{i})".length)
|
191
|
+
end
|
192
|
+
puts "" unless quiet
|
193
|
+
#puts "%-#{@width}s %9s %9s %9s %9s" % [heading, 'user', 'sys', 'total', 'real'] unless quiet
|
194
|
+
puts "%s%s %9s %9s %9s %9s" % [heading, space, 'user', 'sys', 'total', 'real'] unless quiet
|
195
|
+
#; [!3hgos] invokes empty task at first if defined.
|
196
|
+
if @empty_task && !@empty_task.skip?
|
197
|
+
empty_timeset = __invoke(@empty_task, "(Empty)", nil, quiet)
|
198
|
+
t = empty_timeset
|
199
|
+
s = "%9.4f %9.4f %9.4f %9.4f" % [t.user, t.sys, t.total, t.real]
|
200
|
+
#s = "%9.4f %9.4f %9.4f %s" % [t.user, t.sys, t.total, colorize_real('%9.4f' % t.real)]
|
201
|
+
puts s unless quiet
|
202
|
+
#; [!knjls] records result of empty loop into JSON data.
|
203
|
+
rows << ["(Empty)"] + empty_timeset.to_a.collect {|x| ('%9.4f' % x).to_f }
|
204
|
+
Kernel.sleep @sleep if @sleep
|
205
|
+
else
|
206
|
+
empty_timeset = nil
|
207
|
+
end
|
208
|
+
#; [!xf84h] invokes all tasks.
|
209
|
+
@entries.each do |task, result|
|
210
|
+
timeset = __invoke(task, task.name, @hooks[:validate], quiet)
|
211
|
+
#; [!dyofw] prints reason if 'skip:' option specified.
|
212
|
+
if task.skip?
|
213
|
+
reason = task.skip
|
214
|
+
result.skipped = reason
|
215
|
+
puts " # Skipped (reason: #{reason})" unless quiet
|
216
|
+
#; [!ygpx0] records reason of skip into JSON data.
|
217
|
+
rows << [task.name, nil, nil, nil, nil, reason]
|
218
|
+
next
|
219
|
+
end
|
220
|
+
#; [!513ok] subtract timeset of empty loop from timeset of each task.
|
221
|
+
if empty_timeset
|
222
|
+
timeset -= empty_timeset unless task.has_code?
|
223
|
+
timeset -= empty_timeset.div(N_REPEAT) if task.has_code?
|
224
|
+
end
|
225
|
+
t = timeset
|
226
|
+
#s = "%9.4f %9.4f %9.4f %9.4f" % [t.user, t.sys, t.total, t.real]
|
227
|
+
s = "%9.4f %9.4f %9.4f %s" % [t.user, t.sys, t.total, colorize_real('%9.4f' % t.real)]
|
228
|
+
puts s unless quiet
|
229
|
+
result.add(timeset)
|
230
|
+
#; [!ejxif] records result of each task into JSON data.
|
231
|
+
rows << [task.name] + timeset.to_a.collect {|x| ('%9.4f' % x).to_f }
|
232
|
+
#; [!vbhvz] sleeps N seconds after each task if `sleep` option specified.
|
233
|
+
Kernel.sleep @sleep if @sleep
|
234
|
+
end
|
235
|
+
end
|
236
|
+
nil
|
237
|
+
end
|
238
|
+
def __invoke(task, task_name, validator, quiet)
|
239
|
+
print "%-#{@width}s " % task_name unless quiet
|
240
|
+
$stdout.flush() unless quiet
|
241
|
+
#; [!fv4cv] skips task invocation if skip reason is specified.
|
242
|
+
return nil if task.skip?
|
243
|
+
#; [!hbass] calls 'before' hook with task name and tag.
|
244
|
+
call_hook(:before, task.name, task.tag)
|
245
|
+
#; [!6g36c] invokes task with validator if validator defined.
|
246
|
+
begin
|
247
|
+
timeset = task.invoke(@loop, &validator)
|
248
|
+
return timeset
|
249
|
+
#; [!7960c] calls 'after' hook with task name and tag even if error raised.
|
250
|
+
ensure
|
251
|
+
call_hook(:after, task_name, task.tag)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def ignore_skipped_tasks()
|
256
|
+
#; [!5gpo7] removes skipped tasks and leaves other tasks.
|
257
|
+
@entries = @entries.reject {|_, result| result.skipped? }
|
258
|
+
nil
|
259
|
+
end
|
260
|
+
|
261
|
+
def report_environment()
|
262
|
+
#; [!rx7nn] prints ruby version, platform, several options, and so on.
|
263
|
+
s = "loop=#{@loop.inspect}, iter=#{@iter.inspect}, extra=#{@extra.inspect}"
|
264
|
+
s += ", inverse=#{@inverse}" if @inverse
|
265
|
+
kvs = [["title", @title], ["options", s]] + Misc.environment_info()
|
266
|
+
puts kvs.collect {|k, v| "## %-16s %s\n" % ["#{k}:", v] }.join()
|
267
|
+
@jdata[:Environment] = Hash.new(kvs)
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
|
271
|
+
def report_minmax()
|
272
|
+
if @extra > 0
|
273
|
+
rows = _remove_minmax()
|
274
|
+
puts _render_minmax(rows)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def _remove_minmax()
|
279
|
+
#; [!uxe7e] removes best and worst results if 'extra' option specified.
|
280
|
+
tuples = []
|
281
|
+
@entries.each do |task, result|
|
282
|
+
removed_list = result.remove_minmax(@extra)
|
283
|
+
tuples << [task.name, removed_list]
|
284
|
+
end
|
285
|
+
#; [!is6ll] returns removed min and max data.
|
286
|
+
rows = []
|
287
|
+
tuples.each do |task_name, removed_list|
|
288
|
+
removed_list.each_with_index do |(min_t, min_idx, max_t, max_idx), i|
|
289
|
+
task_name = nil if i > 0
|
290
|
+
min_t2 = ("%9.4f" % min_t).to_f
|
291
|
+
max_t2 = ("%9.4f" % max_t).to_f
|
292
|
+
rows << [task_name, min_t2, "(##{min_idx})", max_t2, "(##{max_idx})"]
|
293
|
+
end
|
294
|
+
end
|
295
|
+
#; [!xwddz] sets removed best and worst results into JSON data.
|
296
|
+
@jdata[:RemovedMinMax] = rows
|
297
|
+
return rows
|
298
|
+
end
|
299
|
+
|
300
|
+
def _render_minmax(rows)
|
301
|
+
#; [!p71ax] returns rendered string.
|
302
|
+
buf = ["\n"]
|
303
|
+
heading = "## Removed Min & Max"
|
304
|
+
buf << "%-#{@width+4}s %5s %9s %9s %9s\n" % [heading, 'min', 'iter', 'max', 'iter']
|
305
|
+
rows.each do |row|
|
306
|
+
#buf << "%-#{@width}s %9.4f %9s %9.4f %9s\n" % row
|
307
|
+
task_name, min_t, min_i, max_t, max_i = row
|
308
|
+
arr = [task_name, colorize_real('%9.4f' % min_t), colorize_iter('%9s' % min_i),
|
309
|
+
colorize_real('%9.4f' % max_t), colorize_iter('%9s' % max_i)]
|
310
|
+
buf << "%-#{@width}s %s %s %s %s\n" % arr
|
311
|
+
end
|
312
|
+
return buf.join()
|
313
|
+
end
|
314
|
+
|
315
|
+
def report_average()
|
316
|
+
if @iter > 1 || @extra > 0
|
317
|
+
rows = _calc_average()
|
318
|
+
puts _render_average(rows)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def _calc_average()
|
323
|
+
#; [!qu29s] calculates average of real times for each task.
|
324
|
+
rows = @entries.collect {|task, result|
|
325
|
+
avg_timeset = result.calc_average()
|
326
|
+
[task.name] + avg_timeset.to_a.collect {|x| ("%9.4f" % x).to_f }
|
327
|
+
}
|
328
|
+
#; [!jxf28] sets average results into JSON data.
|
329
|
+
@jdata[:Average] = rows
|
330
|
+
return rows
|
331
|
+
end
|
332
|
+
|
333
|
+
def _render_average(rows)
|
334
|
+
#; [!j9wlv] returns rendered string.
|
335
|
+
buf = ["\n"]
|
336
|
+
heading = "## Average of #{@iter}"
|
337
|
+
heading += " (=#{@iter + 2 * @extra}-2*#{@extra})" if @extra > 0
|
338
|
+
buf << "%-#{@width+4}s %5s %9s %9s %9s\n" % [heading, 'user', 'sys', 'total', 'real']
|
339
|
+
rows.each do |row|
|
340
|
+
#buf << "%-#{@width}s %9.4f %9.4f %9.4f %9.4f\n" % row
|
341
|
+
real = colorize_real('%9.4f' % row.pop())
|
342
|
+
buf << "%-#{@width}s %9.4f %9.4f %9.4f %s\n" % (row + [real])
|
343
|
+
end
|
344
|
+
return buf.join()
|
345
|
+
end
|
346
|
+
|
347
|
+
def report_stats()
|
348
|
+
#; [!0jn7d] sorts results by real sec.
|
349
|
+
pairs = @entries.collect {|task, result|
|
350
|
+
#real = @iter > 1 || @extra > 0 ? result.calc_average().real : result[0].real
|
351
|
+
real = result.calc_average().real
|
352
|
+
[task.name, real]
|
353
|
+
}
|
354
|
+
pairs = pairs.sort_by {|_, real| real }
|
355
|
+
print _render_ranking(pairs)
|
356
|
+
print _render_matrix(pairs)
|
357
|
+
end
|
358
|
+
|
359
|
+
def _render_ranking(pairs)
|
360
|
+
#; [!2lu55] calculates ranking data and sets it into JSON data.
|
361
|
+
rows = []
|
362
|
+
base = nil
|
363
|
+
pairs.each do |task_name, sec|
|
364
|
+
base ||= sec
|
365
|
+
percent = 100.0 * base / sec
|
366
|
+
barchart = '*' * (percent / 5.0).round() # max 20 chars (=100%)
|
367
|
+
loop = @inverse == true ? (@loop || 1) : (@inverse || @loop || 1)
|
368
|
+
rows << [task_name, ("%.4f" % sec).to_f, "%.1f%%" % percent,
|
369
|
+
"%.2f times/sec" % (loop / sec), barchart]
|
370
|
+
end
|
371
|
+
@jdata[:Ranking] = rows
|
372
|
+
#; [!55x8r] returns rendered string of ranking.
|
373
|
+
buf = ["\n"]
|
374
|
+
heading = "## Ranking"
|
375
|
+
if @inverse
|
376
|
+
buf << "%-#{@width}s %9s%30s\n" % [heading, 'real', 'times/sec']
|
377
|
+
else
|
378
|
+
buf << "%-#{@width}s %9s\n" % [heading, 'real']
|
379
|
+
end
|
380
|
+
rows.each do |task_name, sec, percent, inverse, barchart|
|
381
|
+
s = @inverse ? "%20s" % inverse.split()[0] : barchart
|
382
|
+
#buf << "%-#{@width}s %9.4f (%6s) %s\n" % [task_name, sec, percent, s]
|
383
|
+
buf << "%-#{@width}s %s (%6s) %s\n" % [task_name, colorize_real('%9.4f' % sec), percent, s]
|
384
|
+
end
|
385
|
+
return buf.join()
|
386
|
+
end
|
387
|
+
|
388
|
+
def _render_matrix(pairs)
|
389
|
+
#; [!2lu55] calculates ranking data and sets it into JSON data.
|
390
|
+
rows = []
|
391
|
+
pairs.each_with_index do |(task_name, sec), i|
|
392
|
+
base = pairs[i][1]
|
393
|
+
row = ["[#{i+1}] #{task_name}", ("%9.4f" % sec).to_f]
|
394
|
+
pairs.each {|_, r| row << "%.1f%%" % (100.0 * r / base) }
|
395
|
+
rows << row
|
396
|
+
end
|
397
|
+
@jdata[:Matrix] = rows
|
398
|
+
#; [!rwfxu] returns rendered string of matrix.
|
399
|
+
buf = ["\n"]
|
400
|
+
heading = "## Matrix"
|
401
|
+
s = "%-#{@width}s %9s" % [heading, 'real']
|
402
|
+
(1..pairs.length).each {|i| s += " %8s" % "[#{i}]" }
|
403
|
+
buf << "#{s}\n"
|
404
|
+
rows.each do |task_name, real, *percents|
|
405
|
+
s = "%-#{@width}s %s" % [task_name, colorize_real('%9.4f' % real)]
|
406
|
+
percents.each {|p| s += " %8s" % p }
|
407
|
+
buf << "#{s}\n"
|
408
|
+
end
|
409
|
+
return buf.join()
|
410
|
+
end
|
411
|
+
|
412
|
+
def write_outfile()
|
413
|
+
#; [!o8ah6] writes result data into JSON file if 'outfile' option specified.
|
414
|
+
if @outfile
|
415
|
+
filename = @outfile
|
416
|
+
require 'json'
|
417
|
+
jstr = JSON.pretty_generate(@jdata, indent: ' ', space: ' ')
|
418
|
+
if filename == '-'
|
419
|
+
$stdout.puts(jstr)
|
420
|
+
else
|
421
|
+
File.write(filename, jstr)
|
422
|
+
end
|
423
|
+
jstr
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def colorize?
|
428
|
+
#; [!cy10n] returns true if '-c' option specified.
|
429
|
+
#; [!e0gcz] returns false if '-C' option specified.
|
430
|
+
#; [!6v90d] returns result of `Color.colorize?` if neither '-c' nor '-C' specified.
|
431
|
+
return @colorize.nil? ? Color.colorize?() : @colorize
|
432
|
+
end
|
433
|
+
|
434
|
+
def colorize_real(s)
|
435
|
+
colorize?() ? Color.real(s) : s
|
436
|
+
end
|
437
|
+
|
438
|
+
def colorize_iter(s)
|
439
|
+
colorize?() ? Color.iter(s) : s
|
440
|
+
end
|
441
|
+
|
442
|
+
end
|
443
|
+
|
444
|
+
|
445
|
+
class Scope
|
446
|
+
|
447
|
+
def initialize(bm=nil)
|
448
|
+
@__bm = bm
|
449
|
+
end
|
450
|
+
|
451
|
+
def task(name, code=nil, binding=nil, tag: nil, skip: nil, &block)
|
452
|
+
#; [!843ju] when code argument provided...
|
453
|
+
if code
|
454
|
+
#; [!bwfak] code argument and block argument are exclusive.
|
455
|
+
! block_given?() or
|
456
|
+
raise TaskError, "task(#{name.inspect}): cannot accept #{code.class} argument when block argument given."
|
457
|
+
#; [!4dm9q] generates block argument if code argument passed.
|
458
|
+
location = caller_locations(1, 1).first
|
459
|
+
defcode = "proc do #{(code+';') * N_REPEAT} end" # repeat code 100 times
|
460
|
+
binding ||= ::TOPLEVEL_BINDING
|
461
|
+
block = eval defcode, binding, location.path, location.lineno+1
|
462
|
+
end
|
463
|
+
#; [!kh7r9] define empty-loop task if name is nil.
|
464
|
+
return @__bm.define_empty_task(code, tag: tag, skip: skip, &block) if name.nil?
|
465
|
+
#; [!j6pmr] creates new task object.
|
466
|
+
return @__bm.define_task(name, code, tag: tag, skip: skip, &block)
|
467
|
+
end
|
468
|
+
alias report task # for compatibility with 'benchamrk.rb'
|
469
|
+
|
470
|
+
def empty_task(code=nil, binding=nil, &block)
|
471
|
+
#; [!ycoch] creates new empty-loop task object.
|
472
|
+
return task(nil, code, binding, &block)
|
473
|
+
end
|
474
|
+
|
475
|
+
def assert_eq(actual, expected, errmsg=nil)
|
476
|
+
#; [!8m6bh] do nothing if ectual == expected.
|
477
|
+
#; [!f9ey6] raises error unless actual == expected.
|
478
|
+
return if actual == expected
|
479
|
+
errmsg ||= "#{actual.inspect} == #{expected.inspect}: failed."
|
480
|
+
assert false, errmsg
|
481
|
+
end
|
482
|
+
|
483
|
+
def before(&block)
|
484
|
+
#; [!2ir4q] defines 'before' hook.
|
485
|
+
@__bm.define_hook(:before, &block)
|
486
|
+
end
|
487
|
+
|
488
|
+
def after(&block)
|
489
|
+
#; [!05up6] defines 'after' hook.
|
490
|
+
@__bm.define_hook(:after, &block)
|
491
|
+
end
|
492
|
+
|
493
|
+
def before_all(&block)
|
494
|
+
#; [!1oier] defines 'before_all' hook.
|
495
|
+
@__bm.define_hook(:before_all, &block)
|
496
|
+
end
|
497
|
+
|
498
|
+
def after_all(&block)
|
499
|
+
#; [!z7xop] defines 'after_all' hook.
|
500
|
+
@__bm.define_hook(:after_all, &block)
|
501
|
+
end
|
502
|
+
|
503
|
+
def validate(&block)
|
504
|
+
#; [!q2aev] defines validator.
|
505
|
+
return @__bm.define_hook(:validate, &block)
|
506
|
+
end
|
507
|
+
|
508
|
+
def assert(expr, errmsg)
|
509
|
+
#; [!a0c7e] do nothing if assertion succeeded.
|
510
|
+
#; [!5vmbc] raises error if assertion failed.
|
511
|
+
#; [!7vt5l] puts newline if assertion failed.
|
512
|
+
return if expr
|
513
|
+
puts ""
|
514
|
+
raise ValidationFailed, errmsg
|
515
|
+
rescue => exc
|
516
|
+
#; [!mhw59] makes error backtrace compact.
|
517
|
+
exc.backtrace.reject! {|x| x =~ /[\/\\:]benchmarker\.rb.*:/ }
|
518
|
+
raise
|
519
|
+
end
|
520
|
+
|
521
|
+
end
|
522
|
+
|
523
|
+
|
524
|
+
class ValidationFailed < StandardError
|
525
|
+
end
|
526
|
+
|
527
|
+
|
528
|
+
class TaskError < StandardError
|
529
|
+
end
|
530
|
+
|
531
|
+
|
532
|
+
class Task
|
533
|
+
|
534
|
+
def initialize(name, code=nil, tag: nil, skip: nil, &block)
|
535
|
+
@name = name
|
536
|
+
@code = code
|
537
|
+
@tag = tag
|
538
|
+
@skip = skip # reason to skip
|
539
|
+
@block = block
|
540
|
+
end
|
541
|
+
|
542
|
+
attr_reader :name, :tag, :skip, :block
|
543
|
+
|
544
|
+
def has_code?
|
545
|
+
return !!@code
|
546
|
+
end
|
547
|
+
|
548
|
+
def skip?
|
549
|
+
return !!@skip
|
550
|
+
end
|
551
|
+
|
552
|
+
def invoke(loop=1, &validator)
|
553
|
+
#; [!s2f6v] when task block is build from repeated code...
|
554
|
+
if @code
|
555
|
+
n_repeat = N_REPEAT # == 100
|
556
|
+
#; [!i2r8o] error when number of loop is less than 100.
|
557
|
+
loop >= n_repeat or
|
558
|
+
raise TaskError, "task(#{@name.inspect}): number of loop (=#{loop}) should be >= #{n_repeat}, but not."
|
559
|
+
#; [!kzno6] error when number of loop is not a multiple of 100.
|
560
|
+
loop % n_repeat == 0 or
|
561
|
+
raise TaskError, "task(#{@name.inspect}): number of loop (=#{loop}) should be a multiple of #{n_repeat}, but not."
|
562
|
+
#; [!gbukv] changes number of loop to 1/100.
|
563
|
+
loop = loop / n_repeat
|
564
|
+
end
|
565
|
+
#; [!frq25] kicks GC before calling task block.
|
566
|
+
GC.start()
|
567
|
+
#; [!tgql6] invokes block N times.
|
568
|
+
block = @block
|
569
|
+
t1 = Process.times
|
570
|
+
start_t = Time.now
|
571
|
+
while (loop -= 1) >= 0
|
572
|
+
ret = block.call()
|
573
|
+
end
|
574
|
+
end_t = Time.now
|
575
|
+
t2 = Process.times
|
576
|
+
#; [!zw4kt] yields validator with result value of block.
|
577
|
+
yield ret, @name, @tag if block_given?()
|
578
|
+
#; [!9e5pr] returns TimeSet object.
|
579
|
+
user = t2.utime - t1.utime
|
580
|
+
sys = t2.stime - t1.stime
|
581
|
+
total = user + sys
|
582
|
+
real = end_t - start_t
|
583
|
+
return TimeSet.new(user, sys, total, real)
|
584
|
+
end
|
585
|
+
|
586
|
+
end
|
587
|
+
|
588
|
+
TASK = Task
|
589
|
+
|
590
|
+
|
591
|
+
TimeSet = Struct.new('TimeSet', :user, :sys, :total, :real) do
|
592
|
+
|
593
|
+
def -(t)
|
594
|
+
#; [!cpwgf] returns new TimeSet object.
|
595
|
+
user = self.user - t.user
|
596
|
+
sys = self.sys - t.sys
|
597
|
+
total = self.total - t.total
|
598
|
+
real = self.real - t.real
|
599
|
+
return TimeSet.new(user, sys, total, real)
|
600
|
+
end
|
601
|
+
|
602
|
+
def div(n)
|
603
|
+
#; [!4o9ns] returns new TimeSet object which values are divided by n.
|
604
|
+
user = self.user / n
|
605
|
+
sys = self.sys / n
|
606
|
+
total = self.total / n
|
607
|
+
real = self.real / n
|
608
|
+
return TimeSet.new(user, sys, total, real)
|
609
|
+
end
|
610
|
+
|
611
|
+
end
|
612
|
+
|
613
|
+
|
614
|
+
class Result
|
615
|
+
|
616
|
+
def initialize()
|
617
|
+
@iterations = []
|
618
|
+
end
|
619
|
+
|
620
|
+
def [](idx)
|
621
|
+
return @iterations[idx]
|
622
|
+
end
|
623
|
+
|
624
|
+
def length()
|
625
|
+
return @iterations.length
|
626
|
+
end
|
627
|
+
|
628
|
+
def each(&b)
|
629
|
+
@iterations.each(&b)
|
630
|
+
end
|
631
|
+
|
632
|
+
def add(timeset)
|
633
|
+
#; [!thyms] adds timeset and returns self.
|
634
|
+
@iterations << timeset
|
635
|
+
self
|
636
|
+
end
|
637
|
+
|
638
|
+
def clear()
|
639
|
+
#; [!fxrn6] clears timeset array.
|
640
|
+
@iterations.clear()
|
641
|
+
self
|
642
|
+
end
|
643
|
+
|
644
|
+
def skipped=(reason)
|
645
|
+
@reason = reason
|
646
|
+
end
|
647
|
+
|
648
|
+
def skipped?
|
649
|
+
#; [!bvzk9] returns true if reason has set, or returns false.
|
650
|
+
return !!@reason
|
651
|
+
end
|
652
|
+
|
653
|
+
def remove_minmax(extra, key=:real)
|
654
|
+
#; [!b55zh] removes best and worst timeset and returns them.
|
655
|
+
i = 0
|
656
|
+
pairs = @iterations.collect {|t| [t, i+=1] }
|
657
|
+
pairs = pairs.sort_by {|pair| pair[0].__send__(key) }
|
658
|
+
removed = []
|
659
|
+
extra.times do
|
660
|
+
min_timeset, min_idx = pairs.shift()
|
661
|
+
max_timeset, max_idx = pairs.pop()
|
662
|
+
min_t = min_timeset.__send__(key)
|
663
|
+
max_t = max_timeset.__send__(key)
|
664
|
+
removed << [min_t, min_idx, max_t, max_idx]
|
665
|
+
end
|
666
|
+
remained = pairs.sort_by {|_, i| i }.collect {|t, _| t }
|
667
|
+
@iterations = remained
|
668
|
+
return removed
|
669
|
+
end
|
670
|
+
|
671
|
+
def calc_average()
|
672
|
+
#; [!b91w3] returns average of timeddata.
|
673
|
+
user = sys = total = real = 0.0
|
674
|
+
@iterations.each do |t|
|
675
|
+
user += t.user
|
676
|
+
sys += t.sys
|
677
|
+
total += t.total
|
678
|
+
real += t.real
|
679
|
+
end
|
680
|
+
n = @iterations.length
|
681
|
+
return TimeSet.new(user/n, sys/n, total/n, real/n)
|
682
|
+
end
|
683
|
+
|
684
|
+
end
|
685
|
+
|
686
|
+
|
687
|
+
module Misc
|
688
|
+
|
689
|
+
module_function
|
690
|
+
|
691
|
+
def environment_info()
|
692
|
+
#; [!w1xfa] returns environment info in key-value list.
|
693
|
+
ruby_engine_version = (RUBY_ENGINE_VERSION rescue nil)
|
694
|
+
cc_version_msg = RbConfig::CONFIG['CC_VERSION_MESSAGE']
|
695
|
+
return [
|
696
|
+
["benchmarker" , "release #{VERSION}"],
|
697
|
+
["ruby engine" , "#{RUBY_ENGINE} (engine version #{ruby_engine_version})"],
|
698
|
+
["ruby version" , "#{RUBY_VERSION} (patch level #{RUBY_PATCHLEVEL})"],
|
699
|
+
["ruby platform" , RUBY_PLATFORM],
|
700
|
+
["ruby path" , RbConfig.ruby],
|
701
|
+
["compiler" , cc_version_msg ? cc_version_msg.split(/\r?\n/)[0] : nil],
|
702
|
+
["os name" , os_name()],
|
703
|
+
["cpu model" , cpu_model()],
|
704
|
+
]
|
705
|
+
end
|
706
|
+
|
707
|
+
def os_name()
|
708
|
+
#; [!83vww] returns string representing os name.
|
709
|
+
if File.file?("/usr/bin/sw_vers") # macOS
|
710
|
+
s = `/usr/bin/sw_vers`
|
711
|
+
s =~ /^ProductName:\s+(.*)/; product = $1
|
712
|
+
s =~ /^ProductVersion:\s+(.*)/; version = $1
|
713
|
+
return "#{product} #{version}"
|
714
|
+
end
|
715
|
+
if File.file?("/etc/lsb-release") # Linux
|
716
|
+
s = File.read("/etc/lsb-release", encoding: 'utf-8')
|
717
|
+
if s =~ /^DISTRIB_DESCRIPTION="(.*)"/
|
718
|
+
return $1
|
719
|
+
end
|
720
|
+
end
|
721
|
+
if File.file?("/usr/bin/uname") # UNIX
|
722
|
+
s = `/usr/bin/uname -srm`
|
723
|
+
return s.strip
|
724
|
+
end
|
725
|
+
if RUBY_PLATFORM =~ /win/ # Windows
|
726
|
+
s = `systeminfo` # TODO: not tested yet
|
727
|
+
s =~ /^OS Name:\s+(?:Microsft )?(.*)/; product = $1
|
728
|
+
s =~ /^OS Version:\s+(.*)/; version = $1 ? $1.split()[0] : nil
|
729
|
+
return "#{product} #{version}"
|
730
|
+
end
|
731
|
+
return nil
|
732
|
+
end
|
733
|
+
|
734
|
+
def cpu_model()
|
735
|
+
#; [!6ncgq] returns string representing cpu model.
|
736
|
+
if File.exist?("/usr/sbin/sysctl") # macOS
|
737
|
+
s = `/usr/sbin/sysctl machdep.cpu.brand_string`
|
738
|
+
s =~ /^machdep\.cpu\.brand_string: (.*)/
|
739
|
+
return $1
|
740
|
+
elsif File.exist?("/proc/cpuinfo") # Linux
|
741
|
+
s = `cat /proc/cpuinfo`
|
742
|
+
s =~ /^model name\s*: (.*)/
|
743
|
+
return $1
|
744
|
+
elsif File.exist?("/var/run/dmesg.boot") # FreeBSD
|
745
|
+
s = `grep ^CPU: /var/run/dmesg.boot`
|
746
|
+
s =~ /^CPU: (.*)/
|
747
|
+
return $1
|
748
|
+
elsif RUBY_PLATFORM =~ /win/ # Windows
|
749
|
+
s = `systeminfo`
|
750
|
+
s =~ /^\s+\[01\]: (.*)/ # TODO: not tested yet
|
751
|
+
return $1
|
752
|
+
else
|
753
|
+
return nil
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
def sample_code()
|
758
|
+
return <<'END'
|
759
|
+
# -*- coding: utf-8 -*-
|
760
|
+
|
761
|
+
require 'benchmarker' # https://kwatch.github.io/benchmarker/ruby.html
|
762
|
+
|
763
|
+
nums = (1..10000).to_a
|
764
|
+
|
765
|
+
title = "calculate sum of integers"
|
766
|
+
Benchmarker.scope(title, width: 24, loop: 1000, iter: 5, extra: 1) do
|
767
|
+
## other options -- inverse: true, outfile: "result.json", quiet: true,
|
768
|
+
## sleep: 1, colorize: true, filter: "task=*foo*"
|
769
|
+
|
770
|
+
## hooks
|
771
|
+
#before_all do end
|
772
|
+
#after_all do end
|
773
|
+
#before do end # or: before do |task_name, tag| end
|
774
|
+
#after do end # or: after do |task_name, tag| end
|
775
|
+
|
776
|
+
## tasks
|
777
|
+
task nil do # empty-loop task
|
778
|
+
# do nothing
|
779
|
+
end
|
780
|
+
|
781
|
+
task "each() & '+='" do
|
782
|
+
total = 0
|
783
|
+
nums.each {|n| total += n }
|
784
|
+
total
|
785
|
+
end
|
786
|
+
|
787
|
+
task "inject()" do
|
788
|
+
total = nums.inject(0) {|t, n| t += n }
|
789
|
+
total
|
790
|
+
end
|
791
|
+
|
792
|
+
task "while statement" do
|
793
|
+
total = 0; i = -1; len = nums.length
|
794
|
+
while (i += 1) < len
|
795
|
+
total += nums[i]
|
796
|
+
end
|
797
|
+
total
|
798
|
+
end
|
799
|
+
|
800
|
+
#task "name", tag: "curr", skip: (!condition ? nil : "...reason...") do
|
801
|
+
# ... run benchmark code ...
|
802
|
+
#end
|
803
|
+
|
804
|
+
## validation
|
805
|
+
validate do |val| # or: validate do |val, task_name, tag|
|
806
|
+
n = nums.last
|
807
|
+
expected = n * (n+1) / 2
|
808
|
+
assert_eq val, expected
|
809
|
+
# or: assert val == expected, "expected #{expected} but got #{val}"
|
810
|
+
end
|
811
|
+
|
812
|
+
end
|
813
|
+
END
|
814
|
+
end
|
815
|
+
|
816
|
+
end
|
817
|
+
|
818
|
+
|
819
|
+
module Color
|
820
|
+
|
821
|
+
module_function
|
822
|
+
|
823
|
+
def black(s); "\e[0;30m#{s}\e[0m"; end
|
824
|
+
def red(s); "\e[0;31m#{s}\e[0m"; end
|
825
|
+
def green(s); "\e[0;32m#{s}\e[0m"; end
|
826
|
+
def yellow(s); "\e[0;33m#{s}\e[0m"; end
|
827
|
+
def blue(s); "\e[0;34m#{s}\e[0m"; end
|
828
|
+
def magenta(s); "\e[0;35m#{s}\e[0m"; end
|
829
|
+
def cyan(s); "\e[0;36m#{s}\e[0m"; end
|
830
|
+
def white(s); "\e[0;37m#{s}\e[0m"; end
|
831
|
+
|
832
|
+
class << self
|
833
|
+
alias real cyan
|
834
|
+
alias iter magenta
|
835
|
+
end
|
836
|
+
|
837
|
+
def colorize?
|
838
|
+
#; [!fc741] returns true if stdout is a tty, else returns false.
|
839
|
+
return $stdout.tty?
|
840
|
+
end
|
841
|
+
|
842
|
+
end
|
843
|
+
|
844
|
+
|
845
|
+
class OptionParser
|
846
|
+
|
847
|
+
def initialize(opts_noparam, opts_hasparam, opts_mayparam="")
|
848
|
+
@opts_noparam = opts_noparam
|
849
|
+
@opts_hasparam = opts_hasparam
|
850
|
+
@opts_mayparam = opts_mayparam
|
851
|
+
end
|
852
|
+
|
853
|
+
def parse(argv)
|
854
|
+
#; [!2gq7g] returns options and keyvals.
|
855
|
+
options = {}; keyvals = {}
|
856
|
+
while !argv.empty? && argv[0] =~ /^-/
|
857
|
+
argstr = argv.shift
|
858
|
+
case argstr
|
859
|
+
#; [!ulfpu] stops parsing when '--' found.
|
860
|
+
when '--'
|
861
|
+
break
|
862
|
+
#; [!8f085] regards '--long=option' as key-value.
|
863
|
+
when /^--/
|
864
|
+
argstr =~ /^--(\w[-\w]*)(?:=(.*))?$/ or
|
865
|
+
yield "#{argstr}: invalid option."
|
866
|
+
key = $1; val = $2
|
867
|
+
keyvals[key] = val || true
|
868
|
+
#; [!dkq1u] parses short options.
|
869
|
+
when /^-/
|
870
|
+
i = 1
|
871
|
+
while i < argstr.length
|
872
|
+
c = argstr[i]
|
873
|
+
if @opts_noparam.include?(c)
|
874
|
+
options[c] = true
|
875
|
+
i += 1
|
876
|
+
elsif @opts_hasparam.include?(c)
|
877
|
+
#; [!8xqla] error when required argument is not provided.
|
878
|
+
options[c] = i+1 < argstr.length ? argstr[(i+1)..-1] : argv.shift() or
|
879
|
+
yield "-#{c}: argument required."
|
880
|
+
break
|
881
|
+
elsif @opts_mayparam.include?(c)
|
882
|
+
options[c] = i+1 < argstr.length ? argstr[(i+1)..-1] : true
|
883
|
+
break
|
884
|
+
#; [!tmx6o] error when option is unknown.
|
885
|
+
else
|
886
|
+
yield "-#{c}: unknown option."
|
887
|
+
i += 1
|
888
|
+
end
|
889
|
+
end
|
890
|
+
else
|
891
|
+
raise "** internall error"
|
892
|
+
end
|
893
|
+
end
|
894
|
+
return options, keyvals
|
895
|
+
end
|
896
|
+
|
897
|
+
def self.parse_options(argv=ARGV, &b)
|
898
|
+
parser = self.new("hvqcCS", "wnixosF", "I")
|
899
|
+
options, keyvals = parser.parse(argv, &b)
|
900
|
+
#; [!v19y5] converts option argument into integer if necessary.
|
901
|
+
"wnixI".each_char do |c|
|
902
|
+
next if !options.key?(c)
|
903
|
+
next if options[c] == true
|
904
|
+
#; [!frfz2] yields error message when argument of '-n/i/x/I' is not an integer.
|
905
|
+
options[c] =~ /\A\d+\z/ or
|
906
|
+
yield "-#{c}#{c == 'I' ? '' : ' '}#{options[c]}: integer expected."
|
907
|
+
options[c] = options[c].to_i
|
908
|
+
end
|
909
|
+
#; [!nz15w] convers '-s' option value into number (integer or float).
|
910
|
+
"s".each_char do |c|
|
911
|
+
next unless options.key?(c)
|
912
|
+
case options[c]
|
913
|
+
when /\A\d+\z/ ; options[c] = options[c].to_i
|
914
|
+
when /\A\d+\.\d+\z/ ; options[c] = options[c].to_f
|
915
|
+
else
|
916
|
+
#; [!3x1m7] yields error message when argument of '-s' is not a number.
|
917
|
+
yield "-#{c} #{options[c]}: number expected."
|
918
|
+
end
|
919
|
+
end
|
920
|
+
#
|
921
|
+
if options['F']
|
922
|
+
#; [!emavm] yields error message when argumetn of '-F' option is invalid.
|
923
|
+
if options['F'] !~ /^(\w+)(=|!=)[^=]/
|
924
|
+
yield "-F #{options['F']}: invalid filter (expected operator is '=' or '!=')."
|
925
|
+
elsif ! ($1 == 'task' || $1 == 'tag')
|
926
|
+
yield "-F #{options['F']}: expected 'task=...' or 'tag=...'."
|
927
|
+
end
|
928
|
+
end
|
929
|
+
return options, keyvals
|
930
|
+
end
|
931
|
+
|
932
|
+
def self.help_message(command=nil)
|
933
|
+
#; [!jnm2w] returns help message.
|
934
|
+
command ||= File.basename($0)
|
935
|
+
return <<"END"
|
936
|
+
Usage: #{command} [<options>]
|
937
|
+
-h, --help : help message
|
938
|
+
-v : print Benchmarker version
|
939
|
+
-w <N> : width of task name (default: 30)
|
940
|
+
-n <N> : loop N times in each benchmark (default: 1)
|
941
|
+
-i <N> : iterates all benchmark tasks N times (default: 1)
|
942
|
+
-x <N> : ignore worst N results and best N results (default: 0)
|
943
|
+
-I[<N>] : print inverse number (= N/sec) (default: same as '-n')
|
944
|
+
-o <file> : output file in JSON format
|
945
|
+
-q : quiet a little (suppress output of each iteration)
|
946
|
+
-c : enable colorized output
|
947
|
+
-C : disable colorized output
|
948
|
+
-s <N> : sleep N seconds after each benchmark task
|
949
|
+
-S : print sample code
|
950
|
+
-F task=<...> : filter benchmark task by name (operator: '=' or '!=')
|
951
|
+
-F tag=<...> : filter benchmark task by tag (operator: '=' or '!=')
|
952
|
+
--<key>[=<val>]: define global variable `$opt_<key> = "<val>"`
|
953
|
+
END
|
954
|
+
end
|
955
|
+
|
956
|
+
end
|
957
|
+
|
958
|
+
|
959
|
+
def self.parse_cmdopts(argv=ARGV)
|
960
|
+
#; [!348ip] parses command-line options.
|
961
|
+
#; [!snqxo] exits with status code 1 if error in command option.
|
962
|
+
options, keyvals = OptionParser.parse_options(argv) do |errmsg|
|
963
|
+
$stderr.puts errmsg
|
964
|
+
exit 1
|
965
|
+
end
|
966
|
+
#; [!p3b93] prints help message if '-h' or '--help' option specified.
|
967
|
+
if options['h'] || keyvals['help']
|
968
|
+
puts OptionParser.help_message()
|
969
|
+
exit 0
|
970
|
+
end
|
971
|
+
#; [!iaryj] prints version number if '-v' option specified.
|
972
|
+
if options['v']
|
973
|
+
puts VERSION
|
974
|
+
exit 0
|
975
|
+
end
|
976
|
+
#; [!nrxsb] prints sample code if '-S' option specified.
|
977
|
+
if options['S']
|
978
|
+
puts Misc.sample_code()
|
979
|
+
exit 0
|
980
|
+
end
|
981
|
+
#; [!s7y6x] keeps command-line options in order to overwirte existing options.
|
982
|
+
#; [!nexi8] option '-w' specifies task name width.
|
983
|
+
#; [!raki9] option '-n' specifies count of loop.
|
984
|
+
#; [!mt7lw] option '-i' specifies number of iteration.
|
985
|
+
#; [!7f2k3] option '-x' specifies number of best/worst tasks removed.
|
986
|
+
#; [!r0439] option '-I' specifies inverse switch.
|
987
|
+
#; [!4c73x] option '-o' specifies outout JSON file.
|
988
|
+
#; [!02ml5] option '-q' specifies quiet mode.
|
989
|
+
#; [!e5hv0] option '-c' specifies colorize enabled.
|
990
|
+
#; [!eb5ck] option '-C' specifies colorize disabled.
|
991
|
+
#; [!6nxi8] option '-s' specifies sleep time.
|
992
|
+
#; [!muica] option '-F' specifies filter.
|
993
|
+
OPTIONS[:width] = options['w'] if options['w']
|
994
|
+
OPTIONS[:loop] = options['n'] if options['n']
|
995
|
+
OPTIONS[:iter] = options['i'] if options['i']
|
996
|
+
OPTIONS[:extra] = options['x'] if options['x']
|
997
|
+
OPTIONS[:inverse] = options['I'] if options['I']
|
998
|
+
OPTIONS[:outfile] = options['o'] if options['o']
|
999
|
+
OPTIONS[:quiet] = options['q'] if options['q']
|
1000
|
+
OPTIONS[:colorize] = true if options['c']
|
1001
|
+
OPTIONS[:colorize] = false if options['C']
|
1002
|
+
OPTIONS[:sleep] = options['s'] if options['s']
|
1003
|
+
OPTIONS[:filter] = options['F'] if options['F']
|
1004
|
+
#; [!3khc4] sets global variables if long option specified.
|
1005
|
+
keyvals.each {|k, v| eval "$opt_#{k} = #{v.inspect}" }
|
1006
|
+
#
|
1007
|
+
return options, keyvals # for testing
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
unless defined?(::BENCHMARKER_IGNORE_CMDOPTS) && ::BENCHMARKER_IGNORE_CMDOPTS
|
1011
|
+
self.parse_cmdopts(ARGV)
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
|
1018
|
+
|
1019
|
+
## for compatibility with 'benchmark.rb' (standard library)
|
1020
|
+
module Benchmark
|
1021
|
+
|
1022
|
+
def self.__new_bm(width, &block) # :nodoc:
|
1023
|
+
bm = Benchmarker.new(width: width)
|
1024
|
+
scope = Benchmarker::Scope.new(bm)
|
1025
|
+
scope.instance_exec(scope, &block)
|
1026
|
+
return bm
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
def self.bm(width=nil, &block)
|
1030
|
+
#; [!2nf07] defines and runs benchmark.
|
1031
|
+
__new_bm(width, &block).run()
|
1032
|
+
nil
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
def self.bmbm(width=nil, &block)
|
1036
|
+
#; [!ezbb8] defines and runs benchmark twice, reports only 2nd result.
|
1037
|
+
__new_bm(width, &block).run(warmup: true)
|
1038
|
+
nil
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
end unless defined?(::Benchmark)
|