benchmark_driver 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -4
  3. data/.travis.yml +10 -6
  4. data/Gemfile +7 -2
  5. data/Gemfile.lock +30 -0
  6. data/README.md +125 -117
  7. data/Rakefile +14 -7
  8. data/benchmark_driver.gemspec +2 -4
  9. data/bin/console +1 -1
  10. data/examples/call.rb +12 -0
  11. data/examples/call_blank.rb +13 -0
  12. data/examples/call_erb.rb +33 -0
  13. data/examples/call_interpolation.rb +13 -0
  14. data/examples/exec_blank.rb +14 -0
  15. data/examples/exec_interpolation.rb +15 -0
  16. data/examples/yaml/array_duration_time.yml +3 -0
  17. data/examples/yaml/array_loop.yml +3 -0
  18. data/examples/yaml/array_loop_memory.yml +6 -0
  19. data/examples/yaml/array_loop_time.yml +4 -0
  20. data/examples/yaml/blank_hash.yml +8 -0
  21. data/examples/yaml/blank_hash_array.yml +10 -0
  22. data/examples/yaml/blank_loop.yml +9 -0
  23. data/examples/yaml/blank_loop_time.yml +10 -0
  24. data/examples/yaml/blank_string.yml +6 -0
  25. data/examples/yaml/blank_string_array.yml +8 -0
  26. data/examples/yaml/example_multi.yml +6 -0
  27. data/{benchmarks → examples/yaml}/example_single.yml +0 -0
  28. data/exe/benchmark-driver +44 -18
  29. data/lib/benchmark/driver.rb +52 -257
  30. data/lib/benchmark/driver/benchmark_result.rb +21 -0
  31. data/lib/benchmark/driver/configuration.rb +65 -0
  32. data/lib/benchmark/driver/duration_runner.rb +24 -0
  33. data/lib/benchmark/driver/error.rb +16 -0
  34. data/lib/benchmark/driver/repeatable_runner.rb +18 -0
  35. data/lib/benchmark/driver/ruby_dsl_parser.rb +57 -0
  36. data/lib/benchmark/driver/time.rb +12 -0
  37. data/lib/benchmark/driver/version.rb +2 -2
  38. data/lib/benchmark/driver/yaml_parser.rb +103 -0
  39. data/lib/benchmark/output.rb +16 -0
  40. data/lib/benchmark/output/ips.rb +114 -0
  41. data/lib/benchmark/output/memory.rb +57 -0
  42. data/lib/benchmark/output/time.rb +57 -0
  43. data/lib/benchmark/runner.rb +13 -0
  44. data/lib/benchmark/runner/call.rb +97 -0
  45. data/lib/benchmark/runner/exec.rb +190 -0
  46. metadata +40 -10
  47. data/benchmarks/core/array.yml +0 -4
  48. data/benchmarks/example_multi.yml +0 -10
  49. data/benchmarks/lib/erb.yml +0 -30
@@ -0,0 +1,21 @@
1
+ # @param [Benchmark::Driver::Configuration::Job] job
2
+ # @param [Integer] iterations - Executed iterations of benchmark script in the job
3
+ # @param [Float] real - Real time taken by the job
4
+ # @param [Integer] max_rss - Maximum resident set size of the process during its lifetime, in Kilobytes.
5
+ class Benchmark::Driver::BenchmarkResult < Struct.new(:job, :iterations, :real, :max_rss)
6
+ alias :duration :real
7
+
8
+ def ips
9
+ iterations / real
10
+ end
11
+
12
+ def ip100ms
13
+ ips / 10
14
+ end
15
+
16
+ def iterations
17
+ # runner's warmup uses `result.ips` to calculate `job.loop_count`, and thus
18
+ # at that moment `job.loop_count` isn't available and we need to use `super`.
19
+ super || job.loop_count
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ # @param [Array<Benchmark::Driver::Configuration::Job>] jobs
2
+ # @param [Benchmark::Driver::Configuration::RunnerOptions] runner_options
3
+ # @param [Benchmark::Driver::Configuration::OutputOptions] output_options
4
+ class Benchmark::Driver::Configuration < Struct.new(:jobs, :runner_options, :output_options)
5
+ # @param [String,nil] name
6
+ # @param [String,Proc] sctipt
7
+ # @param [String,nil] prelude
8
+ # @param [Integer,nil] loop_count - If this is nil, loop count is automatically estimated by warmup.
9
+ class Job < Struct.new(:name, :script, :prelude, :loop_count)
10
+ # @param [Integer,nil] guessed_count - Set by runner only when loop_count is nil. This is not configuration.
11
+ attr_accessor :guessed_count
12
+
13
+ def warmup_needed?
14
+ # This needs to check original configuration
15
+ self[:loop_count].nil?
16
+ end
17
+
18
+ def loop_count
19
+ super || guessed_count
20
+ end
21
+ end
22
+
23
+ # @param [String] name
24
+ # @param [String] path
25
+ Executable = Struct.new(:name, :path)
26
+
27
+ DEFAULT_EXECUTABLES = [Executable.new(RUBY_VERSION, RbConfig.ruby)]
28
+
29
+ # @param [Symbol] type - Type of runner
30
+ # @param [Array<Benchmark::Driver::Configuration::Executable>] executables
31
+ # @param [Integer,nil] repeat_count - Times to repeat benchmarks. When this is not nil, benchmark_driver will use the best result.
32
+ class RunnerOptions < Struct.new(:type, :executables, :repeat_count)
33
+ def initialize(*)
34
+ super
35
+ self.executables = DEFAULT_EXECUTABLES
36
+ end
37
+
38
+ def executables_specified?
39
+ executables != DEFAULT_EXECUTABLES
40
+ end
41
+ end
42
+
43
+ # @param [Symbol] type - Type of output
44
+ # @param [TrueClass,FalseClass] compare
45
+ OutputOptions = Struct.new(:type, :compare)
46
+
47
+ # @param [Object] config
48
+ def self.symbolize_keys!(config)
49
+ case config
50
+ when Hash
51
+ config.keys.each do |key|
52
+ config[key.to_sym] = symbolize_keys!(config.delete(key))
53
+ end
54
+ when Array
55
+ config.map! { |c| symbolize_keys!(c) }
56
+ end
57
+ config
58
+ end
59
+
60
+ # @param [String] str
61
+ def self.camelize(str)
62
+ return str if str !~ /_/ && str =~ /[A-Z]+.*/
63
+ str.split('_').map { |e| e.capitalize }.join
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ class Benchmark::Driver::DurationRunner
2
+ # @param [Benchmark::Driver::Configuration::Job] job
3
+ def initialize(job)
4
+ @job = job
5
+ end
6
+
7
+ # @param [Integer,Float] seconds
8
+ # @param [Integer] unit_iters
9
+ # @param [Proc] runner - should take (job, unit_iters) and return duration.
10
+ # @return [Benchmark::Driver::BenchmarkResult]
11
+ def run(seconds:, unit_iters:, runner:)
12
+ real_time = 0.0
13
+ iterations = 0
14
+ unit_iters = unit_iters.to_i
15
+
16
+ benchmark_until = Benchmark::Driver::Time.now + seconds
17
+ while Benchmark::Driver::Time.now < benchmark_until
18
+ real_time += runner.call(@job, unit_iters)
19
+ iterations += unit_iters
20
+ end
21
+
22
+ Benchmark::Driver::BenchmarkResult.new(@job, iterations, real_time)
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Benchmark::Driver
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ExecutionTimeTooShort < Error
6
+ def initialize(job, iterations)
7
+ @job = job
8
+ @iterations = iterations
9
+ end
10
+
11
+ def message
12
+ "Execution time of job #{@job.name.dump} was too short in #{@iterations} executions;\n "\
13
+ 'Please retry, try slower script or increase loop_count.'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ class Benchmark::Driver::RepeatableRunner
2
+ # @param [Benchmark::Driver::Configuration::Job] job
3
+ def initialize(job)
4
+ @job = job
5
+ end
6
+
7
+ # @param [Integer] repeat_count
8
+ # @param [Proc] runner - should take (job, unit_iters) and return duration.
9
+ # @return [Benchmark::Driver::BenchmarkResult]
10
+ def run(repeat_count:, runner:)
11
+ real_times = (repeat_count || 1).times.map do
12
+ runner.call(@job, @job.loop_count)
13
+ end
14
+ Benchmark::Driver::BenchmarkResult.new(@job).tap do |result|
15
+ result.real = real_times.select { |d| d > 0 }.min || real_times.max
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ require 'benchmark/driver/configuration'
2
+
3
+ class Benchmark::Driver::RubyDslParser
4
+ def initialize(runner: :call, output: :ips)
5
+ @prelude = nil
6
+ @jobs = []
7
+ @runner_options = Benchmark::Driver::Configuration::RunnerOptions.new(runner)
8
+ @output_options = Benchmark::Driver::Configuration::OutputOptions.new(output)
9
+ end
10
+
11
+ # API to fetch configuration parsed from DSL
12
+ # @return [Benchmark::Driver::Configuration]
13
+ def configuration
14
+ @jobs.each do |job|
15
+ job.prelude = @prelude
16
+ end
17
+ Benchmark::Driver::Configuration.new(@jobs).tap do |c|
18
+ c.runner_options = @runner_options
19
+ c.output_options = @output_options
20
+ end
21
+ end
22
+
23
+ # @param [String] prelude - Script required for benchmark whose execution time is not measured.
24
+ def prelude=(prelude)
25
+ unless prelude.is_a?(String)
26
+ raise ArgumentError.new("prelude must be String but got #{prelude.inspect}")
27
+ end
28
+ unless @prelude.nil?
29
+ raise ArgumentError.new("prelude is already set:\n#{@prelude}")
30
+ end
31
+
32
+ @prelude = prelude
33
+ end
34
+
35
+ # @param [String,nil] name - Name shown on result output. This must be provided if block is given.
36
+ # @param [String,nil] script - Benchmarked script in String. Only either of script or block must be provided.
37
+ # @param [Proc,nil] block - Benchmarked Proc object.
38
+ def report(name = nil, script: nil, &block)
39
+ if script.nil? && !block_given?
40
+ raise ArgumentError.new('script or block must be provided')
41
+ elsif !script.nil? && block_given?
42
+ raise ArgumentError.new('script and block cannot be specified at the same time')
43
+ elsif name.nil? && block_given?
44
+ raise ArgumentError.new('name must be specified if block is given')
45
+ elsif !name.nil? && !name.is_a?(String)
46
+ raise ArgumentError.new("name must be String but got #{name.inspect}")
47
+ elsif !script.nil? && !script.is_a?(String)
48
+ raise ArgumentError.new("script must be String but got #{script.inspect}")
49
+ end
50
+
51
+ @jobs << Benchmark::Driver::Configuration::Job.new(name || script, script || block)
52
+ end
53
+
54
+ def compare!
55
+ @output_options.compare = true
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ module Benchmark::Driver::Time
2
+ if defined?(Process::CLOCK_MONOTONIC)
3
+ def self.now
4
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
5
+ end
6
+ else
7
+ $stderr.puts "Process::CLOCK_MONOTONIC was unavailable. Using Time."
8
+ def self.now
9
+ ::Time.now
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,5 @@
1
1
  module Benchmark
2
- class Driver
3
- VERSION = '0.3.0'
2
+ module Driver
3
+ VERSION = '0.4.0'
4
4
  end
5
5
  end
@@ -0,0 +1,103 @@
1
+ module Benchmark::Driver::YamlParser
2
+ DEFAULT_RUNNER = :exec # In YamlParser, we can't use :call.
3
+ DEFAULT_OUTPUT = :ips
4
+
5
+ class << self
6
+ # @param [String] prelude
7
+ # @param [Integer,nil] loop_count
8
+ # @param [String,Array<String,Hash{ Symbol => String }>,Hash{ Symbol => String }] benchmark
9
+ # @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] runner
10
+ # @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] output
11
+ # @return [Benchmark::Driver::Configuration]
12
+ def parse(prelude: '', loop_count: nil, benchmark:, runner: {}, output: {})
13
+ jobs = parse_benchmark(benchmark)
14
+ jobs.each do |job|
15
+ job.prelude = prelude
16
+ job.loop_count ||= loop_count
17
+ end
18
+
19
+ config = Benchmark::Driver::Configuration.new(jobs)
20
+ config.runner_options = parse_runner(runner)
21
+ config.output_options = parse_output(output)
22
+ config
23
+ end
24
+
25
+ private
26
+
27
+ # @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] runner
28
+ def parse_runner(runner)
29
+ case runner
30
+ when String, Symbol
31
+ Benchmark::Driver::Configuration::RunnerOptions.new(runner.to_sym)
32
+ when Hash
33
+ parse_runner_options(runner)
34
+ else
35
+ raise ArgumentError.new("Expected String, Symbol or Hash in runner, but got: #{runner.inspect}")
36
+ end
37
+ end
38
+
39
+ def parse_runner_options(type: DEFAULT_RUNNER, repeat_count: nil)
40
+ Benchmark::Driver::Configuration::RunnerOptions.new.tap do |r|
41
+ r.type = type.to_sym
42
+ r.repeat_count = Integer(repeat_count) if repeat_count
43
+ end
44
+ end
45
+
46
+ # @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] output
47
+ def parse_output(output)
48
+ case output
49
+ when String, Symbol
50
+ Benchmark::Driver::Configuration::OutputOptions.new(output.to_sym)
51
+ when Hash
52
+ parse_output_options(output)
53
+ else
54
+ raise ArgumentError.new("Expected String, Symbol or Hash in output, but got: #{output.inspect}")
55
+ end
56
+ end
57
+
58
+ def parse_output_options(type: DEFAULT_OUTPUT, compare: false)
59
+ Benchmark::Driver::Configuration::OutputOptions.new.tap do |r|
60
+ r.type = type.to_sym
61
+ r.compare = compare
62
+ end
63
+ end
64
+
65
+ # Parse "benchmark" declarative. This may have multiple benchmarks.
66
+ # @param [String,Array<String,Hash{ Symbol => String }>,Hash{ Symbol => String }] benchmark
67
+ def parse_benchmark(benchmark)
68
+ case benchmark
69
+ when String
70
+ [parse_each_benchmark(benchmark)]
71
+ when Array
72
+ benchmark.map { |b| parse_each_benchmark(b) }
73
+ when Hash
74
+ benchmark.map do |key, value|
75
+ Benchmark::Driver::Configuration::Job.new(key.to_s, value)
76
+ end
77
+ else
78
+ raise ArgumentError.new("benchmark must be String, Array or Hash, but got: #{benchmark.inspect}")
79
+ end
80
+ end
81
+
82
+ # Parse one benchmark specified in "benchmark" declarative.
83
+ # @param [String,Hash{ Symbol => String }>] job
84
+ def parse_each_benchmark(benchmark)
85
+ case benchmark
86
+ when String
87
+ Benchmark::Driver::Configuration::Job.new(benchmark, benchmark)
88
+ when Hash
89
+ parse_job(benchmark)
90
+ else
91
+ raise ArgumentError.new("Expected String or Hash in element of benchmark, but got: #{benchmark.inspect}")
92
+ end
93
+ end
94
+
95
+ # @param [String,nil] name
96
+ # @param [String] script
97
+ # TODO: support benchmark-specific prelude
98
+ def parse_job(name: nil, script:)
99
+ name = script if name.nil?
100
+ Benchmark::Driver::Configuration::Job.new(name, script)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,16 @@
1
+ module Benchmark::Output
2
+ class << self
3
+ # Benchmark::Output is pluggable.
4
+ # Create `Benchmark::Output::FooBar` as benchmark-output-foo_bar.gem and specify `output: foo_bar`.
5
+ #
6
+ # @param [Symbol] name
7
+ def find(name)
8
+ class_name = Benchmark::Driver::Configuration.camelize(name.to_s)
9
+ Benchmark::Output.const_get(class_name, false)
10
+ end
11
+ end
12
+ end
13
+
14
+ require 'benchmark/output/ips'
15
+ require 'benchmark/output/memory'
16
+ require 'benchmark/output/time'
@@ -0,0 +1,114 @@
1
+ class Benchmark::Output::Ips
2
+ # This class requires runner to measure following fields in `Benchmark::Driver::BenchmarkResult` to show output.
3
+ REQUIRED_FIELDS = [:real]
4
+
5
+ NAME_LENGTH = 20
6
+
7
+ # @param [Array<Benchmark::Driver::Configuration::Job>] jobs
8
+ # @param [Array<Benchmark::Driver::Configuration::Executable>] executables
9
+ # @param [Benchmark::Driver::Configuration::OutputOptions] options
10
+ def initialize(jobs:, executables:, options:)
11
+ @jobs = jobs
12
+ @executables = executables
13
+ @options = options
14
+ @results = []
15
+ @name_by_result = {}
16
+ end
17
+
18
+ def start_warming
19
+ $stdout.puts 'Warming up --------------------------------------'
20
+ end
21
+
22
+ # @param [String] name
23
+ def warming(name)
24
+ if name.length > NAME_LENGTH
25
+ $stdout.puts(name)
26
+ else
27
+ $stdout.print("%#{NAME_LENGTH}s" % name)
28
+ end
29
+ end
30
+
31
+ # @param [Benchmark::Driver::BenchmarkResult] result
32
+ def warmup_stats(result)
33
+ $stdout.puts "#{humanize(result.ip100ms)} i/100ms"
34
+ end
35
+
36
+ def start_running
37
+ $stdout.puts 'Calculating -------------------------------------'
38
+ if @executables.size > 1
39
+ $stdout.print(' ' * NAME_LENGTH)
40
+ @executables.each do |executable|
41
+ $stdout.print(" %10s " % executable.name)
42
+ end
43
+ $stdout.puts
44
+ end
45
+ end
46
+
47
+ def running(name)
48
+ warming(name)
49
+ @row_results = []
50
+ end
51
+
52
+ # @param [Benchmark::Driver::BenchmarkResult] result
53
+ def benchmark_stats(result)
54
+ executable = @executables[@row_results.size]
55
+ $stdout.print("#{humanize(result.ips, [10, executable.name.length].max)} ")
56
+
57
+ @results << result
58
+ @row_results << result
59
+ if @row_results.size == @executables.size
60
+ $stdout.print("i/s - #{humanize(result.iterations)} in")
61
+ @row_results.each do |r|
62
+ $stdout.print(" %3.6fs" % r.real)
63
+ end
64
+ $stdout.puts
65
+ end
66
+
67
+ @name_by_result[result] = executable.name
68
+ end
69
+
70
+ def finish
71
+ if @results.size > 1 && @options.compare
72
+ compare
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def humanize(value, width = 10)
79
+ scale = (Math.log10(value) / 3).to_i
80
+ suffix =
81
+ case scale
82
+ when 1; 'k'
83
+ when 2; 'M'
84
+ when 3; 'B'
85
+ when 4; 'T'
86
+ when 5; 'Q'
87
+ else # < 1000 or > 10^15, no scale or suffix
88
+ scale = 0
89
+ ' '
90
+ end
91
+ "%#{width}.3f#{suffix}" % (value.to_f / (1000 ** scale))
92
+ end
93
+
94
+ def compare
95
+ $stdout.puts("\nComparison:")
96
+ results = @results.sort_by { |r| -r.ips }
97
+ first = results.first
98
+
99
+ results.each do |result|
100
+ if result == first
101
+ slower = ''
102
+ else
103
+ slower = '- %.2fx slower' % (first.ips / result.ips)
104
+ end
105
+
106
+ name = result.job.name
107
+ if @executables.size > 1
108
+ name = "#{name} (#{@name_by_result.fetch(result)})"
109
+ end
110
+ $stdout.puts("%#{NAME_LENGTH}s: %11.1f i/s #{slower}" % [name, result.ips])
111
+ end
112
+ $stdout.puts
113
+ end
114
+ end