bootsnap 0.2.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/.gitignore +17 -0
- data/.rubocop.yml +7 -0
- data/CONTRIBUTING.md +21 -0
- data/Gemfile +4 -0
- data/README.md +49 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/testunit +8 -0
- data/bootsnap.gemspec +34 -0
- data/dev.yml +5 -0
- data/ext/bootsnap/bootsnap.c +542 -0
- data/ext/bootsnap/bootsnap.h +10 -0
- data/ext/bootsnap/crc32.c +16 -0
- data/ext/bootsnap/extconf.rb +7 -0
- data/lib/bootsnap.rb +38 -0
- data/lib/bootsnap/compile_cache.rb +16 -0
- data/lib/bootsnap/compile_cache/iseq.rb +74 -0
- data/lib/bootsnap/compile_cache/yaml.rb +54 -0
- data/lib/bootsnap/explicit_require.rb +44 -0
- data/lib/bootsnap/load_path_cache.rb +52 -0
- data/lib/bootsnap/load_path_cache/cache.rb +150 -0
- data/lib/bootsnap/load_path_cache/change_observer.rb +56 -0
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +68 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +88 -0
- data/lib/bootsnap/load_path_cache/path.rb +93 -0
- data/lib/bootsnap/load_path_cache/path_scanner.rb +44 -0
- data/lib/bootsnap/load_path_cache/store.rb +77 -0
- data/lib/bootsnap/version.rb +3 -0
- metadata +172 -0
@@ -0,0 +1,7 @@
|
|
1
|
+
require "mkmf"
|
2
|
+
$CFLAGS << ' -O3 -msse4.2 -std=c99'
|
3
|
+
have_header('x86intrin.h')
|
4
|
+
$CFLAGS << ' -Wall -Wextra -Wpedantic -Werror'
|
5
|
+
$CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is.
|
6
|
+
$CFLAGS << ' -Wno-keyword-macro' # hiding return
|
7
|
+
create_makefile("bootsnap/bootsnap")
|
data/lib/bootsnap.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'bootsnap/version'
|
2
|
+
require_relative 'bootsnap/load_path_cache'
|
3
|
+
require_relative 'bootsnap/compile_cache'
|
4
|
+
|
5
|
+
module Bootsnap
|
6
|
+
InvalidConfiguration = Class.new(StandardError)
|
7
|
+
|
8
|
+
def self.setup(
|
9
|
+
cache_dir:,
|
10
|
+
development_mode: true,
|
11
|
+
load_path_cache: true,
|
12
|
+
autoload_paths_cache: true,
|
13
|
+
disable_trace: false,
|
14
|
+
compile_cache_iseq: true,
|
15
|
+
compile_cache_yaml: true
|
16
|
+
)
|
17
|
+
if autoload_paths_cache && !load_path_cache
|
18
|
+
raise InvalidConfiguration, "feature 'autoload_paths_cache' depends on feature 'load_path_cache'"
|
19
|
+
end
|
20
|
+
|
21
|
+
setup_disable_trace if disable_trace
|
22
|
+
|
23
|
+
Bootsnap::LoadPathCache.setup(
|
24
|
+
cache_path: cache_dir + '/bootsnap-load-path-cache',
|
25
|
+
development_mode: development_mode,
|
26
|
+
active_support: autoload_paths_cache
|
27
|
+
) if load_path_cache
|
28
|
+
|
29
|
+
Bootsnap::CompileCache.setup(
|
30
|
+
iseq: compile_cache_iseq,
|
31
|
+
yaml: compile_cache_yaml
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.setup_disable_trace
|
36
|
+
RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'compile_cache/iseq'
|
2
|
+
require_relative 'compile_cache/yaml'
|
3
|
+
|
4
|
+
module Bootsnap
|
5
|
+
module CompileCache
|
6
|
+
def self.setup(iseq:, yaml:)
|
7
|
+
if iseq
|
8
|
+
Bootsnap::CompileCache::ISeq.install!
|
9
|
+
end
|
10
|
+
|
11
|
+
if yaml
|
12
|
+
Bootsnap::CompileCache::YAML.install!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'bootsnap/bootsnap'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Bootsnap
|
5
|
+
module CompileCache
|
6
|
+
module ISeq
|
7
|
+
def self.input_to_storage(_, path)
|
8
|
+
RubyVM::InstructionSequence.compile_file(path).to_binary
|
9
|
+
rescue SyntaxError
|
10
|
+
raise Uncompilable, 'syntax error'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.storage_to_output(binary)
|
14
|
+
RubyVM::InstructionSequence.load_from_binary(binary)
|
15
|
+
rescue RuntimeError => e
|
16
|
+
if e.message == 'broken binary format'
|
17
|
+
STDERR.puts "[Bootsnap::CompileCache] warning: rejecting broken binary"
|
18
|
+
return nil
|
19
|
+
else
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.input_to_output(_)
|
25
|
+
nil # ruby handles this
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstructionSequenceMixin
|
29
|
+
def load_iseq(path)
|
30
|
+
Bootsnap::CompileCache::Native.fetch(
|
31
|
+
path.to_s,
|
32
|
+
Bootsnap::CompileCache::ISeq
|
33
|
+
)
|
34
|
+
rescue RuntimeError => e
|
35
|
+
if e.message =~ /unmatched platform/
|
36
|
+
puts "unmatched platform for file #{path}"
|
37
|
+
end
|
38
|
+
raise
|
39
|
+
rescue Errno::ERANGE
|
40
|
+
STDERR.puts <<~EOF
|
41
|
+
\x1b[31mError loading ISeq from cache for \x1b[1;34m#{path}\x1b[0;31m!
|
42
|
+
You can likely fix this by running:
|
43
|
+
\x1b[1;32mxattr -c #{path}
|
44
|
+
\x1b[0;31m...but, first, please make sure \x1b[1;34m@burke\x1b[0;31m knows you ran into this bug!
|
45
|
+
He will want to see the results of:
|
46
|
+
\x1b[1;32m/bin/ls -l@ #{path}
|
47
|
+
\x1b[0;31mand:
|
48
|
+
\x1b[1;32mxattr -p user.aotcc.key #{path}\x1b[0m
|
49
|
+
EOF
|
50
|
+
raise
|
51
|
+
end
|
52
|
+
|
53
|
+
def compile_option=(hash)
|
54
|
+
super(hash)
|
55
|
+
Bootsnap::CompileCache::ISeq.compile_option_updated
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.compile_option_updated
|
60
|
+
option = RubyVM::InstructionSequence.compile_option
|
61
|
+
crc = Zlib.crc32(option.inspect)
|
62
|
+
Bootsnap::CompileCache::Native.compile_option_crc32 = crc
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.install!
|
66
|
+
Bootsnap::CompileCache::ISeq.compile_option_updated
|
67
|
+
class << RubyVM::InstructionSequence
|
68
|
+
prepend InstructionSequenceMixin
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Bootsnap
|
2
|
+
module CompileCache
|
3
|
+
module YAML
|
4
|
+
class << self
|
5
|
+
attr_accessor :msgpack_factory
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.input_to_storage(contents, _)
|
9
|
+
obj = ::YAML.load(contents)
|
10
|
+
msgpack_factory.packer.write(obj).to_s
|
11
|
+
rescue NoMethodError, RangeError
|
12
|
+
# if the object included things that we can't serialize, fall back to
|
13
|
+
# Marshal. It's a bit slower, but can encode anything yaml can.
|
14
|
+
# NoMethodError is unexpected types; RangeError is Bignums
|
15
|
+
return Marshal.dump(obj)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.storage_to_output(data)
|
19
|
+
# This could have a meaning in messagepack, and we're being a little lazy
|
20
|
+
# about it. -- but a leading 0x04 would indicate the contents of the YAML
|
21
|
+
# is a positive integer, which is rare, to say the least.
|
22
|
+
if data[0] == 0x04.chr && data[1] == 0x08.chr
|
23
|
+
Marshal.load(data)
|
24
|
+
else
|
25
|
+
msgpack_factory.unpacker.feed(data).read
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.input_to_output(data)
|
30
|
+
::YAML.load(data)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.install!
|
34
|
+
require 'yaml'
|
35
|
+
require 'msgpack'
|
36
|
+
|
37
|
+
# MessagePack serializes symbols as strings by default.
|
38
|
+
# We want them to roundtrip cleanly, so we use a custom factory.
|
39
|
+
# see: https://github.com/msgpack/msgpack-ruby/pull/122
|
40
|
+
factory = MessagePack::Factory.new
|
41
|
+
factory.register_type(0x00, Symbol)
|
42
|
+
Bootsnap::CompileCache::YAML.msgpack_factory = factory
|
43
|
+
|
44
|
+
klass = class << ::YAML; self; end
|
45
|
+
klass.send(:define_method, :load_file) do |path|
|
46
|
+
Bootsnap::CompileCache::Native.fetch(
|
47
|
+
path.to_s,
|
48
|
+
Bootsnap::CompileCache::YAML
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Bootsnap
|
2
|
+
module ExplicitRequire
|
3
|
+
ARCHDIR = RbConfig::CONFIG['archdir']
|
4
|
+
RUBYLIBDIR = RbConfig::CONFIG['rubylibdir']
|
5
|
+
DLEXT = RbConfig::CONFIG['DLEXT']
|
6
|
+
|
7
|
+
def self.from_self(feature)
|
8
|
+
require_relative "../#{feature}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.from_rubylibdir(feature)
|
12
|
+
require(File.join(RUBYLIBDIR, "#{feature}.rb"))
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_archdir(feature)
|
16
|
+
require(File.join(ARCHDIR, "#{feature}.#{DLEXT}"))
|
17
|
+
end
|
18
|
+
|
19
|
+
# Given a set of gems, run a block with the LOAD_PATH narrowed to include
|
20
|
+
# only core ruby source paths and these gems -- that is, roughly,
|
21
|
+
# temporarily remove all gems not listed in this call from the LOAD_PATH.
|
22
|
+
#
|
23
|
+
# This is useful before bootsnap is fully-initialized to load gems that it
|
24
|
+
# depends on, without forcing full LOAD_PATH traversals.
|
25
|
+
def self.with_gems(*gems)
|
26
|
+
orig = $LOAD_PATH.dup
|
27
|
+
$LOAD_PATH.clear
|
28
|
+
gems.each do |gem|
|
29
|
+
pat = %r{
|
30
|
+
/
|
31
|
+
(gems|extensions/[^/]+/[^/]+) # "gems" or "extensions/x64_64-darwin16/2.3.0"
|
32
|
+
/
|
33
|
+
#{Regexp.escape(gem)}-(\h{12}|(\d+\.)) # msgpack-1.2.3 or msgpack-1234567890ab
|
34
|
+
}x
|
35
|
+
$LOAD_PATH.concat(orig.grep(pat))
|
36
|
+
end
|
37
|
+
$LOAD_PATH << ARCHDIR
|
38
|
+
$LOAD_PATH << RUBYLIBDIR
|
39
|
+
yield
|
40
|
+
ensure
|
41
|
+
$LOAD_PATH.replace(orig)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Bootsnap
|
2
|
+
module LoadPathCache
|
3
|
+
ReturnFalse = Class.new(StandardError)
|
4
|
+
FallbackScan = Class.new(StandardError)
|
5
|
+
|
6
|
+
DOT_RB = '.rb'
|
7
|
+
DOT_SO = '.so'
|
8
|
+
SLASH = '/'
|
9
|
+
|
10
|
+
DL_EXTENSIONS = ::RbConfig::CONFIG
|
11
|
+
.values_at('DLEXT', 'DLEXT2')
|
12
|
+
.reject { |ext| !ext || ext.empty? }
|
13
|
+
.map { |ext| ".#{ext}" }
|
14
|
+
.freeze
|
15
|
+
DLEXT = DL_EXTENSIONS[0]
|
16
|
+
# This is nil on linux and darwin, but I think it's '.o' on some other
|
17
|
+
# platform. I'm not really sure which, but it seems better to replicate
|
18
|
+
# ruby's semantics as faithfully as possible.
|
19
|
+
DLEXT2 = DL_EXTENSIONS[1]
|
20
|
+
|
21
|
+
CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
|
22
|
+
|
23
|
+
class << self
|
24
|
+
attr_reader :load_path_cache, :autoload_paths_cache
|
25
|
+
|
26
|
+
def setup(cache_path:, development_mode:, active_support: true)
|
27
|
+
store = Store.new(cache_path)
|
28
|
+
|
29
|
+
@load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
|
30
|
+
require_relative 'load_path_cache/core_ext/kernel_require'
|
31
|
+
|
32
|
+
if active_support
|
33
|
+
# this should happen after setting up the initial cache because it
|
34
|
+
# loads a lot of code. It's better to do after +require+ is optimized.
|
35
|
+
require 'active_support/dependencies'
|
36
|
+
@autoload_paths_cache = Cache.new(
|
37
|
+
store,
|
38
|
+
::ActiveSupport::Dependencies.autoload_paths,
|
39
|
+
development_mode: development_mode
|
40
|
+
)
|
41
|
+
require_relative 'load_path_cache/core_ext/active_support'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
require_relative 'load_path_cache/path_scanner'
|
49
|
+
require_relative 'load_path_cache/path'
|
50
|
+
require_relative 'load_path_cache/cache'
|
51
|
+
require_relative 'load_path_cache/store'
|
52
|
+
require_relative 'load_path_cache/change_observer'
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require_relative '../load_path_cache'
|
2
|
+
require_relative '../explicit_require'
|
3
|
+
Bootsnap::ExplicitRequire.from_archdir('thread')
|
4
|
+
|
5
|
+
module Bootsnap
|
6
|
+
module LoadPathCache
|
7
|
+
class Cache
|
8
|
+
AGE_THRESHOLD = 30 # seconds
|
9
|
+
|
10
|
+
def initialize(store, path_obj, development_mode: false)
|
11
|
+
@development_mode = development_mode
|
12
|
+
@store = store
|
13
|
+
@mutex = ::Thread::Mutex.new
|
14
|
+
@path_obj = path_obj
|
15
|
+
reinitialize
|
16
|
+
end
|
17
|
+
|
18
|
+
# Does this directory exist as a child of one of the path items?
|
19
|
+
# e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], has_dir?("c/d")
|
20
|
+
# is true.
|
21
|
+
def has_dir?(dir)
|
22
|
+
reinitialize if stale?
|
23
|
+
@mutex.synchronize { @dirs[dir] }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Try to resolve this feature to an absolute path without traversing the
|
27
|
+
# loadpath.
|
28
|
+
def find(feature)
|
29
|
+
reinitialize if stale?
|
30
|
+
feature = feature.to_s
|
31
|
+
return feature if feature.start_with?(SLASH)
|
32
|
+
return File.expand_path(feature) if feature.start_with?('./')
|
33
|
+
@mutex.synchronize do
|
34
|
+
x = search_index(feature)
|
35
|
+
return x if x
|
36
|
+
|
37
|
+
# The feature wasn't found on our preliminary search through the index.
|
38
|
+
# We resolve this differently depending on what the extension was.
|
39
|
+
case File.extname(feature)
|
40
|
+
# If the extension was one of the ones we explicitly cache (.rb and the
|
41
|
+
# native dynamic extension, e.g. .bundle or .so), we know it was a
|
42
|
+
# failure and there's nothing more we can do to find the file.
|
43
|
+
when *CACHED_EXTENSIONS # .rb, .bundle or .so
|
44
|
+
nil
|
45
|
+
# If no extension was specified, it's the same situation, since we
|
46
|
+
# try appending both cachable extensions in search_index. However,
|
47
|
+
# there's a special-case for 'enumerator'. Before ruby 1.9, you had
|
48
|
+
# to `require 'enumerator'` to use it. In 1.9+, it's pre-loaded, but
|
49
|
+
# doesn't correspond to any entry on the filesystem. Ruby lies. So we
|
50
|
+
# lie too, forcing our monkeypatch to return false like ruby would.
|
51
|
+
when ""
|
52
|
+
raise LoadPathCache::ReturnFalse if feature == 'enumerator'
|
53
|
+
nil
|
54
|
+
# Ruby allows specifying native extensions as '.so' even when DLEXT
|
55
|
+
# is '.bundle'. This is where we handle that case.
|
56
|
+
when DOT_SO
|
57
|
+
x = search_index(feature[0..-4] + DLEXT)
|
58
|
+
return x if x
|
59
|
+
if DLEXT2
|
60
|
+
search_index(feature[0..-4] + DLEXT2)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
# other, unknown extension. For example, `.rake`. Since we haven't
|
64
|
+
# cached these, we legitimately need to run the load path search.
|
65
|
+
raise LoadPathCache::FallbackScan
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def unshift_paths(sender, *paths)
|
71
|
+
return unless sender == @path_obj
|
72
|
+
@mutex.synchronize { unshift_paths_locked(*paths) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def push_paths(sender, *paths)
|
76
|
+
return unless sender == @path_obj
|
77
|
+
@mutex.synchronize { push_paths_locked(*paths) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def each_requirable
|
81
|
+
@mutex.synchronize do
|
82
|
+
@index.each do |rel, entry|
|
83
|
+
yield "#{entry}/#{rel}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def reinitialize(path_obj = @path_obj)
|
89
|
+
@mutex.synchronize do
|
90
|
+
@path_obj = path_obj
|
91
|
+
ChangeObserver.register(self, @path_obj)
|
92
|
+
@index = {}
|
93
|
+
@dirs = Hash.new(false)
|
94
|
+
@generated_at = now
|
95
|
+
push_paths_locked(*@path_obj)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def push_paths_locked(*paths)
|
102
|
+
@store.transaction do
|
103
|
+
paths.each do |path|
|
104
|
+
p = Path.new(path)
|
105
|
+
entries, dirs = p.entries_and_dirs(@store)
|
106
|
+
# push -> low precedence -> set only if unset
|
107
|
+
dirs.each { |dir| @dirs[dir] ||= true }
|
108
|
+
entries.each { |rel| @index[rel] ||= path }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def unshift_paths_locked(*paths)
|
114
|
+
@store.transaction do
|
115
|
+
paths.reverse.each do |path|
|
116
|
+
p = Path.new(path)
|
117
|
+
entries, dirs = p.entries_and_dirs(@store)
|
118
|
+
# unshift -> high precedence -> unconditional set
|
119
|
+
dirs.each { |dir| @dirs[dir] = true }
|
120
|
+
entries.each { |rel| @index[rel] = path }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def stale?
|
126
|
+
@development_mode && @generated_at + AGE_THRESHOLD < now
|
127
|
+
end
|
128
|
+
|
129
|
+
def now
|
130
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
|
131
|
+
end
|
132
|
+
|
133
|
+
if DLEXT2
|
134
|
+
def search_index(f)
|
135
|
+
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
|
136
|
+
end
|
137
|
+
else
|
138
|
+
def search_index(f)
|
139
|
+
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def try_index(f)
|
144
|
+
if p = @index[f]
|
145
|
+
p + '/' + f
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|