benchmark_driver 0.3.0 → 0.4.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.
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