module BenchmarkDriver

def build_contexts(contexts, executables:)

def build_contexts(contexts, executables:)
  # If contexts are not specified, just use executables as contexts.
  if !contexts.is_a?(Array) || contexts.empty?
    return executables.map { |exec|
      BenchmarkDriver::Context.new(name: exec.name, executable: exec)
    }
  end
  with_executables, without_executables = contexts.partition { |context| context.name && context.executable }
  with_executables + build_contexts_with_executables(without_executables, executables)
end

def build_contexts_with_executables(contexts, executables)

def build_contexts_with_executables(contexts, executables)
  # Create direct product of contexts
  contexts.product(executables).map do |context, executable|
    name = context.name
    if name.nil?
      # Use the first gem name and version by default
      name = context.gems.first.join(' ')
      # Append Ruby executable name if it's matrix
      if executables.size > 1
        name = "#{name} (#{executable.name})"
      end
    end
    BenchmarkDriver::Context.new(
      name: name,
      executable: executable,
      gems: context.gems,
      prelude: context.prelude,
    )
  end
end

def camelize(str)

def camelize(str)
  str.split('_').map(&:capitalize).join
end

def configure_defaults(klass, defaults)

def configure_defaults(klass, defaults)
  class << klass
    attr_accessor :defaults
  end
  klass.defaults = defaults
  klass.prepend(Module.new {
    def initialize(*)
      super
      self.class.defaults.each do |key, value|
        if public_send(key).nil?
          begin
            value = value.dup
          rescue TypeError # for Ruby <= 2.3, like `true.dup`
          end
          public_send("#{key}=", value)
        end
      end
    end
  })
  def klass.inherited(child)
    child.defaults = self.defaults
  end
end

def description

Returns:
  • (String) - - Return result of `ruby -v`. This is for convenience of output plugins.
def description
  @cache[:description] ||= IO.popen([*command, '-v'], &:read).rstrip
end

def force_deep_freeze(klass)

def force_deep_freeze(klass)
  klass.class_eval do
    def freeze
      members.each do |member|
        value = public_send(member)
        if value.is_a?(Array)
          value.each(&:freeze)
        end
        value.freeze
      end
      super
    end
  end
end

def freeze

def freeze
  members.each do |member|
    value = public_send(member)
    if value.is_a?(Array)
      value.each(&:freeze)
    end
    value.freeze
  end
  super
end

def initialize(*)

def initialize(*)
  super
  @cache = {} # modifiable storage even after `#freeze`
end

def initialize(**args)

Parameters:
  • args (Hash{ Symbol => Object }) --
def initialize(**args)
  args.each do |key, value|
    unless members.include?(key)
      raise ArgumentError.new("unknown keywords: #{key}")
      next
    end
    public_send("#{key}=", value)
  end
end

def initialize(*)

def initialize(*)
  super
  self.class.defaults.each do |key, value|
    if public_send(key).nil?
      begin
        value = value.dup
      rescue TypeError # for Ruby <= 2.3, like `true.dup`
      end
      public_send("#{key}=", value)
    end
  end
end

def keyword_init_struct(*args, &block)

Polyfill for Ruby < 2.5.0
def keyword_init_struct(*args, &block)
  ::Struct.new(*args).tap do |klass|
    klass.prepend(Module.new {
      # @param [Hash{ Symbol => Object }] args
      def initialize(**args)
        args.each do |key, value|
          unless members.include?(key)
            raise ArgumentError.new("unknown keywords: #{key}")
            next
          end
          public_send("#{key}=", value)
        end
      end
    })
    klass.prepend(Module.new(&block))
  end
end

def new(*args, defaults: {}, &block)

Parameters:
  • defaults (Hash{ Symbol => Object }) --
  • args (Array) --
def new(*args, defaults: {}, &block)
  # Polyfill `keyword_init: true`
  if ::Struct::SUPPORT_KEYWORD_P
    klass = ::Struct.new(*args, keyword_init: true, &block)
  else
    klass = keyword_init_struct(*args, &block)
  end
  # Default value config
  configure_defaults(klass, defaults)
  # Force deeply freezing members
  force_deep_freeze(klass)
  klass
end

def parse(config, working_directory: nil)

Parameters:
  • working_directory (Hash) -- - YAML-specific special parameter for "command_stdout" and a relative path in type
  • config (Hash) --
def parse(config, working_directory: nil)
  config = symbolize_keys(config)
  type = config.fetch(:type)
  if !type.is_a?(String)
    raise ArgumentError.new("Invalid type: #{config[:type].inspect} (expected String)")
  elsif !type.match(/\A[A-Za-z0-9_\/]+\z/)
    raise ArgumentError.new("Invalid type: #{config[:type].inspect} (expected to include only [A-Za-z0-9_\/])")
  end
  config.delete(:type)
  # Dynamic dispatch for plugin support
  if type.include?('/')
    require File.join(working_directory || '.', type)
    type = File.basename(type)
  else
    require "benchmark_driver/runner/#{type}"
  end
  job = ::BenchmarkDriver.const_get("Runner::#{camelize(type)}::JobParser", false).parse(**config)
  if job.respond_to?(:working_directory) && job.respond_to?(:working_directory=) && job.working_directory.nil?
    job.working_directory = working_directory
  end
  job
end

def run(jobs, config:)

Parameters:
  • config (BenchmarkDriver::Config) --
  • jobs (Array) --
def run(jobs, config:)
  if config.verbose >= 1
    config.executables.each do |exec|
      $stdout.puts "#{exec.name}: #{IO.popen([*exec.command, '-v'], &:read)}"
    end
  end
  runner_config = Config::RunnerConfig.new
  runner_config.members.each do |member|
    runner_config[member] = config[member]
  end
  jobs.group_by{ |j| j.respond_to?(:contexts) && j.contexts }.each do |contexts, contexts_jobs|
    contexts_jobs.group_by(&:metrics).each do |metrics, metrics_jobs|
      metrics_jobs.group_by(&:class).each do |klass, klass_jobs|
        runner = runner_for(klass)
        if runner_config.alternate && runner != BenchmarkDriver::Runner::RubyStdout
          abort "--alternate is supported only for ruby_stdout runner for now"
        end
        contexts = build_contexts(contexts, executables: config.executables)
        output = Output.new(
          type: config.output_type,
          metrics: metrics,
          jobs: klass_jobs.map { |job| BenchmarkDriver::Job.new(name: job.name) },
          contexts: contexts,
          options: config.output_opts,
        )
        with_clean_env do
          runner.new(config: runner_config, output: output, contexts: contexts).run(klass_jobs)
        end
      end
    end
  end
end

def runner_for(klass)

Returns:
  • (Class) -

Parameters:
  • klass (Class) -- - BenchmarkDriver::*::Job
def runner_for(klass)
  unless match = klass.name.match(/\ABenchmarkDriver::Runner::(?<namespace>[^:]+)::Job\z/)
    raise "Unexpected job class: #{klass}"
  end
  BenchmarkDriver.const_get("Runner::#{match[:namespace]}", false)
end

def symbolize_keys(config)

Parameters:
  • config (Object) --
def symbolize_keys(config)
  case config
  when Hash
    config.dup.tap do |hash|
      hash.keys.each do |key|
        case key
        when String, Symbol
          hash[key.to_sym] = symbolize_keys(hash.delete(key))
        else # Struct
          hash[key] = symbolize_keys(hash.delete(key))
        end
      end
    end
  when Array
    config.map { |c| symbolize_keys(c) }
  else
    config
  end
end

def version

Returns:
  • (String) - - Return RUBY_VERSION
def version
  @cache[:version] ||= IO.popen([*command, '-e', 'print RUBY_VERSION'], &:read)
end

def with_clean_env(&block)

def with_clean_env(&block)
  # chruby sets GEM_HOME and GEM_PATH in your shell. We have to unset it in the child
  # process to avoid installing gems to the version that is running benchmark-driver.
  env = nil
  ['GEM_HOME', 'GEM_PATH'].each do |var|
    if ENV.key?(var)
      env ||= ENV.to_h
      ENV.delete(var)
    end
  end
  unless defined?(Gem::Version)
    # default-gem Bundler can be loaded and broken with --disable-gems
    return block.call
  end
  require 'bundler'
  if Bundler.respond_to?(:with_unbundled_env)
    Bundler.with_unbundled_env do
      block.call
    end
  else
    Bundler.with_clean_env do
      block.call
    end
  end
rescue LoadError
  block.call
ensure
  if env
    ENV.replace(env)
  end
end