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 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.
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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,13 @@
1
+ module Perfer
2
+ module Platform
3
+ OS = RbConfig::CONFIG['host_os']
4
+
5
+ if /mingw|mswin/ =~ OS
6
+ require Path.relative('platform/windows')
7
+ extend Windows
8
+ else
9
+ require Path.relative('platform/posix')
10
+ extend POSIX
11
+ end
12
+ end
13
+ 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
@@ -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
@@ -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: