startup-time 1.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.md +192 -0
- data/README.md +121 -0
- data/bin/startup-time +8 -0
- data/lib/startup_time.rb +9 -0
- data/lib/startup_time/app.rb +142 -0
- data/lib/startup_time/builder.rb +220 -0
- data/lib/startup_time/options.rb +140 -0
- data/lib/startup_time/registry.rb +83 -0
- data/lib/startup_time/services.rb +22 -0
- data/lib/startup_time/util.rb +17 -0
- data/lib/startup_time/version.rb +5 -0
- data/resources/src/HelloJava.java +5 -0
- data/resources/src/HelloKotlin.kt +3 -0
- data/resources/src/HelloScala.scala +3 -0
- data/resources/src/hello.bash +1 -0
- data/resources/src/hello.c +5 -0
- data/resources/src/hello.cpp +7 -0
- data/resources/src/hello.cr +1 -0
- data/resources/src/hello.d +5 -0
- data/resources/src/hello.go +7 -0
- data/resources/src/hello.hs +1 -0
- data/resources/src/hello.js +1 -0
- data/resources/src/hello.lua +1 -0
- data/resources/src/hello.nim +1 -0
- data/resources/src/hello.p6 +1 -0
- data/resources/src/hello.pl +1 -0
- data/resources/src/hello.py +1 -0
- data/resources/src/hello.rb +3 -0
- data/resources/src/hello.rs +3 -0
- data/resources/tests.yaml +286 -0
- metadata +219 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rake'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
require_relative 'util'
|
|
6
|
+
|
|
7
|
+
module StartupTime
|
|
8
|
+
# StartupTime::Builder - clean and prepare the build directory
|
|
9
|
+
#
|
|
10
|
+
# this class provides two methods which clean (i.e. remove) and prepare the build
|
|
11
|
+
# directory. the latter is done by executing the following tasks:
|
|
12
|
+
#
|
|
13
|
+
# 1) copy source files from the source directory to the build directory
|
|
14
|
+
# 2) compile all of the target files that need to be compiled from source files
|
|
15
|
+
#
|
|
16
|
+
# once these tasks are complete, everything required to run the benchmark tests
|
|
17
|
+
# will be available in the build directory
|
|
18
|
+
class Builder
|
|
19
|
+
SRC_DIR = File.absolute_path('../../resources/src', __dir__)
|
|
20
|
+
|
|
21
|
+
include Rake::DSL
|
|
22
|
+
include Util # for `which`
|
|
23
|
+
include Services.mixin %i[options selected_tests]
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@verbosity = options.verbosity
|
|
27
|
+
@build_dir = options.build_dir
|
|
28
|
+
|
|
29
|
+
Rake.verbose(@verbosity != :quiet)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# remove the build directory and its contents
|
|
33
|
+
def clean!
|
|
34
|
+
rm_rf @build_dir
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# ensure the build directory is in a fit state to run the tests i.e. copy
|
|
38
|
+
# source files and compile target files
|
|
39
|
+
def build!
|
|
40
|
+
verbose(@verbosity == :verbose) do
|
|
41
|
+
mkdir_p(@build_dir) unless Dir.exist?(@build_dir)
|
|
42
|
+
cd @build_dir
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
register_tasks
|
|
46
|
+
|
|
47
|
+
Rake::Task[:build].invoke
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# a wrapper for Rake's +FileUtils#sh+ method (which wraps +Kernel#spawn+)
|
|
53
|
+
# which allows the command's environment to be included in the final options
|
|
54
|
+
# hash rather than cramming it in as the first argument i.e.:
|
|
55
|
+
#
|
|
56
|
+
# before:
|
|
57
|
+
#
|
|
58
|
+
# sh FOO_VERBOSE: "0", "foo -c hello.foo -o hello", out: File::NULL
|
|
59
|
+
#
|
|
60
|
+
# after:
|
|
61
|
+
#
|
|
62
|
+
# shell "foo -c hello.foo -o hello", env: { FOO_VERBOSE: "0" }, out: File::NULL
|
|
63
|
+
#
|
|
64
|
+
def shell(args, **options)
|
|
65
|
+
args = Array(args) # args is a string or array
|
|
66
|
+
env = options.delete(:env)
|
|
67
|
+
args.unshift(env) if env
|
|
68
|
+
sh(*args, options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# a conditional version of Rake's `file` task which compiles a source file to
|
|
72
|
+
# a target file via the block provided. if the compiler isn't installed, the
|
|
73
|
+
# task is skipped.
|
|
74
|
+
#
|
|
75
|
+
# returns a truthy value (the target filename) if the task is created, or nil
|
|
76
|
+
# otherwise
|
|
77
|
+
def compile_if(id, **options)
|
|
78
|
+
tests = options[:force] ? Registry::TESTS : selected_tests
|
|
79
|
+
|
|
80
|
+
# look up the test's spec among the remaining tests which haven't been
|
|
81
|
+
# excluded by --omit or --only
|
|
82
|
+
return unless (test = tests[id])
|
|
83
|
+
|
|
84
|
+
# the compiler name (e.g. "crystal") is usually the same as the ID for
|
|
85
|
+
# the test (e.g. "crystal"), but can be supplied explicitly in the test
|
|
86
|
+
# spec e.g. { id: "java-native", compiler: "native-image" }
|
|
87
|
+
compiler = test[:compiler] || id
|
|
88
|
+
|
|
89
|
+
return unless (compiler_path = which(compiler))
|
|
90
|
+
|
|
91
|
+
# the source filename must be supplied
|
|
92
|
+
source = test.fetch(:source)
|
|
93
|
+
|
|
94
|
+
# infer the target if not specified
|
|
95
|
+
unless (target = test[:target])
|
|
96
|
+
command = Array(test[:command])
|
|
97
|
+
|
|
98
|
+
if command.length == 1
|
|
99
|
+
target = command.first
|
|
100
|
+
elsif source.match?(/^[A-Z]/) # JVM language
|
|
101
|
+
target = source.pathmap('%n.class')
|
|
102
|
+
else # native executable
|
|
103
|
+
target = '%s.out' % source
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# update the test hash's compiler field to point to the compiler's
|
|
108
|
+
# absolute path (which may be mocked)
|
|
109
|
+
test = test.merge(compiler: compiler_path)
|
|
110
|
+
|
|
111
|
+
# pass the test object as the block's second argument. Rake passes an
|
|
112
|
+
# instance of +Rake::TaskArguments+, a Hash-like object which provides
|
|
113
|
+
# access to the command-line arguments for a Rake task e.g. { name:
|
|
114
|
+
# "world" } for `rake greet[world]`. since we're not relying on Rake's
|
|
115
|
+
# limited option-handling support, we have no use for that here, so we
|
|
116
|
+
# simply replace it with the test data.
|
|
117
|
+
wrapper = ->(task, _) { yield(task, test) }
|
|
118
|
+
|
|
119
|
+
# declare the prerequisites for the target file.
|
|
120
|
+
# compiler_path: recompile if the compiler has been
|
|
121
|
+
# updated since the target was last built
|
|
122
|
+
file(target => [source, compiler_path], &wrapper)
|
|
123
|
+
|
|
124
|
+
# add the target file to the build task
|
|
125
|
+
task :build => target
|
|
126
|
+
|
|
127
|
+
target
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# register the prerequisites of the :build task. creates file tasks which:
|
|
131
|
+
#
|
|
132
|
+
# a) keep the build directory sources in sync with the source directory
|
|
133
|
+
# b) rebuild target files if their source files are modified
|
|
134
|
+
# c) rebuild target files if their compilers are updated
|
|
135
|
+
def register_tasks
|
|
136
|
+
copy_source_files
|
|
137
|
+
compile_target_files
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# ensure each file in the source directory is mirrored to the build
|
|
141
|
+
# directory, and add each task which ensures this as a prerequisite
|
|
142
|
+
# of the master task (:build)
|
|
143
|
+
def copy_source_files
|
|
144
|
+
Dir["#{SRC_DIR}/*.*"].each do |path|
|
|
145
|
+
filename = File.basename(path)
|
|
146
|
+
|
|
147
|
+
source = file(filename => path) do
|
|
148
|
+
verbose(@verbosity == :verbose) { cp path, filename }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
task build: source
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# run a shell command (string) by substituting the compiler path, source
|
|
156
|
+
# file, and target file into the supplied template and executing the
|
|
157
|
+
# resulting command with the test's (optional) environment hash
|
|
158
|
+
def run(template, task, test)
|
|
159
|
+
replacements = {
|
|
160
|
+
compiler: Shellwords.escape(test[:compiler]),
|
|
161
|
+
source: Shellwords.escape(task.source),
|
|
162
|
+
target: Shellwords.escape(task.name),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
command = template % replacements
|
|
166
|
+
shell(command, env: test[:env])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# make sure the target files (e.g. native executables and JVM .class files)
|
|
170
|
+
# are built if their compilers are installed
|
|
171
|
+
def compile_target_files
|
|
172
|
+
# handle the tests which have compile templates by turning them into
|
|
173
|
+
# blocks which substitute the compiler, source file and target file into
|
|
174
|
+
# the corresponding placeholders in the template and then execute the
|
|
175
|
+
# command via `shell`
|
|
176
|
+
selected_tests.each do |id, selected|
|
|
177
|
+
if (command = selected[:compile])
|
|
178
|
+
block = ->(task, test) { run(command, task, test) }
|
|
179
|
+
compile_if(id, &block)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# native-image compiles .class files to native binaries. it differs from
|
|
184
|
+
# the other tasks because it depends on a target file rather than a
|
|
185
|
+
# source file i.e. it depends on the target of the javac task
|
|
186
|
+
java_native = compile_if('java-native', connect: false) do |t, test|
|
|
187
|
+
# XXX native-image doesn't provide a way to silence its output, so
|
|
188
|
+
# send it to /dev/null
|
|
189
|
+
shell [test[:compiler], "-H:Name=#{t.name}", '--no-server', '-O1', t.source.ext], {
|
|
190
|
+
out: File::NULL
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if java_native
|
|
195
|
+
javac = compile_if(:javac, force: true) do |task, test|
|
|
196
|
+
run('%{compiler} -d . %{source}', task, test)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if javac
|
|
200
|
+
task :build => java_native
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
compile_if :javac do |task, test|
|
|
204
|
+
run('%{compiler} -d . %{source}', task, test)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
compile_if 'kotlinc-native' do |t, test|
|
|
209
|
+
# XXX kotlinc-native doesn't provide a way to silence
|
|
210
|
+
# its debug messages, so file them under /dev/null
|
|
211
|
+
shell %W[#{test[:compiler]} -opt -o #{t.name} #{t.source}], out: File::NULL
|
|
212
|
+
|
|
213
|
+
# XXX work around a kotlinc-native "feature"
|
|
214
|
+
# https://github.com/JetBrains/kotlin-native/issues/967
|
|
215
|
+
exe = "#{t.name}.kexe" # XXX or .exe, or...
|
|
216
|
+
verbose(@verbosity == :verbose) { mv exe, t.name } if File.exist?(exe)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'env_paths'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
module StartupTime
|
|
7
|
+
# StartupTime::Options - a struct-like interface to the app options set or
|
|
8
|
+
# overridden on the command line
|
|
9
|
+
class Options
|
|
10
|
+
BUILD_DIR = EnvPaths.get('startup-time', suffix: false).cache
|
|
11
|
+
ROUNDS = 10
|
|
12
|
+
|
|
13
|
+
attr_reader :action, :build_dir, :format, :rounds, :verbosity
|
|
14
|
+
|
|
15
|
+
include Services.mixin %i[registry]
|
|
16
|
+
|
|
17
|
+
def initialize(args)
|
|
18
|
+
@action = :benchmark
|
|
19
|
+
@build_dir = BUILD_DIR
|
|
20
|
+
@format = :default
|
|
21
|
+
@parser = nil
|
|
22
|
+
@rounds = ROUNDS
|
|
23
|
+
@verbosity = :default
|
|
24
|
+
|
|
25
|
+
parse! args
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# the usage message (string) generated by the option parser for this tool
|
|
29
|
+
def usage
|
|
30
|
+
@parser.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# process the command-line options and assign values to the corresponding
|
|
36
|
+
# instance variables
|
|
37
|
+
def parse!(args)
|
|
38
|
+
@parser = OptionParser.new do |opts|
|
|
39
|
+
opts.on(
|
|
40
|
+
'-c',
|
|
41
|
+
'--count',
|
|
42
|
+
'--rounds INTEGER',
|
|
43
|
+
Integer,
|
|
44
|
+
"The number of times to execute each test (default: #{ROUNDS})"
|
|
45
|
+
) do |value|
|
|
46
|
+
@rounds = value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on(
|
|
50
|
+
'--clean',
|
|
51
|
+
'Remove the build directory and exit (targets will be ',
|
|
52
|
+
'recompiled on the next run)'
|
|
53
|
+
) do
|
|
54
|
+
@action = :clean
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts.on(
|
|
58
|
+
'-d',
|
|
59
|
+
'--dir PATH',
|
|
60
|
+
String,
|
|
61
|
+
"Specify the build directory (default: #{BUILD_DIR})"
|
|
62
|
+
) do |value|
|
|
63
|
+
@build_dir = value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on(
|
|
67
|
+
'-h',
|
|
68
|
+
'--help',
|
|
69
|
+
'Show this help message and exit'
|
|
70
|
+
) do
|
|
71
|
+
@action = :help
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
opts.on(
|
|
75
|
+
'-H',
|
|
76
|
+
'--help-only',
|
|
77
|
+
'--help-omit',
|
|
78
|
+
'Show the IDs and groups that can be passed to --only and --omit'
|
|
79
|
+
) do
|
|
80
|
+
@action = :show_ids
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
opts.on(
|
|
84
|
+
'-j',
|
|
85
|
+
'--json',
|
|
86
|
+
'Output the results in JSON format (implies --quiet)'
|
|
87
|
+
) do
|
|
88
|
+
@format = :json
|
|
89
|
+
@verbosity = :quiet
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
opts.on(
|
|
93
|
+
'-o',
|
|
94
|
+
'--only LIST',
|
|
95
|
+
Array, # comma-separated strings
|
|
96
|
+
'Only execute the specified tests (comma-separated list of ',
|
|
97
|
+
'IDs/groups)'
|
|
98
|
+
) do |values|
|
|
99
|
+
values.each { |value| registry.only(value.strip) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
opts.on(
|
|
103
|
+
'-O',
|
|
104
|
+
'--omit LIST',
|
|
105
|
+
Array, # comma-separated strings
|
|
106
|
+
"Don't execute the specified tests (comma-separated list ",
|
|
107
|
+
'of IDs/groups)'
|
|
108
|
+
) do |values|
|
|
109
|
+
values.each { |value| registry.omit(value.strip) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
opts.on(
|
|
113
|
+
'-q',
|
|
114
|
+
'--quiet',
|
|
115
|
+
'Suppress all inessential output'
|
|
116
|
+
) do
|
|
117
|
+
@verbosity = :quiet
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
opts.on(
|
|
121
|
+
'-v',
|
|
122
|
+
'--verbose',
|
|
123
|
+
'Enable verbose logging'
|
|
124
|
+
) do
|
|
125
|
+
@verbosity = :verbose
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
opts.on(
|
|
129
|
+
'-V',
|
|
130
|
+
'--version',
|
|
131
|
+
'Display the version and exit'
|
|
132
|
+
) do
|
|
133
|
+
@action = :version
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@parser.parse!(args)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
5
|
+
require 'active_support/core_ext/hash/slice' # XXX in core since 2.5
|
|
6
|
+
require 'set'
|
|
7
|
+
require 'shellwords'
|
|
8
|
+
require 'yaml'
|
|
9
|
+
|
|
10
|
+
module StartupTime
|
|
11
|
+
# StartupTime::Registry - an interface to the tests configured in
|
|
12
|
+
# resources/tests.yaml
|
|
13
|
+
class Registry
|
|
14
|
+
# the path to the test configuration file
|
|
15
|
+
TEST_DATA = File.join(__dir__, '../../resources/tests.yaml')
|
|
16
|
+
|
|
17
|
+
# map each group (e.g. "jvm") to an array of its member IDs
|
|
18
|
+
# (e.g. ["java", "kotlin", "scala"])
|
|
19
|
+
GROUPS = Hash.new { |h, k| h[k] = [] }.with_indifferent_access
|
|
20
|
+
|
|
21
|
+
# the top-level hash in tests.yaml. keys are test IDs (e.g. "ruby"); values
|
|
22
|
+
# are test specs
|
|
23
|
+
TESTS = YAML.load_file(TEST_DATA).with_indifferent_access
|
|
24
|
+
|
|
25
|
+
# perform some basic sanity checks on the test specs and populate the group
|
|
26
|
+
# -> tests map
|
|
27
|
+
TESTS.each do |id, test|
|
|
28
|
+
if test[:compile] && !test[:source]
|
|
29
|
+
abort "invalid test spec (#{id}): compiled tests must define a source file"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test[:groups].each do |group|
|
|
33
|
+
abort "invalid test spec (#{id}): group ID (#{group}) conflicts with test ID" if TESTS[group]
|
|
34
|
+
GROUPS[group] << id
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# returns a hash which maps test-ID keys (e.g. "scala") to their
|
|
39
|
+
# corresponding group names (e.g. "compiled, jvm, slow")
|
|
40
|
+
def self.ids_to_groups
|
|
41
|
+
TESTS.entries.map { |id, test| [id, test[:groups].sort.join(', ')] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@omit = Set.new
|
|
46
|
+
@only = Set.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# remove the specified test(s) from the set of enabled tests
|
|
50
|
+
def omit(id)
|
|
51
|
+
@omit.merge(ids_for(id))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# add the specified test(s) to the set of enabled tests
|
|
55
|
+
def only(id)
|
|
56
|
+
@only.merge(ids_for(id))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# the subset of candidate tests which satisfy the `only` and `omit` criteria
|
|
60
|
+
def selected_tests
|
|
61
|
+
only = !@only.empty? ? @only : TESTS.keys.to_set
|
|
62
|
+
test_ids = (only - @omit).to_a
|
|
63
|
+
TESTS.slice(*test_ids)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# takes a test ID or group ID and resolves it into its corresponding test IDs.
|
|
69
|
+
# if it's already a test ID, it's wrapped in an array; otherwise, an array
|
|
70
|
+
# of test IDs belonging to the group is returned.
|
|
71
|
+
def ids_for(id)
|
|
72
|
+
if TESTS[id]
|
|
73
|
+
ids = [id]
|
|
74
|
+
elsif GROUPS.key?(id)
|
|
75
|
+
ids = GROUPS[id]
|
|
76
|
+
else
|
|
77
|
+
abort "Can't resolve IDs for: #{id.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
ids
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'wireless'
|
|
4
|
+
|
|
5
|
+
module StartupTime
|
|
6
|
+
# shared dependencies required by multiple components
|
|
7
|
+
Services = Wireless.new do
|
|
8
|
+
# the component responsible for managing the build directory
|
|
9
|
+
once(:builder) { Builder.new }
|
|
10
|
+
|
|
11
|
+
# a hash which maps test IDs (e.g. "scala") to group names
|
|
12
|
+
# (e.g. "compiled, jvm, slow")
|
|
13
|
+
once(:ids_to_groups) { Registry.ids_to_groups }
|
|
14
|
+
|
|
15
|
+
# an interface to the tests configured in resources/tests.yaml
|
|
16
|
+
once(:registry) { Registry.new }
|
|
17
|
+
|
|
18
|
+
# the tests which remain after the --only and --omit filters have been
|
|
19
|
+
# applied
|
|
20
|
+
once(:selected_tests) { |wl| wl[:registry].selected_tests }
|
|
21
|
+
end
|
|
22
|
+
end
|