perfer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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:
|