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