fruity 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.irbrc +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +26 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +120 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/fruity.rb +3 -0
- data/lib/fruity/base.rb +44 -0
- data/lib/fruity/baseline.rb +79 -0
- data/lib/fruity/comparison_run.rb +105 -0
- data/lib/fruity/group.rb +102 -0
- data/lib/fruity/named_block_collector.rb +12 -0
- data/lib/fruity/runner.rb +70 -0
- data/lib/fruity/util.rb +192 -0
- data/spec/baseline_spec.rb +36 -0
- data/spec/comparison_run_spec.rb +20 -0
- data/spec/find_thresold.rb +19 -0
- data/spec/fruity_spec.rb +18 -0
- data/spec/group_spec.rb +58 -0
- data/spec/manual_test.rb +14 -0
- data/spec/runner_spec.rb +26 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/util_spec.rb +69 -0
- metadata +108 -0
data/.irbrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "./lib/fruity"
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.3.0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.6.4"
|
12
|
+
# gem "rcov", ">= 0"
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.3)
|
5
|
+
git (1.2.5)
|
6
|
+
jeweler (1.6.4)
|
7
|
+
bundler (~> 1.0)
|
8
|
+
git (>= 1.2.5)
|
9
|
+
rake
|
10
|
+
rake (0.9.2.2)
|
11
|
+
rspec (2.3.0)
|
12
|
+
rspec-core (~> 2.3.0)
|
13
|
+
rspec-expectations (~> 2.3.0)
|
14
|
+
rspec-mocks (~> 2.3.0)
|
15
|
+
rspec-core (2.3.1)
|
16
|
+
rspec-expectations (2.3.0)
|
17
|
+
diff-lcs (~> 1.1.2)
|
18
|
+
rspec-mocks (2.3.0)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
bundler (~> 1.0.0)
|
25
|
+
jeweler (~> 1.6.4)
|
26
|
+
rspec (~> 2.3.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Marc-Andre Lafortune
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
= Fruity
|
2
|
+
|
3
|
+
Make sure you're not comparing apples and oranges!
|
4
|
+
|
5
|
+
Fruity makes it easy to *accurately* and *quickly* compare the performance of different Ruby algorithms.
|
6
|
+
|
7
|
+
No need to decide how many times to run your stuff or to try to gauge how significant a speed difference can be, Fruity does it for you.
|
8
|
+
|
9
|
+
= Install
|
10
|
+
|
11
|
+
gem install fruity
|
12
|
+
|
13
|
+
= Usage
|
14
|
+
|
15
|
+
require 'fruity'
|
16
|
+
compare do
|
17
|
+
slow { sleep(0.06) }
|
18
|
+
also_slow { sleep(0.03); sleep(0.03) }
|
19
|
+
quick { sleep(0.03) }
|
20
|
+
quicker { sleep(0.01) }
|
21
|
+
end
|
22
|
+
|
23
|
+
Prints:
|
24
|
+
|
25
|
+
quicker is faster than quick by 3.0x ± 0.01
|
26
|
+
quick is faster than slow by 1.99x ± 0.01
|
27
|
+
slow is similar to also_slow
|
28
|
+
|
29
|
+
= Alternate APIs
|
30
|
+
|
31
|
+
There are many ways to specify what to compare.
|
32
|
+
|
33
|
+
== Methods
|
34
|
+
|
35
|
+
class Foo
|
36
|
+
def slow
|
37
|
+
delay(0.2)
|
38
|
+
end
|
39
|
+
def faster
|
40
|
+
delay(0.1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
compare :slow, :faster, :on => Foo.new
|
45
|
+
|
46
|
+
# Also, no need to specify the :on option for global methods:
|
47
|
+
def foo
|
48
|
+
# ...
|
49
|
+
end
|
50
|
+
def bar
|
51
|
+
# ...
|
52
|
+
end
|
53
|
+
|
54
|
+
compare :foo, :bar
|
55
|
+
|
56
|
+
== Hash
|
57
|
+
|
58
|
+
You can pass a hash with executable values; the keys will be used for to name the results.
|
59
|
+
|
60
|
+
compare(
|
61
|
+
foo: ->{ 2 * 2 },
|
62
|
+
bar: ->{ 2 ** 2 },
|
63
|
+
)
|
64
|
+
|
65
|
+
== List of callables
|
66
|
+
|
67
|
+
You can pass a list of callable objects:
|
68
|
+
|
69
|
+
compare(
|
70
|
+
->{ "foo".upcase },
|
71
|
+
Proc.new{ "foo".upcase },
|
72
|
+
"foo".method(:upcase),
|
73
|
+
)
|
74
|
+
|
75
|
+
These will be named "Code 1", "Code 2" & "Code 3" respectively.
|
76
|
+
|
77
|
+
== Block with parameter
|
78
|
+
|
79
|
+
If you prefer, you can pass a block that accepts a parameter:
|
80
|
+
|
81
|
+
compare do |cp|
|
82
|
+
cp.foo { ... }
|
83
|
+
cp.bar { ... }
|
84
|
+
end
|
85
|
+
|
86
|
+
== Approach
|
87
|
+
|
88
|
+
Benchmarking is not trivial. A well-behaved comparison tool should:
|
89
|
+
1) report there is no significant difference for identical blocks
|
90
|
+
2) but report a very small difference if it is systematic (e.g between ->{ sleep(1.0) } and ->{ sleep(1.001) } )
|
91
|
+
3) report the right performance factor, e.g. a ~2x speed difference for a block that does exactly twice what another is doing, like ->{ 2+2; 2+2 } vs ->{ 2+2 })
|
92
|
+
|
93
|
+
I know of no other benchmarking tool that passes these tests.
|
94
|
+
|
95
|
+
For example, the most scientifically minded tool I found (better-benchmark) fails 1 & 3; if reports a statistically significant difference of 0.5% for two identical blocks, while reporting a 4% difference for a block doing twice what the previous block is doing.
|
96
|
+
|
97
|
+
In addition, all benchmarking tools require the user to either write their own inner loop (e.g. `1000.times{...}` ) or provide a number of inner iterations. Both can be better done automatically by a computer to minimize the time taken to do the test and provide acceptable reliability.
|
98
|
+
|
99
|
+
== Algorithm
|
100
|
+
|
101
|
+
We first determine the number of inner iterations needed to get a meaningful clock measurement (see sufficient_magnitude).
|
102
|
+
When timing an execution, we always compare to a baseline (the time taken by an empty loop of the same magnitude).
|
103
|
+
We call the different executables in succession, to minimize the impact on the order of execution.
|
104
|
+
We calculate the error by taking into account the standard deviation of the time samples.
|
105
|
+
|
106
|
+
== Contributing to fruity
|
107
|
+
|
108
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
109
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
110
|
+
* Fork the project
|
111
|
+
* Start a feature/bugfix branch
|
112
|
+
* Commit and push until you are happy with your contribution
|
113
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
114
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
115
|
+
|
116
|
+
== Copyright
|
117
|
+
|
118
|
+
Copyright (c) 2012 Marc-Andre Lafortune. See LICENSE.txt for
|
119
|
+
further details.
|
120
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "fruity"
|
18
|
+
gem.homepage = "http://github.com/marcandre/fruity"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Cool performance comparison tool}
|
21
|
+
gem.description = %Q{Comparing apples with apples}
|
22
|
+
gem.email = "github@marc-andre.ca"
|
23
|
+
gem.authors = ["Marc-Andre Lafortune"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "fruity #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/fruity.rb
ADDED
data/lib/fruity/base.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative "baseline"
|
2
|
+
require_relative "util"
|
3
|
+
require_relative "runner"
|
4
|
+
require_relative "comparison_run"
|
5
|
+
require_relative "group"
|
6
|
+
require_relative "named_block_collector"
|
7
|
+
|
8
|
+
Fruity::GLOBAL_SCOPE = self
|
9
|
+
|
10
|
+
module Fruity
|
11
|
+
DEFAULT_OPTIONS = {
|
12
|
+
:on => GLOBAL_SCOPE,
|
13
|
+
:samples => 20,
|
14
|
+
:disable_gc => false,
|
15
|
+
:filter => [0, 0.2],
|
16
|
+
:baseline => :split,
|
17
|
+
}
|
18
|
+
|
19
|
+
OTHER_OPTIONS = [
|
20
|
+
:magnitude,
|
21
|
+
:args,
|
22
|
+
:self,
|
23
|
+
:verbose,
|
24
|
+
]
|
25
|
+
|
26
|
+
OPTIONS = DEFAULT_OPTIONS.keys + OTHER_OPTIONS
|
27
|
+
|
28
|
+
def report(*stuff, &block)
|
29
|
+
Fruity::Runner.new(Fruity::Group.new(*stuff, &block)).run(&:feedback)
|
30
|
+
end
|
31
|
+
|
32
|
+
def compare(*stuff, &block)
|
33
|
+
puts report(*stuff, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def study(*stuff, &block)
|
37
|
+
run = Fruity::Runner.new(Fruity::Group.new(*stuff, &block)).run(:baseline => :single, &:feedback)
|
38
|
+
path = run.export
|
39
|
+
`open "#{path}"`
|
40
|
+
run
|
41
|
+
end
|
42
|
+
|
43
|
+
extend self
|
44
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Fruity
|
2
|
+
|
3
|
+
# Utility module for building
|
4
|
+
# baseline equivalents for callables.
|
5
|
+
#
|
6
|
+
module Baseline
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Returns the baseline for the given callable object
|
10
|
+
# The signature (number of arguments) and type (proc, ...)
|
11
|
+
# will be copied as much as possible.
|
12
|
+
#
|
13
|
+
def [](exec)
|
14
|
+
kind = callable_kind(exec)
|
15
|
+
signature = callable_signature(exec)
|
16
|
+
NOOPs[kind][signature] ||= build_baseline(kind, signature)
|
17
|
+
end
|
18
|
+
|
19
|
+
NOOPs = Hash.new{|h, k| h[k] = {}}
|
20
|
+
|
21
|
+
def callable_kind(exec)
|
22
|
+
if exec.is_a?(Method)
|
23
|
+
exec.source_location ? :method : :builtin_method
|
24
|
+
elsif exec.lambda?
|
25
|
+
:lambda
|
26
|
+
else
|
27
|
+
:proc
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def callable_signature(exec)
|
32
|
+
if exec.respond_to?(:parameters)
|
33
|
+
exec.parameters.map(&:first)
|
34
|
+
else
|
35
|
+
# Ruby 1.8 didn't have parameters, so rely on arity
|
36
|
+
opt = exec.arity < 0
|
37
|
+
req = opt ? -1-exec.arity : exec.arity
|
38
|
+
signature = [:req] * req
|
39
|
+
signature << :rest if opt
|
40
|
+
signature
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
PARAM_MAP = {
|
45
|
+
:req => "%{name}",
|
46
|
+
:opt => "%{name} = nil",
|
47
|
+
:rest => "*%{name}",
|
48
|
+
:block => "&%{name}",
|
49
|
+
}
|
50
|
+
|
51
|
+
def arg_list(signature)
|
52
|
+
signature.map.with_index{|kind, i| PARAM_MAP[kind] % {:name => "p#{i}"}}.join(",")
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_baseline(kind, signature)
|
56
|
+
args = "|#{arg_list(signature)}|"
|
57
|
+
case kind
|
58
|
+
when :lambda, :proc
|
59
|
+
eval("#{kind}{#{args}}")
|
60
|
+
when :builtin_method
|
61
|
+
case signature
|
62
|
+
when []
|
63
|
+
nil.method(:nil?)
|
64
|
+
when [:req]
|
65
|
+
nil.method(:==)
|
66
|
+
else
|
67
|
+
Array.method(:[])
|
68
|
+
end
|
69
|
+
when :method
|
70
|
+
@method_counter ||= 0
|
71
|
+
@method_counter += 1
|
72
|
+
name = "baseline_#{@method_counter}"
|
73
|
+
eval("define_method(:#{name}){#{args}}")
|
74
|
+
method(name)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Fruity
|
4
|
+
class ComparisonRun < Struct.new(:group, :timings, :baselines)
|
5
|
+
attr_reader :stats
|
6
|
+
|
7
|
+
# +timings+ must be an array of size `group.size` of arrays of delays
|
8
|
+
# or of arrays of [delay, baseline]
|
9
|
+
#
|
10
|
+
def initialize(group, timings, baselines)
|
11
|
+
raise ArgumentError, "Expected timings to be an array with #{group.size} elements (was #{timings.size})" unless timings.size == group.size
|
12
|
+
super
|
13
|
+
|
14
|
+
filter = group.options.fetch(:filter)
|
15
|
+
|
16
|
+
baseline = Util.filter(baselines, *filter) if baseline_type == :single
|
17
|
+
|
18
|
+
@stats = timings.map.with_index do |series, i|
|
19
|
+
case baseline_type
|
20
|
+
when :split
|
21
|
+
Util.difference(Util.filter(series, *filter), Util.filter(baselines.fetch(i), *filter))
|
22
|
+
when :single
|
23
|
+
Util.difference(Util.filter(series, *filter), baseline)
|
24
|
+
when :none
|
25
|
+
Util.stats(series)
|
26
|
+
end
|
27
|
+
end.freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
order = (0...group.size).sort_by{|i| @stats[i][:mean] }
|
32
|
+
results = group.elements.map{|n, exec| Util.result_of(exec, group.options) }
|
33
|
+
order.each_cons(2).map do |i, j|
|
34
|
+
cmp = comparison(i, j)
|
35
|
+
s = if cmp[:factor] == 1
|
36
|
+
"%{cur} is similar to %{vs}%{different}"
|
37
|
+
else
|
38
|
+
"%{cur} is faster than %{vs} by %{ratio}%{different}"
|
39
|
+
end
|
40
|
+
s % {
|
41
|
+
:cur => group.elements.keys[i],
|
42
|
+
:vs => group.elements.keys[j],
|
43
|
+
:ratio => format_comparison(cmp),
|
44
|
+
:different => results[i] == results[j] ? "" : " (results differ: #{results[i]} vs #{results[j]})"
|
45
|
+
}
|
46
|
+
end.join("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
def export(fn = (require "tmpdir"; "#{Dir.tmpdir}/export.csv"))
|
50
|
+
require "csv"
|
51
|
+
CSV.open(fn, "wb") do |csv|
|
52
|
+
head = group.elements.keys
|
53
|
+
case baseline_type
|
54
|
+
when :split
|
55
|
+
head = head.flat_map{|h| [h, "#{head} bl"]}
|
56
|
+
data = timings.zip(baselines).flatten(1).transpose
|
57
|
+
when :single
|
58
|
+
data = (timings + [baselines]).transpose
|
59
|
+
head << "baseline"
|
60
|
+
else
|
61
|
+
data = timings.transpose
|
62
|
+
end
|
63
|
+
csv << head
|
64
|
+
data.each{|vals| csv << vals}
|
65
|
+
end
|
66
|
+
fn
|
67
|
+
end
|
68
|
+
|
69
|
+
def size
|
70
|
+
timings.first.size
|
71
|
+
end
|
72
|
+
|
73
|
+
def factor(cur = 0, vs = 1)
|
74
|
+
comparison(cur, vs)[:factor]
|
75
|
+
end
|
76
|
+
|
77
|
+
def factor_range(cur = 0, vs =1)
|
78
|
+
comparison(cur, vs)[:min]..comparison(cur, vs)[:max]
|
79
|
+
end
|
80
|
+
|
81
|
+
def comparison(cur = 0, vs = 1)
|
82
|
+
Util.compare_stats(@stats[cur], @stats[vs])
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_comparison(cmp)
|
86
|
+
ratio = cmp[:factor]
|
87
|
+
prec = cmp[:precision]
|
88
|
+
if ratio.abs > 1.8
|
89
|
+
"#{ratio}x ± #{prec}"
|
90
|
+
else
|
91
|
+
"#{(ratio - 1)*100}% ± #{prec*100}%"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def baseline_type
|
96
|
+
if baselines.nil?
|
97
|
+
:none
|
98
|
+
elsif baselines.first.is_a?(Array)
|
99
|
+
:split
|
100
|
+
else
|
101
|
+
:single
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/fruity/group.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Fruity
|
2
|
+
|
3
|
+
# A group of callable objects
|
4
|
+
#
|
5
|
+
class Group
|
6
|
+
attr_reader :elements
|
7
|
+
attr_reader :options
|
8
|
+
|
9
|
+
# Pass either a list of callable objects, a Hash of names and callable objects
|
10
|
+
# or an Array of methods names with the option :on specifying which object to call
|
11
|
+
# them on (or else the methods are assumed to be global, see the README)
|
12
|
+
# Another possibility is to use a block; if it accepts an argument,
|
13
|
+
#
|
14
|
+
def initialize(*args, &block)
|
15
|
+
@options = DEFAULT_OPTIONS.dup
|
16
|
+
@elements = {}
|
17
|
+
@counter = 0
|
18
|
+
compare(*args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds things to compare. See +new+ for details on interface
|
22
|
+
#
|
23
|
+
def compare(*args, &block)
|
24
|
+
if args.last.is_a?(Hash) && (args.last.keys - OPTIONS).empty?
|
25
|
+
@options.merge!(args.pop)
|
26
|
+
end
|
27
|
+
case args.first
|
28
|
+
when Hash
|
29
|
+
raise ArgumentError, "Expected only one hash of {value => executable}, got #{args.size-1} extra arguments" unless args.size == 1
|
30
|
+
raise ArgumentError, "Expected values to be executable" unless args.first.values.all?{|v| v.respond_to?(:call)}
|
31
|
+
compare_hash(args.first)
|
32
|
+
when Symbol, String
|
33
|
+
compare_methods(*args)
|
34
|
+
else
|
35
|
+
compare_lambdas(*args)
|
36
|
+
end
|
37
|
+
compare_block(block) if block
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the maximal sufficient_magnitude for all elements
|
41
|
+
# See Util.sufficient_magnitude
|
42
|
+
#
|
43
|
+
def sufficient_magnitude
|
44
|
+
elements.map{|name, exec| Util.sufficient_magnitude(exec, options) }.max
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the maximal sufficient_magnitude for all elements
|
48
|
+
# and the approximate delay taken for the whole group
|
49
|
+
# See Util.sufficient_magnitude
|
50
|
+
#
|
51
|
+
def sufficient_magnitude_and_delay
|
52
|
+
mags_and_delays = elements.map{|name, exec| Util.sufficient_magnitude_and_delay(exec, options) }
|
53
|
+
mag = mags_and_delays.map(&:first).max
|
54
|
+
delay = mags_and_delays.map{|m, d| d * mag / m}.inject(:+)
|
55
|
+
[mag, delay]
|
56
|
+
end
|
57
|
+
|
58
|
+
def size
|
59
|
+
elements.size
|
60
|
+
end
|
61
|
+
|
62
|
+
def run(options = {})
|
63
|
+
Runner.new(self).run(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def compare_hash(h)
|
68
|
+
elements.merge!(h)
|
69
|
+
end
|
70
|
+
|
71
|
+
def compare_methods(*args)
|
72
|
+
on = @options[:on]
|
73
|
+
args.each do |m|
|
74
|
+
elements[m] = on.method(m)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def compare_lambdas(*lambdas)
|
79
|
+
lambdas.flat_map{|o| Array(o)}
|
80
|
+
lambdas.each do |name, callable|
|
81
|
+
name, callable = generate_name(name), name unless callable
|
82
|
+
raise "Excepted a callable object, got #{callable}" unless callable.respond_to?(:call)
|
83
|
+
elements[name] = callable
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def compare_block(block)
|
88
|
+
collect = NamedBlockCollector.new(@elements)
|
89
|
+
if block.arity == 0
|
90
|
+
@options[:self] = block.binding.eval("self")
|
91
|
+
collect.instance_eval(&block)
|
92
|
+
else
|
93
|
+
block.call(collect)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def generate_name(callable)
|
98
|
+
"Code #{@counter += 1}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Fruity
|
2
|
+
class Runner < Struct.new(:group)
|
3
|
+
attr_reader :options, :delay, :timings, :baselines
|
4
|
+
|
5
|
+
def run(options = {})
|
6
|
+
prepare(options)
|
7
|
+
yield self if block_given?
|
8
|
+
sample
|
9
|
+
end
|
10
|
+
|
11
|
+
def feedback
|
12
|
+
mess = "Running each test " << (options[:magnitude] == 1 ? "once." : "#{options[:magnitude]} times.")
|
13
|
+
if d = delay
|
14
|
+
if d > 60
|
15
|
+
d = (d / 60).round
|
16
|
+
unit = "minute"
|
17
|
+
end
|
18
|
+
mess << " Test will take about #{d.ceil} #{unit || 'second'}#{d > 1 ? 's' : ''}."
|
19
|
+
end
|
20
|
+
puts mess
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def prepare(opt)
|
25
|
+
@options = group.options.merge(opt)
|
26
|
+
unless options[:magnitude]
|
27
|
+
options[:magnitude], @delay = group.sufficient_magnitude_and_delay
|
28
|
+
@delay *= options.fetch(:samples)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def sample
|
33
|
+
send(:"sample_baseline_#{options.fetch(:baseline)}")
|
34
|
+
ComparisonRun.new(group, timings, baselines)
|
35
|
+
end
|
36
|
+
|
37
|
+
def sample_baseline_split
|
38
|
+
baselines = group.elements.map{|name, exec| Baseline[exec]}
|
39
|
+
exec_and_baselines = group.elements.values.zip(baselines)
|
40
|
+
@baselines, @timings = options.fetch(:samples).times.map do
|
41
|
+
exec_and_baselines.flat_map do |exec, baseline|
|
42
|
+
[
|
43
|
+
Util.real_time(baseline, options),
|
44
|
+
Util.real_time(exec, options),
|
45
|
+
]
|
46
|
+
end
|
47
|
+
end.transpose.each_slice(2).to_a.transpose
|
48
|
+
end
|
49
|
+
|
50
|
+
def sample_baseline_single
|
51
|
+
baseline = Baseline[group.elements.first.last]
|
52
|
+
@baselines = []
|
53
|
+
@timings = options.fetch(:samples).times.map do
|
54
|
+
baselines << Util.real_time(baseline, options)
|
55
|
+
group.elements.map do |name, exec|
|
56
|
+
Util.real_time(exec, options)
|
57
|
+
end
|
58
|
+
end.transpose
|
59
|
+
end
|
60
|
+
|
61
|
+
def sample_baseline_none
|
62
|
+
@baselines = nil
|
63
|
+
@timings = options.fetch(:samples).times.map do
|
64
|
+
group.elements.map do |name, exec|
|
65
|
+
Util.real_time(exec, options)
|
66
|
+
end
|
67
|
+
end.transpose
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/fruity/util.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
module Fruity
|
3
|
+
|
4
|
+
# Utility module doing most of the maths
|
5
|
+
#
|
6
|
+
module Util
|
7
|
+
extend self
|
8
|
+
|
9
|
+
PROPER_TIME_RELATIVE_ERROR = 0.001
|
10
|
+
|
11
|
+
MEASUREMENTS_BY_REALTIME = 2
|
12
|
+
MEASUREMENTS_BY_PROPER_TIME = 2 * MEASUREMENTS_BY_REALTIME
|
13
|
+
|
14
|
+
# Measures the smallest obtainable delta of two time measurements
|
15
|
+
#
|
16
|
+
def clock_precision
|
17
|
+
@clock_precision ||= 10.times.map do
|
18
|
+
t = Time.now
|
19
|
+
delta = Time.now - t
|
20
|
+
while delta == 0
|
21
|
+
delta = Time.now - t
|
22
|
+
end
|
23
|
+
delta
|
24
|
+
end.min
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
APPROX_POWER = 5
|
29
|
+
# Calculates the number +n+ that needs to be passed
|
30
|
+
# to +real_time+ to get a result that is precise
|
31
|
+
# to within +PROPER_TIME_RELATIVE_ERROR+ when compared
|
32
|
+
# to the baseline.
|
33
|
+
#
|
34
|
+
# For example, ->{ sleep(1) } needs only to be run once to get
|
35
|
+
# a measurement that will not be affected by the inherent imprecision
|
36
|
+
# of the time measurement (or of the inner loop), but ->{ 2 + 2 } needs to be called
|
37
|
+
# a huge number of times so that the returned time measurement is not
|
38
|
+
# due in big part to the imprecision of the measurement itself
|
39
|
+
# or the inner loop itself.
|
40
|
+
#
|
41
|
+
def sufficient_magnitude(exec, options = {})
|
42
|
+
mag, delay = sufficient_magnitude_and_delay(exec, options)
|
43
|
+
mag
|
44
|
+
end
|
45
|
+
|
46
|
+
BASELINE_THRESHOLD = 1.02 # Ratio between two identical baselines is typically < 1.02, while {2+2} compared to baseline is typically > 1.02
|
47
|
+
|
48
|
+
def sufficient_magnitude_and_delay(exec, options = {})
|
49
|
+
power = 0
|
50
|
+
min_desired_delta = clock_precision * MEASUREMENTS_BY_PROPER_TIME / PROPER_TIME_RELATIVE_ERROR
|
51
|
+
# First, make a gross approximation with a single sample and no baseline
|
52
|
+
min_approx_delay = min_desired_delta / (1 << APPROX_POWER)
|
53
|
+
while (delay = real_time(exec, options.merge(:magnitude => 1 << power))) < min_approx_delay
|
54
|
+
power += [Math.log(min_approx_delay.div(delay + clock_precision), 2), 1].max.floor
|
55
|
+
end
|
56
|
+
|
57
|
+
# Then take a couple of samples, along with a baseline
|
58
|
+
power += 1 unless delay > 2 * min_approx_delay
|
59
|
+
group = Group.new(exec, Baseline[exec], options.merge(:baseline => :none, :samples => 5, :filter => [0, 0.25], :magnitude => 1 << power))
|
60
|
+
stats = group.run.stats
|
61
|
+
if stats[0][:mean] / stats[1][:mean] < 2
|
62
|
+
# Quite close to baseline, which means we need to be more discriminant
|
63
|
+
power += APPROX_POWER
|
64
|
+
stats = group.run(:samples => 40, :magnitude => 1 << power).stats
|
65
|
+
raise "Given callable can not be reasonably distinguished from an empty block" if stats[0][:mean] / stats[1][:mean] < BASELINE_THRESHOLD
|
66
|
+
end
|
67
|
+
delta = stats[0][:mean] - stats[1][:mean]
|
68
|
+
addl_power = [Math.log(min_desired_delta.div(delta), 2), 0].max.floor
|
69
|
+
[
|
70
|
+
1 << (power + addl_power),
|
71
|
+
stats[0][:mean] * (1 << addl_power),
|
72
|
+
]
|
73
|
+
end
|
74
|
+
|
75
|
+
# The proper time is the real time taken by calling +exec+
|
76
|
+
# number of times given by +options[:magnitude]+ minus
|
77
|
+
# the real time for calling an empty executable instead.
|
78
|
+
#
|
79
|
+
# If +options[:magnitude]+ is not given, it will be calculated to be meaningful.
|
80
|
+
#
|
81
|
+
def proper_time(exec, options = {})
|
82
|
+
unless options.has_key?(:magnitude)
|
83
|
+
options = {:magnitude => sufficient_magnitude(exec, options)}.merge(options)
|
84
|
+
end
|
85
|
+
real_time(exec, options) - real_time(Baseline[exec], options)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the real time taken by calling +exec+
|
89
|
+
# number of times given by +options[:magnitude]+
|
90
|
+
#
|
91
|
+
def real_time(exec, options = {})
|
92
|
+
GC.start
|
93
|
+
GC.disable if options[:disable_gc]
|
94
|
+
n = options.fetch(:magnitude)
|
95
|
+
if options.has_key?(:self)
|
96
|
+
new_self = options[:self]
|
97
|
+
if args = options[:args] and args.size > 0
|
98
|
+
Benchmark.realtime{ n.times{ new_self.instance_exec(*args, &exec) } }
|
99
|
+
else
|
100
|
+
Benchmark.realtime{ n.times{ new_self.instance_eval(&exec) } }
|
101
|
+
end
|
102
|
+
else
|
103
|
+
if args = options[:args] and args.size > 0
|
104
|
+
Benchmark.realtime{ n.times{ exec.call(*args) } }
|
105
|
+
else
|
106
|
+
Benchmark.realtime{ n.times{ exec.call } }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
ensure
|
110
|
+
GC.enable
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the result of calling +exec+
|
114
|
+
#
|
115
|
+
def result_of(exec, options = {})
|
116
|
+
args = (options[:args] || [])
|
117
|
+
if options.has_key?(:self)
|
118
|
+
options[:self].instance_exec(*args, &exec)
|
119
|
+
else
|
120
|
+
exec.call(*args)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the inherent precision of +proper_time+
|
125
|
+
#
|
126
|
+
def proper_time_precision
|
127
|
+
MEASUREMENTS_BY_PROPER_TIME * clock_precision
|
128
|
+
end
|
129
|
+
|
130
|
+
# Calculates stats on some values: {:min, :max, :mean, :sample_std_dev }
|
131
|
+
#
|
132
|
+
def stats(values)
|
133
|
+
sum = values.inject(0, :+)
|
134
|
+
# See http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
|
135
|
+
q = mean = 0
|
136
|
+
values.each_with_index do |x, k|
|
137
|
+
prev_mean = mean
|
138
|
+
mean += (x - prev_mean) / (k + 1)
|
139
|
+
q += (x - mean) * (x - prev_mean)
|
140
|
+
end
|
141
|
+
sample_std_dev = Math.sqrt( q / (values.size-1) )
|
142
|
+
min, max = values.minmax
|
143
|
+
{
|
144
|
+
:min => min,
|
145
|
+
:max => max,
|
146
|
+
:mean => mean,
|
147
|
+
:sample_std_dev => sample_std_dev
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
# Calculates the stats of the difference of +values+ and +baseline+
|
152
|
+
# (which can be stats or timings)
|
153
|
+
#
|
154
|
+
def difference(values, baseline)
|
155
|
+
values, baseline = [values, baseline].map{|x| x.is_a?(Hash) ? x : stats(x)}
|
156
|
+
{
|
157
|
+
:min => values[:min] - baseline[:max],
|
158
|
+
:max => values[:max] - baseline[:min],
|
159
|
+
:mean => values[:mean] - baseline[:mean],
|
160
|
+
:sample_std_dev => Math.sqrt(values[:sample_std_dev] ** 2 + values[:sample_std_dev] ** 2),
|
161
|
+
# See http://stats.stackexchange.com/questions/6096/correct-way-to-calibrate-means
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
# Given two stats +cur+ and +vs+, returns a hash with
|
166
|
+
# the ratio between the two, the precision, etc.
|
167
|
+
#
|
168
|
+
def compare_stats(cur, vs)
|
169
|
+
err = (vs[:sample_std_dev] +
|
170
|
+
cur[:sample_std_dev] * vs[:mean] / cur[:mean]
|
171
|
+
) / cur[:mean]
|
172
|
+
|
173
|
+
rounding = err > 0 ? -Math.log(err, 10) : 666
|
174
|
+
mean = vs[:mean] / cur[:mean]
|
175
|
+
{
|
176
|
+
:mean => mean,
|
177
|
+
:factor => (mean).round(rounding),
|
178
|
+
:max => (vs[:mean] + vs[:sample_std_dev]) / (cur[:mean] - cur[:sample_std_dev]),
|
179
|
+
:min => (vs[:mean] - vs[:sample_std_dev]) / (cur[:mean] + cur[:sample_std_dev]),
|
180
|
+
:rounding => rounding,
|
181
|
+
:precision => 10.0 **(-rounding.round),
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def filter(series, remove_min_ratio, remove_max_ratio = remove_min_ratio)
|
186
|
+
series.sort![
|
187
|
+
(remove_min_ratio * series.size).floor ...
|
188
|
+
((1-remove_max_ratio) * series.size).ceil
|
189
|
+
]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module Fruity
|
4
|
+
describe Baseline do
|
5
|
+
describe "#[]" do
|
6
|
+
it "returns an object of the right type" do
|
7
|
+
b = Baseline[Proc.new{ 42 }]
|
8
|
+
b.class.should == Proc
|
9
|
+
b.lambda?.should == false
|
10
|
+
b.call.should == nil
|
11
|
+
|
12
|
+
b = Baseline[lambda{ 42 }]
|
13
|
+
b.class.should == Proc
|
14
|
+
b.lambda?.should == true
|
15
|
+
b.call.should == nil
|
16
|
+
|
17
|
+
b = Baseline[Fruity.method(:compare)]
|
18
|
+
b.class.should == Method
|
19
|
+
b.call.should == nil
|
20
|
+
end
|
21
|
+
|
22
|
+
it "copies the arity" do
|
23
|
+
b = Baseline[lambda{|a, b| 42}]
|
24
|
+
b.call(1, 2).should == nil
|
25
|
+
lambda{
|
26
|
+
b.call(1)
|
27
|
+
}.should raise_error
|
28
|
+
lambda{
|
29
|
+
b.call(1, 2, 3)
|
30
|
+
}.should raise_error
|
31
|
+
|
32
|
+
Baseline[lambda{|a, *b| 42}].call(1, 2, 3, 4, 5, 6).should == nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module Fruity
|
4
|
+
describe ComparisonRun do
|
5
|
+
let(:group) { Group.new ->{1}, ->{2} }
|
6
|
+
let(:timings){ ([[1.0, 2.0]] * 10).transpose }
|
7
|
+
subject { @run = ComparisonRun.new(group, timings, nil) }
|
8
|
+
|
9
|
+
its(:comparison) { should == {
|
10
|
+
:mean=>2.0,
|
11
|
+
:factor=>2.0,
|
12
|
+
:max=>2.0,
|
13
|
+
:min=>2.0,
|
14
|
+
:rounding=>666,
|
15
|
+
:precision=>0.0,
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "../lib/fruity"
|
2
|
+
|
3
|
+
power = 15
|
4
|
+
x = 2
|
5
|
+
n = 30
|
6
|
+
min = 1
|
7
|
+
max = 2
|
8
|
+
s = 10
|
9
|
+
n.times.map do
|
10
|
+
group = Fruity::Group.new(*[->{}] * s, *[->{ 2 + 2 }] * s, :baseline => :none, :samples => 20, :magnitude => 1 << power)
|
11
|
+
means = group.run.stats.map{|s| s[:mean]}
|
12
|
+
noops = means.first(s).sort
|
13
|
+
plus = means.last(s).sort
|
14
|
+
min = [min, noops.last / noops.first].max
|
15
|
+
max = [max, plus.first / noops.last].min
|
16
|
+
p min, max
|
17
|
+
raise "Damn, #{min} >= #{max}" if min >= max
|
18
|
+
end.sort
|
19
|
+
puts "Threshold between #{min} and #{max} are ok, suggesting #{Math.sqrt(min * max)}"
|
data/spec/fruity_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Fruity do
|
4
|
+
it "is able to report that two identical blocks are identical" do
|
5
|
+
report = Fruity.report(->{ 2 ** 2 }, ->{ 2 ** 2 })
|
6
|
+
report.factor.should == 1.0
|
7
|
+
end
|
8
|
+
|
9
|
+
it "is able to report very small performance differences" do
|
10
|
+
report = Fruity.report(->{ sleep(1.0) }, ->{ sleep(1.001) })
|
11
|
+
report.factor.should == 1.001
|
12
|
+
end
|
13
|
+
|
14
|
+
it "is able to report a 2x difference even for small blocks" do
|
15
|
+
report = Fruity.report(->{ 2 ** 2 }, ->{ 2 ** 2 ; 2 ** 2 })
|
16
|
+
report.factor.should == 2.0
|
17
|
+
end
|
18
|
+
end
|
data/spec/group_spec.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
TOP_LEVEL = self
|
4
|
+
def foo; end
|
5
|
+
def bar; end
|
6
|
+
|
7
|
+
module Fruity
|
8
|
+
describe Group do
|
9
|
+
lambdas = [->{1},->{2}]
|
10
|
+
|
11
|
+
it "can be built from lambdas" do
|
12
|
+
group = Group.new(*lambdas)
|
13
|
+
group.elements.values.should == lambdas
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can be built from a block with no parameter" do
|
17
|
+
group = Group.new do
|
18
|
+
first(&lambdas.first)
|
19
|
+
last(&lambdas.last)
|
20
|
+
end
|
21
|
+
group.elements.should == {
|
22
|
+
:first => lambdas.first,
|
23
|
+
:last => lambdas.last,
|
24
|
+
}
|
25
|
+
group.options[:self].object_id.should equal(self.object_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can be built from a block taking a parameter" do
|
29
|
+
group = Group.new do |cmp|
|
30
|
+
cmp.first(&lambdas.first)
|
31
|
+
cmp.last(&lambdas.last)
|
32
|
+
->{ second{} }.should raise_error(NameError)
|
33
|
+
end
|
34
|
+
group.elements.should == {
|
35
|
+
:first => lambdas.first,
|
36
|
+
:last => lambdas.last,
|
37
|
+
}
|
38
|
+
group.options[:self].should == nil
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can be built from list of method names and an object" do
|
42
|
+
str = "Hello"
|
43
|
+
group = Group.new(:upcase, :downcase, :on => str)
|
44
|
+
group.elements.should == {
|
45
|
+
:upcase => str.method(:upcase),
|
46
|
+
:downcase => str.method(:downcase),
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can be built from list of method names and an object" do
|
51
|
+
group = Group.new(:foo, :bar)
|
52
|
+
group.elements.should == {
|
53
|
+
:foo => TOP_LEVEL.method(:foo),
|
54
|
+
:bar => TOP_LEVEL.method(:bar),
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/manual_test.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative "../lib/fruity"
|
2
|
+
|
3
|
+
compare do
|
4
|
+
one { 42.nil? }
|
5
|
+
two { 42.nil?; 42.nil? }
|
6
|
+
end
|
7
|
+
|
8
|
+
compare do
|
9
|
+
two { 42.nil?; 42.nil? }
|
10
|
+
two_again { 42.nil?; 42.nil? }
|
11
|
+
three { 42.nil?; 42.nil?; 42.nil? }
|
12
|
+
four { 42.nil?; 42.nil?; 42.nil?; 42.nil? }
|
13
|
+
five { 42.nil?; 42.nil?; 42.nil?; 42.nil?; 42.nil? }
|
14
|
+
end
|
data/spec/runner_spec.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module Fruity
|
4
|
+
describe Runner do
|
5
|
+
let(:group) { Group.new(:upcase, :downcase, :on => "Hello") }
|
6
|
+
let(:runner){ Runner.new(group) }
|
7
|
+
|
8
|
+
it "runs from a Group" do
|
9
|
+
run = runner.run(:samples => 42, :magnitude => 100)
|
10
|
+
run.timings.should be_array_of_size(2, 42)
|
11
|
+
run.baselines.should be_array_of_size(2, 42)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "can use a single baseline" do
|
15
|
+
run = runner.run(:samples => 42, :magnitude => 100, :baseline => :single)
|
16
|
+
run.timings.should be_array_of_size(2, 42)
|
17
|
+
run.baselines.should be_array_of_size(42)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can use no baseline" do
|
21
|
+
run = runner.run(:samples => 42, :magnitude => 100, :baseline => :none)
|
22
|
+
run.timings.should be_array_of_size(2, 42)
|
23
|
+
run.baselines.should == nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'fruity'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec::Matchers.define :be_between do |low,high|
|
15
|
+
match do |actual|
|
16
|
+
@low, @high = low, high
|
17
|
+
actual.between? low, high
|
18
|
+
end
|
19
|
+
|
20
|
+
failure_message_for_should do |actual|
|
21
|
+
"expected to be between #{@low} and #{@high}, but was #{actual}"
|
22
|
+
end
|
23
|
+
|
24
|
+
failure_message_for_should_not do |actual|
|
25
|
+
"expected not to be between #{@low} and #{@high}, but was #{actual}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec::Matchers.define :be_array_of_size do |*dimensions|
|
30
|
+
match do |actual|
|
31
|
+
@dimensions = dimensions
|
32
|
+
@actual = []
|
33
|
+
while actual.is_a?(Array)
|
34
|
+
@actual << actual.size
|
35
|
+
actual = actual.first
|
36
|
+
end
|
37
|
+
@dimensions == @actual
|
38
|
+
end
|
39
|
+
|
40
|
+
failure_message_for_should do |actual|
|
41
|
+
"expected to be an array of dimension #{@dimensions.join('x')} but was #{@actual.join('x')}"
|
42
|
+
end
|
43
|
+
|
44
|
+
failure_message_for_should_not do |actual|
|
45
|
+
"expected not to be an array of dimension #{@dimensions.join('x')}"
|
46
|
+
end
|
47
|
+
end
|
data/spec/util_spec.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module Fruity
|
4
|
+
describe Util do
|
5
|
+
its(:clock_precision) { should == 1.0e-6 }
|
6
|
+
|
7
|
+
its(:proper_time_precision) { should == 4.0e-06 }
|
8
|
+
|
9
|
+
describe :sufficient_magnitude do
|
10
|
+
it "returns a big value for a quick (but not trivial)" do
|
11
|
+
Util.sufficient_magnitude(->{ 2.nil? }).should > 10000
|
12
|
+
end
|
13
|
+
|
14
|
+
it "return 1 for a sufficiently slow block" do
|
15
|
+
Util.sufficient_magnitude(->{sleep(0.01)}).should == 1
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should raise an error for a trivial block" do
|
19
|
+
->{
|
20
|
+
Util.sufficient_magnitude(Proc.new{})
|
21
|
+
}.should raise_error
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe :stats do
|
26
|
+
it "returns cools stats on the given values" do
|
27
|
+
Util.stats([0, 4]).should == {
|
28
|
+
:min => 0,
|
29
|
+
:max => 4,
|
30
|
+
:mean => 2,
|
31
|
+
:sample_std_dev => 2.8284271247461903,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe :difference do
|
37
|
+
it "returns stats for the difference of two series" do
|
38
|
+
s = Util.stats([0, 4])
|
39
|
+
Util.difference(s, s).should == {
|
40
|
+
:min => -4,
|
41
|
+
:max => 4,
|
42
|
+
:mean => 0,
|
43
|
+
:sample_std_dev => 4,
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
it "gives similar results when comparing an exec and its baseline from stats on proper_time" do
|
48
|
+
exec = ->{ 2 ** 3 ** 4 }
|
49
|
+
options = {:magnitude => Util.sufficient_magnitude(exec) }
|
50
|
+
n = 100
|
51
|
+
timings = [exec, ->{}].map do |e|
|
52
|
+
n.times.map { Util.real_time(e, options) }
|
53
|
+
end
|
54
|
+
proper = Util.stats(timings.transpose.map{|e, b| e - b})
|
55
|
+
diff = Util.difference(*timings)
|
56
|
+
diff[:mean].should be_within(Float::EPSILON).of(proper[:mean])
|
57
|
+
diff[:max].should be_between(proper[:max], proper[:max]*2)
|
58
|
+
diff[:min].should <= proper[:min]
|
59
|
+
diff[:sample_std_dev].should be_between(proper[:sample_std_dev], 2 * proper[:sample_std_dev])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe :filter do
|
64
|
+
it "returns the filtered series" do
|
65
|
+
Util.filter([4, 5, 2, 3, 1], 0.21, 0.39).should == [2, 3, 4]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fruity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Marc-Andre Lafortune
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &2170028460 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.3.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2170028460
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bundler
|
27
|
+
requirement: &2170027780 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2170027780
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: jeweler
|
38
|
+
requirement: &2170047220 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.6.4
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2170047220
|
47
|
+
description: Comparing apples with apples
|
48
|
+
email: github@marc-andre.ca
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE.txt
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- .irbrc
|
56
|
+
- Gemfile
|
57
|
+
- Gemfile.lock
|
58
|
+
- LICENSE.txt
|
59
|
+
- README.rdoc
|
60
|
+
- Rakefile
|
61
|
+
- VERSION
|
62
|
+
- lib/fruity.rb
|
63
|
+
- lib/fruity/base.rb
|
64
|
+
- lib/fruity/baseline.rb
|
65
|
+
- lib/fruity/comparison_run.rb
|
66
|
+
- lib/fruity/group.rb
|
67
|
+
- lib/fruity/named_block_collector.rb
|
68
|
+
- lib/fruity/runner.rb
|
69
|
+
- lib/fruity/util.rb
|
70
|
+
- spec/baseline_spec.rb
|
71
|
+
- spec/comparison_run_spec.rb
|
72
|
+
- spec/find_thresold.rb
|
73
|
+
- spec/fruity_spec.rb
|
74
|
+
- spec/group_spec.rb
|
75
|
+
- spec/manual_test.rb
|
76
|
+
- spec/runner_spec.rb
|
77
|
+
- spec/spec.opts
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/util_spec.rb
|
80
|
+
homepage: http://github.com/marcandre/fruity
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
hash: -3031735817276906554
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 1.8.10
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: Cool performance comparison tool
|
108
|
+
test_files: []
|