flori-bullshit 0.1.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.
- data/COPYING +340 -0
- data/Rakefile +95 -0
- data/VERSION +1 -0
- data/bullshit.gemspec +25 -0
- data/examples/compare.rb +11 -0
- data/examples/fibonacci.rb +75 -0
- data/examples/iteration.rb +116 -0
- data/examples/josephus.rb +136 -0
- data/examples/sorting.rb +363 -0
- data/install.rb +15 -0
- data/lib/bullshit.rb +2244 -0
- data/lib/bullshit/version.rb +8 -0
- data/make_doc.rb +4 -0
- data/tests/test_analysis.rb +321 -0
- data/tests/test_bullshit.rb +222 -0
- data/tests/test_continued_fraction.rb +40 -0
- data/tests/test_distribution.rb +69 -0
- data/tests/test_functions.rb +33 -0
- data/tests/test_newton_bisection.rb +28 -0
- data/tests/test_window.rb +34 -0
- metadata +94 -0
data/install.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rbconfig'
|
4
|
+
require 'fileutils'
|
5
|
+
include FileUtils::Verbose
|
6
|
+
|
7
|
+
include Config
|
8
|
+
|
9
|
+
file = 'lib/bullshit.rb'
|
10
|
+
libdir = CONFIG["sitelibdir"]
|
11
|
+
install(file, libdir, :mode => 0755)
|
12
|
+
mkdir_p subdir = File.join(libdir, 'bullshit')
|
13
|
+
for f in Dir['lib/bullshit/*.rb']
|
14
|
+
install(f, subdir)
|
15
|
+
end
|
data/lib/bullshit.rb
ADDED
@@ -0,0 +1,2244 @@
|
|
1
|
+
# = Bullshit - Benchmarking in Ruby
|
2
|
+
#
|
3
|
+
# == Description
|
4
|
+
#
|
5
|
+
# == Usage
|
6
|
+
#
|
7
|
+
# == Author
|
8
|
+
#
|
9
|
+
# Florian Frank mailto:flori@ping.de
|
10
|
+
#
|
11
|
+
# == License
|
12
|
+
#
|
13
|
+
# This is free software; you can redistribute it and/or modify it under the
|
14
|
+
# terms of the GNU General Public License Version 2 as published by the Free
|
15
|
+
# Software Foundation: www.gnu.org/copyleft/gpl.html
|
16
|
+
#
|
17
|
+
# == Download
|
18
|
+
#
|
19
|
+
# The latest version of this library can be downloaded at
|
20
|
+
#
|
21
|
+
# * http://rubyforge.org/frs/?group_id=8323
|
22
|
+
#
|
23
|
+
# The homepage of this library is located at
|
24
|
+
#
|
25
|
+
# * http://bullshit.rubyforge.org
|
26
|
+
#
|
27
|
+
# == Example
|
28
|
+
#
|
29
|
+
|
30
|
+
require 'dslkit'
|
31
|
+
require 'enumerator'
|
32
|
+
|
33
|
+
begin
|
34
|
+
require 'bullshit/version'
|
35
|
+
rescue LoadError
|
36
|
+
end
|
37
|
+
|
38
|
+
# Module that includes all constants of the bullshit library.
|
39
|
+
module Bullshit
|
40
|
+
COLUMNS = 79 # Number of columns in the output.
|
41
|
+
|
42
|
+
NAME_COLUMN_SIZE = 5 # Number of columns used for row names.
|
43
|
+
|
44
|
+
Infinity = 1.0 / 0 # Refers to floating point infinity.
|
45
|
+
|
46
|
+
RUBY_DESCRIPTION = "ruby %s (%s patchlevel %s) [%s]" %
|
47
|
+
[ RUBY_VERSION, RUBY_RELEASE_DATE, RUBY_PATCHLEVEL, RUBY_PLATFORM ]
|
48
|
+
|
49
|
+
# This class implements a continued fraction of the form:
|
50
|
+
#
|
51
|
+
# b_1
|
52
|
+
# a_0 + -------------------------
|
53
|
+
# b_2
|
54
|
+
# a_1 + --------------------
|
55
|
+
# b_3
|
56
|
+
# a_2 + ---------------
|
57
|
+
# b_4
|
58
|
+
# a_3 + ----------
|
59
|
+
# b_5
|
60
|
+
# a_4 + -----
|
61
|
+
# ...
|
62
|
+
#
|
63
|
+
class ContinuedFraction
|
64
|
+
# Creates a continued fraction instance. With the defaults for_a { 1 } and
|
65
|
+
# for_b { 1 } it approximates the golden ration phi if evaluated.
|
66
|
+
def initialize
|
67
|
+
@a = proc { 1.0 }
|
68
|
+
@b = proc { 1.0 }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Creates a ContinuedFraction instances and passes its arguments to a call
|
72
|
+
# to for_a.
|
73
|
+
def self.for_a(arg = nil, &block)
|
74
|
+
new.for_a(arg, &block)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Creates a ContinuedFraction instances and passes its arguments to a call
|
78
|
+
# to for_b.
|
79
|
+
def self.for_b(arg = nil, &block)
|
80
|
+
new.for_b(arg, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
# This method either takes a block or an argument +arg+. The argument +arg+
|
84
|
+
# has to respond to an integer index n >= 0 and return the value a_n. The
|
85
|
+
# block has to return the value for a_n when +n+ is passed as the first
|
86
|
+
# argument to the block. If a_n is dependent on an +x+ value (see the call
|
87
|
+
# method) the +x+ will be the second argument of the block.
|
88
|
+
def for_a(arg = nil, &block)
|
89
|
+
if arg and !block
|
90
|
+
@a = arg
|
91
|
+
elsif block and !arg
|
92
|
+
@a = block
|
93
|
+
else
|
94
|
+
raise ArgumentError, "exactly one argument or one block required"
|
95
|
+
end
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# This method either takes a block or an argument +arg+. The argument +arg+
|
100
|
+
# has to respond to an integer index n >= 1 and return the value b_n. The
|
101
|
+
# block has to return the value for b_n when +n+ is passed as the first
|
102
|
+
# argument to the block. If b_n is dependent on an +x+ value (see the call
|
103
|
+
# method) the +x+ will be the second argument of the block.
|
104
|
+
def for_b(arg = nil, &block)
|
105
|
+
if arg and !block
|
106
|
+
@b = arg
|
107
|
+
elsif block and !arg
|
108
|
+
@b = block
|
109
|
+
else
|
110
|
+
raise ArgumentError, "exactly one argument or one block required"
|
111
|
+
end
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the value for a_n or a_n(x).
|
116
|
+
def a(n, x = nil)
|
117
|
+
result = if x
|
118
|
+
@a[n, x]
|
119
|
+
else
|
120
|
+
@a[n]
|
121
|
+
end and result.to_f
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the value for b_n or b_n(x).
|
125
|
+
def b(n, x = nil)
|
126
|
+
result = if x
|
127
|
+
@b[n, x]
|
128
|
+
else
|
129
|
+
@b[n]
|
130
|
+
end and result.to_f
|
131
|
+
end
|
132
|
+
|
133
|
+
# Evaluates the continued fraction for the value +x+ (if any) with the
|
134
|
+
# accuracy +epsilon+ and +max_iterations+ as the maximum number of
|
135
|
+
# iterations using the Wallis-method with scaling.
|
136
|
+
def call(x = nil, epsilon = 1E-16, max_iterations = 1 << 31)
|
137
|
+
c_0, c_1 = 1.0, a(0, x)
|
138
|
+
c_1 == nil and return 0 / 0.0
|
139
|
+
d_0, d_1 = 0.0, 1.0
|
140
|
+
result = c_1 / d_1
|
141
|
+
n = 0
|
142
|
+
error = 1 / 0.0
|
143
|
+
$DEBUG and warn "n=%u, a=%f, b=nil, c=%f, d=%f result=%f, error=nil" %
|
144
|
+
[ n, c_1, c_1, d_1, result ]
|
145
|
+
while n < max_iterations and error > epsilon
|
146
|
+
n += 1
|
147
|
+
a_n, b_n = a(n, x), b(n, x)
|
148
|
+
a_n and b_n or break
|
149
|
+
c_2 = a_n * c_1 + b_n * c_0
|
150
|
+
d_2 = a_n * d_1 + b_n * d_0
|
151
|
+
if c_2.infinite? or d_2.infinite?
|
152
|
+
if a_n != 0
|
153
|
+
c_2 = c_1 + (b_n / a_n * c_0)
|
154
|
+
d_2 = d_1 + (b_n / a_n * d_0)
|
155
|
+
elsif b_n != 0
|
156
|
+
c_2 = (a_n / b_n * c_1) + c_0
|
157
|
+
d_2 = (a_n / b_n * d_1) + d_0
|
158
|
+
else
|
159
|
+
raise Errno::ERANGE
|
160
|
+
end
|
161
|
+
end
|
162
|
+
r = c_2 / d_2
|
163
|
+
error = (r / result - 1).abs
|
164
|
+
|
165
|
+
result = r
|
166
|
+
|
167
|
+
$DEBUG and warn "n=%u, a=%f, b=%f, c=%f, d=%f, result=%f, error=%.16f" %
|
168
|
+
[ n, a_n, b_n, c_1, d_1, result, error ]
|
169
|
+
|
170
|
+
c_0, c_1 = c_1, c_2
|
171
|
+
d_0, d_1 = d_1, d_2
|
172
|
+
end
|
173
|
+
n >= max_iterations and raise Errno::ERANGE
|
174
|
+
result
|
175
|
+
end
|
176
|
+
|
177
|
+
alias [] call
|
178
|
+
|
179
|
+
# Returns this continued fraction as a Proc object which takes the same
|
180
|
+
# arguments like its call method does.
|
181
|
+
def to_proc
|
182
|
+
proc { |*a| call(*a) }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
module ModuleFunctions
|
187
|
+
module_function
|
188
|
+
|
189
|
+
# Return the angle +degree+ in radians.
|
190
|
+
def angle(degree)
|
191
|
+
Math.tan(Math::PI * degree / 180)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return the percentage number as a value in the range 0..1.
|
195
|
+
def percent(number)
|
196
|
+
number / 100.0
|
197
|
+
end
|
198
|
+
|
199
|
+
# Let a window of size +window_size+ slide over the array +array+ and yield
|
200
|
+
# to the window array.
|
201
|
+
def array_window(array, window_size)
|
202
|
+
window_size < 1 and raise ArgumentError, "window_size = #{window_size} < 1"
|
203
|
+
window_size = window_size.to_i
|
204
|
+
window_size += 1 if window_size % 2 == 0
|
205
|
+
radius = window_size / 2
|
206
|
+
array.each_index do |i|
|
207
|
+
ws = window_size
|
208
|
+
from = i - radius
|
209
|
+
negative_from = false
|
210
|
+
if from < 0
|
211
|
+
negative_from = true
|
212
|
+
ws += from
|
213
|
+
from = 0
|
214
|
+
end
|
215
|
+
a = array[from, ws]
|
216
|
+
if (diff = window_size - a.size) > 0
|
217
|
+
mean = a.inject(0.0) { |s, x| s + x } / a.size
|
218
|
+
a = if negative_from
|
219
|
+
[ mean ] * diff + a
|
220
|
+
else
|
221
|
+
a + [ mean ] * diff
|
222
|
+
end
|
223
|
+
end
|
224
|
+
yield a
|
225
|
+
end
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# An excpeption raised by the bullshit library.
|
231
|
+
class BullshitException < StandardError; end
|
232
|
+
|
233
|
+
# A Clock instance is used to take measurements while benchmarking.
|
234
|
+
class Clock
|
235
|
+
TIMES = [ :real, :total, :user, :system ]
|
236
|
+
|
237
|
+
ALL_COLUMNS = [ :scatter ] + TIMES + [ :repeat ]
|
238
|
+
|
239
|
+
TIMES_MAX = TIMES.map { |t| t.to_s.size }.max
|
240
|
+
|
241
|
+
# Returns a Clock instance for CaseMethod instance +bc_method+.
|
242
|
+
def initialize(bc_method)
|
243
|
+
@bc_method = bc_method
|
244
|
+
@times = Hash.new { |h, k| h[k] = [] }
|
245
|
+
@repeat = 0
|
246
|
+
@scatter = 0
|
247
|
+
end
|
248
|
+
|
249
|
+
# Use a Clock instance to measure the time necessary to do
|
250
|
+
# bc_method.case.iterations repetitions of +bc_method+.
|
251
|
+
def self.repeat(bc_method)
|
252
|
+
clock = new(bc_method)
|
253
|
+
bs = clock.case.batch_size.abs
|
254
|
+
bs = 1 if !bs or bs < 0
|
255
|
+
clock.case.iterations.times do
|
256
|
+
bc_method.before_run
|
257
|
+
$DEBUG and warn "Calling #{bc_method.name}."
|
258
|
+
clock.inc_scatter
|
259
|
+
clock.measure do
|
260
|
+
bs.times { yield }
|
261
|
+
end
|
262
|
+
bc_method.after_run
|
263
|
+
end
|
264
|
+
clock
|
265
|
+
end
|
266
|
+
|
267
|
+
# Use a Clock instance to measure how many repetitions of +bc_method+ can
|
268
|
+
# be done in bc_method.case.duration seconds (a float value). If the
|
269
|
+
# bc_method.case.batch_size is >1 duration is multiplied by batch_size
|
270
|
+
# because the measured times are averaged by batch_size.
|
271
|
+
def self.time(bc_method)
|
272
|
+
clock = new(bc_method)
|
273
|
+
duration = clock.case.duration.abs
|
274
|
+
if bs = clock.case.batch_size and bs > 1
|
275
|
+
duration *= bs
|
276
|
+
end
|
277
|
+
until_at = Time.now + duration
|
278
|
+
bs = clock.case.batch_size.abs
|
279
|
+
bs = 1 if !bs or bs < 0
|
280
|
+
begin
|
281
|
+
bc_method.before_run
|
282
|
+
$DEBUG and warn "Calling #{bc_method.name}."
|
283
|
+
clock.inc_scatter
|
284
|
+
clock.measure do
|
285
|
+
bs.times { yield }
|
286
|
+
end
|
287
|
+
bc_method.after_run
|
288
|
+
end until clock.time > until_at
|
289
|
+
clock
|
290
|
+
end
|
291
|
+
|
292
|
+
# Iterate over the +range+ of the RangeCase instance of this +bc_method+
|
293
|
+
# and take measurements (including scattering).
|
294
|
+
def self.scale_range(bc_method)
|
295
|
+
clock = new(bc_method)
|
296
|
+
my_case = clock.case
|
297
|
+
bs = my_case.batch_size.abs
|
298
|
+
bs = 1 if !bs or bs < 0
|
299
|
+
for a in my_case.range
|
300
|
+
begin
|
301
|
+
my_case.args = (a.dup rescue a).freeze
|
302
|
+
clock.inc_scatter
|
303
|
+
my_case.scatter.times do
|
304
|
+
bc_method.before_run
|
305
|
+
$DEBUG and warn "Calling #{bc_method.name}."
|
306
|
+
clock.measure do
|
307
|
+
bs.times { yield }
|
308
|
+
end
|
309
|
+
bc_method.after_run
|
310
|
+
end
|
311
|
+
ensure
|
312
|
+
my_case.args = nil
|
313
|
+
end
|
314
|
+
end
|
315
|
+
clock
|
316
|
+
end
|
317
|
+
|
318
|
+
# The benchmark case class this clock belongs to (via bc_method).
|
319
|
+
def case
|
320
|
+
@bc_method.case.class
|
321
|
+
end
|
322
|
+
|
323
|
+
# Returns the benchmark method for this Clock instance.
|
324
|
+
attr_reader :bc_method
|
325
|
+
|
326
|
+
# Number of repetitions this clock has measured.
|
327
|
+
attr_accessor :repeat
|
328
|
+
|
329
|
+
# Last time object used for real time measurement.
|
330
|
+
attr_reader :time
|
331
|
+
|
332
|
+
# Return all the slopes of linear regressions computed during data
|
333
|
+
# truncation phase.
|
334
|
+
attr_reader :slopes
|
335
|
+
|
336
|
+
# Add the array +times+ to this clock's time measurements. +times+ consists
|
337
|
+
# of the time measurements in float values in order of TIMES.
|
338
|
+
def <<(times)
|
339
|
+
r = times.shift
|
340
|
+
@repeat += 1 if @times[:repeat].last != r
|
341
|
+
@times[:repeat] << r
|
342
|
+
TIMES.zip(times) { |t, time| @times[t] << time.to_f }
|
343
|
+
self
|
344
|
+
end
|
345
|
+
|
346
|
+
# Returns a Hash of Analysis object for all of TIMES's time keys.
|
347
|
+
def analysis
|
348
|
+
@analysis ||= Hash.new do |h, time|
|
349
|
+
time = time.to_sym
|
350
|
+
times = @times[time]
|
351
|
+
h[time] = Analysis.new(times)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Return true, if other's mean value is indistinguishable from this
|
356
|
+
# object's mean after filtering out the noise from the measurements with a
|
357
|
+
# Welch's t-Test. This mean's that differences in the mean of both clocks
|
358
|
+
# might not inidicate a real performance difference and may be caused by
|
359
|
+
# chance.
|
360
|
+
def cover?(other)
|
361
|
+
time = self.case.compare_time.to_sym
|
362
|
+
analysis[time].cover?(other.analysis[time], self.case.covering.alpha_level.abs)
|
363
|
+
end
|
364
|
+
|
365
|
+
# Return column names in relation to Clock#to_a method.
|
366
|
+
def self.to_a
|
367
|
+
%w[ #scatter ] + TIMES + %w[ repeat ]
|
368
|
+
end
|
369
|
+
|
370
|
+
# Returns the measurements as an array of arrays.
|
371
|
+
def to_a
|
372
|
+
if @repeat >= 1
|
373
|
+
(::Bullshit::Clock::ALL_COLUMNS).map do |t|
|
374
|
+
analysis[t].measurements
|
375
|
+
end.transpose
|
376
|
+
else
|
377
|
+
[]
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Takes the times an returns an array, consisting of the times in the order
|
382
|
+
# of enumerated in the TIMES constant.
|
383
|
+
def take_time
|
384
|
+
@time, times = Time.now, Process.times
|
385
|
+
user_time = times.utime + times.cutime # user time of this process and its children
|
386
|
+
system_time = times.stime + times.cstime # system time of this process and its children
|
387
|
+
total_time = user_time + system_time # total time of this process and its children
|
388
|
+
[ @time.to_f, total_time, user_time, system_time ]
|
389
|
+
end
|
390
|
+
|
391
|
+
# Increment scatter counter by one.
|
392
|
+
def inc_scatter
|
393
|
+
@scatter += 1
|
394
|
+
end
|
395
|
+
|
396
|
+
# Take a single measurement. This method should be called with the code to
|
397
|
+
# benchmark in a block.
|
398
|
+
def measure
|
399
|
+
before = take_time
|
400
|
+
yield
|
401
|
+
after = take_time
|
402
|
+
@repeat += 1
|
403
|
+
@times[:repeat] << @repeat
|
404
|
+
@times[:scatter] << @scatter
|
405
|
+
bs = self.case.batch_size.abs
|
406
|
+
if bs and bs > 1
|
407
|
+
TIMES.each_with_index { |t, i| @times[t] << (after[i] - before[i]) / bs }
|
408
|
+
else
|
409
|
+
TIMES.each_with_index { |t, i| @times[t] << after[i] - before[i] }
|
410
|
+
end
|
411
|
+
@analysis = nil
|
412
|
+
end
|
413
|
+
|
414
|
+
# Returns the sample standard deviation for the +time+ (one of TIMES'
|
415
|
+
# symbols).
|
416
|
+
def sample_standard_deviation(time)
|
417
|
+
analysis[time.to_sym].sample_standard_deviation
|
418
|
+
end
|
419
|
+
|
420
|
+
# Returns the sample standard deviation for the +time+ (one of TIMES'
|
421
|
+
# symbols) in percentage of its arithmetic mean.
|
422
|
+
def sample_standard_deviation_percentage(time)
|
423
|
+
analysis[time.to_sym].sample_standard_deviation_percentage
|
424
|
+
end
|
425
|
+
|
426
|
+
# Returns the minimum for the +time+ (one of TIMES' symbols).
|
427
|
+
def min(time)
|
428
|
+
analysis[time.to_sym].min
|
429
|
+
end
|
430
|
+
|
431
|
+
# Returns the maximum for the +time+ (one of TIMES' symbols).
|
432
|
+
def max(time)
|
433
|
+
analysis[time.to_sym].max
|
434
|
+
end
|
435
|
+
|
436
|
+
# Returns the median of the +time+ values (one of TIMES' symbols).
|
437
|
+
def median(time)
|
438
|
+
analysis[time.to_sym].median
|
439
|
+
end
|
440
|
+
|
441
|
+
# Returns the +p+-percentile of the +time+ values (one of TIMES' symbols).
|
442
|
+
def percentile(time, p = 50)
|
443
|
+
analysis[time.to_sym].percentile p
|
444
|
+
end
|
445
|
+
|
446
|
+
# Returns the q value for the Ljung-Box statistic of this +time+'s
|
447
|
+
# analysis.detect_autocorrelation method.
|
448
|
+
def detect_autocorrelation(time)
|
449
|
+
analysis[time.to_sym].detect_autocorrelation(
|
450
|
+
self.case.autocorrelation.max_lags.to_i,
|
451
|
+
self.case.autocorrelation.alpha_level.abs)
|
452
|
+
end
|
453
|
+
|
454
|
+
# Return a result hash with the number of :very_low, :low, :high, and
|
455
|
+
# :very_high outliers, determined by the box plotting algorithm run with
|
456
|
+
# :median and :iqr parameters. If no outliers were found or the iqr is less
|
457
|
+
# than epsilon, nil is returned.
|
458
|
+
def detect_outliers(time)
|
459
|
+
analysis[time.to_sym].detect_outliers(self.case.outliers_factor.abs)
|
460
|
+
end
|
461
|
+
|
462
|
+
TIMES.each do |time|
|
463
|
+
define_method(time) { analysis[time].sum }
|
464
|
+
end
|
465
|
+
|
466
|
+
# Seconds per call (mean)
|
467
|
+
def call_time_mean
|
468
|
+
mean(self.case.compare_time)
|
469
|
+
end
|
470
|
+
|
471
|
+
# Calls per second of the +call_time_type+, e. g. :call_time_mean or
|
472
|
+
# :call_time_median.
|
473
|
+
def calls(call_time_type)
|
474
|
+
__send__(call_time_type) ** -1
|
475
|
+
end
|
476
|
+
|
477
|
+
# Calls per second (mean)
|
478
|
+
def calls_mean
|
479
|
+
call_time_mean ** -1
|
480
|
+
end
|
481
|
+
|
482
|
+
# Seconds per call (median)
|
483
|
+
def call_time_median
|
484
|
+
median(self.case.compare_time)
|
485
|
+
end
|
486
|
+
|
487
|
+
# Calls per second (median)
|
488
|
+
def calls_median
|
489
|
+
call_time_median ** -1
|
490
|
+
end
|
491
|
+
|
492
|
+
# Returns the arithmetic mean of +time+.
|
493
|
+
def arithmetic_mean(time)
|
494
|
+
analysis[time.to_sym].mean
|
495
|
+
end
|
496
|
+
|
497
|
+
alias mean arithmetic_mean
|
498
|
+
|
499
|
+
# Returns the harmonic mean of +time+.
|
500
|
+
def harmonic_mean(time)
|
501
|
+
analysis[time.to_sym].harmonic_mean
|
502
|
+
end
|
503
|
+
|
504
|
+
# Returns the geometric mean of +time+.
|
505
|
+
def geometric_mean(time)
|
506
|
+
analysis[time.to_sym].geometric_mean
|
507
|
+
end
|
508
|
+
|
509
|
+
# The times which should be displayed in the output.
|
510
|
+
def self.times
|
511
|
+
TIMES.map { |t| t.to_s }
|
512
|
+
end
|
513
|
+
|
514
|
+
# Return the Histogram for the +time+ values.
|
515
|
+
def histogram(time)
|
516
|
+
analysis[time.to_sym].histogram(self.case.histogram.bins)
|
517
|
+
end
|
518
|
+
|
519
|
+
# Return the array of autocorrelation values for +time+.
|
520
|
+
def autocorrelation(time)
|
521
|
+
analysis[time.to_sym].autocorrelation
|
522
|
+
end
|
523
|
+
|
524
|
+
# Returns the arrays for the autocorrelation plot, the first array for the
|
525
|
+
# numbers of lag measured, the second for the autocorrelation value.
|
526
|
+
def autocorrelation_plot(time)
|
527
|
+
r = autocorrelation time
|
528
|
+
start = @times[:repeat].first
|
529
|
+
ende = (start + r.size)
|
530
|
+
(start...ende).to_a.zip(r)
|
531
|
+
end
|
532
|
+
|
533
|
+
# Return the result of CaseMethod#file_path for this clock's bc_method.
|
534
|
+
def file_path(*args)
|
535
|
+
@bc_method.file_path(*args)
|
536
|
+
end
|
537
|
+
|
538
|
+
# Truncate the measurements stored in this clock starting from the integer
|
539
|
+
# +offset+.
|
540
|
+
def truncate_data(offset)
|
541
|
+
for t in ALL_COLUMNS
|
542
|
+
times = @times[t]
|
543
|
+
@times[t] = @times[t][offset, times.size]
|
544
|
+
@repeat = @times[t].size
|
545
|
+
end
|
546
|
+
@analysis = nil
|
547
|
+
self
|
548
|
+
end
|
549
|
+
|
550
|
+
# Find an offset from the start of the measurements in this clock to
|
551
|
+
# truncate the initial data until a stable state has been reached and
|
552
|
+
# return it as an integer.
|
553
|
+
def find_truncation_offset
|
554
|
+
truncation = self.case.truncate_data
|
555
|
+
slope_angle = self.case.truncate_data.slope_angle.abs
|
556
|
+
time = self.case.compare_time.to_sym
|
557
|
+
ms = analysis[time].measurements.reverse
|
558
|
+
offset = ms.size - 1
|
559
|
+
@slopes = []
|
560
|
+
ModuleFunctions.array_window(ms, truncation.window_size) do |data|
|
561
|
+
lr = LinearRegression.new(data)
|
562
|
+
a = lr.a
|
563
|
+
@slopes << [ offset, a ]
|
564
|
+
a.abs > slope_angle and break
|
565
|
+
offset -= 1
|
566
|
+
end
|
567
|
+
offset < 0 ? 0 : offset
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
# A histogram gives an overview of measurement time values.
|
572
|
+
class Histogram
|
573
|
+
# Create a Histogram for +clock+ using the measurements for +time+.
|
574
|
+
def initialize(analysis, bins)
|
575
|
+
@analysis = analysis
|
576
|
+
@bins = bins
|
577
|
+
@result = compute
|
578
|
+
end
|
579
|
+
|
580
|
+
# Number of bins for this Histogram.
|
581
|
+
attr_reader :bins
|
582
|
+
|
583
|
+
# Return the computed histogram as an array of arrays.
|
584
|
+
def to_a
|
585
|
+
@result
|
586
|
+
end
|
587
|
+
|
588
|
+
# Display this histogram to +output+, +width+ is the parameter for
|
589
|
+
# +prepare_display+
|
590
|
+
def display(output = $stdout, width = 50)
|
591
|
+
d = prepare_display(width)
|
592
|
+
for l, bar, r in d
|
593
|
+
output << "%11.5f -|%s\n" % [ (l + r) / 2.0, "*" * bar ]
|
594
|
+
end
|
595
|
+
self
|
596
|
+
end
|
597
|
+
|
598
|
+
private
|
599
|
+
|
600
|
+
# Returns an array of tuples (l, c, r) where +l+ is the left bin edge, +c+
|
601
|
+
# the +width+-normalized frequence count value, and +r+ the right bin
|
602
|
+
# edge. +width+ is usually an integer number representing the width of a
|
603
|
+
# histogram bar.
|
604
|
+
def prepare_display(width)
|
605
|
+
r = @result.reverse
|
606
|
+
factor = width.to_f / (r.transpose[1].max)
|
607
|
+
r.map { |l, c, r| [ l, (c * factor).round, r ] }
|
608
|
+
end
|
609
|
+
|
610
|
+
# Computes the histogram and returns it as an array of tuples (l, c, r).
|
611
|
+
def compute
|
612
|
+
@analysis.measurements.empty? and return []
|
613
|
+
last_r = -Infinity
|
614
|
+
min = @analysis.min
|
615
|
+
max = @analysis.max
|
616
|
+
step = (max - min) / bins.to_f
|
617
|
+
Array.new(bins) do |i|
|
618
|
+
l = min + i * step
|
619
|
+
r = min + (i + 1) * step
|
620
|
+
c = 0
|
621
|
+
@analysis.measurements.each do |x|
|
622
|
+
x > last_r and (x <= r || i == bins - 1) and c += 1
|
623
|
+
end
|
624
|
+
last_r = r
|
625
|
+
[ l, c, r ]
|
626
|
+
end
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
# This class is used to find the root of a function with Newton's bisection
|
631
|
+
# method.
|
632
|
+
class NewtonBisection
|
633
|
+
# Creates a NewtonBisection instance for +function+, a one-argument block.
|
634
|
+
def initialize(&function)
|
635
|
+
@function = function
|
636
|
+
end
|
637
|
+
|
638
|
+
# The function, passed into the constructor.
|
639
|
+
attr_reader :function
|
640
|
+
|
641
|
+
# Return a bracket around a root, starting from the initial +range+. The
|
642
|
+
# method returns nil, if no such bracket around a root could be found after
|
643
|
+
# +n+ tries with the scaling +factor+.
|
644
|
+
def bracket(range = -1..1, n = 50, factor = 1.6)
|
645
|
+
x1, x2 = range.first.to_f, range.last.to_f
|
646
|
+
x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
|
647
|
+
f1, f2 = @function[x1], @function[x2]
|
648
|
+
n.times do
|
649
|
+
f1 * f2 < 0 and return x1..x2
|
650
|
+
if f1.abs < f2.abs
|
651
|
+
f1 = @function[x1 += factor * (x1 - x2)]
|
652
|
+
else
|
653
|
+
f2 = @function[x2 += factor * (x2 - x1)]
|
654
|
+
end
|
655
|
+
end
|
656
|
+
return
|
657
|
+
end
|
658
|
+
|
659
|
+
# Find the root of function in +range+ and return it. The method raises a
|
660
|
+
# BullshitException, if no such root could be found after +n+ tries and in
|
661
|
+
# the +epsilon+ environment.
|
662
|
+
def solve(range = nil, n = 1 << 16, epsilon = 1E-16)
|
663
|
+
if range
|
664
|
+
x1, x2 = range.first.to_f, range.last.to_f
|
665
|
+
x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
|
666
|
+
elsif range = bracket
|
667
|
+
x1, x2 = range.first, range.last
|
668
|
+
else
|
669
|
+
raise ArgumentError, "bracket could not be determined"
|
670
|
+
end
|
671
|
+
f = @function[x1]
|
672
|
+
fmid = @function[x2]
|
673
|
+
f * fmid >= 0 and raise ArgumentError, "root must be bracketed in #{range}"
|
674
|
+
root = if f < 0
|
675
|
+
dx = x2 - x1
|
676
|
+
x1
|
677
|
+
else
|
678
|
+
dx = x1 - x2
|
679
|
+
x2
|
680
|
+
end
|
681
|
+
n.times do
|
682
|
+
fmid = @function[xmid = root + (dx *= 0.5)]
|
683
|
+
fmid < 0 and root = xmid
|
684
|
+
dx.abs < epsilon or fmid == 0 and return root
|
685
|
+
end
|
686
|
+
raise BullshitException, "too many iterations (#{n})"
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
module Functions
|
691
|
+
module_function
|
692
|
+
|
693
|
+
include Math
|
694
|
+
extend Math
|
695
|
+
|
696
|
+
LANCZOS_COEFFICIENTS = [
|
697
|
+
0.99999999999999709182,
|
698
|
+
57.156235665862923517,
|
699
|
+
-59.597960355475491248,
|
700
|
+
14.136097974741747174,
|
701
|
+
-0.49191381609762019978,
|
702
|
+
0.33994649984811888699e-4,
|
703
|
+
0.46523628927048575665e-4,
|
704
|
+
-0.98374475304879564677e-4,
|
705
|
+
0.15808870322491248884e-3,
|
706
|
+
-0.21026444172410488319e-3,
|
707
|
+
0.21743961811521264320e-3,
|
708
|
+
-0.16431810653676389022e-3,
|
709
|
+
0.84418223983852743293e-4,
|
710
|
+
-0.26190838401581408670e-4,
|
711
|
+
0.36899182659531622704e-5,
|
712
|
+
]
|
713
|
+
|
714
|
+
HALF_LOG_2_PI = 0.5 * log(2 * Math::PI)
|
715
|
+
|
716
|
+
# Returns the natural logarithm of Euler gamma function value for +x+ using
|
717
|
+
# the Lanczos approximation.
|
718
|
+
if method_defined?(:lgamma)
|
719
|
+
def log_gamma(x)
|
720
|
+
lgamma(x).first
|
721
|
+
end
|
722
|
+
else
|
723
|
+
def log_gamma(x)
|
724
|
+
if x.nan? || x <= 0
|
725
|
+
0 / 0.0
|
726
|
+
else
|
727
|
+
sum = 0.0
|
728
|
+
(LANCZOS_COEFFICIENTS.size - 1).downto(1) do |i|
|
729
|
+
sum += LANCZOS_COEFFICIENTS[i] / (x + i)
|
730
|
+
end
|
731
|
+
sum += LANCZOS_COEFFICIENTS[0]
|
732
|
+
tmp = x + 607.0 / 128 + 0.5
|
733
|
+
(x + 0.5) * log(tmp) - tmp + HALF_LOG_2_PI + log(sum / x)
|
734
|
+
end
|
735
|
+
rescue Errno::ERANGE, Errno::EDOM
|
736
|
+
0 / 0.0
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
740
|
+
# Returns the natural logarithm of the beta function value for +(a, b)+.
|
741
|
+
def log_beta(a, b)
|
742
|
+
log_gamma(a) + log_gamma(b) - log_gamma(a + b)
|
743
|
+
rescue Errno::ERANGE, Errno::EDOM
|
744
|
+
0 / 0.0
|
745
|
+
end
|
746
|
+
|
747
|
+
# Return an approximation value of Euler's regularized beta function for
|
748
|
+
# +x+, +a+, and +b+ with an error <= +epsilon+, but only iterate
|
749
|
+
# +max_iterations+-times.
|
750
|
+
def beta_regularized(x, a, b, epsilon = 1E-16, max_iterations = 1 << 16)
|
751
|
+
x, a, b = x.to_f, a.to_f, b.to_f
|
752
|
+
case
|
753
|
+
when a.nan? || b.nan? || x.nan? || a <= 0 || b <= 0 || x < 0 || x > 1
|
754
|
+
0 / 0.0
|
755
|
+
when x > (a + 1) / (a + b + 2)
|
756
|
+
1 - beta_regularized(1 - x, b, a, epsilon, max_iterations)
|
757
|
+
else
|
758
|
+
fraction = ContinuedFraction.for_b do |n, x|
|
759
|
+
if n % 2 == 0
|
760
|
+
m = n / 2.0
|
761
|
+
(m * (b - m) * x) / ((a + (2 * m) - 1) * (a + (2 * m)))
|
762
|
+
else
|
763
|
+
m = (n - 1) / 2.0
|
764
|
+
-((a + m) * (a + b + m) * x) / ((a + 2 * m) * (a + 2 * m + 1))
|
765
|
+
end
|
766
|
+
end
|
767
|
+
exp(a * log(x) + b * log(1.0 - x) - log(a) - log_beta(a, b)) /
|
768
|
+
fraction[x, epsilon, max_iterations]
|
769
|
+
end
|
770
|
+
rescue Errno::ERANGE, Errno::EDOM
|
771
|
+
0 / 0.0
|
772
|
+
end
|
773
|
+
|
774
|
+
# Return an approximation of the regularized gammaP function for +x+ and
|
775
|
+
# +a+ with an error of <= +epsilon+, but only iterate
|
776
|
+
# +max_iterations+-times.
|
777
|
+
def gammaP_regularized(x, a, epsilon = 1E-16, max_iterations = 1 << 16)
|
778
|
+
x, a = x.to_f, a.to_f
|
779
|
+
case
|
780
|
+
when a.nan? || x.nan? || a <= 0 || x < 0
|
781
|
+
0 / 0.0
|
782
|
+
when x == 0
|
783
|
+
0.0
|
784
|
+
when 1 <= a && a < x
|
785
|
+
1 - gammaQ_regularized(x, a, epsilon, max_iterations)
|
786
|
+
else
|
787
|
+
n = 0
|
788
|
+
an = 1 / a
|
789
|
+
sum = an
|
790
|
+
while an.abs > epsilon && n < max_iterations
|
791
|
+
n += 1
|
792
|
+
an *= x / (a + n)
|
793
|
+
sum += an
|
794
|
+
end
|
795
|
+
if n >= max_iterations
|
796
|
+
raise Errno::ERANGE
|
797
|
+
else
|
798
|
+
exp(-x + a * log(x) - log_gamma(a)) * sum
|
799
|
+
end
|
800
|
+
end
|
801
|
+
rescue Errno::ERANGE, Errno::EDOM
|
802
|
+
0 / 0.0
|
803
|
+
end
|
804
|
+
|
805
|
+
# Return an approximation of the regularized gammaQ function for +x+ and
|
806
|
+
# +a+ with an error of <= +epsilon+, but only iterate
|
807
|
+
# +max_iterations+-times.
|
808
|
+
def gammaQ_regularized(x, a, epsilon = 1E-16, max_iterations = 1 << 16)
|
809
|
+
x, a = x.to_f, a.to_f
|
810
|
+
case
|
811
|
+
when a.nan? || x.nan? || a <= 0 || x < 0
|
812
|
+
0 / 0.0
|
813
|
+
when x == 0
|
814
|
+
1.0
|
815
|
+
when a > x || a < 1
|
816
|
+
1 - gammaP_regularized(x, a, epsilon, max_iterations)
|
817
|
+
else
|
818
|
+
fraction = ContinuedFraction.for_a do |n, x|
|
819
|
+
(2 * n + 1) - a + x
|
820
|
+
end.for_b do |n, x|
|
821
|
+
n * (a - n)
|
822
|
+
end
|
823
|
+
exp(-x + a * log(x) - log_gamma(a)) *
|
824
|
+
fraction[x, epsilon, max_iterations] ** -1
|
825
|
+
end
|
826
|
+
rescue Errno::ERANGE, Errno::EDOM
|
827
|
+
0 / 0.0
|
828
|
+
end
|
829
|
+
|
830
|
+
ROOT2 = sqrt(2)
|
831
|
+
|
832
|
+
A = -8 * (Math::PI - 3) / (3 * Math::PI * (Math::PI - 4))
|
833
|
+
|
834
|
+
# Returns an approximate value for the error function's value for +x+.
|
835
|
+
def erf(x)
|
836
|
+
r = sqrt(1 - exp(-x ** 2 * (4 / Math::PI + A * x ** 2) / (1 + A * x ** 2)))
|
837
|
+
x < 0 ? -r : r
|
838
|
+
end unless method_defined?(:erf)
|
839
|
+
end
|
840
|
+
|
841
|
+
# This class is used to compute the T-Distribution.
|
842
|
+
class TDistribution
|
843
|
+
include Functions
|
844
|
+
|
845
|
+
# Returns a TDistribution instance for the degrees of freedom +df+.
|
846
|
+
def initialize(df)
|
847
|
+
@df = df
|
848
|
+
end
|
849
|
+
|
850
|
+
# Degrees of freedom.
|
851
|
+
attr_reader :df
|
852
|
+
|
853
|
+
# Returns the cumulative probability (p-value) of the TDistribution for the
|
854
|
+
# t-value +x+.
|
855
|
+
def probability(x)
|
856
|
+
if x == 0
|
857
|
+
0.5
|
858
|
+
else
|
859
|
+
t = beta_regularized(@df / (@df + x ** 2.0), 0.5 * @df, 0.5)
|
860
|
+
if x < 0.0
|
861
|
+
0.5 * t
|
862
|
+
else
|
863
|
+
1 - 0.5 * t
|
864
|
+
end
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
# Returns the inverse cumulative probability (t-value) of the TDistribution
|
869
|
+
# for the probability +p+.
|
870
|
+
def inverse_probability(p)
|
871
|
+
case
|
872
|
+
when p <= 0
|
873
|
+
-1 / 0.0
|
874
|
+
when p >= 1
|
875
|
+
1 / 0.0
|
876
|
+
else
|
877
|
+
begin
|
878
|
+
bisect = NewtonBisection.new { |x| probability(x) - p }
|
879
|
+
range = bisect.bracket(-10..10)
|
880
|
+
bisect.solve(range, 1_000_000)
|
881
|
+
rescue
|
882
|
+
0 / 0.0
|
883
|
+
end
|
884
|
+
end
|
885
|
+
end
|
886
|
+
end
|
887
|
+
|
888
|
+
# This class is used to compute the Normal Distribution.
|
889
|
+
class NormalDistribution
|
890
|
+
include Functions
|
891
|
+
|
892
|
+
# Creates a NormalDistribution instance for the values +mu+ and +sigma+.
|
893
|
+
def initialize(mu = 0.0, sigma = 1.0)
|
894
|
+
@mu, @sigma = mu.to_f, sigma.to_f
|
895
|
+
end
|
896
|
+
|
897
|
+
attr_reader :mu
|
898
|
+
|
899
|
+
attr_reader :sigma
|
900
|
+
|
901
|
+
# Returns the cumulative probability (p-value) of the NormalDistribution
|
902
|
+
# for the value +x+.
|
903
|
+
def probability(x)
|
904
|
+
0.5 * (1 + erf((x - @mu) / (@sigma * ROOT2)))
|
905
|
+
end
|
906
|
+
|
907
|
+
# Returns the inverse cumulative probability value of the
|
908
|
+
# NormalDistribution for the probability +p+.
|
909
|
+
def inverse_probability(p)
|
910
|
+
case
|
911
|
+
when p <= 0
|
912
|
+
-1 / 0.0
|
913
|
+
when p >= 1
|
914
|
+
1 / 0.0
|
915
|
+
when p == 0.5 # This is a bit sloppy, maybe improve this later.
|
916
|
+
@mu
|
917
|
+
else
|
918
|
+
begin
|
919
|
+
NewtonBisection.new { |x| probability(x) - p }.solve(nil, 1_000_000)
|
920
|
+
rescue
|
921
|
+
0 / 0.0
|
922
|
+
end
|
923
|
+
end
|
924
|
+
end
|
925
|
+
end
|
926
|
+
|
927
|
+
STD_NORMAL_DISTRIBUTION = NormalDistribution.new
|
928
|
+
|
929
|
+
# This class is used to compute the Chi-Square Distribution.
|
930
|
+
class ChiSquareDistribution
|
931
|
+
include Functions
|
932
|
+
|
933
|
+
# Creates a ChiSquareDistribution for +df+ degrees of freedom.
|
934
|
+
def initialize(df)
|
935
|
+
@df = df
|
936
|
+
@df_half = @df / 2.0
|
937
|
+
end
|
938
|
+
|
939
|
+
attr_reader :df
|
940
|
+
|
941
|
+
# Returns the cumulative probability (p-value) of the ChiSquareDistribution
|
942
|
+
# for the value +x+.
|
943
|
+
def probability(x)
|
944
|
+
if x < 0
|
945
|
+
0.0
|
946
|
+
else
|
947
|
+
gammaP_regularized(x / 2, @df_half)
|
948
|
+
end
|
949
|
+
end
|
950
|
+
|
951
|
+
# Returns the inverse cumulative probability value of the
|
952
|
+
# NormalDistribution for the probability +p+.
|
953
|
+
def inverse_probability(p)
|
954
|
+
case
|
955
|
+
when p <= 0, p >= 1
|
956
|
+
0.0
|
957
|
+
else
|
958
|
+
begin
|
959
|
+
bisect = NewtonBisection.new { |x| probability(x) - p }
|
960
|
+
range = bisect.bracket 0.5..10
|
961
|
+
bisect.solve(range, 1_000_000)
|
962
|
+
rescue
|
963
|
+
0 / 0.0
|
964
|
+
end
|
965
|
+
end
|
966
|
+
end
|
967
|
+
end
|
968
|
+
|
969
|
+
# This class computes a linear regression for the given image and domain data
|
970
|
+
# sets.
|
971
|
+
class LinearRegression
|
972
|
+
def initialize(image, domain = (0...image.size).to_a)
|
973
|
+
image.size != domain.size and raise ArgumentError,
|
974
|
+
"image and domain have unequal sizes"
|
975
|
+
@image, @domain = image, domain
|
976
|
+
compute
|
977
|
+
end
|
978
|
+
|
979
|
+
# The image data as an array.
|
980
|
+
attr_reader :image
|
981
|
+
|
982
|
+
# The domain data as an array.
|
983
|
+
attr_reader :domain
|
984
|
+
|
985
|
+
# The slope of the line.
|
986
|
+
attr_reader :a
|
987
|
+
|
988
|
+
# The offset of the line.
|
989
|
+
attr_reader :b
|
990
|
+
|
991
|
+
# Return true if the slope of the underlying data (not the sample data
|
992
|
+
# passed into the constructor of this LinearRegression instance) is likely
|
993
|
+
# (with alpha level _alpha_) to be zero.
|
994
|
+
def slope_zero?(alpha = 0.05)
|
995
|
+
df = @image.size - 2
|
996
|
+
return true if df <= 0 # not enough values to check
|
997
|
+
t = tvalue(alpha)
|
998
|
+
td = TDistribution.new df
|
999
|
+
t.abs <= td.inverse_probability(1 - alpha.abs / 2.0).abs
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Returns the residues of this linear regression in relation to the given
|
1003
|
+
# domain and image.
|
1004
|
+
def residues
|
1005
|
+
result = []
|
1006
|
+
@domain.zip(@image) do |x, y|
|
1007
|
+
result << y - (@a * x + @b)
|
1008
|
+
end
|
1009
|
+
result
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
private
|
1013
|
+
|
1014
|
+
def compute
|
1015
|
+
size = @image.size
|
1016
|
+
sum_xx = sum_xy = sum_x = sum_y = 0.0
|
1017
|
+
@domain.zip(@image) do |x, y|
|
1018
|
+
x += 1
|
1019
|
+
sum_xx += x ** 2
|
1020
|
+
sum_xy += x * y
|
1021
|
+
sum_x += x
|
1022
|
+
sum_y += y
|
1023
|
+
end
|
1024
|
+
@a = (size * sum_xy - sum_x * sum_y) / (size * sum_xx - sum_x ** 2)
|
1025
|
+
@b = (sum_y - @a * sum_x) / size
|
1026
|
+
self
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
def tvalue(alpha = 0.05)
|
1030
|
+
df = @image.size - 2
|
1031
|
+
return 0.0 if df <= 0
|
1032
|
+
sse_y = 0.0
|
1033
|
+
@domain.zip(@image) do |x, y|
|
1034
|
+
f_x = a * x + b
|
1035
|
+
sse_y += (y - f_x) ** 2
|
1036
|
+
end
|
1037
|
+
mean = @image.inject(0.0) { |s, y| s + y } / @image.size
|
1038
|
+
sse_x = @domain.inject(0.0) { |s, x| s + (x - mean) ** 2 }
|
1039
|
+
t = a / (Math.sqrt(sse_y / df) / Math.sqrt(sse_x))
|
1040
|
+
t.nan? ? 0.0 : t
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
# This class is used to analyse the time measurements and compute their
|
1045
|
+
# statistics.
|
1046
|
+
class Analysis
|
1047
|
+
def initialize(measurements)
|
1048
|
+
@measurements = measurements
|
1049
|
+
@measurements.freeze
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
# Returns the array of measurements.
|
1053
|
+
attr_reader :measurements
|
1054
|
+
|
1055
|
+
# Returns the number of measurements, on which the analysis is based.
|
1056
|
+
def size
|
1057
|
+
@measurements.size
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
# Returns the variance of the measurements.
|
1061
|
+
def variance
|
1062
|
+
@variance ||= sum_of_squares / size
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
# Returns the sample_variance of the measurements.
|
1066
|
+
def sample_variance
|
1067
|
+
@sample_variance ||= size > 1 ? sum_of_squares / (size - 1.0) : 0.0
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
# Returns the sum of squares (the sum of the squared deviations) of the
|
1071
|
+
# measurements.
|
1072
|
+
def sum_of_squares
|
1073
|
+
@sum_of_squares ||= @measurements.inject(0.0) { |s, t| s + (t - arithmetic_mean) ** 2 }
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
# Returns the standard deviation of the measurements.
|
1077
|
+
def standard_deviation
|
1078
|
+
@sample_deviation ||= Math.sqrt(variance)
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
# Returns the standard deviation of the measurements in percentage of the
|
1082
|
+
# arithmetic mean.
|
1083
|
+
def standard_deviation_percentage
|
1084
|
+
@standard_deviation_percentage ||= 100.0 * standard_deviation / arithmetic_mean
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
# Returns the sample standard deviation of the measurements.
|
1088
|
+
def sample_standard_deviation
|
1089
|
+
@sample_standard_deviation ||= Math.sqrt(sample_variance)
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
# Returns the sample standard deviation of the measurements in percentage
|
1093
|
+
# of the arithmetic mean.
|
1094
|
+
def sample_standard_deviation_percentage
|
1095
|
+
@sample_standard_deviation_percentage ||= 100.0 * sample_standard_deviation / arithmetic_mean
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
# Returns the sum of all measurements.
|
1099
|
+
def sum
|
1100
|
+
@sum ||= @measurements.inject(0.0) { |s, t| s + t }
|
1101
|
+
end
|
1102
|
+
|
1103
|
+
# Returns the arithmetic mean of the measurements.
|
1104
|
+
def arithmetic_mean
|
1105
|
+
@arithmetic_mean ||= sum / size
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
alias mean arithmetic_mean
|
1109
|
+
|
1110
|
+
# Returns the harmonic mean of the measurements. If any of the measurements
|
1111
|
+
# is less than or equal to 0.0, this method returns NaN.
|
1112
|
+
def harmonic_mean
|
1113
|
+
@harmonic_mean ||= (
|
1114
|
+
sum = @measurements.inject(0.0) { |s, t|
|
1115
|
+
if t > 0
|
1116
|
+
s + 1.0 / t
|
1117
|
+
else
|
1118
|
+
break nil
|
1119
|
+
end
|
1120
|
+
}
|
1121
|
+
sum ? size / sum : 0 / 0.0
|
1122
|
+
)
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
# Returns the geometric mean of the measurements. If any of the
|
1126
|
+
# measurements is less than 0.0, this method returns NaN.
|
1127
|
+
def geometric_mean
|
1128
|
+
@geometric_mean ||= (
|
1129
|
+
sum = @measurements.inject(0.0) { |s, t|
|
1130
|
+
case
|
1131
|
+
when t > 0
|
1132
|
+
s + Math.log(t)
|
1133
|
+
when t == 0
|
1134
|
+
break :null
|
1135
|
+
else
|
1136
|
+
break nil
|
1137
|
+
end
|
1138
|
+
}
|
1139
|
+
case sum
|
1140
|
+
when :null
|
1141
|
+
0.0
|
1142
|
+
when Float
|
1143
|
+
Math.exp(sum / size)
|
1144
|
+
else
|
1145
|
+
0 / 0.0
|
1146
|
+
end
|
1147
|
+
)
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
# Returns the minimum of the measurements.
|
1151
|
+
def min
|
1152
|
+
@min ||= @measurements.min
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
# Returns the maximum of the measurements.
|
1156
|
+
def max
|
1157
|
+
@max ||= @measurements.max
|
1158
|
+
end
|
1159
|
+
|
1160
|
+
# Returns the +p+-percentile of the measurements.
|
1161
|
+
# There are many methods to compute the percentile, this method uses the
|
1162
|
+
# the weighted average at x_(n + 1)p, which allows p to be in 0...100
|
1163
|
+
# (excluding the 100).
|
1164
|
+
def percentile(p = 50)
|
1165
|
+
(0...100).include?(p) or
|
1166
|
+
raise ArgumentError, "p = #{p}, but has to be in (0...100)"
|
1167
|
+
p /= 100.0
|
1168
|
+
@sorted ||= @measurements.sort
|
1169
|
+
r = p * (@sorted.size + 1)
|
1170
|
+
r_i = r.to_i
|
1171
|
+
r_f = r - r_i
|
1172
|
+
if r_i >= 1
|
1173
|
+
result = @sorted[r_i - 1]
|
1174
|
+
if r_i < @sorted.size
|
1175
|
+
result += r_f * (@sorted[r_i] - @sorted[r_i - 1])
|
1176
|
+
end
|
1177
|
+
else
|
1178
|
+
result = @sorted[0]
|
1179
|
+
end
|
1180
|
+
result
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
alias median percentile
|
1184
|
+
|
1185
|
+
# Use an approximation of the Welch-Satterthwaite equation to compute the
|
1186
|
+
# degrees of freedom for Welch's t-test.
|
1187
|
+
def compute_welch_df(other)
|
1188
|
+
(sample_variance / size + other.sample_variance / other.size) ** 2 / (
|
1189
|
+
(sample_variance ** 2 / (size ** 2 * (size - 1))) +
|
1190
|
+
(other.sample_variance ** 2 / (other.size ** 2 * (other.size - 1))))
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
# Returns the t value of the Welch's t-test between this Analysis
|
1194
|
+
# instance and the +other+.
|
1195
|
+
def t_welch(other)
|
1196
|
+
signal = arithmetic_mean - other.arithmetic_mean
|
1197
|
+
noise = Math.sqrt(sample_variance / size +
|
1198
|
+
other.sample_variance / other.size)
|
1199
|
+
signal / noise
|
1200
|
+
rescue Errno::EDOM
|
1201
|
+
0.0
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
# Returns an estimation of the common standard deviation of the
|
1205
|
+
# measurements of this and +other+.
|
1206
|
+
def common_standard_deviation(other)
|
1207
|
+
Math.sqrt(common_variance(other))
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
# Returns an estimation of the common variance of the measurements of this
|
1211
|
+
# and +other+.
|
1212
|
+
def common_variance(other)
|
1213
|
+
(size - 1) * sample_variance + (other.size - 1) * other.sample_variance /
|
1214
|
+
(size + other.size - 2)
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
# Compute the # degrees of freedom for Student's t-test.
|
1218
|
+
def compute_student_df(other)
|
1219
|
+
size + other.size - 2
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
# Returns the t value of the Student's t-test between this Analysis
|
1223
|
+
# instance and the +other+.
|
1224
|
+
def t_student(other)
|
1225
|
+
signal = arithmetic_mean - other.arithmetic_mean
|
1226
|
+
noise = common_standard_deviation(other) *
|
1227
|
+
Math.sqrt(size ** -1 + size ** -1)
|
1228
|
+
rescue Errno::EDOM
|
1229
|
+
0.0
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
# Compute a sample size, that will more likely yield a mean difference
|
1233
|
+
# between this instance's measurements and those of +other+. Use +alpha+
|
1234
|
+
# and +beta+ as levels for the first- and second-order errors.
|
1235
|
+
def suggested_sample_size(other, alpha = 0.05, beta = 0.05)
|
1236
|
+
alpha, beta = alpha.abs, beta.abs
|
1237
|
+
signal = arithmetic_mean - other.arithmetic_mean
|
1238
|
+
df = size + other.size - 2
|
1239
|
+
pooled_variance_estimate = (sum_of_squares + other.sum_of_squares) / df
|
1240
|
+
td = TDistribution.new df
|
1241
|
+
(((td.inverse_probability(alpha) + td.inverse_probability(beta)) *
|
1242
|
+
Math.sqrt(pooled_variance_estimate)) / signal) ** 2
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
# Return true, if the Analysis instance covers the +other+, that is their
|
1246
|
+
# arithmetic mean value is most likely to be equal for the +alpha+ error
|
1247
|
+
# level.
|
1248
|
+
def cover?(other, alpha = 0.05)
|
1249
|
+
t = t_welch(other)
|
1250
|
+
td = TDistribution.new(compute_welch_df(other))
|
1251
|
+
t.abs < td.inverse_probability(1 - alpha.abs / 2.0)
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
# Return the confidence interval for the arithmetic mean with alpha level +alpha+ of
|
1255
|
+
# the measurements of this Analysis instance as a Range object.
|
1256
|
+
def confidence_interval(alpha = 0.05)
|
1257
|
+
td = TDistribution.new(size - 1)
|
1258
|
+
t = td.inverse_probability(alpha / 2).abs
|
1259
|
+
delta = t * sample_standard_deviation / Math.sqrt(size)
|
1260
|
+
(arithmetic_mean - delta)..(arithmetic_mean + delta)
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
# Returns the array of autovariances (of length size - 1).
|
1264
|
+
def autovariance
|
1265
|
+
Array.new(size - 1) do |k|
|
1266
|
+
s = 0.0
|
1267
|
+
0.upto(size - k - 1) do |i|
|
1268
|
+
s += (@measurements[i] - arithmetic_mean) * (@measurements[i + k] - arithmetic_mean)
|
1269
|
+
end
|
1270
|
+
s / size
|
1271
|
+
end
|
1272
|
+
end
|
1273
|
+
|
1274
|
+
# Returns the array of autocorrelation values c_k / c_0 (of length size -
|
1275
|
+
# 1).
|
1276
|
+
def autocorrelation
|
1277
|
+
c = autovariance
|
1278
|
+
Array.new(c.size) { |k| c[k] / c[0] }
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
# Returns the d-value for the Durbin-Watson statistic. The value is d << 2
|
1282
|
+
# for positive, d >> 2 for negative and d around 2 for no autocorrelation.
|
1283
|
+
def durbin_watson_statistic
|
1284
|
+
e = linear_regression.residues
|
1285
|
+
e.size <= 1 and return 2.0
|
1286
|
+
(1...e.size).inject(0.0) { |s, i| s + (e[i] - e[i - 1]) ** 2 } /
|
1287
|
+
e.inject(0.0) { |s, x| s + x ** 2 }
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
# Returns the q value of the Ljung-Box statistic for the number of lags
|
1291
|
+
# +lags+. A higher value might indicate autocorrelation in the measurements of
|
1292
|
+
# this Analysis instance. This method returns nil if there weren't enough
|
1293
|
+
# (at least lags) lags available.
|
1294
|
+
def ljung_box_statistic(lags = 20)
|
1295
|
+
r = autocorrelation
|
1296
|
+
lags >= r.size and return
|
1297
|
+
n = size
|
1298
|
+
n * (n + 2) * (1..lags).inject(0.0) { |s, i| s + r[i] ** 2 / (n - i) }
|
1299
|
+
end
|
1300
|
+
|
1301
|
+
# This method tries to detect autocorrelation with the Ljung-Box
|
1302
|
+
# statistic. If enough lags can be considered it returns a hash with
|
1303
|
+
# results, otherwise nil is returned. The keys are
|
1304
|
+
# :lags: the number of lags,
|
1305
|
+
# :alpha_level: the alpha level for the test,
|
1306
|
+
# :q: the value of the ljung_box_statistic,
|
1307
|
+
# :p: the p-value computed, if p is higher than alpha no correlation was detected,
|
1308
|
+
# :detected: true if a correlation was found.
|
1309
|
+
def detect_autocorrelation(lags = 20, alpha_level = 0.05)
|
1310
|
+
if q = ljung_box_statistic(lags)
|
1311
|
+
p = ChiSquareDistribution.new(lags).probability(q)
|
1312
|
+
return {
|
1313
|
+
:lags => lags,
|
1314
|
+
:alpha_level => alpha_level,
|
1315
|
+
:q => q,
|
1316
|
+
:p => p,
|
1317
|
+
:detected => p >= 1 - alpha_level,
|
1318
|
+
}
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
# Return a result hash with the number of :very_low, :low, :high, and
|
1323
|
+
# :very_high outliers, determined by the box plotting algorithm run with
|
1324
|
+
# :median and :iqr parameters. If no outliers were found or the iqr is
|
1325
|
+
# less than epsilon, nil is returned.
|
1326
|
+
def detect_outliers(factor = 3.0, epsilon = 1E-5)
|
1327
|
+
half_factor = factor / 2.0
|
1328
|
+
quartile1 = percentile(25)
|
1329
|
+
quartile3 = percentile(75)
|
1330
|
+
iqr = quartile3 - quartile1
|
1331
|
+
iqr < epsilon and return
|
1332
|
+
result = @measurements.inject(Hash.new(0)) do |h, t|
|
1333
|
+
extreme =
|
1334
|
+
case t
|
1335
|
+
when -Infinity..(quartile1 - factor * iqr)
|
1336
|
+
:very_low
|
1337
|
+
when (quartile1 - factor * iqr)..(quartile1 - half_factor * iqr)
|
1338
|
+
:low
|
1339
|
+
when (quartile1 + half_factor * iqr)..(quartile3 + factor * iqr)
|
1340
|
+
:high
|
1341
|
+
when (quartile3 + factor * iqr)..Infinity
|
1342
|
+
:very_high
|
1343
|
+
end and h[extreme] += 1
|
1344
|
+
h
|
1345
|
+
end
|
1346
|
+
unless result.empty?
|
1347
|
+
result[:median] = median
|
1348
|
+
result[:iqr] = iqr
|
1349
|
+
result[:factor] = factor
|
1350
|
+
result
|
1351
|
+
end
|
1352
|
+
end
|
1353
|
+
|
1354
|
+
# Returns the LinearRegression object for the equation a * x + b which
|
1355
|
+
# represents the line computed by the linear regression algorithm.
|
1356
|
+
def linear_regression
|
1357
|
+
@linear_regression ||= LinearRegression.new @measurements
|
1358
|
+
end
|
1359
|
+
|
1360
|
+
# Returns a Histogram instance with +bins+ as the number of bins for this
|
1361
|
+
# analysis' measurements.
|
1362
|
+
def histogram(bins)
|
1363
|
+
Histogram.new(self, bins)
|
1364
|
+
end
|
1365
|
+
end
|
1366
|
+
|
1367
|
+
CaseMethod = Struct.new(:name, :case, :clock)
|
1368
|
+
|
1369
|
+
# This class' instance represents a method to be benchmarked.
|
1370
|
+
class CaseMethod
|
1371
|
+
# Return the short name of this CaseMethod instance, that is without the
|
1372
|
+
# "benchmark_" prefix, e. g. "foo".
|
1373
|
+
def short_name
|
1374
|
+
@short_name ||= name.sub(/\Abenchmark_/, '')
|
1375
|
+
end
|
1376
|
+
|
1377
|
+
# The comment for this method.
|
1378
|
+
attr_accessor :comment
|
1379
|
+
|
1380
|
+
# Returns the long_name of this CaseMethod of the form Foo#bar.
|
1381
|
+
def long_name
|
1382
|
+
result = "#{self.case}##{short_name}"
|
1383
|
+
result = "#{result} (#{comment})" if comment
|
1384
|
+
result
|
1385
|
+
end
|
1386
|
+
|
1387
|
+
# Return the setup_name, e. g. "setup_foo".
|
1388
|
+
def setup_name
|
1389
|
+
'setup_' + short_name
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
# Return the before_name, e. g. "before_foo".
|
1393
|
+
def before_name
|
1394
|
+
'before_' + short_name
|
1395
|
+
end
|
1396
|
+
|
1397
|
+
# Return the after_name, e. g. "after_foo".
|
1398
|
+
def after_name
|
1399
|
+
'after_' + short_name
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
# Return the teardown_name, e. g. "teardown_foo".
|
1403
|
+
def teardown_name
|
1404
|
+
'teardown_' + short_name
|
1405
|
+
end
|
1406
|
+
|
1407
|
+
# Return true if this CaseMethod#clock covers other.clock.
|
1408
|
+
def cover?(other)
|
1409
|
+
clock.cover?(other.clock)
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
# Call before method of this CaseMethod before benchmarking it.
|
1413
|
+
def before_run
|
1414
|
+
if self.case.respond_to? before_name
|
1415
|
+
$DEBUG and warn "Calling #{before_name}."
|
1416
|
+
self.case.__send__(before_name)
|
1417
|
+
end
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
# Call after method of this CaseMethod after benchmarking it.
|
1421
|
+
def after_run
|
1422
|
+
if self.case.respond_to? after_name
|
1423
|
+
$DEBUG and warn "Calling #{after_name}."
|
1424
|
+
self.case.__send__(after_name)
|
1425
|
+
end
|
1426
|
+
end
|
1427
|
+
|
1428
|
+
# Return the file name for +type+ with +suffix+ (if any) for this clock.
|
1429
|
+
def file_path(type = nil, suffix = '.dat')
|
1430
|
+
name = self.case.class.benchmark_name.dup
|
1431
|
+
name << '#' << short_name
|
1432
|
+
type and name << '-' << type
|
1433
|
+
name << suffix
|
1434
|
+
File.expand_path(name, self.case.class.output_dir)
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
# Load the data of file +fp+ into this clock.
|
1438
|
+
def load(fp = file_path)
|
1439
|
+
self.clock = self.case.class.clock.new self
|
1440
|
+
$DEBUG and warn "Loading '#{fp}' into clock."
|
1441
|
+
File.open(fp, 'r') do |f|
|
1442
|
+
f.each do |line|
|
1443
|
+
line.chomp!
|
1444
|
+
line =~ /^\s*#/ and next
|
1445
|
+
clock << line.split(/\t/)
|
1446
|
+
end
|
1447
|
+
end
|
1448
|
+
self
|
1449
|
+
end
|
1450
|
+
end
|
1451
|
+
|
1452
|
+
module CommonConstants
|
1453
|
+
extend DSLKit::Constant
|
1454
|
+
|
1455
|
+
constant :yes, true
|
1456
|
+
|
1457
|
+
constant :no, false
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
# This is the base class of all Benchmarking Cases.
|
1461
|
+
class Case
|
1462
|
+
|
1463
|
+
# All subclasses of Case are extended with this module.
|
1464
|
+
module CaseExtension
|
1465
|
+
def inherited(klass)
|
1466
|
+
Case.cases << klass
|
1467
|
+
end
|
1468
|
+
|
1469
|
+
extend DSLKit::DSLAccessor
|
1470
|
+
extend DSLKit::Constant
|
1471
|
+
|
1472
|
+
include CommonConstants
|
1473
|
+
|
1474
|
+
dsl_accessor :benchmark_name do name end
|
1475
|
+
|
1476
|
+
dsl_accessor :clock, Clock
|
1477
|
+
|
1478
|
+
constant :real
|
1479
|
+
|
1480
|
+
constant :total
|
1481
|
+
|
1482
|
+
constant :user
|
1483
|
+
|
1484
|
+
constant :system
|
1485
|
+
|
1486
|
+
dsl_accessor :compare_time, :real
|
1487
|
+
|
1488
|
+
dsl_accessor :warmup, false
|
1489
|
+
|
1490
|
+
dsl_accessor :batch_size, 1
|
1491
|
+
|
1492
|
+
class TruncateData
|
1493
|
+
extend DSLKit::DSLAccessor
|
1494
|
+
extend DSLKit::Constant
|
1495
|
+
|
1496
|
+
include CommonConstants
|
1497
|
+
include ModuleFunctions
|
1498
|
+
|
1499
|
+
dsl_accessor :alpha_level, 0.05
|
1500
|
+
|
1501
|
+
dsl_accessor :window_size, 10
|
1502
|
+
|
1503
|
+
dsl_accessor :slope_angle, ModuleFunctions.angle(0.1)
|
1504
|
+
|
1505
|
+
dsl_accessor :enabled, false
|
1506
|
+
|
1507
|
+
def initialize(enable, &block)
|
1508
|
+
if block
|
1509
|
+
enable.nil? or raise ArgumentError, "block form doesn't take an argument"
|
1510
|
+
instance_eval(&block)
|
1511
|
+
enabled true
|
1512
|
+
else
|
1513
|
+
enabled enable.nil? ? false : enable
|
1514
|
+
end
|
1515
|
+
end
|
1516
|
+
end
|
1517
|
+
|
1518
|
+
def truncate_data(enable = nil, &block)
|
1519
|
+
@truncate_data ||= TruncateData.new(enable, &block)
|
1520
|
+
end
|
1521
|
+
|
1522
|
+
constant :aggressive, :aggressive
|
1523
|
+
|
1524
|
+
class Covering
|
1525
|
+
extend DSLKit::DSLAccessor
|
1526
|
+
|
1527
|
+
dsl_accessor :alpha_level, 0.05
|
1528
|
+
|
1529
|
+
dsl_accessor :beta_level, 0.05
|
1530
|
+
|
1531
|
+
def initialize(&block)
|
1532
|
+
block and instance_eval(&block)
|
1533
|
+
end
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
def covering(&block)
|
1537
|
+
@covering = Covering.new(&block)
|
1538
|
+
end
|
1539
|
+
|
1540
|
+
dsl_accessor :data_file, false
|
1541
|
+
|
1542
|
+
dsl_accessor :output_dir, Dir.pwd
|
1543
|
+
|
1544
|
+
class Histogram
|
1545
|
+
extend DSLKit::DSLAccessor
|
1546
|
+
extend DSLKit::Constant
|
1547
|
+
|
1548
|
+
include CommonConstants
|
1549
|
+
|
1550
|
+
dsl_accessor :bins, 10
|
1551
|
+
|
1552
|
+
dsl_accessor :enabled, false
|
1553
|
+
|
1554
|
+
dsl_accessor :file, false
|
1555
|
+
|
1556
|
+
def initialize(enable = nil, &block)
|
1557
|
+
if block
|
1558
|
+
enable.nil? or raise ArgumentError, "block form doesn't take an argument"
|
1559
|
+
instance_eval(&block)
|
1560
|
+
enabled true
|
1561
|
+
else
|
1562
|
+
enabled enable.nil? ? true : enable
|
1563
|
+
end
|
1564
|
+
end
|
1565
|
+
end
|
1566
|
+
|
1567
|
+
def histogram(enable = nil, &block)
|
1568
|
+
@histogram ||= Histogram.new(enable, &block)
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
dsl_accessor :detect_outliers, true
|
1572
|
+
|
1573
|
+
dsl_accessor :outliers_factor, 3.0
|
1574
|
+
|
1575
|
+
class Autocorrelation
|
1576
|
+
extend DSLKit::DSLAccessor
|
1577
|
+
extend DSLKit::Constant
|
1578
|
+
|
1579
|
+
include CommonConstants
|
1580
|
+
|
1581
|
+
dsl_accessor :n_limit, 50
|
1582
|
+
|
1583
|
+
dsl_accessor :alpha_level, 0.05
|
1584
|
+
|
1585
|
+
dsl_accessor :max_lags, 20
|
1586
|
+
|
1587
|
+
dsl_accessor :enabled, false
|
1588
|
+
|
1589
|
+
dsl_accessor :file, true
|
1590
|
+
|
1591
|
+
def initialize(enable = nil, &block)
|
1592
|
+
if block
|
1593
|
+
enable.nil? or raise ArgumentError, "block form doesn't take an argument"
|
1594
|
+
instance_eval(&block)
|
1595
|
+
enabled true
|
1596
|
+
else
|
1597
|
+
enabled enable.nil? ? false : enable
|
1598
|
+
end
|
1599
|
+
end
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
def autocorrelation(enable = nil, &block)
|
1603
|
+
@autocorrelation ||= Autocorrelation.new(enable, &block)
|
1604
|
+
end
|
1605
|
+
|
1606
|
+
dsl_accessor :linear_regression, true
|
1607
|
+
|
1608
|
+
include ModuleFunctions
|
1609
|
+
end
|
1610
|
+
|
1611
|
+
class << self
|
1612
|
+
def inherited(klass)
|
1613
|
+
klass.extend CaseExtension
|
1614
|
+
end
|
1615
|
+
|
1616
|
+
extend DSLKit::DSLAccessor
|
1617
|
+
|
1618
|
+
dsl_reader :cases, []
|
1619
|
+
|
1620
|
+
dsl_accessor :output, STDOUT
|
1621
|
+
|
1622
|
+
def output_filename(name)
|
1623
|
+
path = File.expand_path(name, output_dir)
|
1624
|
+
output File.new(path, 'a+')
|
1625
|
+
end
|
1626
|
+
|
1627
|
+
# Returns the total number of run counts +run_count+.
|
1628
|
+
def run_count
|
1629
|
+
cases.inject(0) { |s, c| s + c.run_count }
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
dsl_accessor :autorun, true
|
1633
|
+
|
1634
|
+
# Iterate over all subclasses of class Case.
|
1635
|
+
def each(&block)
|
1636
|
+
cases.each(&block)
|
1637
|
+
end
|
1638
|
+
|
1639
|
+
# Run all subclasses' instances, that is all Bullshit cases, unless they
|
1640
|
+
# already have run.
|
1641
|
+
def run_all
|
1642
|
+
each do |bc_class|
|
1643
|
+
bc_class.run_count == 0 and bc_class.run
|
1644
|
+
end
|
1645
|
+
end
|
1646
|
+
|
1647
|
+
# Autorun all subclasses' instances, that is all Bullshit cases. If its
|
1648
|
+
# autorun dsl_accessor is false or it has already run, don't run the
|
1649
|
+
# case.
|
1650
|
+
def autorun_all
|
1651
|
+
each do |bc_class|
|
1652
|
+
bc_class.autorun and bc_class.run_count == 0 and bc_class.run
|
1653
|
+
end
|
1654
|
+
end
|
1655
|
+
end
|
1656
|
+
|
1657
|
+
# Returns a Case instance, that is used to run benchmark methods and
|
1658
|
+
# measure their running time.
|
1659
|
+
def initialize
|
1660
|
+
@clocks = []
|
1661
|
+
@comparison = Comparison.new
|
1662
|
+
@comparison.output self.class.output
|
1663
|
+
end
|
1664
|
+
|
1665
|
+
# Return the name of the benchmark case as a string.
|
1666
|
+
def to_s
|
1667
|
+
self.class.benchmark_name
|
1668
|
+
end
|
1669
|
+
|
1670
|
+
# Return all benchmark methods of this Case instance lexicographically
|
1671
|
+
# sorted.
|
1672
|
+
def self.sorted_bmethods
|
1673
|
+
instance_methods.map { |x| x.to_s }.grep(/\Abenchmark_/).sort
|
1674
|
+
end
|
1675
|
+
|
1676
|
+
# Return all benchmark methods of this Case instance in a random order.
|
1677
|
+
def bmethods
|
1678
|
+
unless @bmethods
|
1679
|
+
@bmethods = self.class.sorted_bmethods.sort_by do
|
1680
|
+
rand
|
1681
|
+
end
|
1682
|
+
@bmethods.map! { |n| CaseMethod.new(n, self) }
|
1683
|
+
end
|
1684
|
+
@bmethods
|
1685
|
+
end
|
1686
|
+
|
1687
|
+
# Return the CaseMethod instance for +method_name+ or nil, if there isn't
|
1688
|
+
# any method of this name.
|
1689
|
+
def [](method_name)
|
1690
|
+
method_name = "benchmark_#{method_name}"
|
1691
|
+
bmethods.find { |bm| bm.name == method_name }
|
1692
|
+
end
|
1693
|
+
|
1694
|
+
# Return the length of the longest_name of all these methods' names.
|
1695
|
+
def longest_name
|
1696
|
+
bmethods.empty? and return 0
|
1697
|
+
bmethods.map { |x| x.short_name.size }.max
|
1698
|
+
end
|
1699
|
+
|
1700
|
+
# Run benchmark case once and output results.
|
1701
|
+
def run_once
|
1702
|
+
self.class.run_count(self.class.run_count + 1)
|
1703
|
+
self.class.output.puts Time.now.strftime(' %FT%T %Z ').center(COLUMNS, '=')
|
1704
|
+
self.class.output.puts "Benchmarking on #{RUBY_DESCRIPTION}."
|
1705
|
+
self.class.output.puts self.class.message
|
1706
|
+
self.class.output.puts '=' * COLUMNS, ''
|
1707
|
+
@clocks.clear
|
1708
|
+
if self.class.warmup == :aggressive
|
1709
|
+
self.class.output.puts "Aggressively run all benchmarks for warmup first.", ''
|
1710
|
+
bmethods.each do |bc_method|
|
1711
|
+
GC.start
|
1712
|
+
clock = run_method bc_method
|
1713
|
+
self.class.output.puts evaluation(clock)
|
1714
|
+
GC.start
|
1715
|
+
end
|
1716
|
+
self.class.output.puts "Aggressive warmup done.", '', '=' * COLUMNS, ''
|
1717
|
+
end
|
1718
|
+
first = true
|
1719
|
+
bmethods.each do |bc_method|
|
1720
|
+
if first
|
1721
|
+
first = false
|
1722
|
+
else
|
1723
|
+
self.class.output.puts '-' * COLUMNS, ''
|
1724
|
+
end
|
1725
|
+
if self.class.warmup
|
1726
|
+
self.class.output.puts "This first run is only for warmup."
|
1727
|
+
GC.start
|
1728
|
+
clock = run_method bc_method
|
1729
|
+
self.class.output.puts evaluation(clock)
|
1730
|
+
GC.start
|
1731
|
+
end
|
1732
|
+
clock = run_method(bc_method)
|
1733
|
+
if self.class.truncate_data.enabled
|
1734
|
+
message = ''
|
1735
|
+
offset = clock.find_truncation_offset
|
1736
|
+
if clock.case.data_file
|
1737
|
+
slopes_file_path = clock.file_path 'slopes'
|
1738
|
+
message << "Writing slopes data file '#{slopes_file_path}'.\n"
|
1739
|
+
File.open(slopes_file_path, 'w') do |slopes_file|
|
1740
|
+
slopes_file.puts %w[#scatter slope] * "\t"
|
1741
|
+
slopes_file.puts clock.slopes.map { |s| s * "\t" }
|
1742
|
+
end
|
1743
|
+
end
|
1744
|
+
case offset
|
1745
|
+
when 0
|
1746
|
+
message << "No initial data truncated.\n =>"\
|
1747
|
+
" System may have been in a steady state from the beginning."
|
1748
|
+
when clock.repeat
|
1749
|
+
message << "After truncating measurements no data would have"\
|
1750
|
+
" remained.\n => No steady state could be detected."
|
1751
|
+
else
|
1752
|
+
if clock.case.data_file
|
1753
|
+
data_file_path = clock.file_path 'untruncated'
|
1754
|
+
message << "Writing untruncated measurement data file '#{data_file_path}'.\n"
|
1755
|
+
File.open(data_file_path, 'w') do |data_file|
|
1756
|
+
data_file.puts clock.class.to_a * "\t"
|
1757
|
+
data_file.puts clock.to_a.map { |times| times * "\t" }
|
1758
|
+
end
|
1759
|
+
end
|
1760
|
+
remaining = clock.repeat - offset
|
1761
|
+
offset_percentage = 100 * offset.to_f / clock.repeat
|
1762
|
+
message << sprintf("Truncated initial %u measurements: "\
|
1763
|
+
"%u -> %u (-%0.2f%%).\n", offset, clock.repeat, remaining,
|
1764
|
+
offset_percentage)
|
1765
|
+
clock.truncate_data(offset)
|
1766
|
+
end
|
1767
|
+
self.class.output.puts evaluation(clock), message
|
1768
|
+
else
|
1769
|
+
self.class.output.puts evaluation(clock)
|
1770
|
+
end
|
1771
|
+
@clocks << clock
|
1772
|
+
@comparison.benchmark(self, bc_method.short_name, :run => false)
|
1773
|
+
end
|
1774
|
+
@clocks
|
1775
|
+
end
|
1776
|
+
|
1777
|
+
# The clock instances, that were used during a run of this benchmark case.
|
1778
|
+
attr_reader :clocks
|
1779
|
+
|
1780
|
+
# Setup, run all benchmark cases (warmup and the real run) and output
|
1781
|
+
# results, run method speed comparisons, and teardown.
|
1782
|
+
def run(comparison = true)
|
1783
|
+
old_sync, self.class.output.sync = self.class.output.sync, true
|
1784
|
+
$DEBUG and warn "Calling setup."
|
1785
|
+
setup
|
1786
|
+
run_once
|
1787
|
+
comparison and @comparison.display
|
1788
|
+
self
|
1789
|
+
rescue => e
|
1790
|
+
warn "Caught #{e.class}: #{e}\n\n#{e.backtrace.map { |x| "\t#{x}\n" }}"
|
1791
|
+
ensure
|
1792
|
+
$DEBUG and warn "Calling teardown."
|
1793
|
+
teardown
|
1794
|
+
@clocks and write_files
|
1795
|
+
self.class.output.sync = old_sync
|
1796
|
+
end
|
1797
|
+
|
1798
|
+
# Creates an instance of this class and run it.
|
1799
|
+
def self.run
|
1800
|
+
new.run
|
1801
|
+
end
|
1802
|
+
|
1803
|
+
# Write all output files after run.
|
1804
|
+
def write_files
|
1805
|
+
for clock in @clocks
|
1806
|
+
if clock.case.data_file data_file_path = clock.file_path
|
1807
|
+
self.class.output.puts "Writing measurement data file '#{data_file_path}'."
|
1808
|
+
File.open(data_file_path, 'w') do |data_file|
|
1809
|
+
data_file.puts clock.class.to_a * "\t"
|
1810
|
+
data_file.puts clock.to_a.map { |times| times * "\t" }
|
1811
|
+
end
|
1812
|
+
end
|
1813
|
+
if clock.case.histogram.enabled and clock.case.histogram.file
|
1814
|
+
histogram_file_path = clock.file_path 'histogram'
|
1815
|
+
self.class.output.puts "Writing histogram file '#{histogram_file_path}'."
|
1816
|
+
File.open(histogram_file_path, 'w') do |data_file|
|
1817
|
+
data_file.puts %w[#binleft frequency binright] * "\t"
|
1818
|
+
data_file.puts clock.histogram(clock.case.compare_time).to_a.map { |times| times * "\t" }
|
1819
|
+
end
|
1820
|
+
end
|
1821
|
+
if clock.case.autocorrelation.enabled and clock.case.autocorrelation.file
|
1822
|
+
ac_plot_file_path = clock.file_path 'autocorrelation'
|
1823
|
+
self.class.output.puts "Writing autocorrelation plot file '#{ac_plot_file_path}'."
|
1824
|
+
File.open(ac_plot_file_path, 'w') do |data_file|
|
1825
|
+
data_file.puts %w[#lag autocorrelation] * "\t"
|
1826
|
+
data_file.puts clock.autocorrelation_plot(clock.case.compare_time).to_a.map { |ac| ac * "\t" }
|
1827
|
+
end
|
1828
|
+
end
|
1829
|
+
end
|
1830
|
+
end
|
1831
|
+
|
1832
|
+
# Output before +bc_method+ is run.
|
1833
|
+
def pre_run(bc_method)
|
1834
|
+
setup_name = bc_method.setup_name
|
1835
|
+
if respond_to? setup_name
|
1836
|
+
$DEBUG and warn "Calling #{setup_name}."
|
1837
|
+
__send__(setup_name)
|
1838
|
+
end
|
1839
|
+
self.class.output.puts "#{bc_method.long_name}:"
|
1840
|
+
end
|
1841
|
+
|
1842
|
+
# Run only pre_run and post_run methods. Yield to the block, if one was
|
1843
|
+
# given.
|
1844
|
+
def run_method(bc_method)
|
1845
|
+
pre_run bc_method
|
1846
|
+
clock = self.class.clock.__send__(self.class.clock_method, bc_method) do
|
1847
|
+
__send__(bc_method.name)
|
1848
|
+
end
|
1849
|
+
bc_method.clock = clock
|
1850
|
+
post_run bc_method
|
1851
|
+
clock
|
1852
|
+
end
|
1853
|
+
|
1854
|
+
# This method has to be implemented in subclasses, it should return the
|
1855
|
+
# evaluation results of the benchmarks as a string.
|
1856
|
+
def evaluation(clock)
|
1857
|
+
raise NotImplementedError, "has to be implemented in subclasses"
|
1858
|
+
end
|
1859
|
+
|
1860
|
+
# Output after +bc_method+ is run.
|
1861
|
+
def post_run(bc_method)
|
1862
|
+
teardown_name = bc_method.teardown_name
|
1863
|
+
if respond_to? teardown_name
|
1864
|
+
$DEBUG and warn "Calling #{teardown_name}."
|
1865
|
+
__send__(bc_method.teardown_name)
|
1866
|
+
end
|
1867
|
+
end
|
1868
|
+
|
1869
|
+
# General setup for all the benchmark methods.
|
1870
|
+
def setup
|
1871
|
+
end
|
1872
|
+
|
1873
|
+
# General teardown for all the benchmark methods.
|
1874
|
+
def teardown
|
1875
|
+
end
|
1876
|
+
end
|
1877
|
+
|
1878
|
+
# This module contains methods, that can be used in the evaluation method of
|
1879
|
+
# benchmark cases.
|
1880
|
+
module EvaluationModule
|
1881
|
+
def statistics_table(clock)
|
1882
|
+
result = ' ' * NAME_COLUMN_SIZE << ('%17s ' * 4) % times << "\n"
|
1883
|
+
result << evaluation_line('sum', times.map { |t| clock.__send__(t) })
|
1884
|
+
result << evaluation_line('min', times.map { |t| clock.min(t) })
|
1885
|
+
result << evaluation_line('std-', times.map { |t| clock.arithmetic_mean(t) - clock.sample_standard_deviation(t) })
|
1886
|
+
result << evaluation_line('mean', times.map { |t| clock.arithmetic_mean(t) })
|
1887
|
+
result << evaluation_line('std+', times.map { |t| clock.arithmetic_mean(t) + clock.sample_standard_deviation(t) })
|
1888
|
+
result << evaluation_line('max', times.map { |t| clock.max(t) })
|
1889
|
+
result << evaluation_line('std', times.map { |t| clock.sample_standard_deviation(t) })
|
1890
|
+
result << evaluation_line('std%', times.map { |t| clock.sample_standard_deviation_percentage(t) })
|
1891
|
+
result << evaluation_line('harm', times.map { |t| clock.harmonic_mean(t) })
|
1892
|
+
result << evaluation_line('geo', times.map { |t| clock.geometric_mean(t) })
|
1893
|
+
result << evaluation_line('q1', times.map { |t| clock.percentile(t, 25) })
|
1894
|
+
result << evaluation_line('med', times.map { |t| clock.median(t) })
|
1895
|
+
result << evaluation_line('q3', times.map { |t| clock.percentile(t, 75) })
|
1896
|
+
result << ' ' * NAME_COLUMN_SIZE << "%17u %17.5f %17.9f\n" % [ clock.repeat, clock.calls_mean, clock.call_time_mean ]
|
1897
|
+
result << ' ' * NAME_COLUMN_SIZE << "%17s %17s %17s\n" % %w[calls calls/sec secs/call]
|
1898
|
+
end
|
1899
|
+
|
1900
|
+
def histogram(clock)
|
1901
|
+
result = "\n"
|
1902
|
+
if clock.case.histogram.enabled
|
1903
|
+
clock.histogram(clock.case.compare_time).display(result, 50)
|
1904
|
+
end
|
1905
|
+
result
|
1906
|
+
end
|
1907
|
+
|
1908
|
+
def detect_outliers(clock)
|
1909
|
+
result = ''
|
1910
|
+
if clock.case.detect_outliers and
|
1911
|
+
outliers = clock.detect_outliers(clock.case.compare_time)
|
1912
|
+
then
|
1913
|
+
result << "\nOutliers detected with box plot algo "\
|
1914
|
+
"(median=%.5f, iqr=%.5f, factor=%.2f):\n" % outliers.values_at(:median, :iqr, :factor)
|
1915
|
+
result << outliers.select { |n, |
|
1916
|
+
[ :very_low, :low, :high, :very_high ].include?(n)
|
1917
|
+
}.map { |n, v| "#{n}=#{v}" } * ' '
|
1918
|
+
result << "\n"
|
1919
|
+
else
|
1920
|
+
result << "\nNo outliers detected with box plot algo.\n"
|
1921
|
+
end
|
1922
|
+
result
|
1923
|
+
end
|
1924
|
+
|
1925
|
+
def detect_autocorrelation(clock)
|
1926
|
+
result = ''
|
1927
|
+
clock.case.autocorrelation.enabled or return result
|
1928
|
+
if r = clock.detect_autocorrelation(clock.case.compare_time)
|
1929
|
+
result << "\nLjung-Box statistics: q=%.5f (alpha=#{r[:alpha_level]},"\
|
1930
|
+
" df=#{r[:lags]}).\n" % r[:q]
|
1931
|
+
if r[:detected]
|
1932
|
+
result << "%.5f >= %.5f => Autocorrelation was detected.\n" %
|
1933
|
+
[ r[:p], 1 - r[:alpha_level] ]
|
1934
|
+
else
|
1935
|
+
result << "%.5f < %.5f => No autocorrelation was detected.\n" %
|
1936
|
+
[ r[:p], 1 - r[:alpha_level] ]
|
1937
|
+
end
|
1938
|
+
else
|
1939
|
+
result << "\nDidn't have enough lags to compute Ljung-Box statistics.\n"
|
1940
|
+
end
|
1941
|
+
result
|
1942
|
+
end
|
1943
|
+
|
1944
|
+
private
|
1945
|
+
|
1946
|
+
def evaluation_line(name, values)
|
1947
|
+
name.ljust(NAME_COLUMN_SIZE) << ('%17.9f ' * 4) % values << "\n"
|
1948
|
+
end
|
1949
|
+
|
1950
|
+
def times
|
1951
|
+
self.class.clock.times
|
1952
|
+
end
|
1953
|
+
end
|
1954
|
+
|
1955
|
+
# This is a Benchmarking Case that uses a time limit.
|
1956
|
+
class TimeCase < Case
|
1957
|
+
include EvaluationModule
|
1958
|
+
|
1959
|
+
class << self
|
1960
|
+
extend DSLKit::DSLAccessor
|
1961
|
+
extend DSLKit::Constant
|
1962
|
+
|
1963
|
+
constant :clock_method, :time
|
1964
|
+
|
1965
|
+
dsl_accessor :duration
|
1966
|
+
|
1967
|
+
dsl_accessor :run_count, 0
|
1968
|
+
|
1969
|
+
def message
|
1970
|
+
"Running '#{self}' for a duration of #{duration} secs/method"\
|
1971
|
+
" (compare_time=#{compare_time}):"
|
1972
|
+
end
|
1973
|
+
|
1974
|
+
dsl_accessor(:output) { ::Bullshit::Case.output }
|
1975
|
+
end
|
1976
|
+
|
1977
|
+
# Returns the evaluation for +bullshit_case+ with the results of the
|
1978
|
+
# benchmarking as a String.
|
1979
|
+
def evaluation(clock)
|
1980
|
+
clock.repeat == 0 and
|
1981
|
+
raise BullshitException, "no measurements were gauged"
|
1982
|
+
result = ''
|
1983
|
+
result << statistics_table(clock)
|
1984
|
+
result << histogram(clock)
|
1985
|
+
result << detect_outliers(clock)
|
1986
|
+
result << detect_autocorrelation(clock)
|
1987
|
+
result << "\n"
|
1988
|
+
end
|
1989
|
+
|
1990
|
+
# Check if duration has been set. If yes call Case#run, otherwise raise a
|
1991
|
+
# BullshitException exception.
|
1992
|
+
def run(*)
|
1993
|
+
self.class.duration or
|
1994
|
+
raise BullshitException, 'duration has to be set'
|
1995
|
+
super
|
1996
|
+
end
|
1997
|
+
end
|
1998
|
+
|
1999
|
+
# This is a Benchmarking Case that uses a repetition limit.
|
2000
|
+
class RepeatCase < Case
|
2001
|
+
include EvaluationModule
|
2002
|
+
|
2003
|
+
class << self
|
2004
|
+
extend DSLKit::DSLAccessor
|
2005
|
+
extend DSLKit::Constant
|
2006
|
+
|
2007
|
+
constant :clock_method, :repeat
|
2008
|
+
|
2009
|
+
dsl_accessor :iterations
|
2010
|
+
|
2011
|
+
dsl_accessor :run_count, 0
|
2012
|
+
|
2013
|
+
def message
|
2014
|
+
"Running '#{self}' for #{iterations} iterations/method"\
|
2015
|
+
" (compare_time=#{compare_time})"
|
2016
|
+
end
|
2017
|
+
|
2018
|
+
dsl_accessor(:output) { ::Bullshit::Case.output }
|
2019
|
+
end
|
2020
|
+
|
2021
|
+
# Returns the evaluation for +bullshit_case+ with the results of the
|
2022
|
+
# benchmarking as a String.
|
2023
|
+
def evaluation(clock)
|
2024
|
+
clock.repeat == 0 and
|
2025
|
+
raise BullshitException, "no measurements were gauged"
|
2026
|
+
result = ''
|
2027
|
+
result << statistics_table(clock)
|
2028
|
+
result << histogram(clock)
|
2029
|
+
result << detect_outliers(clock)
|
2030
|
+
result << detect_autocorrelation(clock)
|
2031
|
+
result << "\n"
|
2032
|
+
end
|
2033
|
+
|
2034
|
+
# vn heck if iterations has been set. If yes call Case#run, otherwise raise a
|
2035
|
+
# BullshitException exception.
|
2036
|
+
def run(*)
|
2037
|
+
self.class.iterations or
|
2038
|
+
raise BullshitException, 'iterations have to be set'
|
2039
|
+
super
|
2040
|
+
end
|
2041
|
+
end
|
2042
|
+
|
2043
|
+
# A range case is a benchmark case, where each iteration depends on a
|
2044
|
+
# different argument, which might cause a non-constant running time. The
|
2045
|
+
# evaluation of the case doesn't check for constancy and steady-state
|
2046
|
+
# properties.
|
2047
|
+
class RangeCase < Case
|
2048
|
+
include EvaluationModule
|
2049
|
+
|
2050
|
+
class << self
|
2051
|
+
extend DSLKit::DSLAccessor
|
2052
|
+
extend DSLKit::Constant
|
2053
|
+
|
2054
|
+
constant :clock_method, :scale_range
|
2055
|
+
|
2056
|
+
dsl_accessor :range
|
2057
|
+
|
2058
|
+
dsl_accessor :scatter, 1
|
2059
|
+
|
2060
|
+
dsl_accessor :run_count, 0
|
2061
|
+
|
2062
|
+
def message
|
2063
|
+
"Running '#{self}' for range #{range.inspect}"\
|
2064
|
+
" (compare_time=#{compare_time})"
|
2065
|
+
end
|
2066
|
+
|
2067
|
+
dsl_accessor(:output) { ::Bullshit::Case.output }
|
2068
|
+
|
2069
|
+
attr_accessor :args
|
2070
|
+
end
|
2071
|
+
|
2072
|
+
def args
|
2073
|
+
self.class.args
|
2074
|
+
end
|
2075
|
+
private :args
|
2076
|
+
|
2077
|
+
# Returns the evaluation for +bullshit_case+ with the results of the
|
2078
|
+
# benchmarking as a String.
|
2079
|
+
def evaluation(clock)
|
2080
|
+
clock.repeat == 0 and
|
2081
|
+
raise BullshitException, "no measurements were gauged"
|
2082
|
+
result = ''
|
2083
|
+
result << statistics_table(clock)
|
2084
|
+
result << "\n"
|
2085
|
+
end
|
2086
|
+
|
2087
|
+
# Check if iterations has been set. If yes call Case#run, otherwise raise a
|
2088
|
+
# BullshitException exception.
|
2089
|
+
def run(*)
|
2090
|
+
self.class.range or
|
2091
|
+
raise BullshitException,
|
2092
|
+
'range has to be set to an enumerable of arguments'
|
2093
|
+
super
|
2094
|
+
end
|
2095
|
+
end
|
2096
|
+
|
2097
|
+
# A Comparison instance compares the benchmark results of different
|
2098
|
+
# case methods and outputs the results.
|
2099
|
+
class Comparison
|
2100
|
+
extend DSLKit::Constant
|
2101
|
+
extend DSLKit::DSLAccessor
|
2102
|
+
|
2103
|
+
include CommonConstants
|
2104
|
+
|
2105
|
+
dsl_accessor :output, STDOUT
|
2106
|
+
|
2107
|
+
dsl_accessor :output_dir, Dir.pwd
|
2108
|
+
|
2109
|
+
# Output results to the file named +name+.
|
2110
|
+
def output_filename(name)
|
2111
|
+
path = File.expand_path(name, output_dir)
|
2112
|
+
output File.new(path, 'a+')
|
2113
|
+
end
|
2114
|
+
|
2115
|
+
# Return a comparison object configured by +block+.
|
2116
|
+
def initialize(&block)
|
2117
|
+
@cases = {}
|
2118
|
+
@benchmark_methods = []
|
2119
|
+
block and instance_eval(&block)
|
2120
|
+
end
|
2121
|
+
|
2122
|
+
# Benchmark case method +method+ of +bc_class+. Options are:
|
2123
|
+
# * :run to not run this +bc_class+ if set to false (defaults to true),
|
2124
|
+
# * :load to load the data of a previous run for this +method+, if set to
|
2125
|
+
# true. If the true value is a file path string, load the data from the
|
2126
|
+
# given file at the path.
|
2127
|
+
def benchmark(bc_class, method, opts = {})
|
2128
|
+
opts = { :run => true, :combine => true }.merge opts
|
2129
|
+
if Case === bc_class
|
2130
|
+
bullshit_case, bc_class = bc_class, bullshit_case.class
|
2131
|
+
@cases[bc_class] ||= []
|
2132
|
+
if opts[:combine]
|
2133
|
+
if @cases[bc_class].empty?
|
2134
|
+
@cases[bc_class] << bullshit_case
|
2135
|
+
else
|
2136
|
+
bullshit_case = @cases[bc_class].first
|
2137
|
+
end
|
2138
|
+
else
|
2139
|
+
@cases[bc_class] << bullshit_case
|
2140
|
+
end
|
2141
|
+
else
|
2142
|
+
@cases[bc_class] ||= []
|
2143
|
+
if opts[:combine]
|
2144
|
+
unless bullshit_case = @cases[bc_class].first
|
2145
|
+
bullshit_case = bc_class.new
|
2146
|
+
@cases[bc_class] << bullshit_case
|
2147
|
+
end
|
2148
|
+
else
|
2149
|
+
bullshit_case = bc_class.new
|
2150
|
+
@cases[bc_class] << bullshit_case
|
2151
|
+
end
|
2152
|
+
end
|
2153
|
+
bc_method = bullshit_case[method] or raise BullshitException,
|
2154
|
+
"unknown benchmark method #{bc_class}##{method}"
|
2155
|
+
if comment = opts[:comment]
|
2156
|
+
bc_method.comment = comment
|
2157
|
+
end
|
2158
|
+
@benchmark_methods << bc_method
|
2159
|
+
if file_path = opts[:load]
|
2160
|
+
if file_path != true
|
2161
|
+
bc_method.load file_path
|
2162
|
+
else
|
2163
|
+
bc_method.load
|
2164
|
+
end
|
2165
|
+
else
|
2166
|
+
opts[:run] and bullshit_case.run false
|
2167
|
+
end
|
2168
|
+
nil
|
2169
|
+
end
|
2170
|
+
|
2171
|
+
# Return all benchmark methods for all the cached benchmark cases.
|
2172
|
+
attr_reader :benchmark_methods
|
2173
|
+
|
2174
|
+
# Return all benchmark methods ordered by the result of comparator call to
|
2175
|
+
# their clock values.
|
2176
|
+
def compare_methods(comparator)
|
2177
|
+
benchmark_methods.sort_by { |m| m.clock.__send__(comparator) }
|
2178
|
+
end
|
2179
|
+
|
2180
|
+
# Return the length of the longest name of all benchmark methods.
|
2181
|
+
def longest_name_size
|
2182
|
+
benchmark_methods.map { |m| m.long_name.size }.max
|
2183
|
+
end
|
2184
|
+
|
2185
|
+
# Returns the prefix_string for a method speed comparison in the output.
|
2186
|
+
def prefix_string(method)
|
2187
|
+
"% -#{longest_name_size}s %u repeats:" %
|
2188
|
+
[ method.long_name , method.clock.repeat ]
|
2189
|
+
end
|
2190
|
+
|
2191
|
+
# Output all speed comparisons between methods.
|
2192
|
+
def display
|
2193
|
+
output.puts Time.now.strftime(' %FT%T %Z ').center(COLUMNS, '=')
|
2194
|
+
for comparator in [ :call_time_mean, :call_time_median ]
|
2195
|
+
output.puts
|
2196
|
+
methods = compare_methods(comparator)
|
2197
|
+
methods.size < 2 and return
|
2198
|
+
max = methods.last.clock.__send__(comparator)
|
2199
|
+
output.puts "Comparing times (#{comparator}):"
|
2200
|
+
methods.each_with_index do |m, i|
|
2201
|
+
covers = []
|
2202
|
+
for x in methods
|
2203
|
+
if m != x and m.cover?(x)
|
2204
|
+
j = 0
|
2205
|
+
if methods.find { |y| j += 1; x == y }
|
2206
|
+
my_case = m.case.class
|
2207
|
+
iterations = m.clock.analysis[my_case.compare_time].suggested_sample_size(
|
2208
|
+
x.clock.analysis[my_case.compare_time], my_case.covering.alpha_level, my_case.covering.beta_level)
|
2209
|
+
if iterations.nan? or iterations.infinite?
|
2210
|
+
covers << "#{j} (?)"
|
2211
|
+
else
|
2212
|
+
min_iterations = iterations.ceil
|
2213
|
+
min_iterations >= 1E6 and min_iterations = "%f" % (1 / 0.0)
|
2214
|
+
covers << "#{j} (>=#{min_iterations})"
|
2215
|
+
end
|
2216
|
+
end
|
2217
|
+
end
|
2218
|
+
end
|
2219
|
+
covers *= ', '
|
2220
|
+
output.printf\
|
2221
|
+
"% 2u #{prefix_string(m)}\n %17.9f"\
|
2222
|
+
" (%#{::Bullshit::Clock::TIMES_MAX}s) -> %8.3fx %s\n"\
|
2223
|
+
" %17.9f\n",
|
2224
|
+
i + 1, m.clock.calls(comparator), m.case.class.compare_time,
|
2225
|
+
max / m.clock.__send__(comparator), covers, m.clock.__send__(comparator)
|
2226
|
+
end
|
2227
|
+
output.puts " %17s (%#{::Bullshit::Clock::TIMES_MAX}s) -> %8s %s\n"\
|
2228
|
+
" %17s\n"\
|
2229
|
+
% %w[calls/sec time speed covers secs/call]
|
2230
|
+
end
|
2231
|
+
output.puts '=' * COLUMNS
|
2232
|
+
end
|
2233
|
+
end
|
2234
|
+
|
2235
|
+
# Create a Comparison instance configured by +block+ and compute and display
|
2236
|
+
# the results.
|
2237
|
+
def self.compare(&block)
|
2238
|
+
Comparison.new(&block).display
|
2239
|
+
end
|
2240
|
+
|
2241
|
+
at_exit do
|
2242
|
+
Case.autorun and Case.run_all
|
2243
|
+
end
|
2244
|
+
end
|