class StartupTime::App

StartupTime::App - the entry point for the app. selects an action based on the command-line options and runs it.

Constants

EXPECTED_OUTPUT

Public Class Methods

new(args = ARGV) click to toggle source
# File lib/startup_time/app.rb, line 26
def initialize(args = ARGV)
  @options = Options.new(args)
  @json = @options.format == :json
  @verbosity = @options.verbosity
  @times = []

  # provide/publish the Options instance we've just created so it's
  # available to other components
  Services.once(:options) { @options }
end

Public Instance Methods

run() click to toggle source

run the command corresponding to the command-line options: either an auxiliary command (e.g. clean the build directory or print a help message) or the default command, which runs the selected benchmark-tests

# File lib/startup_time/app.rb, line 41
def run
  case @options.action
  when :clean
    builder.clean!
  when :help
    puts @options.usage
  when :version
    puts VERSION
  when :show_ids
    render_ids_to_groups
  else
    benchmark
  end
end

Private Instance Methods

benchmark() click to toggle source

run the selected benchmark tests:

1) ensure everything in the build directory is up to date 2) run the tests in random order and time each one 3) sort the results from the fastest to the slowest 4) display the results in the specified format (default: ASCII table)

# File lib/startup_time/app.rb, line 64
def benchmark
  builder.build!

  tests = runnable_tests

  if tests.empty?
    puts '[]' if @json
    return
  end

  spec = @options.spec

  if spec.type == :duration
    spec = spec.with(value: spec.value.to_f / tests.length)
  end

  tests.shuffle.each do |test|
    time(spec, test)
  end

  sorted = @times.sort_by { |result| result[:time] }

  if @json
    puts sorted.to_json
  else
    pairs = sorted.map { |result| [result[:name], '%.02f' % result[:time]] }
    table = TTY::Table.new(['Test', 'Time (ms)'], pairs)
    puts unless @verbosity == :quiet
    puts table.render(:basic, alignments: %i[left right])
  end
end
render_ids_to_groups() click to toggle source

print a JSON or ASCII-table representation of the mapping from test IDs (e.g. “scala”) to group IDs (e.g. [“compiled”, “jvm”, “slow”])

# File lib/startup_time/app.rb, line 98
def render_ids_to_groups
  if @json
    puts Registry.ids_to_groups(format: :json).to_json
  else
    table = TTY::Table.new(%w[Test Groups], Registry.ids_to_groups)
    puts table.render
  end
end
runnable_tests() click to toggle source

return the subset of selected tests that are runnable.

a test is runnable if these requirements are met:

- (if it's interpreted): the interpreter exists
- (if it's compiled): the compiler exists

otherwise, skip it

the compiler path is resolved in the builder, which can also disable a test (by setting test = true) if the compilation prerequisites aren't installed

note that some tests are both interpreted and compiled, so both the compiler and the interpreter must exist, e.g.:

- interpreter: java
- compiler: javac
# File lib/startup_time/app.rb, line 126
def runnable_tests
  selected_tests.each_with_object([]) do |(id, test), tests|
    next if test[:disabled]

    args = Array(test[:command])
    compiler = test[:compiler]

    if args.length == 1 # native executable
      executable = File.absolute_path(args.first)
      next unless File.exist?(executable)
    else # interpreter + source/bytecode
      executable = which(args.first)
      next unless (interpreter = executable)
    end

    tests << {
      args:        args,
      compiler:    compiler,
      executable:  executable,
      id:          id,
      interpreter: interpreter,
      test:        test,
    }
  end
end
time(spec, args:, compiler:, executable:, id:, interpreter:, test:) click to toggle source

takes a test configuration and measures how long it takes to execute the test

# File lib/startup_time/app.rb, line 154
def time(spec, args:, compiler:, executable:, id:, interpreter:, test:)
  # dump the compiler/interpreter's version if running in verbose mode
  if @verbosity == :verbose
    puts
    puts "test: #{id}"

    # false (don't print the program's version); otherwise, a command
    # (template string) to execute to dump the version
    version = test[:version]

    unless version == false
      # if the test is both interpreted and compiled, default to
      # dumping the compiler version
      version ||= '%{compiler} --version' if compiler
      version ||= '%{interpreter} --version' if interpreter

      version_command = version % {
        compiler: compiler || interpreter,
        interpreter: interpreter,
      }

      sh version_command
    end
  end

  argv0 = args.shift
  command = [executable, *args]

  unless @verbosity == :quiet
    if @verbosity == :verbose
      puts "command: #{command.shelljoin}"
    else
      print '.'
    end
  end

  # make sure the command produces the expected output
  result = Komenda.run(command)
  output = result.output

  if result.error?
    abort "error running #{id} (#{result.status}): #{output}"
  end

  unless output.match?(EXPECTED_OUTPUT)
    abort "invalid output for #{id}: #{output.inspect}"
  end

  times = []

  # the bundler environment slows down ruby and breaks truffle-ruby,
  # so make sure it's disabled for the benchmark
  Bundler.with_clean_env do
    if spec.type == :duration # how long to run the tests for
      duration = spec.value
      elapsed = 0
      start = Time.now

      loop do
        time = Benchmark.realtime do
          system([executable, argv0], *args, out: File::NULL)
        end

        elapsed = Time.now - start
        times << time

        break if elapsed >= duration
      end
    else # how many times to run the tests
      spec.value.times do
        times << Benchmark.realtime do
          system([executable, argv0], *args, out: File::NULL)
        end
      end
    end
  end

  @times << {
    id: id,
    name: test[:name],
    time: (times.min * 1000).truncate(2)
  }
end