perfer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.md +33 -0
- data/lib/perfer.rb +54 -0
- data/lib/perfer/cli.rb +139 -0
- data/lib/perfer/configuration.rb +42 -0
- data/lib/perfer/errors.rb +11 -0
- data/lib/perfer/formatter.rb +73 -0
- data/lib/perfer/formatter/measurements_formatter.rb +22 -0
- data/lib/perfer/formatter/results_formatter.rb +43 -0
- data/lib/perfer/formatter/session_formatter.rb +22 -0
- data/lib/perfer/git.rb +36 -0
- data/lib/perfer/job.rb +32 -0
- data/lib/perfer/job/input_size_job.rb +44 -0
- data/lib/perfer/job/iteration_job.rb +156 -0
- data/lib/perfer/platform.rb +13 -0
- data/lib/perfer/platform/posix.rb +81 -0
- data/lib/perfer/platform/windows.rb +83 -0
- data/lib/perfer/result.rb +38 -0
- data/lib/perfer/session.rb +114 -0
- data/lib/perfer/statistics.rb +94 -0
- data/lib/perfer/store.rb +53 -0
- data/perfer.gemspec +9 -0
- metadata +67 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2012 Benoit Daloze
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Perfer - A benchmark tool for all rubies!
|
2
|
+
|
3
|
+
<!-- usage -->
|
4
|
+
```text
|
5
|
+
Usage:
|
6
|
+
perfer <command> [options] arguments
|
7
|
+
|
8
|
+
Commands:
|
9
|
+
run files+ - run with current ruby
|
10
|
+
report files+ - show the results
|
11
|
+
config reset - reset the configuration file to the defaults (or create it)
|
12
|
+
help - show this help
|
13
|
+
results
|
14
|
+
path files+ - show the paths to the result files
|
15
|
+
rm,delete files+ - remove the result files
|
16
|
+
|
17
|
+
<files+> are a set of benchmark files
|
18
|
+
|
19
|
+
Common options:
|
20
|
+
-t TIME Minimal time to run (greater usually improve accuracy)
|
21
|
+
-m N Numbers of measurements per job
|
22
|
+
-v Verbose
|
23
|
+
-h, --help Show this help
|
24
|
+
```
|
25
|
+
<!-- usage -->
|
26
|
+
|
27
|
+
See [The GSoC proposal](http://www.google-melange.com/gsoc/proposal/review/google/gsoc2012/eregon/1) for more details.
|
28
|
+
|
29
|
+
And the [Wiki](https://github.com/jruby/perfer/wiki).
|
30
|
+
|
31
|
+
## Current status
|
32
|
+
|
33
|
+
This is a work in progress, 0.1.0 is a preview release.
|
data/lib/perfer.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'path'
|
3
|
+
require 'optparse'
|
4
|
+
require 'hitimes'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'ffi'
|
7
|
+
require 'digest/sha1'
|
8
|
+
require 'backports/1.9'
|
9
|
+
|
10
|
+
Path.require_tree('perfer', :except => %w[platform/])
|
11
|
+
|
12
|
+
module Perfer
|
13
|
+
DIR = Path('~/.perfer')
|
14
|
+
TIMES_FIELDS = [:real, :utime, :stime, :cutime, :cstime].freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_reader :sessions, :configuration
|
18
|
+
|
19
|
+
def reset
|
20
|
+
@configuration = Configuration.new
|
21
|
+
@sessions = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def session(name = nil, &block)
|
25
|
+
Session.new(Path.file(caller), name, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Shortcut for Perfer.session { |s| s.iterate ... }
|
29
|
+
def iterate(title = nil, *args, &block)
|
30
|
+
Session.new(Path.file(caller)) { |session|
|
31
|
+
title ||= session.name
|
32
|
+
session.iterate(title, *args, &block)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def measure(&block)
|
37
|
+
times_before = Process.times
|
38
|
+
real = Hitimes::Interval.measure(&block)
|
39
|
+
times = Process.times
|
40
|
+
|
41
|
+
data = { :real => real }
|
42
|
+
times.members.each { |field|
|
43
|
+
# precision of times(3) or getrusage(2) is no more than 1e-6
|
44
|
+
value = (times[field] - times_before[field]).round(6)
|
45
|
+
if value != 0.0 # do not keep these if they measured nothing
|
46
|
+
data[field.to_sym] = value
|
47
|
+
end
|
48
|
+
}
|
49
|
+
data
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Perfer.reset
|
54
|
+
end
|
data/lib/perfer/cli.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
module Perfer
|
2
|
+
class CLI
|
3
|
+
COMMANDS = %w[
|
4
|
+
config
|
5
|
+
help
|
6
|
+
report
|
7
|
+
results
|
8
|
+
run
|
9
|
+
]
|
10
|
+
|
11
|
+
HELP = <<-EOS
|
12
|
+
Usage:
|
13
|
+
perfer <command> [options] arguments
|
14
|
+
|
15
|
+
Commands:
|
16
|
+
run files+ - run with current ruby
|
17
|
+
report files+ - show the results
|
18
|
+
config reset - reset the configuration file to the defaults (or create it)
|
19
|
+
help - show this help
|
20
|
+
results
|
21
|
+
path files+ - show the paths to the result files
|
22
|
+
rm,delete files+ - remove the result files
|
23
|
+
|
24
|
+
<files+> are a set of benchmark files
|
25
|
+
|
26
|
+
Common options:
|
27
|
+
EOS
|
28
|
+
|
29
|
+
def initialize(argv)
|
30
|
+
@argv = argv
|
31
|
+
|
32
|
+
@opts = OptionParser.new do |options|
|
33
|
+
options.banner = HELP
|
34
|
+
common_options(options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute
|
39
|
+
begin
|
40
|
+
@opts.order!(@argv)
|
41
|
+
rescue OptionParser::ParseError => e
|
42
|
+
error e.message
|
43
|
+
end
|
44
|
+
|
45
|
+
@command = @argv.shift
|
46
|
+
error "A command must be given, one of: #{COMMANDS*', '}" unless @command
|
47
|
+
error "Unknown command: #{@command.inspect}" unless COMMANDS.include? @command
|
48
|
+
|
49
|
+
send(@command)
|
50
|
+
end
|
51
|
+
|
52
|
+
def unknown_subcommand(subcommand)
|
53
|
+
if subcommand
|
54
|
+
error "Unknown subcommand for #{@command}: #{subcommand.inspect}"
|
55
|
+
else
|
56
|
+
error "`perfer #{@command}` needs a subcommand"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def error message
|
61
|
+
$stderr.puts message
|
62
|
+
$stderr.puts
|
63
|
+
abort @opts.help
|
64
|
+
end
|
65
|
+
|
66
|
+
def help
|
67
|
+
puts @opts.help
|
68
|
+
end
|
69
|
+
|
70
|
+
def report
|
71
|
+
measurements = (@argv.shift if @argv.first == '--measurements')
|
72
|
+
each_session { |session| session.report_results(:measurements => measurements) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def run
|
76
|
+
# load files
|
77
|
+
files.each do |file|
|
78
|
+
require file.path
|
79
|
+
end
|
80
|
+
Perfer.sessions.each(&:run)
|
81
|
+
end
|
82
|
+
|
83
|
+
def results
|
84
|
+
case subcommand = @argv.shift
|
85
|
+
when "path"
|
86
|
+
each_session { |session| puts session.store.file }
|
87
|
+
when "delete", "rm"
|
88
|
+
each_session { |session| session.store.delete }
|
89
|
+
when "rewrite"
|
90
|
+
each_session { |session| session.store.rewrite }
|
91
|
+
else
|
92
|
+
unknown_subcommand subcommand
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def config
|
97
|
+
case subcommand = @argv.shift
|
98
|
+
when "reset"
|
99
|
+
Perfer.configuration.write_defaults
|
100
|
+
else
|
101
|
+
unknown_subcommand subcommand
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def common_options(options)
|
106
|
+
options.on('-t TIME', Float, "Minimal time to run (greater usually improve accuracy)") do |t|
|
107
|
+
error "Minimal time must be > 0" if t <= 0
|
108
|
+
Perfer.configuration.minimal_time = t
|
109
|
+
end
|
110
|
+
options.on('-m N', Integer, "Numbers of measurements per job") do |n|
|
111
|
+
error "There must be at least 2 measurements" if n < 2
|
112
|
+
Perfer.configuration.measurements = n
|
113
|
+
end
|
114
|
+
options.on('-v', "Verbose") do
|
115
|
+
Perfer.configuration.verbose = true
|
116
|
+
end
|
117
|
+
options.on('-h', '--help', "Show this help") do
|
118
|
+
puts options.help
|
119
|
+
exit
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
def files
|
125
|
+
@argv.map { |file| Path(file).expand }
|
126
|
+
end
|
127
|
+
|
128
|
+
def load_from_files
|
129
|
+
files.each do |file|
|
130
|
+
Session.new(file)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def each_session(&block)
|
135
|
+
load_from_files
|
136
|
+
Perfer.sessions.each(&block)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Perfer
|
2
|
+
class Configuration
|
3
|
+
DEFAULTS = {
|
4
|
+
:minimal_time => 1.0,
|
5
|
+
:measurements => 10,
|
6
|
+
:verbose => false,
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
PROPERTIES = DEFAULTS.keys
|
10
|
+
|
11
|
+
PROPERTIES.each { |property| attr_accessor property }
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@config_file = DIR/'config.yml'
|
15
|
+
|
16
|
+
DEFAULTS.each_pair { |property, value|
|
17
|
+
instance_variable_set(:"@#{property}", value)
|
18
|
+
}
|
19
|
+
|
20
|
+
if @config_file.exist? and !@config_file.empty?
|
21
|
+
YAML.load_file(@config_file).each_pair { |property, value|
|
22
|
+
property = property.to_sym
|
23
|
+
if PROPERTIES.include? property
|
24
|
+
instance_variable_set(:"@#{property}", value)
|
25
|
+
else
|
26
|
+
warn "Unknown property in configuration file: #{property}"
|
27
|
+
end
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def write_defaults
|
33
|
+
@config_file.write YAML.dump DEFAULTS
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_hash
|
37
|
+
PROPERTIES.each_with_object({}) { |property, h|
|
38
|
+
h[property] = instance_variable_get(:"@#{property}")
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Perfer
|
2
|
+
class Error < RuntimeError
|
3
|
+
end
|
4
|
+
|
5
|
+
module Errors
|
6
|
+
MIX_BENCH_TYPES = "Cannot mix iterations and input size benchmarks in the same session"
|
7
|
+
SAME_JOB_TITLES = "Multiple jobs with the same title are not allowed"
|
8
|
+
WRONG_MEASURE_USE = "Cannot use Session#measure with an iteration job"
|
9
|
+
WORKING_DIR_DIRTY = "The working directory is dirty, commit (or stash) your changes first"
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Perfer
|
4
|
+
module Formatter
|
5
|
+
extend self
|
6
|
+
|
7
|
+
TIME_UNITS = {
|
8
|
+
0 => "s ",
|
9
|
+
-3 => "ms",
|
10
|
+
-6 => "µs",
|
11
|
+
-9 => "ns"
|
12
|
+
}
|
13
|
+
|
14
|
+
def max_length_of(enum)
|
15
|
+
max = 0
|
16
|
+
enum.each { |e|
|
17
|
+
alt = yield(e).to_s.length
|
18
|
+
max = alt if alt > max
|
19
|
+
}
|
20
|
+
max
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_ips(ips)
|
24
|
+
if ips > 100
|
25
|
+
ips.round
|
26
|
+
else
|
27
|
+
"%.3g" % ips
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_n(n, maxlen)
|
32
|
+
n.to_s.rjust(maxlen)
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_float(f)
|
36
|
+
('%5.3f' % f)[0...5]
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_error(error, base, scale)
|
40
|
+
"#{format_float(error*10**-scale)} (#{'%4.1f' % (error / base * 100.0)}%)"
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_time(time)
|
44
|
+
time.strftime("%F %T")
|
45
|
+
end
|
46
|
+
|
47
|
+
def float_scale(time)
|
48
|
+
if time == 0 or time > 1.0
|
49
|
+
0
|
50
|
+
elsif time > 0.001
|
51
|
+
-3
|
52
|
+
elsif time > 0.000001
|
53
|
+
-6
|
54
|
+
else
|
55
|
+
-9
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# formats a duration with an 8-chars width
|
60
|
+
def format_duration(time, scale = float_scale(time))
|
61
|
+
if time == 0
|
62
|
+
" 0 "
|
63
|
+
else
|
64
|
+
"#{format_float(time*10**-scale)} #{TIME_UNITS[scale]}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def format_duration_and_error(time, error, after_unit = "")
|
69
|
+
scale = float_scale(time)
|
70
|
+
"#{format_duration(time, scale)}#{after_unit} ± #{format_error(error, time, scale)}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Perfer
|
2
|
+
class MeasurementsFormatter
|
3
|
+
include Formatter
|
4
|
+
|
5
|
+
SEPARATOR = ' '
|
6
|
+
|
7
|
+
def initialize(measurements)
|
8
|
+
@measurements = measurements
|
9
|
+
@fields = Perfer::TIMES_FIELDS.dup
|
10
|
+
@fields.reject! { |field|
|
11
|
+
measurements.none? { |m| m.key? field }
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def report
|
16
|
+
puts @fields.map { |field| field.to_s.center(8) }.join(SEPARATOR).rstrip
|
17
|
+
@measurements.each { |m|
|
18
|
+
puts @fields.map { |field| format_duration(m[field] || 0) }.join(SEPARATOR)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Perfer
|
2
|
+
class ResultsFormatter
|
3
|
+
include Formatter
|
4
|
+
|
5
|
+
# maximal job length is computed from +jobs+ if given,
|
6
|
+
# or deduced from given results
|
7
|
+
def initialize(results, jobs = nil)
|
8
|
+
@results = Array(results)
|
9
|
+
@max_job_length = if jobs
|
10
|
+
max_length_of(jobs, &:title)
|
11
|
+
else
|
12
|
+
max_length_of(@results) { |r| r[:job] }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def job_title(result)
|
17
|
+
result[:job].to_s.ljust(@max_job_length)
|
18
|
+
end
|
19
|
+
|
20
|
+
def max_n_length
|
21
|
+
@max_n_length ||= max_length_of(@results) { |r| r[:n] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def report(options = {})
|
25
|
+
measurements = options[:measurements]
|
26
|
+
@results.each do |result|
|
27
|
+
MeasurementsFormatter.new(result.data).report if measurements
|
28
|
+
r = result
|
29
|
+
stats = r.stats
|
30
|
+
mean = stats.mean
|
31
|
+
error = stats.maximum_absolute_deviation
|
32
|
+
if r[:iterations]
|
33
|
+
time_per_i, ips = mean/r[:iterations], r[:iterations]/mean
|
34
|
+
error /= r[:iterations]
|
35
|
+
puts "#{job_title(r)} #{format_duration_and_error time_per_i, error, '/i'} <=> #{format_ips ips} ips"
|
36
|
+
else
|
37
|
+
n = format_n(r[:n], max_n_length)
|
38
|
+
puts "#{job_title(r)} #{n} in #{format_duration_and_error mean, error}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Perfer
|
2
|
+
class SessionFormatter
|
3
|
+
include Formatter
|
4
|
+
|
5
|
+
def initialize(session)
|
6
|
+
@session = session
|
7
|
+
end
|
8
|
+
|
9
|
+
def report(options = {})
|
10
|
+
return puts "No results available." unless @session.results
|
11
|
+
session_name = @session.results.first[:session]
|
12
|
+
puts session_name
|
13
|
+
@session.results.chunk { |r|
|
14
|
+
r[:run_time]
|
15
|
+
}.each { |run_time, results|
|
16
|
+
puts "Ran at #{format_time run_time} with #{results.first[:ruby]}"
|
17
|
+
ResultsFormatter.new(results).report(options)
|
18
|
+
puts
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/perfer/git.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Perfer
|
2
|
+
module Git
|
3
|
+
class << self
|
4
|
+
def git(command)
|
5
|
+
output = `git #{command}`
|
6
|
+
output.chomp!
|
7
|
+
return nil if output.empty?
|
8
|
+
return false if $?.exitstatus != 0
|
9
|
+
output
|
10
|
+
end
|
11
|
+
|
12
|
+
def repository?
|
13
|
+
git "rev-parse --git-dir 2>#{File::NULL}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_branch
|
17
|
+
branch = git 'symbolic-ref --quiet HEAD'
|
18
|
+
branch = branch[/[^\/]+$/] if branch
|
19
|
+
branch
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_commit
|
23
|
+
git 'rev-parse --quiet --verify HEAD'
|
24
|
+
end
|
25
|
+
|
26
|
+
def working_directory_clean?
|
27
|
+
git('status --porcelain --untracked-files=no') == nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def goto_commit(commit)
|
31
|
+
raise Error, Errors::WORKING_DIR_DIRTY unless working_directory_clean?
|
32
|
+
git "reset --quiet --hard #{commit}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/perfer/job.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Perfer
|
2
|
+
class Job
|
3
|
+
attr_reader :session, :title, :metadata
|
4
|
+
def initialize(session, title, &block)
|
5
|
+
@session = session
|
6
|
+
@title = title
|
7
|
+
@block = block
|
8
|
+
|
9
|
+
@metadata = @session.metadata.merge(:job => @title).freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def results
|
13
|
+
@session.results.select { |result| result[:job] == @title }
|
14
|
+
end
|
15
|
+
|
16
|
+
def minimal_time
|
17
|
+
Perfer.configuration.minimal_time
|
18
|
+
end
|
19
|
+
|
20
|
+
def number_of_measurements
|
21
|
+
Perfer.configuration.measurements
|
22
|
+
end
|
23
|
+
|
24
|
+
def verbose
|
25
|
+
Perfer.configuration.verbose
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
@session.current_job = self
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Perfer
|
2
|
+
class InputSizeJob < Job
|
3
|
+
attr_writer :last_measurement
|
4
|
+
|
5
|
+
def start
|
6
|
+
1024
|
7
|
+
end
|
8
|
+
|
9
|
+
def generator(n)
|
10
|
+
n * 2
|
11
|
+
end
|
12
|
+
|
13
|
+
def measure(n)
|
14
|
+
GC.start
|
15
|
+
@block.call(n)
|
16
|
+
@last_measurement
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
super
|
21
|
+
n = start
|
22
|
+
# find an appropriate maximal n, acts as warm-up
|
23
|
+
loop do
|
24
|
+
time = measure(n)[:real]
|
25
|
+
break if time > minimal_time
|
26
|
+
n = generator(n)
|
27
|
+
end
|
28
|
+
|
29
|
+
max = n
|
30
|
+
n = start
|
31
|
+
loop do
|
32
|
+
result = Result.new(@metadata)
|
33
|
+
result[:n] = n
|
34
|
+
number_of_measurements.times do
|
35
|
+
result << measure(n)
|
36
|
+
end
|
37
|
+
@session.add_result(result)
|
38
|
+
|
39
|
+
break if n == max
|
40
|
+
n = generator(n)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Perfer
|
2
|
+
class IterationJob < Job
|
3
|
+
# This factor ensure some margin,
|
4
|
+
# to avoid endlessly changing the number of iterations
|
5
|
+
CHANGE_ITERATIONS_MARGIN = 0.1
|
6
|
+
UNIQUE_NAME = "a"
|
7
|
+
|
8
|
+
def repeat_eval
|
9
|
+
100
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(session, title, code, data, &block)
|
13
|
+
super(session, title, &block)
|
14
|
+
if code and !block
|
15
|
+
@data = data || {}
|
16
|
+
if obj = @data.delete(:self)
|
17
|
+
klass = obj.singleton_class
|
18
|
+
meth = generate_method_name
|
19
|
+
else
|
20
|
+
klass = singleton_class
|
21
|
+
meth = :measure_call_times_code
|
22
|
+
end
|
23
|
+
|
24
|
+
if klass.method_defined?(meth)
|
25
|
+
raise Error, "method #{meth} already defined on #{klass} (#{obj})!"
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
klass.class_eval <<-EOR
|
30
|
+
def #{meth}(__n#{@data.keys.map { |k| ", #{k}" }.join})
|
31
|
+
::Perfer.measure do
|
32
|
+
__i = 0
|
33
|
+
while __i < __n
|
34
|
+
#{"#{code}; " * repeat_eval}
|
35
|
+
__i += 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
EOR
|
40
|
+
rescue SyntaxError => e
|
41
|
+
raise Error, "There was an error while eval'ing the code: #{code.inspect}\n#{e}"
|
42
|
+
end
|
43
|
+
|
44
|
+
if obj
|
45
|
+
singleton_class.send(:define_method, :measure_call_times_code) do |*args|
|
46
|
+
obj.send(meth, *args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_method_name
|
53
|
+
:"perfer_eval_#{UNIQUE_NAME.succ!}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def measure_call_times(n)
|
57
|
+
GC.start
|
58
|
+
if !@block
|
59
|
+
if n % repeat_eval != 0
|
60
|
+
raise Error, "Implementation error: #{n} not multiple of #{repeat_eval}"
|
61
|
+
end
|
62
|
+
n /= repeat_eval
|
63
|
+
measure_call_times_code(n, *@data.values)
|
64
|
+
elsif @block.arity == 1
|
65
|
+
# give n, the block must iterate n times
|
66
|
+
Perfer.measure { @block.call(n) }
|
67
|
+
else
|
68
|
+
Perfer.measure { n.times(&@block) }
|
69
|
+
end.tap { |m| p m if verbose }
|
70
|
+
end
|
71
|
+
|
72
|
+
def round_for_eval(iterations)
|
73
|
+
if @block
|
74
|
+
iterations
|
75
|
+
else
|
76
|
+
((iterations + repeat_eval - 1) / repeat_eval) * repeat_eval
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def compute_new_iterations(iterations, time)
|
81
|
+
(minimal_time * (1.0 + CHANGE_ITERATIONS_MARGIN) * iterations / time).ceil
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_number_of_iterations_required(last_iterations = 1, last_time = 0)
|
85
|
+
iterations = last_iterations
|
86
|
+
if last_time > 0
|
87
|
+
iterations = compute_new_iterations(last_iterations, last_time)
|
88
|
+
end
|
89
|
+
iterations = round_for_eval(iterations)
|
90
|
+
puts "Start search for iterations: start=#{iterations}" if verbose
|
91
|
+
loop do
|
92
|
+
puts "iterations: #{iterations}" if verbose
|
93
|
+
time = measure_call_times(iterations)[:real]
|
94
|
+
break if time > minimal_time
|
95
|
+
|
96
|
+
if time <= 0
|
97
|
+
iterations *= 2
|
98
|
+
next
|
99
|
+
end
|
100
|
+
|
101
|
+
new_iterations = compute_new_iterations(iterations, time)
|
102
|
+
# ensure the number of iterations increases
|
103
|
+
if new_iterations <= iterations
|
104
|
+
puts "new_iterations <= iterations: #{new_iterations} <= #{iterations}" if verbose
|
105
|
+
new_iterations = (iterations*1.5).ceil
|
106
|
+
end
|
107
|
+
iterations = round_for_eval(new_iterations)
|
108
|
+
end
|
109
|
+
puts "End search for iterations: iterations=#{iterations}" if verbose
|
110
|
+
iterations
|
111
|
+
end
|
112
|
+
|
113
|
+
# median absolute deviation / median
|
114
|
+
def mad(measurements)
|
115
|
+
stats = Statistics.new(measurements.map { |m| m[:real] })
|
116
|
+
mad = stats.median_absolute_deviation
|
117
|
+
mad /= stats.median
|
118
|
+
puts "mad: #{mad}" if verbose
|
119
|
+
mad
|
120
|
+
end
|
121
|
+
|
122
|
+
def run
|
123
|
+
super
|
124
|
+
measurements = []
|
125
|
+
|
126
|
+
# Run one iteration, so system-level buffers and other OS warm-up can take place
|
127
|
+
# This is usually a very inaccurate measurement, so just discard it
|
128
|
+
measure_call_times(round_for_eval(1))
|
129
|
+
|
130
|
+
iterations = find_number_of_iterations_required
|
131
|
+
|
132
|
+
measurements_taken = 0
|
133
|
+
until measurements.size == number_of_measurements and
|
134
|
+
mad(measurements) < 0.01 * measurements_taken / number_of_measurements
|
135
|
+
time = measure_call_times(iterations)
|
136
|
+
measurements_taken += 1
|
137
|
+
if time[:real] < (1.0 - CHANGE_ITERATIONS_MARGIN) * minimal_time
|
138
|
+
# restart and find a more appropriate number of iterations
|
139
|
+
puts "Restarting, #{time[:real]} < #{minimal_time}" if verbose
|
140
|
+
measurements.clear
|
141
|
+
measurements_taken = 0
|
142
|
+
iterations = find_number_of_iterations_required(iterations, time[:real])
|
143
|
+
else
|
144
|
+
# circular buffer needed!
|
145
|
+
measurements.shift if measurements.size == number_of_measurements
|
146
|
+
measurements << time
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
result = Result.new(@metadata)
|
151
|
+
result[:iterations] = iterations
|
152
|
+
result.data = measurements
|
153
|
+
@session.add_result(result)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Perfer::Platform
|
2
|
+
module POSIX
|
3
|
+
class TimeValStruct < FFI::Struct
|
4
|
+
layout :tv_sec, :time_t,
|
5
|
+
:tv_usec, :suseconds_t
|
6
|
+
end
|
7
|
+
|
8
|
+
class RUsageStruct < FFI::Struct
|
9
|
+
# Rubinius FFI can't handle nested structs
|
10
|
+
layout :ru_utime_tv_sec, :time_t, # :ru_utime, TimeValStruct,
|
11
|
+
:ru_utime_tv_usec, :suseconds_t,
|
12
|
+
:ru_stime_tv_sec, :time_t, # :ru_stime, TimeValStruct,
|
13
|
+
:ru_stime_tv_usec, :suseconds_t,
|
14
|
+
:ru_maxrss, :long,
|
15
|
+
:ru_ixrss, :long,
|
16
|
+
:ru_idrss, :long,
|
17
|
+
:ru_isrss, :long,
|
18
|
+
:ru_minflt, :long,
|
19
|
+
:ru_majflt, :long,
|
20
|
+
:ru_nswap, :long,
|
21
|
+
:ru_inblock, :long,
|
22
|
+
:ru_oublock, :long,
|
23
|
+
:ru_msgsnd, :long,
|
24
|
+
:ru_msgrcv, :long,
|
25
|
+
:ru_nsignals, :long,
|
26
|
+
:ru_nvcsw, :long,
|
27
|
+
:ru_nivcsw, :long
|
28
|
+
end
|
29
|
+
|
30
|
+
module LibC
|
31
|
+
extend FFI::Library
|
32
|
+
ffi_lib FFI::Library::LIBC
|
33
|
+
|
34
|
+
RUSAGE_SELF = 0
|
35
|
+
|
36
|
+
attach_function :getrusage, [:int, :pointer], :int
|
37
|
+
end
|
38
|
+
|
39
|
+
PID = Process.pid
|
40
|
+
|
41
|
+
def memory_used
|
42
|
+
case OS
|
43
|
+
when /^darwin/, /^linux/, /^solaris/
|
44
|
+
Integer(`ps -o rss= -p #{PID}`) * 1024
|
45
|
+
else
|
46
|
+
warn "Unknown platform for Platform.command_line: #{os.inspect}"
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def maximum_memory_used
|
52
|
+
rusage = RUsageStruct.new
|
53
|
+
r = LibC.getrusage(LibC::RUSAGE_SELF, rusage)
|
54
|
+
if r != 0
|
55
|
+
warn "Could not retrieve memory information with getrusage(2)"
|
56
|
+
0
|
57
|
+
else
|
58
|
+
m = rusage[:ru_maxrss]
|
59
|
+
warn "Memory information with getrusage(2) is inaccurate, ru_maxrss was 0" if m == 0
|
60
|
+
if /darwin/ =~ OS
|
61
|
+
m # reported in bytes
|
62
|
+
else
|
63
|
+
m * 1024 # reported in KB, as the man page says
|
64
|
+
end
|
65
|
+
end
|
66
|
+
ensure
|
67
|
+
rusage.pointer.free if rusage
|
68
|
+
end
|
69
|
+
|
70
|
+
def command_line
|
71
|
+
return Rubinius::OS_ARGV.join(' ') if defined? Rubinius::OS_ARGV
|
72
|
+
case OS
|
73
|
+
when /^darwin/, /^linux/, /^solaris/
|
74
|
+
`ps -o args= -p #{PID}`.lines.to_a.last.rstrip
|
75
|
+
else
|
76
|
+
warn "Unknown platform for Platform.command_line: #{os.inspect}"
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Perfer::Platform
|
2
|
+
module Windows
|
3
|
+
module Types
|
4
|
+
BOOL = :bool
|
5
|
+
DWORD = :uint32
|
6
|
+
HANDLE = :pointer
|
7
|
+
SIZE_T = :size_t
|
8
|
+
LPSTR = :string
|
9
|
+
end
|
10
|
+
|
11
|
+
class PProcessMemoryCounters < FFI::Struct
|
12
|
+
include Types
|
13
|
+
layout :cb, DWORD,
|
14
|
+
:PageFaultCount, DWORD,
|
15
|
+
:PeakWorkingSetSize, SIZE_T,
|
16
|
+
:WorkingSetSize, SIZE_T,
|
17
|
+
:QuotaPeakPagedPoolUsage, SIZE_T,
|
18
|
+
:QuotaPagedPoolUsage, SIZE_T,
|
19
|
+
:QuotaPeakNonPagedPoolUsage, SIZE_T,
|
20
|
+
:QuotaNonPagedPoolUsage, SIZE_T,
|
21
|
+
:PagefileUsage, SIZE_T,
|
22
|
+
:PeakPagefileUsage, SIZE_T
|
23
|
+
end
|
24
|
+
|
25
|
+
module Kernel32
|
26
|
+
include Types
|
27
|
+
extend FFI::Library
|
28
|
+
ffi_lib 'kernel32'
|
29
|
+
ffi_convention :stdcall
|
30
|
+
|
31
|
+
attach_function :GetCurrentProcess, [], HANDLE
|
32
|
+
attach_function :GetCommandLineA, [], LPSTR
|
33
|
+
end
|
34
|
+
|
35
|
+
module PSAPI
|
36
|
+
include Types
|
37
|
+
extend FFI::Library
|
38
|
+
ffi_lib 'psapi'
|
39
|
+
ffi_convention :stdcall
|
40
|
+
|
41
|
+
attach_function :GetProcessMemoryInfo, [HANDLE, PProcessMemoryCounters, DWORD], BOOL
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_process_memory_info
|
45
|
+
process = Kernel32.GetCurrentProcess
|
46
|
+
info = PProcessMemoryCounters.new
|
47
|
+
# See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683219%28v=vs.85%29.aspx
|
48
|
+
r = PSAPI.GetProcessMemoryInfo(process, info, info.size)
|
49
|
+
if !r
|
50
|
+
warn "Could not retrieve memory information with GetProcessMemoryInfo()"
|
51
|
+
nil
|
52
|
+
else
|
53
|
+
yield(info)
|
54
|
+
end
|
55
|
+
ensure
|
56
|
+
info.pointer.free if info
|
57
|
+
end
|
58
|
+
private :get_process_memory_info
|
59
|
+
|
60
|
+
def memory_used
|
61
|
+
get_process_memory_info { |info|
|
62
|
+
# info[:PeakWorkingSetSize] # RAM
|
63
|
+
info[:PeakPagefileUsage] # RAM + SWAP
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def maximum_memory_used
|
68
|
+
get_process_memory_info { |info|
|
69
|
+
# info[:WorkingSetSize] # RAM
|
70
|
+
info[:PagefileUsage] # RAM + SWAP
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def command_line
|
75
|
+
Kernel32.GetCommandLineA().tap do |command_line|
|
76
|
+
unless command_line
|
77
|
+
warn "Could not get command line via GetCommandLineA()"
|
78
|
+
return nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Perfer
|
2
|
+
# A result for a particular job run
|
3
|
+
class Result
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :metadata
|
7
|
+
attr_accessor :data
|
8
|
+
def initialize(metadata, data = [])
|
9
|
+
@metadata = metadata.dup
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
def_instance_delegators :@data,
|
14
|
+
:<<, :size, :length, :each
|
15
|
+
|
16
|
+
def_instance_delegators :@metadata, :[], :[]=
|
17
|
+
|
18
|
+
def stats
|
19
|
+
Statistics.new(on(:real))
|
20
|
+
end
|
21
|
+
|
22
|
+
def on(field)
|
23
|
+
@data.map { |result| result[field] }
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json(*args)
|
27
|
+
{
|
28
|
+
'json_class' => self.class.name,
|
29
|
+
'data' => @data,
|
30
|
+
'metadata' => @metadata
|
31
|
+
}.to_json(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.json_create json
|
35
|
+
new(json['metadata'], json['data'])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Perfer
|
2
|
+
class Session
|
3
|
+
attr_reader :name, :file, :jobs, :type, :store, :results, :metadata
|
4
|
+
attr_writer :current_job
|
5
|
+
def initialize(file, name = nil, &block)
|
6
|
+
@file = file
|
7
|
+
@name = name || file.base.to_s
|
8
|
+
@store = Store.for_session(self)
|
9
|
+
@results = nil # not an Array, so it errors out if we forgot to load
|
10
|
+
|
11
|
+
setup_for_run(&block) if block_given?
|
12
|
+
|
13
|
+
Perfer.sessions << self
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_for_run
|
17
|
+
@jobs = []
|
18
|
+
@type = nil # will be decided by API usage (iterate/bench)
|
19
|
+
@results_to_save = []
|
20
|
+
|
21
|
+
@metadata = {
|
22
|
+
:file => @file.path,
|
23
|
+
:session => @name,
|
24
|
+
:ruby => RUBY_DESCRIPTION,
|
25
|
+
:command_line => Platform.command_line,
|
26
|
+
:run_time => Time.now
|
27
|
+
}
|
28
|
+
add_config_metadata
|
29
|
+
add_git_metadata
|
30
|
+
add_bench_file_checksum
|
31
|
+
@metadata.freeze
|
32
|
+
|
33
|
+
yield self
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_config_metadata
|
37
|
+
@metadata.merge!(Perfer.configuration.to_hash)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_git_metadata
|
41
|
+
if Git.repository?
|
42
|
+
@metadata[:git_branch] = Git.current_branch
|
43
|
+
@metadata[:git_commit] = Git.current_commit
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_bench_file_checksum
|
48
|
+
checksum = Digest::SHA1.hexdigest(@file.binread)
|
49
|
+
if checksum.respond_to?(:encoding) and checksum.encoding != Encoding::ASCII
|
50
|
+
checksum.force_encoding(Encoding::ASCII)
|
51
|
+
end
|
52
|
+
@metadata[:bench_file_checksum] = checksum
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_results
|
56
|
+
@results = @store.load
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_result(result)
|
60
|
+
@results_to_save << result
|
61
|
+
ResultsFormatter.new(result, @jobs).report
|
62
|
+
end
|
63
|
+
|
64
|
+
def run
|
65
|
+
puts "Session #{@name} with #{@metadata[:ruby]}"
|
66
|
+
print "Taking #{Perfer.configuration.measurements} measurements of"
|
67
|
+
puts " at least #{Perfer.configuration.minimal_time}s"
|
68
|
+
@jobs.each { |job|
|
69
|
+
job.run
|
70
|
+
}
|
71
|
+
@results_to_save.each { |result|
|
72
|
+
@store.append(result)
|
73
|
+
}
|
74
|
+
@results_to_save.clear
|
75
|
+
end
|
76
|
+
|
77
|
+
# not named #report, to avoid any confusion with Benchmark's #report
|
78
|
+
def report_results(options = {})
|
79
|
+
load_results
|
80
|
+
SessionFormatter.new(self).report(options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def iterate(title, code = nil, data = nil, &block)
|
84
|
+
check_benchmark_type(:iterations)
|
85
|
+
check_unique_job_title(title)
|
86
|
+
@jobs << IterationJob.new(self, title, code, data, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def bench(title, &block)
|
90
|
+
check_benchmark_type(:input_size)
|
91
|
+
check_unique_job_title(title)
|
92
|
+
@jobs << InputSizeJob.new(self, title, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def measure(&block)
|
96
|
+
raise Error, WRONG_MEASURE_USE unless InputSizeJob === @current_job
|
97
|
+
@current_job.last_measurement = Perfer.measure(&block)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def check_benchmark_type(expected)
|
102
|
+
unless !@type or @type == expected
|
103
|
+
raise Error, Errors::MIX_BENCH_TYPES
|
104
|
+
end
|
105
|
+
@type ||= expected
|
106
|
+
end
|
107
|
+
|
108
|
+
def check_unique_job_title(title)
|
109
|
+
if @jobs.any? { |job| job.title == title }
|
110
|
+
raise Error, Errors::SAME_JOB_TITLES
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Perfer
|
2
|
+
class Statistics
|
3
|
+
include Math
|
4
|
+
CONFIDENCE_LEVEL = 0.95
|
5
|
+
ALPHA = 1.0 - CONFIDENCE_LEVEL # significance level
|
6
|
+
|
7
|
+
# Student's t quantiles is used as n is small (= number of measurements)
|
8
|
+
# Indexed by: probability, degrees of freedom
|
9
|
+
T_QUANTILES = {
|
10
|
+
0.975 => [
|
11
|
+
nil,
|
12
|
+
12.71, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228, # 1-10
|
13
|
+
2.201, 2.179, 2.160, 2.145, 2.131, 2.120, 2.110, 2.101, 2.093, 2.086, # 11-20
|
14
|
+
2.080, 2.074, 2.069, 2.064, 2.060, 2.056, 2.052, 2.048, 2.045, 2.042, # 21-30
|
15
|
+
2.040, 2.037, 2.035, 2.032, 2.030, 2.028, 2.026, 2.024, 2.023, 2.021 # 31-40
|
16
|
+
]
|
17
|
+
}
|
18
|
+
{ 50 => 2.009, 60 => 2.000, 70 => 1.994,
|
19
|
+
80 => 1.990, 90 => 1.987, 100 => 1.984 }.each_pair { |n, value|
|
20
|
+
T_QUANTILES[0.975][n] = value
|
21
|
+
}
|
22
|
+
|
23
|
+
def self.t_quantile(p, degrees_of_freedom)
|
24
|
+
if degrees_of_freedom <= 40
|
25
|
+
T_QUANTILES[p][degrees_of_freedom]
|
26
|
+
elsif degrees_of_freedom <= 100
|
27
|
+
T_QUANTILES[p][degrees_of_freedom.round(-1)]
|
28
|
+
else
|
29
|
+
1.960
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(sample)
|
34
|
+
@sample = sample
|
35
|
+
end
|
36
|
+
|
37
|
+
def size
|
38
|
+
@sample.size
|
39
|
+
end
|
40
|
+
|
41
|
+
def mean
|
42
|
+
@mean ||= @sample.inject(0.0) { |sum, i| sum + i } / size
|
43
|
+
end
|
44
|
+
|
45
|
+
def median
|
46
|
+
@median ||= begin
|
47
|
+
sorted = @sample.sort
|
48
|
+
if size.odd?
|
49
|
+
sorted[size/2]
|
50
|
+
else
|
51
|
+
(sorted[size/2-1] + sorted[size/2]) / 2.0
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def variance
|
57
|
+
mean = mean()
|
58
|
+
@sample.inject(0.0) { |var, i|
|
59
|
+
d = i - mean
|
60
|
+
var + d*d
|
61
|
+
} / (size - 1) # unbiased sample variance
|
62
|
+
end
|
63
|
+
|
64
|
+
def standard_deviation
|
65
|
+
sqrt(variance)
|
66
|
+
end
|
67
|
+
|
68
|
+
def coefficient_of_variation
|
69
|
+
standard_deviation / mean
|
70
|
+
end
|
71
|
+
|
72
|
+
def standard_error
|
73
|
+
standard_deviation / sqrt(size)
|
74
|
+
end
|
75
|
+
|
76
|
+
def mean_absolute_deviation
|
77
|
+
@sample.inject(0.0) { |dev, i| dev + (i - mean).abs } / size
|
78
|
+
end
|
79
|
+
|
80
|
+
def median_absolute_deviation
|
81
|
+
Statistics.new(@sample.map { |i| (i - median).abs }).median
|
82
|
+
end
|
83
|
+
|
84
|
+
# Assumes a standard normal distribution
|
85
|
+
# This is half the width of the confidence interval for the mean
|
86
|
+
def margin_of_error
|
87
|
+
Statistics.t_quantile(1.0 - ALPHA/2, size-1) * standard_error
|
88
|
+
end
|
89
|
+
|
90
|
+
def maximum_absolute_deviation
|
91
|
+
@sample.map { |v| (v - mean).abs }.max
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/perfer/store.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Perfer
|
2
|
+
class Store
|
3
|
+
attr_reader :file
|
4
|
+
def initialize(file)
|
5
|
+
@file = Path(file)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.for_session(session)
|
9
|
+
path = DIR/'results'
|
10
|
+
path.mkpath unless path.exist?
|
11
|
+
|
12
|
+
bench_file = session.file
|
13
|
+
|
14
|
+
# get the relative path to root, and relocate in @path
|
15
|
+
names = bench_file.each_filename.to_a
|
16
|
+
# prepend drive letter on Windows
|
17
|
+
names.unshift bench_file.path[0..0].upcase if File.dirname('C:') == 'C:.'
|
18
|
+
|
19
|
+
new path.join(*names).add_ext('.yml')
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete
|
23
|
+
@file.unlink
|
24
|
+
end
|
25
|
+
|
26
|
+
def yaml_load_documents
|
27
|
+
docs = @file.open { |f| YAML.load_stream(f) }
|
28
|
+
docs = docs.documents unless Array === docs
|
29
|
+
docs
|
30
|
+
end
|
31
|
+
|
32
|
+
def load
|
33
|
+
return unless @file.exist?
|
34
|
+
yaml_load_documents
|
35
|
+
end
|
36
|
+
|
37
|
+
def append(result)
|
38
|
+
@file.dir.mkpath unless @file.dir.exist?
|
39
|
+
@file.append YAML.dump(result)
|
40
|
+
end
|
41
|
+
|
42
|
+
def save(results)
|
43
|
+
@file.dir.mkpath unless @file.dir.exist?
|
44
|
+
# ensure results are still ordered by :run_time
|
45
|
+
results.sort_by! { |r| r[:run_time] }
|
46
|
+
@file.write YAML.dump_stream(*results)
|
47
|
+
end
|
48
|
+
|
49
|
+
def rewrite
|
50
|
+
save load
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/perfer.gemspec
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'perfer'
|
3
|
+
s.summary = 'A benchmark tool for all rubies!'
|
4
|
+
s.author = 'eregon'
|
5
|
+
s.email = 'eregontp@gmail.com'
|
6
|
+
s.homepage = 'https://github.com/jruby/perfer'
|
7
|
+
s.files = Dir['lib/**/*.rb'] + %w[README.md LICENSE perfer.gemspec]
|
8
|
+
s.version = '0.1.0'
|
9
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: perfer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- eregon
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-15 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: eregontp@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/perfer/cli.rb
|
21
|
+
- lib/perfer/configuration.rb
|
22
|
+
- lib/perfer/errors.rb
|
23
|
+
- lib/perfer/formatter/measurements_formatter.rb
|
24
|
+
- lib/perfer/formatter/results_formatter.rb
|
25
|
+
- lib/perfer/formatter/session_formatter.rb
|
26
|
+
- lib/perfer/formatter.rb
|
27
|
+
- lib/perfer/git.rb
|
28
|
+
- lib/perfer/job/input_size_job.rb
|
29
|
+
- lib/perfer/job/iteration_job.rb
|
30
|
+
- lib/perfer/job.rb
|
31
|
+
- lib/perfer/platform/posix.rb
|
32
|
+
- lib/perfer/platform/windows.rb
|
33
|
+
- lib/perfer/platform.rb
|
34
|
+
- lib/perfer/result.rb
|
35
|
+
- lib/perfer/session.rb
|
36
|
+
- lib/perfer/statistics.rb
|
37
|
+
- lib/perfer/store.rb
|
38
|
+
- lib/perfer.rb
|
39
|
+
- README.md
|
40
|
+
- LICENSE
|
41
|
+
- perfer.gemspec
|
42
|
+
homepage: https://github.com/jruby/perfer
|
43
|
+
licenses: []
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.8.23
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: A benchmark tool for all rubies!
|
66
|
+
test_files: []
|
67
|
+
has_rdoc:
|