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,118 @@
1
+ require 'benchmark_driver/struct'
2
+ require 'benchmark_driver/metric'
3
+ require 'benchmark_driver/default_job'
4
+ require 'benchmark_driver/default_job_parser'
5
+ require 'tempfile'
6
+ require 'shellwords'
7
+
8
+ # Run only once, for testing
9
+ class BenchmarkDriver::Runner::Once
10
+ METRIC = BenchmarkDriver::Metric.new(name: 'Iteration per second', unit: 'i/s')
11
+
12
+ # JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
13
+ Job = Class.new(BenchmarkDriver::DefaultJob)
14
+ # Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
15
+ JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
16
+
17
+ # @param [BenchmarkDriver::Config::RunnerConfig] config
18
+ # @param [BenchmarkDriver::Output] output
19
+ # @param [BenchmarkDriver::Context] contexts
20
+ def initialize(config:, output:, contexts:)
21
+ @config = config
22
+ @output = output
23
+ @contexts = contexts
24
+ end
25
+
26
+ # This method is dynamically called by `BenchmarkDriver::JobRunner.run`
27
+ # @param [Array<BenchmarkDriver::Default::Job>] jobs
28
+ def run(jobs)
29
+ jobs = jobs.map do |job|
30
+ Job.new(job.to_h.merge(loop_count: 1)) # to show this on output
31
+ end
32
+
33
+ @output.with_benchmark do
34
+ jobs.each do |job|
35
+ @output.with_job(name: job.name) do
36
+ job.runnable_contexts(@contexts).each do |context|
37
+ duration = run_benchmark(job, context: context) # no repeat support
38
+ if BenchmarkDriver::Result::ERROR.equal?(duration)
39
+ value = BenchmarkDriver::Result::ERROR
40
+ else
41
+ value = 1.0 / duration
42
+ end
43
+
44
+ @output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
45
+ @output.report(values: { METRIC => value }, duration: duration, loop_count: 1)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # @param [BenchmarkDriver::Runner::Ips::Job] job - loop_count is not nil
56
+ # @param [BenchmarkDriver::Context] context
57
+ # @return [Float] duration
58
+ def run_benchmark(job, context:)
59
+ benchmark = BenchmarkScript.new(
60
+ preludes: [context.prelude, job.prelude],
61
+ script: job.script,
62
+ teardown: job.teardown,
63
+ loop_count: job.loop_count,
64
+ )
65
+
66
+ Tempfile.open(['benchmark_driver-', '.rb']) do |f|
67
+ with_script(benchmark.render(result: f.path)) do |path|
68
+ IO.popen([*context.executable.command, path], &:read) # TODO: print stdout if verbose=2
69
+ if $?.success?
70
+ Float(f.read)
71
+ else
72
+ BenchmarkDriver::Result::ERROR
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def with_script(script)
79
+ if @config.verbose >= 2
80
+ sep = '-' * 30
81
+ $stdout.puts "\n\n#{sep}[Script begin]#{sep}\n#{script}#{sep}[Script end]#{sep}\n\n"
82
+ end
83
+
84
+ Tempfile.open(['benchmark_driver-', '.rb']) do |f|
85
+ f.puts script
86
+ f.close
87
+ return yield(f.path)
88
+ end
89
+ end
90
+
91
+ def execute(*args)
92
+ output = IO.popen(args, err: [:child, :out], &:read) # handle stdout?
93
+ unless $?.success?
94
+ raise "Failed to execute: #{args.shelljoin} (status: #{$?.exitstatus})"
95
+ end
96
+ output
97
+ end
98
+
99
+ # @param [String] prelude
100
+ # @param [String] script
101
+ # @param [String] teardown
102
+ # @param [Integer] loop_count
103
+ BenchmarkScript = ::BenchmarkDriver::Struct.new(:preludes, :script, :teardown, :loop_count) do
104
+ # @param [String] result - A file to write result
105
+ def render(result:)
106
+ prelude = preludes.reject(&:nil?).reject(&:empty?).join("\n")
107
+ <<-RUBY
108
+ #{prelude}
109
+ __bmdv_before = Time.now
110
+ #{script}
111
+ __bmdv_after = Time.now
112
+ File.write(#{result.dump}, (__bmdv_after - __bmdv_before).inspect)
113
+ #{teardown}
114
+ RUBY
115
+ end
116
+ end
117
+ private_constant :BenchmarkScript
118
+ end
@@ -0,0 +1,73 @@
1
+ require 'benchmark_driver/struct'
2
+ require 'benchmark_driver/metric'
3
+ require 'tempfile'
4
+ require 'shellwords'
5
+
6
+ # Run only once, for testing
7
+ class BenchmarkDriver::Runner::Recorded
8
+ # JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
9
+ Job = ::BenchmarkDriver::Struct.new(
10
+ :name, # @param [String] name - This is mandatory for all runner
11
+ :metrics, # @param [Array<BenchmarkDriver::Metric>]
12
+ :warmup_results, # @param [Hash{ BenchmarkDriver::Context => Array<BenchmarkDriver::Metric> } }]
13
+ :benchmark_results, # @param [Hash{ BenchmarkDriver::Context => Array<BenchmarkDriver::Metric> } }]
14
+ :contexts, # @param [Array<BenchmarkDriver::Context>]
15
+ )
16
+ # Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
17
+ class << JobParser = Module.new
18
+ # @param [Hash{ BenchmarkDriver::Job => Hash{ TrueClass,FalseClass => Hash{ BenchmarkDriver::Context => BenchmarkDriver::Result } } }] job_warmup_context_result
19
+ # @param [BenchmarkDriver::Metrics::Type] metrics
20
+ def parse(job_warmup_context_result:, metrics:)
21
+ job_warmup_context_result.map do |job, warmup_context_result|
22
+ Job.new(
23
+ name: job.name,
24
+ warmup_results: warmup_context_result.fetch(true, {}),
25
+ benchmark_results: warmup_context_result.fetch(false, {}),
26
+ metrics: metrics,
27
+ contexts: warmup_context_result.values.map(&:keys).flatten!.tap(&:uniq!),
28
+ )
29
+ end
30
+ end
31
+ end
32
+
33
+ # @param [BenchmarkDriver::Config::RunnerConfig] config
34
+ # @param [BenchmarkDriver::Output] output
35
+ # @param [BenchmarkDriver::Context] contexts
36
+ def initialize(config:, output:, contexts:)
37
+ @config = config
38
+ @output = output
39
+ @contexts = contexts
40
+ end
41
+
42
+ # This method is dynamically called by `BenchmarkDriver::JobRunner.run`
43
+ # @param [Array<BenchmarkDriver::Runner::Recorded::Job>] record
44
+ def run(records)
45
+ records.each do |record|
46
+ unless record.warmup_results.empty?
47
+ # TODO:
48
+ end
49
+ end
50
+
51
+ @output.with_benchmark do
52
+ records.each do |record|
53
+ @output.with_job(name: record.name) do
54
+ record.benchmark_results.each do |context, result|
55
+ @output.with_context(
56
+ name: context.name,
57
+ executable: context.executable,
58
+ gems: context.gems,
59
+ prelude: context.prelude,
60
+ ) do
61
+ @output.report(
62
+ values: result.values,
63
+ duration: result.duration,
64
+ loop_count: result.loop_count,
65
+ environment: result.environment,
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,146 @@
1
+ require 'benchmark_driver/struct'
2
+ require 'benchmark_driver/metric'
3
+ require 'tempfile'
4
+ require 'shellwords'
5
+ require 'open3'
6
+
7
+ # Use stdout of ruby command
8
+ class BenchmarkDriver::Runner::RubyStdout
9
+ # JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
10
+ Job = ::BenchmarkDriver::Struct.new(
11
+ :name, # @param [String] name - This is mandatory for all runner
12
+ :metrics, # @param [Array<BenchmarkDriver::Metric>]
13
+ :command, # @param [Array<String>]
14
+ :working_directory, # @param [String,NilClass]
15
+ :value_from_stdout, # @param [String]
16
+ :environment_from_stdout # @param [Hash{ String => String }]
17
+ )
18
+ # Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
19
+ class << JobParser = Module.new
20
+ # @param [String] name
21
+ # @param [String] command
22
+ # @param [String,NilClass] working_directory
23
+ # @param [Hash] metrics_type
24
+ # @param [String] stdout_to_metrics
25
+ def parse(name:, command:, working_directory: nil, metrics:, environment: {})
26
+ unless metrics.is_a?(Hash)
27
+ raise ArgumentError.new("metrics must be Hash, but got #{metrics.class}")
28
+ end
29
+ if metrics.size == 0
30
+ raise ArgumentError.new('At least one metric must be specified"')
31
+ elsif metrics.size != 1
32
+ raise NotImplementedError.new('Having multiple metrics is not supported yet')
33
+ end
34
+
35
+ metric, value_from_stdout = parse_metric(*metrics.first)
36
+ environment_from_stdout = Hash[environment.map { |k, v| [k, parse_environment(v)] }]
37
+
38
+ Job.new(
39
+ name: name,
40
+ command: command.shellsplit,
41
+ working_directory: working_directory,
42
+ metrics: [metric],
43
+ value_from_stdout: value_from_stdout,
44
+ environment_from_stdout: environment_from_stdout,
45
+ )
46
+ end
47
+
48
+ private
49
+
50
+ def parse_metric(name, unit:, from_stdout:, larger_better: true, worse_word: 'slower')
51
+ metric = BenchmarkDriver::Metric.new(
52
+ name: name,
53
+ unit: unit,
54
+ larger_better: larger_better,
55
+ worse_word: worse_word,
56
+ )
57
+ [metric, from_stdout]
58
+ end
59
+
60
+ def parse_environment(from_stdout:)
61
+ from_stdout
62
+ end
63
+ end
64
+
65
+ # @param [BenchmarkDriver::Config::RunnerConfig] config
66
+ # @param [BenchmarkDriver::Output] output
67
+ # @param [BenchmarkDriver::Context] contexts
68
+ def initialize(config:, output:, contexts:)
69
+ @config = config
70
+ @output = output
71
+ @contexts = contexts
72
+ end
73
+
74
+ # This method is dynamically called by `BenchmarkDriver::JobRunner.run`
75
+ # @param [Array<BenchmarkDriver::Default::Job>] jobs
76
+ def run(jobs)
77
+ metric = jobs.first.metrics.first
78
+
79
+ @output.with_benchmark do
80
+ jobs.each do |job|
81
+ @output.with_job(name: job.name) do
82
+ @contexts.each do |context|
83
+ exec = context.executable
84
+ repeat_params = { config: @config, larger_better: metric.larger_better }
85
+ value, environment = BenchmarkDriver::Repeater.with_repeat(repeat_params) do
86
+ stdout = with_chdir(job.working_directory) do
87
+ with_ruby_prefix(exec) { execute(*exec.command, *job.command) }
88
+ end
89
+ script = StdoutToMetrics.new(
90
+ stdout: stdout,
91
+ value_from_stdout: job.value_from_stdout,
92
+ environment_from_stdout: job.environment_from_stdout,
93
+ )
94
+ [script.value, script.environment]
95
+ end
96
+
97
+ @output.with_context(name: exec.name, executable: exec) do
98
+ @output.report(values: { metric => value }, environment: environment)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def with_ruby_prefix(executable, &block)
109
+ env = ENV.to_h.dup
110
+ ENV['PATH'] = "#{File.dirname(executable.command.first)}:#{ENV['PATH']}"
111
+ block.call
112
+ ensure
113
+ ENV.replace(env)
114
+ end
115
+
116
+ def with_chdir(working_directory, &block)
117
+ if working_directory
118
+ Dir.chdir(working_directory) { block.call }
119
+ else
120
+ block.call
121
+ end
122
+ end
123
+
124
+ def execute(*args)
125
+ stdout, stderr, status = Open3.capture3(*args)
126
+ unless status.success?
127
+ raise "Failed to execute: #{args.shelljoin} (status: #{$?.exitstatus}):\n[stdout]:\n#{stdout}\n[stderr]:\n#{stderr}"
128
+ end
129
+ stdout
130
+ end
131
+
132
+ StdoutToMetrics = ::BenchmarkDriver::Struct.new(:stdout, :value_from_stdout, :environment_from_stdout) do
133
+ def value
134
+ eval(value_from_stdout, binding)
135
+ end
136
+
137
+ def environment
138
+ ret = {}
139
+ environment_from_stdout.each do |name, from_stdout|
140
+ ret[name] = eval(from_stdout, binding)
141
+ end
142
+ ret
143
+ end
144
+ end
145
+ private_constant :StdoutToMetrics
146
+ end
@@ -0,0 +1,20 @@
1
+ require 'benchmark_driver/runner/ips'
2
+
3
+ class BenchmarkDriver::Runner::Time < BenchmarkDriver::Runner::Ips
4
+ METRIC = BenchmarkDriver::Metric.new(name: 'Execution time', unit: 's', larger_better: false)
5
+
6
+ # JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
7
+ Job = Class.new(BenchmarkDriver::DefaultJob)
8
+ # Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
9
+ JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
10
+
11
+ # Overriding BenchmarkDriver::Runner::Ips#metric
12
+ def metric
13
+ METRIC
14
+ end
15
+
16
+ # Overriding BenchmarkDriver::Runner::Ips#value_duration
17
+ def value_duration(duration:, loop_count:)
18
+ [duration, duration]
19
+ end
20
+ end
@@ -0,0 +1,98 @@
1
+ # Extended Struct with:
2
+ # * Polyfilled `keyword_init: true`
3
+ # * Default value configuration
4
+ # * Deeply freezing members
5
+ module BenchmarkDriver
6
+ class ::Struct
7
+ SUPPORT_KEYWORD_P = begin
8
+ ::Struct.new(:a, keyword_init: true)
9
+ true
10
+ rescue TypeError
11
+ false
12
+ end
13
+ end
14
+
15
+ class << Struct = Module.new
16
+ # @param [Array<Symbol>] args
17
+ # @param [Hash{ Symbol => Object }] defaults
18
+ def new(*args, defaults: {}, &block)
19
+ # Polyfill `keyword_init: true`
20
+ if ::Struct::SUPPORT_KEYWORD_P
21
+ klass = ::Struct.new(*args, keyword_init: true, &block)
22
+ else
23
+ klass = keyword_init_struct(*args, &block)
24
+ end
25
+
26
+ # Default value config
27
+ configure_defaults(klass, defaults)
28
+
29
+ # Force deeply freezing members
30
+ force_deep_freeze(klass)
31
+
32
+ klass
33
+ end
34
+
35
+ private
36
+
37
+ # Polyfill for Ruby < 2.5.0
38
+ def keyword_init_struct(*args, &block)
39
+ ::Struct.new(*args).tap do |klass|
40
+ klass.prepend(Module.new {
41
+ # @param [Hash{ Symbol => Object }] args
42
+ def initialize(**args)
43
+ args.each do |key, value|
44
+ unless members.include?(key)
45
+ raise ArgumentError.new("unknown keywords: #{key}")
46
+ next
47
+ end
48
+
49
+ public_send("#{key}=", value)
50
+ end
51
+ end
52
+ })
53
+ klass.prepend(Module.new(&block))
54
+ end
55
+ end
56
+
57
+ def configure_defaults(klass, defaults)
58
+ class << klass
59
+ attr_accessor :defaults
60
+ end
61
+ klass.defaults = defaults
62
+
63
+ klass.prepend(Module.new {
64
+ def initialize(**)
65
+ super
66
+ self.class.defaults.each do |key, value|
67
+ if public_send(key).nil?
68
+ begin
69
+ value = value.dup
70
+ rescue TypeError # for Ruby <= 2.3, like `true.dup`
71
+ end
72
+ public_send("#{key}=", value)
73
+ end
74
+ end
75
+ end
76
+ })
77
+
78
+ def klass.inherited(child)
79
+ child.defaults = self.defaults
80
+ end
81
+ end
82
+
83
+ def force_deep_freeze(klass)
84
+ klass.class_eval do
85
+ def freeze
86
+ members.each do |member|
87
+ value = public_send(member)
88
+ if value.is_a?(Array)
89
+ value.each(&:freeze)
90
+ end
91
+ value.freeze
92
+ end
93
+ super
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end