bootsnap 0.2.0

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