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.
@@ -0,0 +1,10 @@
1
+ #ifndef BOOTSNAP_H
2
+ #define BOOTSNAP_H 1
3
+
4
+ #include <stdint.h>
5
+ #include <sys/types.h>
6
+ #include "ruby.h"
7
+
8
+ uint32_t crc32(const char *bytes, size_t size);
9
+
10
+ #endif /* BOOTSNAP_H */
@@ -0,0 +1,16 @@
1
+ #include "bootsnap.h"
2
+
3
+ #include <x86intrin.h>
4
+
5
+ uint32_t
6
+ crc32(const char *bytes, size_t size)
7
+ {
8
+ size_t i;
9
+ uint32_t hash = 0;
10
+
11
+ for (i = 0; i < size; i++) {
12
+ hash = _mm_crc32_u8(hash, bytes[i]);
13
+ }
14
+ return hash;
15
+ }
16
+
@@ -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")
@@ -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