benchmark_driver_monotonic_raw 0.14.13

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +16 -0
  5. data/CHANGELOG.md +357 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +386 -0
  9. data/Rakefile +9 -0
  10. data/benchmark-driver/.gitignore +12 -0
  11. data/benchmark-driver/CODE_OF_CONDUCT.md +74 -0
  12. data/benchmark-driver/Gemfile +6 -0
  13. data/benchmark-driver/LICENSE.txt +21 -0
  14. data/benchmark-driver/README.md +8 -0
  15. data/benchmark-driver/Rakefile +1 -0
  16. data/benchmark-driver/benchmark-driver.gemspec +21 -0
  17. data/benchmark-driver/bin/console +14 -0
  18. data/benchmark-driver/bin/setup +8 -0
  19. data/benchmark-driver/lib/benchmark-driver.rb +1 -0
  20. data/benchmark-driver/lib/benchmark/driver.rb +1 -0
  21. data/benchmark_driver.gemspec +28 -0
  22. data/bin/console +7 -0
  23. data/bin/setup +8 -0
  24. data/exe/benchmark-driver +118 -0
  25. data/images/optcarrot.png +0 -0
  26. data/lib/benchmark_driver.rb +14 -0
  27. data/lib/benchmark_driver/bulk_output.rb +59 -0
  28. data/lib/benchmark_driver/config.rb +59 -0
  29. data/lib/benchmark_driver/default_job.rb +29 -0
  30. data/lib/benchmark_driver/default_job_parser.rb +91 -0
  31. data/lib/benchmark_driver/job_parser.rb +55 -0
  32. data/lib/benchmark_driver/metric.rb +79 -0
  33. data/lib/benchmark_driver/output.rb +88 -0
  34. data/lib/benchmark_driver/output/compare.rb +216 -0
  35. data/lib/benchmark_driver/output/markdown.rb +107 -0
  36. data/lib/benchmark_driver/output/record.rb +61 -0
  37. data/lib/benchmark_driver/output/simple.rb +103 -0
  38. data/lib/benchmark_driver/rbenv.rb +25 -0
  39. data/lib/benchmark_driver/repeater.rb +52 -0
  40. data/lib/benchmark_driver/ruby_interface.rb +83 -0
  41. data/lib/benchmark_driver/runner.rb +103 -0
  42. data/lib/benchmark_driver/runner/command_stdout.rb +118 -0
  43. data/lib/benchmark_driver/runner/ips.rb +259 -0
  44. data/lib/benchmark_driver/runner/memory.rb +150 -0
  45. data/lib/benchmark_driver/runner/once.rb +118 -0
  46. data/lib/benchmark_driver/runner/recorded.rb +73 -0
  47. data/lib/benchmark_driver/runner/ruby_stdout.rb +146 -0
  48. data/lib/benchmark_driver/runner/time.rb +20 -0
  49. data/lib/benchmark_driver/struct.rb +98 -0
  50. data/lib/benchmark_driver/version.rb +3 -0
  51. metadata +150 -0
@@ -0,0 +1,107 @@
1
+ class BenchmarkDriver::Output::Markdown
2
+ NAME_LENGTH = 8
3
+
4
+ # @param [Array<BenchmarkDriver::Metric>] metrics
5
+ # @param [Array<BenchmarkDriver::Job>] jobs
6
+ # @param [Array<BenchmarkDriver::Context>] contexts
7
+ def initialize(metrics:, jobs:, contexts:)
8
+ @metrics = metrics
9
+ @context_names = contexts.map(&:name)
10
+ @name_length = jobs.map(&:name).map(&:size).max
11
+ end
12
+
13
+ def with_warmup(&block)
14
+ without_stdout_buffering do
15
+ $stdout.print 'warming up'
16
+ block.call
17
+ end
18
+ ensure
19
+ $stdout.puts
20
+ end
21
+
22
+ def with_benchmark(&block)
23
+ @with_benchmark = true
24
+ without_stdout_buffering do
25
+ # Show header
26
+ $stdout.puts "# #{@metrics.first.name} (#{@metrics.first.unit})\n\n"
27
+
28
+ # Show executable names
29
+ $stdout.print("|#{' ' * @name_length} ")
30
+ @context_names.each do |context_name|
31
+ $stdout.print("|%#{NAME_LENGTH}s" % context_name) # same size as humanize
32
+ end
33
+ $stdout.puts('|')
34
+
35
+ # Show header separator
36
+ $stdout.print("|:#{'-' * (@name_length - 1)}--")
37
+ @context_names.each do |context_name|
38
+ $stdout.print("|:#{'-' * (NAME_LENGTH - 1)}") # same size as humanize
39
+ end
40
+ $stdout.puts('|')
41
+
42
+ block.call
43
+ end
44
+ rescue
45
+ @with_benchmark = false
46
+ end
47
+
48
+ # @param [BenchmarkDriver::Job] job
49
+ def with_job(job, &block)
50
+ if @with_benchmark
51
+ $stdout.print("|%-#{@name_length}s " % job.name)
52
+ end
53
+ block.call
54
+ ensure
55
+ if @with_benchmark
56
+ $stdout.puts('|')
57
+ end
58
+ end
59
+
60
+ # @param [BenchmarkDriver::Context] context
61
+ def with_context(context, &block)
62
+ block.call
63
+ end
64
+
65
+ # @param [BenchmarkDriver::Result] result
66
+ def report(result)
67
+ if @with_benchmark
68
+ $stdout.print("|%#{NAME_LENGTH}s" % humanize(result.values.fetch(@metrics.first)))
69
+ else
70
+ $stdout.print '.'
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ # benchmark_driver ouputs logs ASAP. This enables sync flag for it.
77
+ def without_stdout_buffering
78
+ sync, $stdout.sync = $stdout.sync, true
79
+ yield
80
+ ensure
81
+ $stdout.sync = sync
82
+ end
83
+
84
+ def humanize(value)
85
+ if BenchmarkDriver::Result::ERROR.equal?(value)
86
+ return " %#{NAME_LENGTH}s" % 'ERROR'
87
+ elsif value == 0.0
88
+ return " %#{NAME_LENGTH}.3f" % 0.0
89
+ elsif value < 0
90
+ raise ArgumentError.new("Negative value: #{value.inspect}")
91
+ end
92
+
93
+ scale = (Math.log10(value) / 3).to_i
94
+ prefix = "%#{NAME_LENGTH}.3f" % (value.to_f / (1000 ** scale))
95
+ suffix =
96
+ case scale
97
+ when 1; 'k'
98
+ when 2; 'M'
99
+ when 3; 'G'
100
+ when 4; 'T'
101
+ when 5; 'Q'
102
+ else # < 1000 or > 10^15, no scale or suffix
103
+ return " #{prefix}"
104
+ end
105
+ "#{prefix}#{suffix}"
106
+ end
107
+ end
@@ -0,0 +1,61 @@
1
+ class BenchmarkDriver::Output::Record
2
+ # @param [Array<BenchmarkDriver::Metric>] metrics
3
+ # @param [Array<BenchmarkDriver::Job>] jobs
4
+ # @param [Array<BenchmarkDriver::Context>] contexts
5
+ def initialize(metrics:, jobs:, contexts:)
6
+ @metrics = metrics
7
+ @job_warmup_context_result = Hash.new do |h1, job|
8
+ h1[job] = Hash.new do |h2, warmup|
9
+ h2[warmup] = Hash.new do |h3, context|
10
+ h3[context] = {}
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ def with_warmup(&block)
17
+ $stdout.print 'warming up'
18
+ block.call
19
+ ensure
20
+ $stdout.puts
21
+ end
22
+
23
+ def with_benchmark(&block)
24
+ @with_benchmark = true
25
+ $stdout.print 'benchmarking'
26
+ block.call
27
+ ensure
28
+ $stdout.puts
29
+ @with_benchmark = false
30
+ save_record
31
+ end
32
+
33
+ # @param [BenchmarkDriver::Job] job
34
+ def with_job(job, &block)
35
+ @job = job
36
+ block.call
37
+ end
38
+
39
+ # @param [BenchmarkDriver::Context] context
40
+ def with_context(context, &block)
41
+ @context = context
42
+ block.call
43
+ end
44
+
45
+ # @param [BenchmarkDriver::Result] result
46
+ def report(result)
47
+ $stdout.print '.'
48
+ @job_warmup_context_result[@job][!@with_benchmark][@context] = result
49
+ end
50
+
51
+ private
52
+
53
+ def save_record
54
+ yaml = {
55
+ 'type' => 'recorded',
56
+ 'job_warmup_context_result' => @job_warmup_context_result,
57
+ 'metrics' => @metrics,
58
+ }.to_yaml
59
+ File.write('benchmark_driver.record.yml', yaml)
60
+ end
61
+ end
@@ -0,0 +1,103 @@
1
+ class BenchmarkDriver::Output::Simple
2
+ NAME_LENGTH = 10
3
+
4
+ # @param [Array<BenchmarkDriver::Metric>] metrics
5
+ # @param [Array<BenchmarkDriver::Job>] jobs
6
+ # @param [Array<BenchmarkDriver::Context>] contexts
7
+ def initialize(metrics:, jobs:, contexts:)
8
+ @metrics = metrics
9
+ @context_names = contexts.map(&:name)
10
+ @name_length = jobs.map(&:name).map(&:size).max
11
+ end
12
+
13
+ def with_warmup(&block)
14
+ @with_benchmark = false
15
+ without_stdout_buffering do
16
+ $stdout.print 'warming up'
17
+ block.call
18
+ end
19
+ ensure
20
+ $stdout.puts
21
+ end
22
+
23
+ def with_benchmark(&block)
24
+ @with_benchmark = true
25
+ without_stdout_buffering do
26
+ # Show header
27
+ $stdout.puts "#{@metrics.first.name} (#{@metrics.first.unit}):"
28
+
29
+ # Show executable names
30
+ if @context_names.size > 1
31
+ $stdout.print("#{' ' * @name_length} ")
32
+ @context_names.each do |context_name|
33
+ $stdout.print("%#{NAME_LENGTH}s " % context_name)
34
+ end
35
+ $stdout.puts
36
+ end
37
+
38
+ block.call
39
+ end
40
+ ensure
41
+ @with_benchmark = false
42
+ end
43
+
44
+ # @param [BenchmarkDriver::Job] job
45
+ def with_job(job, &block)
46
+ if @with_benchmark
47
+ $stdout.print("%-#{@name_length}s " % job.name)
48
+ end
49
+ block.call
50
+ ensure
51
+ if @with_benchmark
52
+ $stdout.puts
53
+ end
54
+ end
55
+
56
+ # @param [BenchmarkDriver::Context] context
57
+ def with_context(context, &block)
58
+ block.call
59
+ end
60
+
61
+ # @param [BenchmarkDriver::Result] result
62
+ def report(result)
63
+ if @with_benchmark
64
+ $stdout.print("%#{NAME_LENGTH}s " % humanize(result.values.fetch(@metrics.first)))
65
+ else
66
+ $stdout.print '.'
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # benchmark_driver ouputs logs ASAP. This enables sync flag for it.
73
+ def without_stdout_buffering
74
+ sync, $stdout.sync = $stdout.sync, true
75
+ yield
76
+ ensure
77
+ $stdout.sync = sync
78
+ end
79
+
80
+ def humanize(value)
81
+ if BenchmarkDriver::Result::ERROR.equal?(value)
82
+ return " %#{NAME_LENGTH}s" % 'ERROR'
83
+ elsif value == 0.0
84
+ return " %#{NAME_LENGTH}.3f" % 0.0
85
+ elsif value < 0
86
+ raise ArgumentError.new("Negative value: #{value.inspect}")
87
+ end
88
+
89
+ scale = (Math.log10(value) / 3).to_i
90
+ prefix = "%#{NAME_LENGTH}.3f" % (value.to_f / (1000 ** scale))
91
+ suffix =
92
+ case scale
93
+ when 1; 'k'
94
+ when 2; 'M'
95
+ when 3; 'G'
96
+ when 4; 'T'
97
+ when 5; 'Q'
98
+ else # < 1000 or > 10^15, no scale or suffix
99
+ return " #{prefix}"
100
+ end
101
+ "#{prefix}#{suffix}"
102
+ end
103
+ end
@@ -0,0 +1,25 @@
1
+ require 'shellwords'
2
+
3
+ module BenchmarkDriver
4
+ module Rbenv
5
+ # @param [String] version
6
+ def self.ruby_path(version)
7
+ path = `RBENV_VERSION='#{version}' rbenv which ruby`.rstrip
8
+ unless $?.success?
9
+ abort "Failed to execute 'rbenv which ruby'"
10
+ end
11
+ path
12
+ end
13
+
14
+ # @param [String] full_spec - "2.5.0", "2.5.0,--jit", "JIT::2.5.0,--jit", etc.
15
+ def self.parse_spec(full_spec)
16
+ name, spec = full_spec.split('::', 2)
17
+ spec ||= name # if `::` is not given, regard whole string as spec
18
+ version, *args = spec.shellsplit
19
+ BenchmarkDriver::Config::Executable.new(
20
+ name: name,
21
+ command: [BenchmarkDriver::Rbenv.ruby_path(version), *args],
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ module BenchmarkDriver
2
+ # Repeat calling block and return desired result: "best", "worst" or "average".
3
+ module Repeater
4
+ VALID_TYPES = %w[best worst average]
5
+
6
+ class << self
7
+ # `block.call` can return multiple objects, but the first one is used for sort.
8
+ # When `config.repeat_result == 'average'`, how to deal with rest objects is decided
9
+ # by `:rest_on_average` option.
10
+ def with_repeat(config:, larger_better:, rest_on_average: :first, &block)
11
+ values = config.repeat_count.times.map { block.call }
12
+
13
+ case config.repeat_result
14
+ when 'best'
15
+ best_result(values, larger_better)
16
+ when 'worst'
17
+ best_result(values, !larger_better)
18
+ when 'average'
19
+ average_result(values, rest_on_average)
20
+ else
21
+ raise "unexpected repeat_result #{config.repeat_result.inspect}"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def best_result(values, larger_better)
28
+ values.sort_by do |value, *|
29
+ larger_better ? value : -value
30
+ end.last
31
+ end
32
+
33
+ def average_result(values, rest_on_average)
34
+ unless values.first.is_a?(Array)
35
+ return values.inject(&:+) / values.size.to_f
36
+ end
37
+
38
+ case rest_on_average
39
+ when :first
40
+ rest = values.first[1..-1]
41
+ [values.map { |v| v[0] }.inject(&:+) / values.size.to_f, *rest]
42
+ when :average
43
+ values.first.size.times.map do |index|
44
+ values.map { |v| v[index] }.inject(&:+) / values.first.size.to_f
45
+ end
46
+ else
47
+ raise "unexpected rest_on_average #{rest_on_average.inspect}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,83 @@
1
+ module BenchmarkDriver
2
+ class RubyInterface
3
+ def self.run(**args, &block)
4
+ new(**args).tap { |x| block.call(x) }.run
5
+ end
6
+
7
+ # Build jobs and run. This is NOT interface for users.
8
+ def run
9
+ unless @executables.empty?
10
+ @config.executables = @executables
11
+ end
12
+
13
+ jobs = @jobs.flat_map do |job|
14
+ BenchmarkDriver::JobParser.parse({
15
+ type: @config.runner_type,
16
+ prelude: @prelude,
17
+ loop_count: @loop_count,
18
+ }.merge!(job))
19
+ end
20
+ BenchmarkDriver::Runner.run(jobs, config: @config)
21
+ end
22
+
23
+ #
24
+ # Config APIs from here
25
+ #
26
+
27
+ # @param [String,NilClass] output
28
+ # @param [String,NilClass] runner
29
+ def initialize(output: nil, runner: nil, repeat_count: 1)
30
+ @prelude = ''
31
+ @loop_count = nil
32
+ @jobs = []
33
+ @config = BenchmarkDriver::Config.new
34
+ @config.output_type = output.to_s if output
35
+ @config.runner_type = runner.to_s if runner
36
+ @config.repeat_count = Integer(repeat_count)
37
+ @executables = []
38
+ end
39
+
40
+ # @param [String] script
41
+ def prelude(script)
42
+ @prelude << "#{script}\n"
43
+ end
44
+
45
+ # @param [Integer] count
46
+ def loop_count(count)
47
+ @loop_count = count
48
+ end
49
+
50
+ # @param [String] name - Name shown on result output.
51
+ # @param [String,nil] script - Benchmarked script in String. If nil, name is considered as script too.
52
+ def report(name, script = nil)
53
+ if script.nil?
54
+ script = name
55
+ end
56
+ @jobs << { benchmark: [{ name: name, script: script }] }
57
+ end
58
+
59
+ def output(type)
60
+ @config.output_type = type
61
+ end
62
+
63
+ # Backward compatibility. This is actually default now.
64
+ def compare!
65
+ @config.output_type = 'compare'
66
+ end
67
+
68
+ def rbenv(*versions)
69
+ versions.each do |version|
70
+ @executables << BenchmarkDriver::Rbenv.parse_spec(version)
71
+ end
72
+ end
73
+
74
+ def executable(name:, command:)
75
+ raise ArgumentError, "`command' should be an Array" unless command.kind_of? Array
76
+ @executables << BenchmarkDriver::Config::Executable.new(name: name, command: command)
77
+ end
78
+
79
+ def verbose(level = 1)
80
+ @config.verbose = level
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,103 @@
1
+ module BenchmarkDriver
2
+ module Runner
3
+ require 'benchmark_driver/runner/command_stdout'
4
+ require 'benchmark_driver/runner/ips'
5
+ require 'benchmark_driver/runner/memory'
6
+ require 'benchmark_driver/runner/once'
7
+ require 'benchmark_driver/runner/recorded'
8
+ require 'benchmark_driver/runner/ruby_stdout'
9
+ require 'benchmark_driver/runner/time'
10
+ end
11
+
12
+ class << Runner
13
+ # Main function which is used by both CLI and `Benchmark.driver`.
14
+ # @param [Array<BenchmarkDriver::*::Job>] jobs
15
+ # @param [BenchmarkDriver::Config] config
16
+ def run(jobs, config:)
17
+ if config.verbose >= 1
18
+ config.executables.each do |exec|
19
+ $stdout.puts "#{exec.name}: #{IO.popen([*exec.command, '-v'], &:read)}"
20
+ end
21
+ end
22
+
23
+ runner_config = Config::RunnerConfig.new
24
+ runner_config.members.each do |member|
25
+ runner_config[member] = config[member]
26
+ end
27
+
28
+ jobs.group_by{ |j| j.respond_to?(:contexts) && j.contexts }.each do |contexts, contexts_jobs|
29
+ contexts_jobs.group_by(&:metrics).each do |metrics, metrics_jobs|
30
+ metrics_jobs.group_by(&:class).each do |klass, klass_jobs|
31
+ runner = runner_for(klass)
32
+ contexts = build_contexts(contexts, executables: config.executables)
33
+ output = Output.new(
34
+ type: config.output_type,
35
+ metrics: metrics,
36
+ jobs: klass_jobs.map { |job| BenchmarkDriver::Job.new(name: job.name) },
37
+ contexts: contexts,
38
+ )
39
+ with_clean_env do
40
+ runner.new(config: runner_config, output: output, contexts: contexts).run(klass_jobs)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def build_contexts(contexts, executables:)
50
+ # If contexts are not specified, just use executables as contexts.
51
+ if !contexts.is_a?(Array) || contexts.empty?
52
+ return executables.map { |exec|
53
+ BenchmarkDriver::Context.new(name: exec.name, executable: exec)
54
+ }
55
+ end
56
+
57
+ with_executables, without_executables = contexts.partition { |context| context.name && context.executable }
58
+ with_executables + build_contexts_with_executables(without_executables, executables)
59
+ end
60
+
61
+ def build_contexts_with_executables(contexts, executables)
62
+ # Create direct product of contexts
63
+ contexts.product(executables).map do |context, executable|
64
+ name = context.name
65
+ if name.nil?
66
+ # Use the first gem name and version by default
67
+ name = context.gems.first.join(' ')
68
+
69
+ # Append Ruby executable name if it's matrix
70
+ if executables.size > 1
71
+ name = "#{name} (#{executable.name})"
72
+ end
73
+ end
74
+
75
+ BenchmarkDriver::Context.new(
76
+ name: name,
77
+ executable: executable,
78
+ gems: context.gems,
79
+ prelude: context.prelude,
80
+ )
81
+ end
82
+ end
83
+
84
+ # Dynamically find class (BenchmarkDriver::*::JobRunner) for plugin support
85
+ # @param [Class] klass - BenchmarkDriver::*::Job
86
+ # @return [Class]
87
+ def runner_for(klass)
88
+ unless match = klass.name.match(/\ABenchmarkDriver::Runner::(?<namespace>[^:]+)::Job\z/)
89
+ raise "Unexpected job class: #{klass}"
90
+ end
91
+ BenchmarkDriver.const_get("Runner::#{match[:namespace]}", false)
92
+ end
93
+
94
+ def with_clean_env(&block)
95
+ require 'bundler'
96
+ Bundler.with_clean_env do
97
+ block.call
98
+ end
99
+ rescue LoadError
100
+ block.call
101
+ end
102
+ end
103
+ end