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