benchmark_driver 0.8.6 → 0.9.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 (58) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +1 -3
  3. data/CHANGELOG.md +9 -0
  4. data/Gemfile +1 -6
  5. data/README.md +51 -52
  6. data/benchmark_driver.gemspec +3 -2
  7. data/bin/console +4 -11
  8. data/examples/exec_blank.rb +2 -2
  9. data/examples/exec_blank_simple.rb +2 -3
  10. data/exe/benchmark-driver +74 -83
  11. data/lib/benchmark_driver.rb +12 -1
  12. data/lib/benchmark_driver/config.rb +36 -0
  13. data/lib/benchmark_driver/default_job.rb +12 -0
  14. data/lib/benchmark_driver/default_job_parser.rb +68 -0
  15. data/lib/benchmark_driver/job_parser.rb +42 -0
  16. data/lib/benchmark_driver/metrics.rb +17 -0
  17. data/lib/benchmark_driver/output.rb +27 -0
  18. data/lib/benchmark_driver/output/compare.rb +196 -0
  19. data/lib/benchmark_driver/output/markdown.rb +102 -0
  20. data/lib/benchmark_driver/output/simple.rb +97 -0
  21. data/lib/benchmark_driver/rbenv.rb +11 -0
  22. data/lib/benchmark_driver/ruby_interface.rb +51 -0
  23. data/lib/benchmark_driver/runner.rb +42 -0
  24. data/lib/benchmark_driver/runner/ips.rb +239 -0
  25. data/lib/benchmark_driver/runner/memory.rb +142 -0
  26. data/lib/benchmark_driver/runner/time.rb +18 -0
  27. data/lib/benchmark_driver/struct.rb +85 -0
  28. data/lib/benchmark_driver/version.rb +3 -0
  29. metadata +21 -33
  30. data/bin/bench +0 -4
  31. data/examples/call.rb +0 -12
  32. data/examples/call_blank.rb +0 -13
  33. data/examples/call_erb.rb +0 -33
  34. data/examples/call_interpolation.rb +0 -13
  35. data/examples/eval_blank.rb +0 -12
  36. data/examples/eval_blank_loop.rb +0 -13
  37. data/examples/eval_interpolation.rb +0 -15
  38. data/lib/benchmark/driver.rb +0 -101
  39. data/lib/benchmark/driver/benchmark_result.rb +0 -21
  40. data/lib/benchmark/driver/bundle_installer.rb +0 -45
  41. data/lib/benchmark/driver/bundler.rb +0 -12
  42. data/lib/benchmark/driver/configuration.rb +0 -77
  43. data/lib/benchmark/driver/duration_runner.rb +0 -24
  44. data/lib/benchmark/driver/error.rb +0 -16
  45. data/lib/benchmark/driver/repeatable_runner.rb +0 -18
  46. data/lib/benchmark/driver/ruby_dsl_parser.rb +0 -78
  47. data/lib/benchmark/driver/time.rb +0 -12
  48. data/lib/benchmark/driver/version.rb +0 -5
  49. data/lib/benchmark/driver/yaml_parser.rb +0 -55
  50. data/lib/benchmark/output.rb +0 -20
  51. data/lib/benchmark/output/ips.rb +0 -143
  52. data/lib/benchmark/output/markdown.rb +0 -73
  53. data/lib/benchmark/output/memory.rb +0 -57
  54. data/lib/benchmark/output/time.rb +0 -57
  55. data/lib/benchmark/runner.rb +0 -14
  56. data/lib/benchmark/runner/call.rb +0 -97
  57. data/lib/benchmark/runner/eval.rb +0 -147
  58. data/lib/benchmark/runner/exec.rb +0 -193
@@ -1 +1,12 @@
1
- require 'benchmark/driver'
1
+ require 'benchmark_driver/config'
2
+ require 'benchmark_driver/job_parser'
3
+ require 'benchmark_driver/output'
4
+ require 'benchmark_driver/rbenv'
5
+ require 'benchmark_driver/ruby_interface'
6
+ require 'benchmark_driver/runner'
7
+ require 'benchmark_driver/version'
8
+
9
+ require 'benchmark'
10
+ def Benchmark.driver(**args, &block)
11
+ BenchmarkDriver::RubyInterface.run(**args, &block)
12
+ end
@@ -0,0 +1,36 @@
1
+ require 'benchmark_driver/struct'
2
+
3
+ module BenchmarkDriver
4
+ # All CLI options
5
+ Config = ::BenchmarkDriver::Struct.new(
6
+ :runner_type, # @param [String]
7
+ :output_type, # @param [String]
8
+ :paths, # @param [Array<String>]
9
+ :executables, # @param [Array<BenchmarkDriver::Config::Executable>]
10
+ :filters, # @param [Array<Regexp>]
11
+ :repeat_count, # @param [Integer]
12
+ :run_duration, # @param [Integer]
13
+ defaults: {
14
+ runner_type: 'ips',
15
+ output_type: 'compare',
16
+ filters: [],
17
+ repeat_count: 1,
18
+ run_duration: 3,
19
+ },
20
+ )
21
+
22
+ # Subset of FullConfig passed to JobRunner
23
+ Config::RunnerConfig = ::BenchmarkDriver::Struct.new(
24
+ :executables, # @param [Array<BenchmarkDriver::Config::Executable>]
25
+ :repeat_count, # @param [Integer]
26
+ :run_duration, # @param [Integer]
27
+ )
28
+
29
+ Config::Executable = ::BenchmarkDriver::Struct.new(
30
+ :name, # @param [String]
31
+ :command, # @param [Array<String>]
32
+ )
33
+ Config.defaults[:executables] = [
34
+ BenchmarkDriver::Config::Executable.new(name: RUBY_VERSION, command: [RbConfig.ruby]),
35
+ ]
36
+ end
@@ -0,0 +1,12 @@
1
+ require 'benchmark_driver/struct'
2
+
3
+ module BenchmarkDriver
4
+ DefaultJob = ::BenchmarkDriver::Struct.new(
5
+ :name, # @param [String] name
6
+ :script, # @param [String] benchmark
7
+ :prelude, # @param [String,nil] prelude (optional)
8
+ :teardown, # @param [String,nil] after (optional)
9
+ :loop_count, # @param [Integer,nil] loop_count (optional)
10
+ defaults: { prelude: '', teardown: '' },
11
+ )
12
+ end
@@ -0,0 +1,68 @@
1
+ module BenchmarkDriver
2
+ module DefaultJobParser
3
+ # Build default JobParser for given job klass
4
+ def self.for(klass)
5
+ Module.new.tap do |parser|
6
+ class << parser
7
+ include DefaultJobParser
8
+ end
9
+ parser.define_singleton_method(:job_class) do
10
+ klass
11
+ end
12
+ end
13
+ end
14
+
15
+ # This method is dynamically called by `BenchmarkDriver::JobParser.parse`
16
+ # @param [String] prelude
17
+ # @param [String,Array<String,Hash{ Symbol => String }>,Hash{ Symbol => String }] benchmark
18
+ # @param [String] teardown
19
+ # @param [Integer] loop_count
20
+ # @return [Array<BenchmarkDriver::Default::Job>]
21
+ def parse(prelude: nil, benchmark:, teardown: nil, loop_count: nil)
22
+ parse_benchmark(benchmark).each do |job|
23
+ job.prelude.prepend("#{prelude}\n") if prelude
24
+ job.teardown.prepend("#{teardown}\n") if teardown
25
+ job.loop_count ||= loop_count
26
+ end.each(&:freeze)
27
+ end
28
+
29
+ private
30
+
31
+ # @param [String,Array<String,Hash{ Symbol => String }>,Hash{ Symbol => String }] benchmark
32
+ def parse_benchmark(benchmark)
33
+ case benchmark
34
+ when String
35
+ [parse_job(benchmark)]
36
+ when Array
37
+ benchmark.map { |b| parse_job(b) }
38
+ when Hash
39
+ benchmark.map do |key, value|
40
+ job_class.new(name: key.to_s, script: value)
41
+ end
42
+ else
43
+ raise ArgumentError.new("benchmark must be String, Array or Hash, but got: #{benchmark.inspect}")
44
+ end
45
+ end
46
+
47
+ # @param [String,Hash{ Symbol => String }>] bench
48
+ def parse_job(benchmark)
49
+ case benchmark
50
+ when String
51
+ job_class.new(name: benchmark, script: benchmark)
52
+ when Hash
53
+ parse_job_hash(benchmark)
54
+ else
55
+ raise ArgumentError.new("Expected String or Hash in element of benchmark, but got: #{benchmark.inspect}")
56
+ end
57
+ end
58
+
59
+ def parse_job_hash(name: nil, prelude: '', script:, teardown: '', loop_count: nil)
60
+ name ||= script
61
+ job_class.new(name: name, prelude: prelude, script: script, teardown: teardown, loop_count: loop_count)
62
+ end
63
+
64
+ def job_class
65
+ raise NotImplementedError # override this
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,42 @@
1
+ require 'benchmark_driver/runner'
2
+
3
+ module BenchmarkDriver
4
+ class << JobParser = Module.new
5
+ # @param [Hash] config
6
+ def parse(config)
7
+ config = symbolize_keys(config)
8
+ type = config.fetch(:type)
9
+ if !type.is_a?(String)
10
+ raise ArgumentError.new("Invalid type: #{config[:type].inspect} (expected String)")
11
+ elsif !type.match(/\A[A-Za-z0-9_]+\z/)
12
+ raise ArgumentError.new("Invalid type: #{config[:type].inspect} (expected to include only [A-Za-z0-9_])")
13
+ end
14
+ config.delete(:type)
15
+
16
+ # Dynamic dispatch for plugin support
17
+ ::BenchmarkDriver.const_get("Runner::#{camelize(type)}::JobParser", false).parse(config)
18
+ end
19
+
20
+ private
21
+
22
+ def camelize(str)
23
+ str.split('_').map(&:capitalize).join
24
+ end
25
+
26
+ # @param [Object] config
27
+ def symbolize_keys(config)
28
+ case config
29
+ when Hash
30
+ config.dup.tap do |hash|
31
+ hash.keys.each do |key|
32
+ hash[key.to_sym] = symbolize_keys(hash.delete(key))
33
+ end
34
+ end
35
+ when Array
36
+ config.map { |c| symbolize_keys(c) }
37
+ else
38
+ config
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ require 'benchmark_driver/struct'
2
+
3
+ # All benchmark results should be expressed by this model.
4
+ module BenchmarkDriver
5
+ Metrics = ::BenchmarkDriver::Struct.new(
6
+ :value, # @param [Float] - The main field of benchmark result
7
+ :executable, # @param [BenchmarkDriver::Config::Executable] - Measured Ruby executable
8
+ :duration, # @param [Float,nil] - Time taken to run the script (optional)
9
+ )
10
+
11
+ Metrics::Type = ::BenchmarkDriver::Struct.new(
12
+ :unit, # @param [String] - A label of unit for the value.
13
+ :larger_better, # @param [TrueClass,FalseClass] - If true, larger value is preferred when measured multiple times.
14
+ :worse_word, # @param [String] - A label shown when the value is worse.
15
+ defaults: { larger_better: true, worse_word: 'slower' },
16
+ )
17
+ end
@@ -0,0 +1,27 @@
1
+ module BenchmarkDriver
2
+ module Output
3
+ require 'benchmark_driver/output/compare'
4
+ require 'benchmark_driver/output/markdown'
5
+ require 'benchmark_driver/output/simple'
6
+ end
7
+
8
+ class << Output
9
+ # BenchmarkDriver::Output is pluggable.
10
+ # Create `BenchmarkDriver::Output::Foo` as benchmark_dirver-output-foo.gem and specify `-o foo`.
11
+ #
12
+ # @param [String] type
13
+ def find(type)
14
+ if type.include?(':')
15
+ raise ArgumentError.new("Output type '#{type}' cannot contain ':'")
16
+ end
17
+
18
+ ::BenchmarkDriver::Output.const_get(camelize(type), false)
19
+ end
20
+
21
+ private
22
+
23
+ def camelize(str)
24
+ str.split('_').map(&:capitalize).join
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,196 @@
1
+ # Compare output like benchmark-ips
2
+ class BenchmarkDriver::Output::Compare
3
+ NAME_LENGTH = 20
4
+
5
+ # @param [Array<BenchmarkDriver::*::Job>] jobs
6
+ # @param [Array<BenchmarkDriver::Config::Executable>] executables
7
+ # @param [BenchmarkDriver::Metrics::Type] metrics_type
8
+ def initialize(jobs:, executables:, metrics_type:)
9
+ @jobs = jobs
10
+ @executables = executables
11
+ @metrics_type = metrics_type
12
+ end
13
+
14
+ # @param [BenchmarkDriver::Metrics] metrics
15
+ def with_warmup(&block)
16
+ without_stdout_buffering do
17
+ $stdout.puts 'Warming up --------------------------------------'
18
+ # TODO: show exec name if it has multiple ones
19
+ block.call
20
+ end
21
+ end
22
+
23
+ # @param [BenchmarkDriver::Metrics] metrics
24
+ def with_benchmark(&block)
25
+ @metrics_by_job = Hash.new { |h, k| h[k] = [] }
26
+
27
+ without_stdout_buffering do
28
+ $stdout.puts 'Calculating -------------------------------------'
29
+ if @executables.size > 1
30
+ $stdout.print(' ' * NAME_LENGTH)
31
+ @executables.each do |executable|
32
+ $stdout.print(' %10s ' % executable.name)
33
+ end
34
+ $stdout.puts
35
+ end
36
+
37
+ block.call
38
+ end
39
+ ensure
40
+ if @executables.size > 1
41
+ compare_executables
42
+ elsif @jobs.size > 1
43
+ compare_jobs
44
+ end
45
+ end
46
+
47
+ # @param [BenchmarkDriver::*::Job] job
48
+ def with_job(job, &block)
49
+ if job.name.length > NAME_LENGTH
50
+ $stdout.puts(job.name)
51
+ else
52
+ $stdout.print("%#{NAME_LENGTH}s" % job.name)
53
+ end
54
+ @current_job = job
55
+ @job_metrics = []
56
+ block.call
57
+ ensure
58
+ $stdout.print(@metrics_type.unit)
59
+ if job.loop_count
60
+ $stdout.print(" - #{humanize(job.loop_count)} times")
61
+ if @job_metrics.all? { |metrics| metrics.duration }
62
+ $stdout.print(" in")
63
+ show_durations
64
+ end
65
+ end
66
+ $stdout.puts
67
+ end
68
+
69
+ # @param [BenchmarkDriver::Metrics] metrics
70
+ def report(metrics)
71
+ if defined?(@metrics_by_job)
72
+ @metrics_by_job[@current_job] << metrics
73
+ end
74
+
75
+ @job_metrics << metrics
76
+ $stdout.print("#{humanize(metrics.value, [10, metrics.executable.name.length].max)} ")
77
+ end
78
+
79
+ private
80
+
81
+ def show_durations
82
+ @job_metrics.each do |metrics|
83
+ $stdout.print(' %3.6fs' % metrics.duration)
84
+ end
85
+
86
+ # Show pretty seconds / clocks too. As it takes long width, it's shown only with a single executable.
87
+ if @job_metrics.size == 1
88
+ metrics = @job_metrics.first
89
+ sec = metrics.duration
90
+ iter = @current_job.loop_count
91
+ if File.exist?('/proc/cpuinfo') && (clks = estimate_clock(sec, iter)) < 1_000
92
+ $stdout.print(" (#{pretty_sec(sec, iter)}/i, #{clks}clocks/i)")
93
+ else
94
+ $stdout.print(" (#{pretty_sec(sec, iter)}/i)")
95
+ end
96
+ end
97
+ end
98
+
99
+ # benchmark_driver ouputs logs ASAP. This enables sync flag for it.
100
+ def without_stdout_buffering
101
+ sync, $stdout.sync = $stdout.sync, true
102
+ yield
103
+ ensure
104
+ $stdout.sync = sync
105
+ end
106
+
107
+ def humanize(value, width = 10)
108
+ if value < 0
109
+ raise ArgumentError.new("Negative value: #{value.inspect}")
110
+ end
111
+
112
+ scale = (Math.log10(value) / 3).to_i
113
+ prefix = "%#{width}.3f" % (value.to_f / (1000 ** scale))
114
+ suffix =
115
+ case scale
116
+ when 1; 'k'
117
+ when 2; 'M'
118
+ when 3; 'G'
119
+ when 4; 'T'
120
+ when 5; 'Q'
121
+ else # < 1000 or > 10^15, no scale or suffix
122
+ scale = 0
123
+ return " #{prefix}"
124
+ end
125
+ "#{prefix}#{suffix}"
126
+ end
127
+
128
+ def pretty_sec(sec, iter)
129
+ r = Rational(sec, iter)
130
+ case
131
+ when r >= 1
132
+ "#{'%3.2f' % r.to_f}s"
133
+ when r >= 1/1000r
134
+ "#{'%3.2f' % (r * 1_000).to_f}ms"
135
+ when r >= 1/1000_000r
136
+ "#{'%3.2f' % (r * 1_000_000).to_f}μs"
137
+ else
138
+ "#{'%3.2f' % (r * 1_000_000_000).to_f}ns"
139
+ end
140
+ end
141
+
142
+ def estimate_clock sec, iter
143
+ hz = File.read('/proc/cpuinfo').scan(/cpu MHz\s+:\s+([\d\.]+)/){|(f)| break hz = Rational(f.to_f) * 1_000_000}
144
+ r = Rational(sec, iter)
145
+ Integer(r/(1/hz))
146
+ end
147
+
148
+ def compare_jobs
149
+ $stdout.puts "\nComparison:"
150
+ results = @metrics_by_job.map { |job, metrics| Result.new(job: job, metrics: metrics.first) }
151
+ show_results(results, show_executable: false)
152
+ end
153
+
154
+ def compare_executables
155
+ $stdout.puts "\nComparison:"
156
+
157
+ @metrics_by_job.each do |job, metrics|
158
+ $stdout.puts("%#{NAME_LENGTH + 2 + 11}s" % job.name)
159
+ results = metrics.map { |metrics| Result.new(job: job, metrics: metrics) }
160
+ show_results(results, show_executable: true)
161
+ end
162
+ end
163
+
164
+ # @param [Array<BenchmarkDriver::Output::Compare::Result>] results
165
+ # @param [TrueClass,FalseClass] show_executable
166
+ def show_results(results, show_executable:)
167
+ results = results.sort_by do |result|
168
+ if @metrics_type.larger_better
169
+ -result.metrics.value
170
+ else
171
+ result.metrics.value
172
+ end
173
+ end
174
+
175
+ first = results.first
176
+ results.each do |result|
177
+ if result != first
178
+ if @metrics_type.larger_better
179
+ ratio = (first.metrics.value / result.metrics.value)
180
+ else
181
+ ratio = (result.metrics.value / first.metrics.value)
182
+ end
183
+ slower = "- %.2fx #{@metrics_type.worse_word}" % ratio
184
+ end
185
+ if show_executable
186
+ name = result.metrics.executable.name
187
+ else
188
+ name = result.job.name
189
+ end
190
+ $stdout.puts("%#{NAME_LENGTH}s: %11.1f %s #{slower}" % [name, result.metrics.value, @metrics_type.unit])
191
+ end
192
+ $stdout.puts
193
+ end
194
+
195
+ Result = ::BenchmarkDriver::Struct.new(:job, :metrics)
196
+ end
@@ -0,0 +1,102 @@
1
+ class BenchmarkDriver::Output::Markdown
2
+ NAME_LENGTH = 8
3
+
4
+ # @param [Array<BenchmarkDriver::*::Job>] jobs
5
+ # @param [Array<BenchmarkDriver::Config::Executable>] executables
6
+ # @param [BenchmarkDriver::Metrics::Type] metrics_type
7
+ def initialize(jobs:, executables:, metrics_type:)
8
+ @jobs = jobs
9
+ @executables = executables
10
+ @metrics_type = metrics_type
11
+ @name_length = jobs.map { |j| j.name.size }.max
12
+ end
13
+
14
+ # @param [BenchmarkDriver::Metrics] metrics
15
+ def with_warmup(&block)
16
+ without_stdout_buffering do
17
+ $stdout.print 'warming up'
18
+ block.call
19
+ end
20
+ ensure
21
+ $stdout.puts
22
+ end
23
+
24
+ # @param [BenchmarkDriver::Metrics] metrics
25
+ def with_benchmark(&block)
26
+ @with_benchmark = true
27
+ without_stdout_buffering do
28
+ # Show header
29
+ $stdout.puts "# benchmark results (#{@metrics_type.unit})\n\n"
30
+
31
+ # Show executable names
32
+ $stdout.print("|#{' ' * @name_length} ")
33
+ @executables.each do |executable|
34
+ $stdout.print("|%#{NAME_LENGTH}s" % executable.name) # same size as humanize
35
+ end
36
+ $stdout.puts('|')
37
+
38
+ # Show header separator
39
+ $stdout.print("|:#{'-' * (@name_length - 1)}--")
40
+ @executables.each do |executable|
41
+ $stdout.print("|:#{'-' * (NAME_LENGTH - 1)}") # same size as humanize
42
+ end
43
+ $stdout.puts('|')
44
+
45
+ block.call
46
+ end
47
+ rescue
48
+ @with_benchmark = false
49
+ end
50
+
51
+ # @param [BenchmarkDriver::*::Job] job
52
+ def with_job(job, &block)
53
+ if @with_benchmark
54
+ $stdout.print("|%-#{@name_length}s " % job.name)
55
+ end
56
+ block.call
57
+ ensure
58
+ if @with_benchmark
59
+ $stdout.puts('|')
60
+ end
61
+ end
62
+
63
+ # @param [BenchmarkDriver::Metrics] metrics
64
+ def report(metrics)
65
+ if @with_benchmark
66
+ $stdout.print("|%#{NAME_LENGTH}s" % humanize(metrics.value))
67
+ else
68
+ $stdout.print '.'
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ # benchmark_driver ouputs logs ASAP. This enables sync flag for it.
75
+ def without_stdout_buffering
76
+ sync, $stdout.sync = $stdout.sync, true
77
+ yield
78
+ ensure
79
+ $stdout.sync = sync
80
+ end
81
+
82
+ def humanize(value)
83
+ if value < 0
84
+ raise ArgumentError.new("Negative value: #{value.inspect}")
85
+ end
86
+
87
+ scale = (Math.log10(value) / 3).to_i
88
+ prefix = "%6.3f" % (value.to_f / (1000 ** scale))
89
+ suffix =
90
+ case scale
91
+ when 1; 'k'
92
+ when 2; 'M'
93
+ when 3; 'G'
94
+ when 4; 'T'
95
+ when 5; 'Q'
96
+ else # < 1000 or > 10^15, no scale or suffix
97
+ scale = 0
98
+ return " #{prefix}"
99
+ end
100
+ "#{prefix}#{suffix}"
101
+ end
102
+ end