bootsnap 1.1.8-java → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +103 -0
  3. data/README.md +47 -6
  4. data/exe/bootsnap +5 -0
  5. data/ext/bootsnap/bootsnap.c +217 -88
  6. data/ext/bootsnap/extconf.rb +3 -1
  7. data/lib/bootsnap.rb +17 -8
  8. data/lib/bootsnap/bundler.rb +6 -3
  9. data/lib/bootsnap/cli.rb +246 -0
  10. data/lib/bootsnap/cli/worker_pool.rb +131 -0
  11. data/lib/bootsnap/compile_cache.rb +32 -4
  12. data/lib/bootsnap/compile_cache/iseq.rb +32 -15
  13. data/lib/bootsnap/compile_cache/yaml.rb +94 -40
  14. data/lib/bootsnap/explicit_require.rb +2 -1
  15. data/lib/bootsnap/load_path_cache.rb +35 -9
  16. data/lib/bootsnap/load_path_cache/cache.rb +48 -29
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +36 -29
  18. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +39 -7
  19. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +70 -53
  20. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +18 -0
  21. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +148 -0
  22. data/lib/bootsnap/load_path_cache/path.rb +8 -7
  23. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -39
  24. data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
  25. data/lib/bootsnap/load_path_cache/store.rb +20 -14
  26. data/lib/bootsnap/setup.rb +11 -13
  27. data/lib/bootsnap/version.rb +2 -1
  28. metadata +44 -45
  29. data/.gitignore +0 -17
  30. data/.rubocop.yml +0 -20
  31. data/.travis.yml +0 -4
  32. data/CODE_OF_CONDUCT.md +0 -74
  33. data/CONTRIBUTING.md +0 -21
  34. data/Gemfile +0 -8
  35. data/Rakefile +0 -11
  36. data/bin/console +0 -14
  37. data/bin/setup +0 -8
  38. data/bin/testunit +0 -8
  39. data/bootsnap.gemspec +0 -39
  40. data/dev.yml +0 -10
@@ -1,37 +1,37 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module ChangeObserver
4
- def self.register(observer, arr)
5
- # Re-overriding these methods on an array that already has them would
6
- # cause StackOverflowErrors
7
- return if arr.respond_to?(:push_without_lpc)
8
-
5
+ module ArrayMixin
9
6
  # For each method that adds items to one end or another of the array
10
7
  # (<<, push, unshift, concat), override that method to also notify the
11
8
  # observer of the change.
12
- sc = arr.singleton_class
13
- sc.send(:alias_method, :shovel_without_lpc, :<<)
14
- arr.define_singleton_method(:<<) do |entry|
15
- observer.push_paths(self, entry.to_s)
16
- shovel_without_lpc(entry)
9
+ def <<(entry)
10
+ @lpc_observer.push_paths(self, entry.to_s)
11
+ super
12
+ end
13
+
14
+ def push(*entries)
15
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
+ super
17
17
  end
18
18
 
19
- sc.send(:alias_method, :push_without_lpc, :push)
20
- arr.define_singleton_method(:push) do |*entries|
21
- observer.push_paths(self, *entries.map(&:to_s))
22
- push_without_lpc(*entries)
19
+ def unshift(*entries)
20
+ @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
+ super
23
22
  end
24
23
 
25
- sc.send(:alias_method, :unshift_without_lpc, :unshift)
26
- arr.define_singleton_method(:unshift) do |*entries|
27
- observer.unshift_paths(self, *entries.map(&:to_s))
28
- unshift_without_lpc(*entries)
24
+ def concat(entries)
25
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
26
+ super
29
27
  end
30
28
 
31
- sc.send(:alias_method, :concat_without_lpc, :concat)
32
- arr.define_singleton_method(:concat) do |entries|
33
- observer.push_paths(self, *entries.map(&:to_s))
34
- concat_without_lpc(entries)
29
+ # uniq! keeps the first occurrence of each path, otherwise preserving
30
+ # order, preserving the effective load path
31
+ def uniq!(*args)
32
+ ret = super
33
+ @lpc_observer.reinitialize if block_given? || !args.empty?
34
+ ret
35
35
  end
36
36
 
37
37
  # For each method that modifies the array more aggressively, override
@@ -41,16 +41,23 @@ module Bootsnap
41
41
  # accounting cost would be greater than the hit from these, since we
42
42
  # actively discourage calling them.
43
43
  %i(
44
- collect! compact! delete delete_at delete_if fill flatten! insert map!
45
- reject! reverse! select! shuffle! shift slice! sort! sort_by!
46
- ).each do |meth|
47
- sc.send(:alias_method, :"#{meth}_without_lpc", meth)
48
- arr.define_singleton_method(meth) do |*a|
49
- send(:"#{meth}_without_lpc", *a)
50
- observer.reinitialize
44
+ []= clear collect! compact! delete delete_at delete_if fill flatten!
45
+ insert keep_if map! pop reject! replace reverse! rotate! select!
46
+ shift shuffle! slice! sort! sort_by!
47
+ ).each do |method_name|
48
+ define_method(method_name) do |*args, &block|
49
+ ret = super(*args, &block)
50
+ @lpc_observer.reinitialize
51
+ ret
51
52
  end
52
53
  end
53
54
  end
55
+
56
+ def self.register(observer, arr)
57
+ return if arr.frozen? # can't register observer, but no need to.
58
+ arr.instance_variable_set(:@lpc_observer, observer)
59
+ arr.extend(ArrayMixin)
60
+ end
54
61
  end
55
62
  end
56
63
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module CoreExt
@@ -10,6 +11,14 @@ module Bootsnap
10
11
  Thread.current[:without_bootsnap_cache] = prev
11
12
  end
12
13
 
14
+ def self.allow_bootsnap_retry(allowed)
15
+ prev = Thread.current[:without_bootsnap_retry] || false
16
+ Thread.current[:without_bootsnap_retry] = !allowed
17
+ yield
18
+ ensure
19
+ Thread.current[:without_bootsnap_retry] = prev
20
+ end
21
+
13
22
  module ClassMethods
14
23
  def autoload_paths=(o)
15
24
  super
@@ -22,17 +31,25 @@ module Bootsnap
22
31
  Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
23
32
  rescue Bootsnap::LoadPathCache::ReturnFalse
24
33
  nil # doesn't really apply here
34
+ rescue Bootsnap::LoadPathCache::FallbackScan
35
+ nil # doesn't really apply here
25
36
  end
26
37
  end
27
38
 
28
39
  def autoloadable_module?(path_suffix)
29
- Bootsnap::LoadPathCache.autoload_paths_cache.has_dir?(path_suffix)
40
+ Bootsnap::LoadPathCache.autoload_paths_cache.load_dir(path_suffix)
30
41
  end
31
42
 
32
43
  def remove_constant(const)
33
44
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
34
45
  end
35
46
 
47
+ def require_or_load(*)
48
+ CoreExt::ActiveSupport.allow_bootsnap_retry(true) do
49
+ super
50
+ end
51
+ end
52
+
36
53
  # If we can't find a constant using the patched implementation of
37
54
  # search_for_file, try again with the default implementation.
38
55
  #
@@ -40,16 +57,26 @@ module Bootsnap
40
57
  # behaviour. The gymnastics here are a bit awkward, but it prevents
41
58
  # 200+ lines of monkeypatches.
42
59
  def load_missing_constant(from_mod, const_name)
43
- super
60
+ CoreExt::ActiveSupport.allow_bootsnap_retry(false) do
61
+ super
62
+ end
44
63
  rescue NameError => e
64
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
65
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
66
+
67
+ # This function can end up called recursively, we only want to
68
+ # retry at the top-level.
69
+ raise(e) if Thread.current[:without_bootsnap_retry]
70
+ # If we already had cache disabled, there's no use retrying
71
+ raise(e) if Thread.current[:without_bootsnap_cache]
45
72
  # NoMethodError is a NameError, but we only want to handle actual
46
73
  # NameError instances.
47
- raise unless e.class == NameError
74
+ raise(e) unless e.class == NameError
48
75
  # We can only confidently handle cases when *this* constant fails
49
76
  # to load, not other constants referred to by it.
50
- raise unless e.name == const_name
77
+ raise(e) unless e.name == const_name
51
78
  # If the constant was actually loaded, something else went wrong?
52
- raise if from_mod.const_defined?(const_name)
79
+ raise(e) if from_mod.const_defined?(const_name)
53
80
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
54
81
  end
55
82
 
@@ -57,7 +84,12 @@ module Bootsnap
57
84
  # reiterate it with version polymorphism here...
58
85
  def depend_on(*)
59
86
  super
60
- rescue LoadError
87
+ rescue LoadError => e
88
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
89
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
90
+
91
+ # If we already had cache disabled, there's no use retrying
92
+ raise(e) if Thread.current[:without_bootsnap_cache]
61
93
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
62
94
  end
63
95
  end
@@ -69,7 +101,7 @@ end
69
101
  module ActiveSupport
70
102
  module Dependencies
71
103
  class << self
72
- prepend Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods
104
+ prepend(Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods)
73
105
  end
74
106
  end
75
107
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module CoreExt
4
5
  def self.make_load_error(path)
5
- err = LoadError.new("cannot load such file -- #{path}")
6
+ err = LoadError.new(+"cannot load such file -- #{path}")
7
+ err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
6
8
  err.define_singleton_method(:path) { path }
7
9
  err
8
10
  end
@@ -11,78 +13,93 @@ module Bootsnap
11
13
  end
12
14
 
13
15
  module Kernel
14
- alias_method :require_without_cache, :require
15
- def require(path)
16
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
17
- require_without_cache(resolved)
18
- else
19
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
20
- end
21
- rescue Bootsnap::LoadPathCache::ReturnFalse
22
- return false
23
- rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
25
- end
16
+ module_function # rubocop:disable Style/ModuleFunction
26
17
 
27
- alias_method :load_without_cache, :load
28
- def load(path, wrap = false)
29
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
30
- load_without_cache(resolved, wrap)
31
- else
32
- # load also allows relative paths from pwd even when not in $:
33
- relative = File.expand_path(path)
34
- if File.exist?(File.expand_path(path))
35
- return load_without_cache(relative, wrap)
36
- end
37
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
18
+ alias_method(:require_without_bootsnap, :require)
19
+
20
+ # Note that require registers to $LOADED_FEATURES while load does not.
21
+ def require_with_bootsnap_lfi(path, resolved = nil)
22
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
23
+ require_without_bootsnap(resolved || path)
38
24
  end
39
- rescue Bootsnap::LoadPathCache::ReturnFalse
40
- return false
41
- rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
43
25
  end
44
- end
45
26
 
46
- class << Kernel
47
- alias_method :require_without_cache, :require
48
27
  def require(path)
49
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
50
- require_without_cache(resolved)
51
- else
52
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
28
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
29
+
30
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
31
+ return require_with_bootsnap_lfi(path, resolved)
53
32
  end
33
+
34
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
35
+ rescue LoadError => e
36
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
37
+ raise(e)
54
38
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
- return false
39
+ false
56
40
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
41
+ fallback = true
42
+ ensure
43
+ if fallback
44
+ require_with_bootsnap_lfi(path)
45
+ end
58
46
  end
59
47
 
60
- alias_method :load_without_cache, :load
48
+ alias_method(:require_relative_without_bootsnap, :require_relative)
49
+ def require_relative(path)
50
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
+ caller_locations(1..1).first.absolute_path, path
52
+ )
53
+ require(realpath)
54
+ end
55
+
56
+ alias_method(:load_without_bootsnap, :load)
61
57
  def load(path, wrap = false)
62
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
63
- load_without_cache(resolved, wrap)
64
- else
65
- # load also allows relative paths from pwd even when not in $:
66
- relative = File.expand_path(path)
67
- if File.exist?(relative)
68
- return load_without_cache(relative, wrap)
69
- end
70
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
58
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
59
+ return load_without_bootsnap(resolved, wrap)
71
60
  end
61
+
62
+ # load also allows relative paths from pwd even when not in $:
63
+ if File.exist?(relative = File.expand_path(path).freeze)
64
+ return load_without_bootsnap(relative, wrap)
65
+ end
66
+
67
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
68
+ rescue LoadError => e
69
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
70
+ raise(e)
72
71
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
- return false
72
+ false
74
73
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
74
+ fallback = true
75
+ ensure
76
+ if fallback
77
+ load_without_bootsnap(path, wrap)
78
+ end
76
79
  end
77
80
  end
78
81
 
79
82
  class Module
80
- alias_method :autoload_without_cache, :autoload
83
+ alias_method(:autoload_without_bootsnap, :autoload)
81
84
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
85
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
86
+ # obvious how to make it work. This feels like a pretty niche case, unclear
87
+ # if it will ever burn anyone.
88
+ #
89
+ # The challenge is that we don't control the point at which the entry gets
90
+ # added to $LOADED_FEATURES and won't be able to hook that modification
91
+ # since it's done in C-land.
92
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
93
+ rescue LoadError => e
94
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
95
+ raise(e)
83
96
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
- return false
97
+ false
85
98
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
99
+ fallback = true
100
+ ensure
101
+ if fallback
102
+ autoload_without_bootsnap(const, path)
103
+ end
87
104
  end
88
105
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ class << $LOADED_FEATURES
3
+ alias_method(:delete_without_bootsnap, :delete)
4
+ def delete(key)
5
+ Bootsnap::LoadPathCache.loaded_features_index.purge(key)
6
+ delete_without_bootsnap(key)
7
+ end
8
+
9
+ alias_method(:reject_without_bootsnap!, :reject!)
10
+ def reject!(&block)
11
+ backup = dup
12
+
13
+ # FIXME: if no block is passed we'd need to return a decorated iterator
14
+ reject_without_bootsnap!(&block)
15
+
16
+ Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
17
+ end
18
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
6
+ # we can't easily obtain an interface to.
7
+ #
8
+ # This works around an issue where, without bootsnap, *ruby* knows that it
9
+ # has already required a file by its short name (e.g. require 'bundler') if
10
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
11
+ # different absolute path. This class makes bootsnap smart enough to
12
+ # realize that it has already loaded 'bundler', and not just
13
+ # '/path/to/bundler'.
14
+ #
15
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
16
+ #
17
+ # 1. `require 'a'`
18
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
19
+ # 3. `require 'a'`
20
+ #
21
+ # Ruby returns false from step 3.
22
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
23
+ # `a.rb`s.
24
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
25
+ # returning false like ruby.
26
+ class LoadedFeaturesIndex
27
+ def initialize
28
+ @lfi = {}
29
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
30
+
31
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
+ # cache. If this ever comes up in practice — or if you, the
33
+ # enterprising reader, feels inclined to solve this problem — we could
34
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
+ # updates to our @lfi.
36
+ $LOADED_FEATURES.each do |feat|
37
+ hash = feat.hash
38
+ $LOAD_PATH.each do |lpe|
39
+ next unless feat.start_with?(lpe)
40
+ # /a/b/lib/my/foo.rb
41
+ # ^^^^^^^^^
42
+ short = feat[(lpe.length + 1)..-1]
43
+ stripped = strip_extension_if_elidable(short)
44
+ @lfi[short] = hash
45
+ @lfi[stripped] = hash
46
+ end
47
+ end
48
+ end
49
+
50
+ # We've optimized for initialize and register to be fast, and purge to be tolerable.
51
+ # If access patterns make this not-okay, we can lazy-invert the LFI on
52
+ # first purge and work from there.
53
+ def purge(feature)
54
+ @mutex.synchronize do
55
+ feat_hash = feature.hash
56
+ @lfi.reject! { |_, hash| hash == feat_hash }
57
+ end
58
+ end
59
+
60
+ def purge_multi(features)
61
+ rejected_hashes = features.map(&:hash).to_set
62
+ @mutex.synchronize do
63
+ @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ end
65
+ end
66
+
67
+ def key?(feature)
68
+ @mutex.synchronize { @lfi.key?(feature) }
69
+ end
70
+
71
+ # There is a relatively uncommon case where we could miss adding an
72
+ # entry:
73
+ #
74
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
75
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
76
+ # pass `long` (the full expanded absolute path), then we did are not able
77
+ # to confidently add the `bundler.rb` form to @lfi.
78
+ #
79
+ # We could either:
80
+ #
81
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
82
+ # not quite right; or
83
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
+ # entry.
85
+ def register(short, long = nil)
86
+ if long.nil?
87
+ pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
+ len = $LOADED_FEATURES.size
89
+ ret = yield
90
+ long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
91
+ else
92
+ ret = yield
93
+ end
94
+
95
+ hash = long.hash
96
+
97
+ # Do we have a filename with an elidable extension, e.g.,
98
+ # 'bundler.rb', or 'libgit2.so'?
99
+ altname = if extension_elidable?(short)
100
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
101
+ strip_extension_if_elidable(short)
102
+ elsif long && (ext = File.extname(long.freeze))
103
+ # We already know the extension of the actual file this
104
+ # resolves to, so put that back on.
105
+ short + ext
106
+ end
107
+
108
+ @mutex.synchronize do
109
+ @lfi[short] = hash
110
+ (@lfi[altname] = hash) if altname
111
+ end
112
+
113
+ ret
114
+ end
115
+
116
+ private
117
+
118
+ STRIP_EXTENSION = /\.[^.]*?$/
119
+ private_constant(:STRIP_EXTENSION)
120
+
121
+ # Might Ruby automatically search for this extension if
122
+ # someone tries to 'require' the file without it? E.g. Ruby
123
+ # will implicitly try 'x.rb' if you ask for 'x'.
124
+ #
125
+ # This is complex and platform-dependent, and the Ruby docs are a little
126
+ # handwavy about what will be tried when and in what order.
127
+ # So optimistically pretend that all known elidable extensions
128
+ # will be tried on all platforms, and that people are unlikely
129
+ # to name files in a way that assumes otherwise.
130
+ # (E.g. It's unlikely that someone will know that their code
131
+ # will _never_ run on MacOS, and therefore think they can get away
132
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
+ #
134
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135
+ def extension_elidable?(f)
136
+ f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
137
+ end
138
+
139
+ def strip_extension_if_elidable(f)
140
+ if extension_elidable?(f)
141
+ f.sub(STRIP_EXTENSION, '')
142
+ else
143
+ f
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end