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