bootsnap 1.5.1 → 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +31 -13
- data/ext/bootsnap/bootsnap.c +176 -36
- data/ext/bootsnap/extconf.rb +19 -14
- data/lib/bootsnap.rb +87 -15
- data/lib/bootsnap/cli.rb +112 -17
- data/lib/bootsnap/cli/worker_pool.rb +131 -0
- data/lib/bootsnap/compile_cache/iseq.rb +9 -1
- data/lib/bootsnap/compile_cache/yaml.rb +37 -16
- data/lib/bootsnap/load_path_cache.rb +2 -15
- data/lib/bootsnap/setup.rb +1 -36
- data/lib/bootsnap/version.rb +1 -1
- metadata +3 -3
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -107
data/ext/bootsnap/extconf.rb
CHANGED
@@ -1,19 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require("mkmf")
|
3
|
-
$CFLAGS << ' -O3 '
|
4
|
-
$CFLAGS << ' -std=c99'
|
5
3
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
$CFLAGS << ' -Wall'
|
10
|
-
$CFLAGS << ' -Werror'
|
11
|
-
$CFLAGS << ' -Wextra'
|
12
|
-
$CFLAGS << ' -Wpedantic'
|
4
|
+
if RUBY_ENGINE == 'ruby'
|
5
|
+
$CFLAGS << ' -O3 '
|
6
|
+
$CFLAGS << ' -std=c99'
|
13
7
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
# ruby.h has some -Wpedantic fails in some cases
|
9
|
+
# (e.g. https://github.com/Shopify/bootsnap/issues/15)
|
10
|
+
unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC'])
|
11
|
+
$CFLAGS << ' -Wall'
|
12
|
+
$CFLAGS << ' -Werror'
|
13
|
+
$CFLAGS << ' -Wextra'
|
14
|
+
$CFLAGS << ' -Wpedantic'
|
15
|
+
|
16
|
+
$CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is.
|
17
|
+
$CFLAGS << ' -Wno-keyword-macro' # hiding return
|
18
|
+
$CFLAGS << ' -Wno-gcc-compat' # ruby.h 2.6.0 on macos 10.14, dunno
|
19
|
+
end
|
18
20
|
|
19
|
-
create_makefile("bootsnap/bootsnap")
|
21
|
+
create_makefile("bootsnap/bootsnap")
|
22
|
+
else
|
23
|
+
File.write("Makefile", dummy_makefile($srcdir).join(""))
|
24
|
+
end
|
data/lib/bootsnap.rb
CHANGED
@@ -8,42 +8,114 @@ require_relative('bootsnap/compile_cache')
|
|
8
8
|
module Bootsnap
|
9
9
|
InvalidConfiguration = Class.new(StandardError)
|
10
10
|
|
11
|
+
class << self
|
12
|
+
attr_reader :logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.log!
|
16
|
+
self.logger = $stderr.method(:puts)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.logger=(logger)
|
20
|
+
@logger = logger
|
21
|
+
if logger.respond_to?(:debug)
|
22
|
+
self.instrumentation = ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
|
23
|
+
else
|
24
|
+
self.instrumentation = ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.instrumentation=(callback)
|
29
|
+
@instrumentation = callback
|
30
|
+
self.instrumentation_enabled = !!callback
|
31
|
+
end
|
32
|
+
|
33
|
+
def self._instrument(event, path)
|
34
|
+
@instrumentation.call(event, path)
|
35
|
+
end
|
36
|
+
|
11
37
|
def self.setup(
|
12
38
|
cache_dir:,
|
13
39
|
development_mode: true,
|
14
40
|
load_path_cache: true,
|
15
|
-
autoload_paths_cache:
|
16
|
-
disable_trace:
|
41
|
+
autoload_paths_cache: nil,
|
42
|
+
disable_trace: nil,
|
17
43
|
compile_cache_iseq: true,
|
18
44
|
compile_cache_yaml: true
|
19
45
|
)
|
20
|
-
|
21
|
-
|
46
|
+
unless autoload_paths_cache.nil?
|
47
|
+
warn "[DEPRECATED] Bootsnap's `autoload_paths_cache:` option is deprecated and will be removed. " \
|
48
|
+
"If you use Zeitwerk this option is useless, and if you are still using the classic autoloader " \
|
49
|
+
"upgrading is recommended."
|
22
50
|
end
|
23
51
|
|
24
|
-
|
52
|
+
unless disable_trace.nil?
|
53
|
+
warn "[DEPRECATED] Bootsnap's `disable_trace:` option is deprecated and will be removed. " \
|
54
|
+
"If you use Ruby 2.5 or newer this option is useless, if not upgrading is recommended."
|
55
|
+
end
|
56
|
+
|
57
|
+
if compile_cache_iseq && !iseq_cache_supported?
|
58
|
+
warn "Ruby 2.5 has a bug that break code tracing when code is loaded from cache. It is recommened " \
|
59
|
+
"to turn `compile_cache_iseq` off on Ruby 2.5"
|
60
|
+
end
|
25
61
|
|
26
62
|
Bootsnap::LoadPathCache.setup(
|
27
|
-
cache_path: cache_dir + '/bootsnap
|
63
|
+
cache_path: cache_dir + '/bootsnap/load-path-cache',
|
28
64
|
development_mode: development_mode,
|
29
|
-
active_support: autoload_paths_cache
|
30
65
|
) if load_path_cache
|
31
66
|
|
32
67
|
Bootsnap::CompileCache.setup(
|
33
|
-
cache_dir: cache_dir + '/bootsnap
|
68
|
+
cache_dir: cache_dir + '/bootsnap/compile-cache',
|
34
69
|
iseq: compile_cache_iseq,
|
35
70
|
yaml: compile_cache_yaml
|
36
71
|
)
|
37
72
|
end
|
38
73
|
|
39
|
-
def self.
|
40
|
-
if
|
41
|
-
|
42
|
-
|
43
|
-
|
74
|
+
def self.iseq_cache_supported?
|
75
|
+
return @iseq_cache_supported if defined? @iseq_cache_supported
|
76
|
+
|
77
|
+
ruby_version = Gem::Version.new(RUBY_VERSION)
|
78
|
+
@iseq_cache_supported = ruby_version < Gem::Version.new('2.5.0') || ruby_version >= Gem::Version.new('2.6.0')
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.default_setup
|
82
|
+
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV']
|
83
|
+
development_mode = ['', nil, 'development'].include?(env)
|
84
|
+
|
85
|
+
unless ENV['DISABLE_BOOTSNAP']
|
86
|
+
cache_dir = ENV['BOOTSNAP_CACHE_DIR']
|
87
|
+
unless cache_dir
|
88
|
+
config_dir_frame = caller.detect do |line|
|
89
|
+
line.include?('/config/')
|
90
|
+
end
|
91
|
+
|
92
|
+
unless config_dir_frame
|
93
|
+
$stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
|
94
|
+
$stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
|
95
|
+
$stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
|
96
|
+
|
97
|
+
raise("couldn't infer bootsnap cache directory")
|
98
|
+
end
|
99
|
+
|
100
|
+
path = config_dir_frame.split(/:\d+:/).first
|
101
|
+
path = File.dirname(path) until File.basename(path) == 'config'
|
102
|
+
app_root = File.dirname(path)
|
103
|
+
|
104
|
+
cache_dir = File.join(app_root, 'tmp', 'cache')
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
setup(
|
109
|
+
cache_dir: cache_dir,
|
110
|
+
development_mode: development_mode,
|
111
|
+
load_path_cache: !ENV['DISABLE_BOOTSNAP_LOAD_PATH_CACHE'],
|
112
|
+
compile_cache_iseq: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'] && iseq_cache_supported?,
|
113
|
+
compile_cache_yaml: !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'],
|
44
114
|
)
|
45
|
-
|
46
|
-
|
115
|
+
|
116
|
+
if ENV['BOOTSNAP_LOG']
|
117
|
+
log!
|
118
|
+
end
|
47
119
|
end
|
48
120
|
end
|
49
121
|
end
|
data/lib/bootsnap/cli.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bootsnap'
|
4
|
+
require 'bootsnap/cli/worker_pool'
|
4
5
|
require 'optparse'
|
5
6
|
require 'fileutils'
|
7
|
+
require 'etc'
|
6
8
|
|
7
9
|
module Bootsnap
|
8
10
|
class CLI
|
@@ -19,49 +21,68 @@ module Bootsnap
|
|
19
21
|
|
20
22
|
attr_reader :cache_dir, :argv
|
21
23
|
|
22
|
-
attr_accessor :compile_gemfile, :exclude
|
24
|
+
attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :jobs
|
23
25
|
|
24
26
|
def initialize(argv)
|
25
27
|
@argv = argv
|
26
28
|
self.cache_dir = ENV.fetch('BOOTSNAP_CACHE_DIR', 'tmp/cache')
|
27
29
|
self.compile_gemfile = false
|
28
30
|
self.exclude = nil
|
31
|
+
self.verbose = false
|
32
|
+
self.jobs = Etc.nprocessors
|
33
|
+
self.iseq = true
|
34
|
+
self.yaml = true
|
29
35
|
end
|
30
36
|
|
31
37
|
def precompile_command(*sources)
|
32
38
|
require 'bootsnap/compile_cache/iseq'
|
39
|
+
require 'bootsnap/compile_cache/yaml'
|
33
40
|
|
34
41
|
fix_default_encoding do
|
35
42
|
Bootsnap::CompileCache::ISeq.cache_dir = self.cache_dir
|
43
|
+
Bootsnap::CompileCache::YAML.init!
|
44
|
+
Bootsnap::CompileCache::YAML.cache_dir = self.cache_dir
|
45
|
+
|
46
|
+
@work_pool = WorkerPool.create(size: jobs, jobs: {
|
47
|
+
ruby: method(:precompile_ruby),
|
48
|
+
yaml: method(:precompile_yaml),
|
49
|
+
})
|
50
|
+
@work_pool.spawn
|
51
|
+
|
52
|
+
main_sources = sources.map { |d| File.expand_path(d) }
|
53
|
+
precompile_ruby_files(main_sources)
|
54
|
+
precompile_yaml_files(main_sources)
|
36
55
|
|
37
56
|
if compile_gemfile
|
38
|
-
|
57
|
+
# Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
|
58
|
+
gem_exclude = Regexp.union([exclude, '/spec/', '/test/'].compact)
|
59
|
+
precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
|
60
|
+
|
61
|
+
# Gems that include YAML files usually don't put them in `lib/`.
|
62
|
+
# So we look at the gem root.
|
63
|
+
gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems\/[^/]+}
|
64
|
+
gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
|
65
|
+
precompile_yaml_files(gem_paths, exclude: gem_exclude)
|
39
66
|
end
|
40
67
|
|
41
|
-
|
42
|
-
|
43
|
-
list_ruby_files(path).each do |ruby_file|
|
44
|
-
if !exclude || !exclude.match?(ruby_file)
|
45
|
-
CompileCache::ISeq.fetch(ruby_file, cache_dir: cache_dir)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
68
|
+
if exitstatus = @work_pool.shutdown
|
69
|
+
exit(exitstatus)
|
49
70
|
end
|
50
71
|
end
|
51
72
|
0
|
52
73
|
end
|
53
74
|
|
54
75
|
dir_sort = begin
|
55
|
-
Dir[
|
76
|
+
Dir[__FILE__, sort: false]
|
56
77
|
true
|
57
78
|
rescue ArgumentError, TypeError
|
58
79
|
false
|
59
80
|
end
|
60
81
|
|
61
82
|
if dir_sort
|
62
|
-
def
|
83
|
+
def list_files(path, pattern)
|
63
84
|
if File.directory?(path)
|
64
|
-
Dir[File.join(path,
|
85
|
+
Dir[File.join(path, pattern), sort: false]
|
65
86
|
elsif File.exist?(path)
|
66
87
|
[path]
|
67
88
|
else
|
@@ -69,9 +90,9 @@ module Bootsnap
|
|
69
90
|
end
|
70
91
|
end
|
71
92
|
else
|
72
|
-
def
|
93
|
+
def list_files(path, pattern)
|
73
94
|
if File.directory?(path)
|
74
|
-
Dir[File.join(path,
|
95
|
+
Dir[File.join(path, pattern)]
|
75
96
|
elsif File.exist?(path)
|
76
97
|
[path]
|
77
98
|
else
|
@@ -93,6 +114,51 @@ module Bootsnap
|
|
93
114
|
|
94
115
|
private
|
95
116
|
|
117
|
+
def precompile_yaml_files(load_paths, exclude: self.exclude)
|
118
|
+
return unless yaml
|
119
|
+
|
120
|
+
load_paths.each do |path|
|
121
|
+
if !exclude || !exclude.match?(path)
|
122
|
+
list_files(path, '**/*.{yml,yaml}').each do |yaml_file|
|
123
|
+
# We ignore hidden files to not match the various .ci.yml files
|
124
|
+
if !File.basename(yaml_file).start_with?('.') && (!exclude || !exclude.match?(yaml_file))
|
125
|
+
@work_pool.push(:yaml, yaml_file)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def precompile_yaml(*yaml_files)
|
133
|
+
Array(yaml_files).each do |yaml_file|
|
134
|
+
if CompileCache::YAML.precompile(yaml_file, cache_dir: cache_dir)
|
135
|
+
STDERR.puts(yaml_file) if verbose
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def precompile_ruby_files(load_paths, exclude: self.exclude)
|
141
|
+
return unless iseq
|
142
|
+
|
143
|
+
load_paths.each do |path|
|
144
|
+
if !exclude || !exclude.match?(path)
|
145
|
+
list_files(path, '**/*.rb').each do |ruby_file|
|
146
|
+
if !exclude || !exclude.match?(ruby_file)
|
147
|
+
@work_pool.push(:ruby, ruby_file)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def precompile_ruby(*ruby_files)
|
155
|
+
Array(ruby_files).each do |ruby_file|
|
156
|
+
if CompileCache::ISeq.precompile(ruby_file, cache_dir: cache_dir)
|
157
|
+
STDERR.puts(ruby_file) if verbose
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
96
162
|
def fix_default_encoding
|
97
163
|
if Encoding.default_external == Encoding::US_ASCII
|
98
164
|
Encoding.default_external = Encoding::UTF_8
|
@@ -114,7 +180,12 @@ module Bootsnap
|
|
114
180
|
end
|
115
181
|
|
116
182
|
def cache_dir=(dir)
|
117
|
-
@cache_dir = File.expand_path(File.join(dir, 'bootsnap
|
183
|
+
@cache_dir = File.expand_path(File.join(dir, 'bootsnap/compile-cache'))
|
184
|
+
end
|
185
|
+
|
186
|
+
def exclude_pattern(pattern)
|
187
|
+
(@exclude_patterns ||= []) << Regexp.new(pattern)
|
188
|
+
self.exclude = Regexp.union(@exclude_patterns)
|
118
189
|
end
|
119
190
|
|
120
191
|
def parser
|
@@ -131,6 +202,20 @@ module Bootsnap
|
|
131
202
|
self.cache_dir = dir
|
132
203
|
end
|
133
204
|
|
205
|
+
help = <<~EOS
|
206
|
+
Print precompiled paths.
|
207
|
+
EOS
|
208
|
+
opts.on('--verbose', '-v', help.strip) do
|
209
|
+
self.verbose = true
|
210
|
+
end
|
211
|
+
|
212
|
+
help = <<~EOS
|
213
|
+
Number of workers to use. Default to number of processors, set to 0 to disable multi-processing.
|
214
|
+
EOS
|
215
|
+
opts.on('--jobs JOBS', '-j', help.strip) do |jobs|
|
216
|
+
self.jobs = Integer(jobs)
|
217
|
+
end
|
218
|
+
|
134
219
|
opts.separator ""
|
135
220
|
opts.separator "COMMANDS"
|
136
221
|
opts.separator ""
|
@@ -144,7 +229,17 @@ module Bootsnap
|
|
144
229
|
help = <<~EOS
|
145
230
|
Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api'
|
146
231
|
EOS
|
147
|
-
opts.on('--exclude PATTERN', help) { |pattern|
|
232
|
+
opts.on('--exclude PATTERN', help) { |pattern| exclude_pattern(pattern) }
|
233
|
+
|
234
|
+
help = <<~EOS
|
235
|
+
Disable ISeq (.rb) precompilation.
|
236
|
+
EOS
|
237
|
+
opts.on('--no-iseq', help) { self.iseq = false }
|
238
|
+
|
239
|
+
help = <<~EOS
|
240
|
+
Disable YAML precompilation.
|
241
|
+
EOS
|
242
|
+
opts.on('--no-yaml', help) { self.yaml = false }
|
148
243
|
end
|
149
244
|
end
|
150
245
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bootsnap
|
4
|
+
class CLI
|
5
|
+
class WorkerPool
|
6
|
+
class << self
|
7
|
+
def create(size:, jobs:)
|
8
|
+
if size > 0 && Process.respond_to?(:fork)
|
9
|
+
new(size: size, jobs: jobs)
|
10
|
+
else
|
11
|
+
Inline.new(jobs: jobs)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Inline
|
17
|
+
def initialize(jobs: {})
|
18
|
+
@jobs = jobs
|
19
|
+
end
|
20
|
+
|
21
|
+
def push(job, *args)
|
22
|
+
@jobs.fetch(job).call(*args)
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def spawn
|
27
|
+
# noop
|
28
|
+
end
|
29
|
+
|
30
|
+
def shutdown
|
31
|
+
# noop
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Worker
|
36
|
+
attr_reader :to_io, :pid
|
37
|
+
|
38
|
+
def initialize(jobs)
|
39
|
+
@jobs = jobs
|
40
|
+
@pipe_out, @to_io = IO.pipe
|
41
|
+
@pid = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def write(message, block: true)
|
45
|
+
payload = Marshal.dump(message)
|
46
|
+
if block
|
47
|
+
to_io.write(payload)
|
48
|
+
true
|
49
|
+
else
|
50
|
+
to_io.write_nonblock(payload, exception: false) != :wait_writable
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def close
|
55
|
+
to_io.close
|
56
|
+
end
|
57
|
+
|
58
|
+
def work_loop
|
59
|
+
loop do
|
60
|
+
job, *args = Marshal.load(@pipe_out)
|
61
|
+
return if job == :exit
|
62
|
+
@jobs.fetch(job).call(*args)
|
63
|
+
end
|
64
|
+
rescue IOError
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def spawn
|
69
|
+
@pid = Process.fork do
|
70
|
+
to_io.close
|
71
|
+
work_loop
|
72
|
+
exit!(0)
|
73
|
+
end
|
74
|
+
@pipe_out.close
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(size:, jobs: {})
|
80
|
+
@size = size
|
81
|
+
@jobs = jobs
|
82
|
+
@queue = Queue.new
|
83
|
+
@pids = []
|
84
|
+
end
|
85
|
+
|
86
|
+
def spawn
|
87
|
+
@workers = @size.times.map { Worker.new(@jobs) }
|
88
|
+
@workers.each(&:spawn)
|
89
|
+
@dispatcher_thread = Thread.new { dispatch_loop }
|
90
|
+
@dispatcher_thread.abort_on_exception = true
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def dispatch_loop
|
95
|
+
loop do
|
96
|
+
case job = @queue.pop
|
97
|
+
when nil
|
98
|
+
@workers.each do |worker|
|
99
|
+
worker.write([:exit])
|
100
|
+
worker.close
|
101
|
+
end
|
102
|
+
return true
|
103
|
+
else
|
104
|
+
unless @workers.sample.write(job, block: false)
|
105
|
+
free_worker.write(job)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def free_worker
|
112
|
+
IO.select(nil, @workers)[1].sample
|
113
|
+
end
|
114
|
+
|
115
|
+
def push(*args)
|
116
|
+
@queue.push(args)
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def shutdown
|
121
|
+
@queue.close
|
122
|
+
@dispatcher_thread.join
|
123
|
+
@workers.each do |worker|
|
124
|
+
_pid, status = Process.wait2(worker.pid)
|
125
|
+
return status.exitstatus unless status.success?
|
126
|
+
end
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|