benchmark_driver_monotonic_raw 0.14.13

Sign up to get free protection for your applications and to get access to all the features.
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