class StartupTime::Builder

StartupTime::Builder - clean and prepare the build directory

this class provides two methods which clean (i.e. remove) and prepare the build directory. the latter is done by executing the following tasks:

1) copy source files from the source directory to the build directory 2) compile all of the target files that need to be compiled from source files

once these tasks are complete, everything required to run the benchmark tests will be available in the build directory

Constants

CUSTOM_COMPILER
SRC_DIR

Public Class Methods

new() click to toggle source
# File lib/startup_time/builder.rb, line 35
def initialize
  @build_dir = options.build_dir
  @verbosity = options.verbosity

  Rake.verbose(@verbosity != :quiet)
end

Public Instance Methods

build!() click to toggle source

ensure the build directory is in a fit state to run the tests i.e. copy source files and compile target files

# File lib/startup_time/builder.rb, line 44
def build!
  verbose(@verbosity == :verbose) do
    mkdir_p(@build_dir) unless Dir.exist?(@build_dir)
    cd @build_dir
  end

  register_tasks

  Rake::Task[:build].invoke
end
clean!() click to toggle source

remove the build directory and its contents

# File lib/startup_time/builder.rb, line 56
def clean!
  rm_rf @build_dir
end

Private Instance Methods

compile_if(id, **options) { |task, test| ... } click to toggle source

a conditional version of Rake's `file` task which compiles a source file to a target file via the block provided. if the compiler isn't installed, the task is skipped.

returns a truthy value (the target filename) if the task is created, or nil otherwise

# File lib/startup_time/builder.rb, line 68
def compile_if(id, **options)
  tests = options[:force] ? Registry::TESTS : selected_tests

  # look up the test's spec among the remaining tests which haven't been
  # excluded by --omit or --only
  return unless (test = tests[id])

  # the compiler name (e.g. "crystal") is usually the same as the ID for
  # the test (e.g. "crystal"), but can be supplied explicitly in the test
  # spec e.g. { id: "java-native", compiler: "native-image" }
  compiler = test[:compiler] || id

  return unless (compiler_path = which(compiler))

  # update the test spec's compiler field to point to the compiler's
  # absolute path
  #
  # XXX mutation/side-effect
  test[:compiler] = compiler_path

  # the source filename must be supplied
  source = test.fetch(:source)

  # infer the target if not specified
  unless (target = test[:target])
    command = Array(test[:command])

    if command.length == 1
      target = command.first
    elsif source.match?(/\A[A-Z]/) # JVM language
      target = source.pathmap('%n.class')
    else # native executable
      target = '%s.out' % source
    end
  end

  # pass the test object as the `file(...) { ... }` block's second
  # argument. Rake passes an instance of +Rake::TaskArguments+, a Hash-like
  # object which provides access to the command-line arguments for a Rake
  # task e.g. { name: "world" } for `rake greet[world]`. since we're not
  # relying on Rake's limited option-handling support, we have no use for
  # that here, so we simply replace it with the test data.
  wrapper = ->(task, _) { yield(task, test) }

  # declare the prerequisites for the target file.
  # compiler_path: recompile if the compiler has been
  # updated since the target was last built
  file_task = file(target => [source, compiler_path], &wrapper)

  # register the task under the supplied ID so it can be referenced by name
  # rather than by filename
  compile_task = task(id => file_task)

  # add the task which builds the target file to the build task as a
  # prerequisite
  # task(:build => target) unless options[:connect] == false
  task(:build => compile_task) unless options[:connect] == false

  compile_task
end
compile_java_native() click to toggle source

native-image compiles .class files to native binaries. it differs from the other tasks because it depends on a target file rather than a source file i.e. it depends on the target of the javac task

# File lib/startup_time/builder.rb, line 132
def compile_java_native
  java_native = compile_if('java-native', connect: false) do |t, test|
    # XXX native-image doesn't provide a way to silence its output, so
    # send it to /dev/null
    shell [test[:compiler], "-H:Name=#{t.target}", '--no-server', '-O1', t.source.ext], {
      out: File::NULL
    }
  end

  return unless java_native # return a falsey value i.e. disable the test

  javac = Rake.application.lookup(:javac) || begin
    compile_if(:javac, connect: false, force: true) do |task, test|
      run(test[:compile], task, test)
    end
  end

  return unless javac # disable this test if javac is not available

  # prepend the javac task to this task as a prerequisite
  java_native.prepend(javac)

  # register this task as a dependency of the root (:build) task
  task(:build => java_native)

  # uncomment this to see the dependency graph
  # pp Rake::Task.tasks

  java_native
end
compile_kotlinc_native() click to toggle source

implement the compilation step for the kotlinc-native test manually. we need to do this to work around the compiler's non-standard behavior

# File lib/startup_time/builder.rb, line 165
def compile_kotlinc_native
  compile_if 'kotlinc-native' do |t, test|
    # XXX kotlinc-native doesn't provide a way to silence
    # its debug messages, so file them under /dev/null
    shell %W[#{test[:compiler]} -opt -o #{t.target} #{t.source}], out: File::NULL

    # XXX work around a kotlinc-native "feature"
    # https://github.com/JetBrains/kotlin-native/issues/967
    exe = "#{t.target}.kexe" # XXX or .exe, or...
    verbose(@verbosity == :verbose) { mv exe, t.target } if File.exist?(exe)
  end
end
compile_target_files() click to toggle source

make sure the target files (e.g. native executables and JVM .class files) are built if their compilers are installed

# File lib/startup_time/builder.rb, line 180
def compile_target_files
  selected_tests.each do |id, test|
    enabled = true

    # handle the tests which have compile templates by a) turning them into
    # blocks which substitute the compiler, source file and target file into
    # the corresponding placeholders in the template, then b) executing the
    # resulting command via +shell+

    if (command = test[:compile])
      block = ->(task, test_) { run(command, task, test_) }
      enabled = compile_if(id, &block)
    end

    test[:disabled] = !enabled
  end

  # do these after the main pass so they can reuse tasks (if available)
  # e.g. the javac task

  CUSTOM_COMPILER.each do |id, meth|
    selected_tests[id].tap do |test|
      test[:disabled] = !send(meth) if test
    end
  end
end
copy_source_files() click to toggle source

ensure each file in the source directory is mirrored to the build directory, and add each task which ensures this as a prerequisite of the master task (:build)

# File lib/startup_time/builder.rb, line 210
def copy_source_files
  Dir["#{SRC_DIR}/*.*"].each do |path|
    filename = File.basename(path)

    source = file(filename => path) do
      verbose(@verbosity == :verbose) { cp path, filename }
    end

    task build: source
  end
end
register_tasks() click to toggle source

register the prerequisites of the :build task. creates file tasks which:

a) keep the build directory sources in sync with the source directory b) rebuild target files if their source files are modified c) rebuild target files if their compilers are updated

# File lib/startup_time/builder.rb, line 227
def register_tasks
  copy_source_files
  compile_target_files
end
run(template, task, test) click to toggle source

run a shell command (string) by substituting the compiler path, source file, and target file into the supplied template string and executing the resulting command with the test's (optional) environment hash

# File lib/startup_time/builder.rb, line 235
def run(template, task, test)
  replacements = {
    compiler: Shellwords.escape(test[:compiler]),
    source: Shellwords.escape(task.source),
    target: Shellwords.escape(task.target),
  }

  command = template % replacements
  shell(command, env: test[:env])
end
shell(args, **options) click to toggle source

a wrapper for Rake's +FileUtils#sh+ method (which wraps +Kernel#spawn+) which allows the command's environment to be included in the final options hash rather than cramming it in as the first argument i.e.:

before:

sh FOO_VERBOSE: "0", "foo -c hello.foo -o hello", out: File::NULL

after:

shell "foo -c hello.foo -o hello", env: { FOO_VERBOSE: "0" }, out: File::NULL
# File lib/startup_time/builder.rb, line 258
def shell(args, **options)
  args = Array(args) # args is a string or array
  env = options.delete(:env)
  args.unshift(env) if env
  sh(*args, options)
end