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,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: []