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,57 @@
1
+ class Benchmark::Output::Memory
2
+ # This class requires runner to measure following fields in `Benchmark::Driver::BenchmarkResult` to show output.
3
+ REQUIRED_FIELDS = [:max_rss]
4
+
5
+ # @param [Array<Benchmark::Driver::Configuration::Job>] jobs
6
+ # @param [Array<Benchmark::Driver::Configuration::Executable>] executables
7
+ # @param [Benchmark::Driver::Configuration::OutputOptions] options
8
+ def initialize(jobs:, executables:, options:)
9
+ @jobs = jobs
10
+ @executables = executables
11
+ @options = options
12
+ @name_length = jobs.map { |j| j.name.size }.max
13
+ end
14
+
15
+ def start_warming
16
+ $stdout.print 'warming up...'
17
+ end
18
+
19
+ # @param [String] name
20
+ def warming(name)
21
+ # noop
22
+ end
23
+
24
+ # @param [Benchmark::Driver::BenchmarkResult] result
25
+ def warmup_stats(result)
26
+ $stdout.print '.'
27
+ end
28
+
29
+ def start_running
30
+ $stdout.puts if @jobs.any?(&:warmup_needed?)
31
+ $stdout.puts 'max resident memory (KB):'
32
+ $stdout.print("%-#{@name_length}s " % 'ruby')
33
+ @executables.each do |executable|
34
+ $stdout.print('%-6s ' % executable.name)
35
+ end
36
+ $stdout.puts
37
+ end
38
+
39
+ # @param [String] name
40
+ def running(name)
41
+ $stdout.print("%-#{@name_length}s " % name)
42
+ @ran_num = 0
43
+ end
44
+
45
+ # @param [Benchmark::Driver::BenchmarkResult] result
46
+ def benchmark_stats(result)
47
+ $stdout.print('%-6d ' % result.max_rss)
48
+ @ran_num += 1
49
+ if @ran_num == @executables.size
50
+ $stdout.puts
51
+ end
52
+ end
53
+
54
+ def finish
55
+ # compare is not implemented yet
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ class Benchmark::Output::Time
2
+ # This class requires runner to measure following fields in `Benchmark::Driver::BenchmarkResult` to show output.
3
+ REQUIRED_FIELDS = [:real]
4
+
5
+ # @param [Array<Benchmark::Driver::Configuration::Job>] jobs
6
+ # @param [Array<Benchmark::Driver::Configuration::Executable>] executables
7
+ # @param [Benchmark::Driver::Configuration::OutputOptions] options
8
+ def initialize(jobs:, executables:, options:)
9
+ @jobs = jobs
10
+ @executables = executables
11
+ @options = options
12
+ @name_length = jobs.map { |j| j.name.size }.max
13
+ end
14
+
15
+ def start_warming
16
+ $stdout.print 'warming up...'
17
+ end
18
+
19
+ # @param [String] name
20
+ def warming(name)
21
+ # noop
22
+ end
23
+
24
+ # @param [Benchmark::Driver::BenchmarkResult] result
25
+ def warmup_stats(result)
26
+ $stdout.print '.'
27
+ end
28
+
29
+ def start_running
30
+ $stdout.puts if @jobs.any?(&:warmup_needed?)
31
+ $stdout.puts 'benchmark results (s):'
32
+ $stdout.print("%-#{@name_length}s " % 'ruby')
33
+ @executables.each do |executable|
34
+ $stdout.print('%-6s ' % executable.name)
35
+ end
36
+ $stdout.puts
37
+ end
38
+
39
+ # @param [String] name
40
+ def running(name)
41
+ $stdout.print("%-#{@name_length}s " % name)
42
+ @ran_num = 0
43
+ end
44
+
45
+ # @param [Benchmark::Driver::BenchmarkResult] result
46
+ def benchmark_stats(result)
47
+ $stdout.print('%-6.3f ' % result.real)
48
+ @ran_num += 1
49
+ if @ran_num == @executables.size
50
+ $stdout.puts
51
+ end
52
+ end
53
+
54
+ def finish
55
+ # compare is not implemented yet
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ module Benchmark::Runner
2
+ # Benchmark::Runner is pluggable.
3
+ # Create `Benchmark::Runner::FooBar` as benchmark-runner-foo_bar.gem and specify `runner: foo_bar`.
4
+ #
5
+ # @param [Symbol] name
6
+ def self.find(name)
7
+ class_name = Benchmark::Driver::Configuration.camelize(name.to_s)
8
+ Benchmark::Runner.const_get(class_name, false)
9
+ end
10
+ end
11
+
12
+ require 'benchmark/runner/call'
13
+ require 'benchmark/runner/exec'
@@ -0,0 +1,97 @@
1
+ require 'benchmark/driver/benchmark_result'
2
+ require 'benchmark/driver/duration_runner'
3
+ require 'benchmark/driver/repeatable_runner'
4
+ require 'benchmark/driver/time'
5
+
6
+ # Run benchmark by calling #call on running ruby.
7
+ #
8
+ # Multiple Ruby binaries: x
9
+ # Memory output: x
10
+ class Benchmark::Runner::Call
11
+ # This class can provide fields in `Benchmark::Driver::BenchmarkResult` if required by output plugins.
12
+ SUPPORTED_FIELDS = [:real]
13
+
14
+ WARMUP_DURATION = 2
15
+ BENCHMARK_DURATION = 5
16
+
17
+ # @param [Benchmark::Driver::Configuration::RunnerOptions] options
18
+ # @param [Benchmark::Output::*] output - Object that responds to methods used in this class
19
+ def initialize(options, output:)
20
+ @options = options
21
+ @output = output
22
+ end
23
+
24
+ # @param [Benchmark::Driver::Configuration] config
25
+ def run(config)
26
+ validate_config(config)
27
+
28
+ if config.jobs.any?(&:warmup_needed?)
29
+ run_warmup(config.jobs)
30
+ end
31
+
32
+ @output.start_running
33
+
34
+ config.jobs.each do |job|
35
+ @output.running(job.name)
36
+
37
+ result = Benchmark::Driver::RepeatableRunner.new(job).run(
38
+ runner: method(:call_times),
39
+ repeat_count: @options.repeat_count,
40
+ )
41
+
42
+ @output.benchmark_stats(result)
43
+ end
44
+
45
+ @output.finish
46
+ end
47
+
48
+ private
49
+
50
+ def validate_config(config)
51
+ if config.runner_options.executables_specified?
52
+ raise ArgumentError.new("Benchmark::Runner::Call can't run other Ruby executables")
53
+ end
54
+
55
+ config.jobs.each do |job|
56
+ unless job.script.respond_to?(:call)
57
+ raise NotImplementedError.new(
58
+ "#{self.class.name} only accepts objects that respond to :call, but got #{job.script.inspect}"
59
+ )
60
+ end
61
+ end
62
+ end
63
+
64
+ # @param [Array<Benchmark::Driver::Configuration::Job>] jobs
65
+ # @return [Hash{ Benchmark::Driver::Configuration::Job => Integer }] iters_by_job
66
+ def run_warmup(jobs)
67
+ @output.start_warming
68
+
69
+ jobs.each do |job|
70
+ next if job.loop_count
71
+ @output.warming(job.name)
72
+
73
+ result = Benchmark::Driver::DurationRunner.new(job).run(
74
+ seconds: WARMUP_DURATION,
75
+ unit_iters: 1,
76
+ runner: method(:call_times),
77
+ )
78
+ job.guessed_count = (result.ips.to_f * BENCHMARK_DURATION).to_i
79
+
80
+ @output.warmup_stats(result)
81
+ end
82
+ end
83
+
84
+ def call_times(job, times)
85
+ script = job.script
86
+ i = 0
87
+
88
+ before = Benchmark::Driver::Time.now
89
+ while i < times
90
+ script.call
91
+ i += 1
92
+ end
93
+ after = Benchmark::Driver::Time.now
94
+
95
+ after.to_f - before.to_f
96
+ end
97
+ end
@@ -0,0 +1,190 @@
1
+ require 'tempfile'
2
+ require 'shellwords'
3
+ require 'benchmark/driver/benchmark_result'
4
+ require 'benchmark/driver/duration_runner'
5
+ require 'benchmark/driver/repeatable_runner'
6
+ require 'benchmark/driver/error'
7
+ require 'benchmark/driver/time'
8
+
9
+ # Run benchmark by executing another Ruby process.
10
+ #
11
+ # Multiple Ruby binaries: o
12
+ # Memory output: o
13
+ class Benchmark::Runner::Exec
14
+ # This class can provide fields in `Benchmark::Driver::BenchmarkResult` if required by output plugins.
15
+ SUPPORTED_FIELDS = [:real, :max_rss]
16
+
17
+ WARMUP_DURATION = 1
18
+ BENCHMARK_DURATION = 4
19
+ GUESS_TIMES = [1, 1_000, 1_000_000, 10_000_000, 100_000_000]
20
+ GUESS_THRESHOLD = 0.4 # 400ms
21
+
22
+ # @param [Benchmark::Driver::Configuration::RunnerOptions] options
23
+ # @param [Benchmark::Output::*] output - Object that responds to methods used in this class
24
+ def initialize(options, output:)
25
+ @options = options
26
+ @output = output
27
+ end
28
+
29
+ # @param [Benchmark::Driver::Configuration] config
30
+ def run(config)
31
+ validate_config(config)
32
+
33
+ if config.jobs.any?(&:warmup_needed?)
34
+ run_warmup(config.jobs)
35
+ end
36
+
37
+ @output.start_running
38
+
39
+ config.jobs.each do |job|
40
+ @output.running(job.name)
41
+
42
+ @options.executables.each do |executable|
43
+ result = run_benchmark(job, executable)
44
+ @output.benchmark_stats(result)
45
+ end
46
+ end
47
+
48
+ @output.finish
49
+ end
50
+
51
+ private
52
+
53
+ def validate_config(config)
54
+ config.jobs.each do |job|
55
+ unless job.script.is_a?(String)
56
+ raise NotImplementedError.new(
57
+ "#{self.class.name} only accepts String, but got #{job.script.inspect}"
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ # @param [Benchmark::Driver::Configuration::Job] job
64
+ # @param [Benchmark::Driver::Configuration::Executable] executable
65
+ def run_benchmark(job, executable)
66
+ fields = @output.class::REQUIRED_FIELDS
67
+ if fields == [:real]
68
+ Benchmark::Driver::RepeatableRunner.new(job).run(
69
+ runner: build_runner(executable.path),
70
+ repeat_count: @options.repeat_count,
71
+ ).tap do |result|
72
+ if result.real < 0
73
+ raise Benchmark::Driver::ExecutionTimeTooShort.new(job, result.iterations)
74
+ end
75
+ end
76
+ elsif fields == [:max_rss] # TODO: we can also capture other metrics with /usr/bin/time
77
+ raise '/usr/bin/time is not available' unless File.exist?('/usr/bin/time')
78
+
79
+ script = BenchmarkScript.new(job.prelude, job.script).full_script(job.loop_count)
80
+ with_file(script) do |script_path|
81
+ out = IO.popen(['/usr/bin/time', executable.path, script_path], err: [:child, :out], &:read)
82
+ match_data = /^(?<user>\d+.\d+)user\s+(?<system>\d+.\d+)system\s+(?<elapsed1>\d+):(?<elapsed2>\d+.\d+)elapsed.+\([^\s]+\s+(?<maxresident>\d+)maxresident\)k$/.match(out)
83
+ raise "Unexpected format given from /usr/bin/time:\n#{out}" unless match_data[:maxresident]
84
+
85
+ Benchmark::Driver::BenchmarkResult.new(job).tap do |result|
86
+ result.max_rss = Integer(match_data[:maxresident])
87
+ end
88
+ end
89
+ else
90
+ raise "Unexpected REQUIRED_FIELDS for #{self.class.name}: #{fields.inspect}"
91
+ end
92
+ end
93
+
94
+ # @param [Array<Benchmark::Driver::Configuration::Job>] jobs
95
+ # @return [Hash{ Benchmark::Driver::Configuration::Job => Integer }] iters_by_job
96
+ def run_warmup(jobs)
97
+ @output.start_warming
98
+
99
+ jobs.each do |job|
100
+ next if job.loop_count
101
+ @output.warming(job.name)
102
+
103
+ result = Benchmark::Driver::DurationRunner.new(job).run(
104
+ seconds: WARMUP_DURATION,
105
+ unit_iters: guess_ip100ms(job),
106
+ runner: build_runner, # TODO: should use executables instead of RbConfig.ruby
107
+ )
108
+ job.guessed_count = (result.ips.to_f * BENCHMARK_DURATION).to_i
109
+
110
+ if result.duration < 0
111
+ raise Benchmark::Driver::ExecutionTimeTooShort.new(job, result.iterations)
112
+ end
113
+ @output.warmup_stats(result)
114
+ end
115
+ end
116
+
117
+ # @param [Benchmark::Driver::Configuration::Job] job
118
+ def guess_ip100ms(job)
119
+ ip100ms = 0
120
+ GUESS_TIMES.each do |times|
121
+ seconds = build_runner.call(job, times) # TODO: should use executables instead of RbConfig.ruby
122
+ ip100ms = (times.to_f / (seconds * 10.0)).ceil # ceil for times=1
123
+ if GUESS_THRESHOLD < seconds
124
+ return ip100ms
125
+ end
126
+ end
127
+ if ip100ms < 0
128
+ raise Benchmark::Driver::ExecutionTimeTooShort.new(job, GUESS_TIMES.last)
129
+ end
130
+ ip100ms
131
+ end
132
+
133
+ # @param [String] path - Path to Ruby executable
134
+ # @return [Proc] - Lambda to run benchmark
135
+ def build_runner(path = RbConfig.ruby)
136
+ lambda do |job, times|
137
+ benchmark = BenchmarkScript.new(job.prelude, job.script)
138
+ measure_seconds(path, benchmark.full_script(times)) -
139
+ measure_seconds(path, benchmark.overhead_script(times))
140
+ end
141
+ end
142
+
143
+ def with_file(content, &block)
144
+ Tempfile.create(File.basename(__FILE__)) do |f|
145
+ f.write(content)
146
+ f.close
147
+ block.call(f.path)
148
+ end
149
+ end
150
+
151
+ def measure_seconds(ruby, script)
152
+ with_file(script) do |path|
153
+ cmd = [ruby, path].shelljoin
154
+
155
+ before = Benchmark::Driver::Time.now
156
+ system(cmd, out: File::NULL)
157
+ after = Benchmark::Driver::Time.now
158
+
159
+ after.to_f - before.to_f
160
+ end
161
+ end
162
+
163
+ class BenchmarkScript < Struct.new(:prelude, :script)
164
+ BATCH_SIZE = 50
165
+
166
+ def overhead_script(times)
167
+ raise ArgumentError.new("Negative times: #{times}") if times < 0
168
+ <<-RUBY
169
+ #{prelude}
170
+ __benchmark_driver_i = 0
171
+ while __benchmark_driver_i < #{times / BATCH_SIZE}
172
+ __benchmark_driver_i += 1
173
+ end
174
+ RUBY
175
+ end
176
+
177
+ def full_script(times)
178
+ raise ArgumentError.new("Negative times: #{times}") if times < 0
179
+ <<-RUBY
180
+ #{prelude}
181
+ __benchmark_driver_i = 0
182
+ while __benchmark_driver_i < #{times / BATCH_SIZE}
183
+ __benchmark_driver_i += 1
184
+ #{"#{script};" * BATCH_SIZE}
185
+ end
186
+ #{"#{script};" * (times % BATCH_SIZE)}
187
+ RUBY
188
+ end
189
+ end
190
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: benchmark_driver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-22 00:00:00.000000000 Z
11
+ date: 2017-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,7 +38,7 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- description: Benchmark driver for different Ruby executables
41
+ description: Fully-featured accurate benchmark driver for Ruby
42
42
  email:
43
43
  - takashikkbn@gmail.com
44
44
  executables:
@@ -49,19 +49,49 @@ files:
49
49
  - ".gitignore"
50
50
  - ".travis.yml"
51
51
  - Gemfile
52
+ - Gemfile.lock
52
53
  - LICENSE.txt
53
54
  - README.md
54
55
  - Rakefile
55
56
  - benchmark_driver.gemspec
56
- - benchmarks/core/array.yml
57
- - benchmarks/example_multi.yml
58
- - benchmarks/example_single.yml
59
- - benchmarks/lib/erb.yml
60
57
  - bin/console
61
58
  - bin/setup
59
+ - examples/call.rb
60
+ - examples/call_blank.rb
61
+ - examples/call_erb.rb
62
+ - examples/call_interpolation.rb
63
+ - examples/exec_blank.rb
64
+ - examples/exec_interpolation.rb
65
+ - examples/yaml/array_duration_time.yml
66
+ - examples/yaml/array_loop.yml
67
+ - examples/yaml/array_loop_memory.yml
68
+ - examples/yaml/array_loop_time.yml
69
+ - examples/yaml/blank_hash.yml
70
+ - examples/yaml/blank_hash_array.yml
71
+ - examples/yaml/blank_loop.yml
72
+ - examples/yaml/blank_loop_time.yml
73
+ - examples/yaml/blank_string.yml
74
+ - examples/yaml/blank_string_array.yml
75
+ - examples/yaml/example_multi.yml
76
+ - examples/yaml/example_single.yml
62
77
  - exe/benchmark-driver
63
78
  - lib/benchmark/driver.rb
79
+ - lib/benchmark/driver/benchmark_result.rb
80
+ - lib/benchmark/driver/configuration.rb
81
+ - lib/benchmark/driver/duration_runner.rb
82
+ - lib/benchmark/driver/error.rb
83
+ - lib/benchmark/driver/repeatable_runner.rb
84
+ - lib/benchmark/driver/ruby_dsl_parser.rb
85
+ - lib/benchmark/driver/time.rb
64
86
  - lib/benchmark/driver/version.rb
87
+ - lib/benchmark/driver/yaml_parser.rb
88
+ - lib/benchmark/output.rb
89
+ - lib/benchmark/output/ips.rb
90
+ - lib/benchmark/output/memory.rb
91
+ - lib/benchmark/output/time.rb
92
+ - lib/benchmark/runner.rb
93
+ - lib/benchmark/runner/call.rb
94
+ - lib/benchmark/runner/exec.rb
65
95
  - lib/benchmark_driver.rb
66
96
  homepage: https://github.com/k0kubun/benchmark_driver
67
97
  licenses:
@@ -75,7 +105,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
105
  requirements:
76
106
  - - ">="
77
107
  - !ruby/object:Gem::Version
78
- version: 2.3.0
108
+ version: '0'
79
109
  required_rubygems_version: !ruby/object:Gem::Requirement
80
110
  requirements:
81
111
  - - ">="
@@ -83,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
113
  version: '0'
84
114
  requirements: []
85
115
  rubyforge_project:
86
- rubygems_version: 2.7.2
116
+ rubygems_version: 2.6.13
87
117
  signing_key:
88
118
  specification_version: 4
89
- summary: Benchmark driver for different Ruby executables
119
+ summary: Fully-featured accurate benchmark driver for Ruby
90
120
  test_files: []