class Bootsnap::LoadPathCache::LoadedFeaturesIndex

LoadedFeaturesIndex partially mirrors an internal structure in ruby that we can’t easily obtain an interface to.

This works around an issue where, without bootsnap, ruby knows that it has already required a file by its short name (e.g. require ‘bundler’) if a new instance of bundler is added to the $LOAD_PATH which resolves to a different absolute path. This class makes bootsnap smart enough to realize that it has already loaded ‘bundler’, and not just ‘/path/to/bundler’.

If you disable LoadedFeaturesIndex, you can see the problem this solves by:

  1. ‘require ’a’‘

  2. Prepend a new $LOAD_PATH element containing an ‘a.rb`

  3. ‘require ’a’‘

Ruby returns false from step 3. With bootsnap but with no LoadedFeaturesIndex, this loads two different

`a.rb`s.

With bootsnap and with LoadedFeaturesIndex, this skips the second load,

returning false like ruby.

Constants

STRIP_EXTENSION

Public Class Methods

new() click to toggle source
# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 27
def initialize
  @lfi = {}
  @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.

  # In theory the user could mutate $LOADED_FEATURES and invalidate our
  # cache. If this ever comes up in practice — or if you, the
  # enterprising reader, feels inclined to solve this problem — we could
  # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
  # updates to our @lfi.
  $LOADED_FEATURES.each do |feat|
    hash = feat.hash
    $LOAD_PATH.each do |lpe|
      next unless feat.start_with?(lpe)
      # /a/b/lib/my/foo.rb
      #          ^^^^^^^^^
      short = feat[(lpe.length + 1)..-1]
      stripped = strip_extension_if_elidable(short)
      @lfi[short] = hash
      @lfi[stripped] = hash
    end
  end
end

Public Instance Methods

key?(feature) click to toggle source
# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 67
def key?(feature)
  @mutex.synchronize { @lfi.key?(feature) }
end
purge(feature) click to toggle source

We’ve optimized for initialize and register to be fast, and purge to be tolerable. If access patterns make this not-okay, we can lazy-invert the LFI on first purge and work from there.

# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 53
def purge(feature)
  @mutex.synchronize do
    feat_hash = feature.hash
    @lfi.reject! { |_, hash| hash == feat_hash }
  end
end
purge_multi(features) click to toggle source
# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 60
def purge_multi(features)
  rejected_hashes = features.map(&:hash).to_set
  @mutex.synchronize do
    @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
  end
end
register(short, long = nil) { || ... } click to toggle source

There is a relatively uncommon case where we could miss adding an entry:

If the user asked for e.g. ‘require ’bundler’‘, and we went through the `FallbackScan` pathway in `kernel_require.rb` and therefore did not pass `long` (the full expanded absolute path), then we did are not able to confidently add the `bundler.rb` form to @lfi.

We could either:

  1. Just add ‘bundler.rb`, `bundler.so`, and so on, which is close but not quite right; or

  2. Inspect $LOADED_FEATURES upon return from yield to find the matching entry.

# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 85
def register(short, long = nil)
  if long.nil?
    pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
    len = $LOADED_FEATURES.size
    ret = yield
    long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
  else
    ret = yield
  end

  hash = long.hash

  # Do we have a filename with an elidable extension, e.g.,
  # 'bundler.rb', or 'libgit2.so'?
  altname = if extension_elidable?(short)
    # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
    strip_extension_if_elidable(short)
  elsif long && (ext = File.extname(long.freeze))
    # We already know the extension of the actual file this
    # resolves to, so put that back on.
    short + ext
  end

  @mutex.synchronize do
    @lfi[short] = hash
    (@lfi[altname] = hash) if altname
  end

  ret
end

Private Instance Methods

extension_elidable?(f) click to toggle source

Might Ruby automatically search for this extension if someone tries to ‘require’ the file without it? E.g. Ruby will implicitly try ‘x.rb’ if you ask for ‘x’.

This is complex and platform-dependent, and the Ruby docs are a little handwavy about what will be tried when and in what order. So optimistically pretend that all known elidable extensions will be tried on all platforms, and that people are unlikely to name files in a way that assumes otherwise. (E.g. It’s unlikely that someone will know that their code will never run on MacOS, and therefore think they can get away with calling a Ruby file ‘x.dylib.rb’ and then requiring it as ‘x.dylib’.)

See <ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.

# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 135
def extension_elidable?(f)
  f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
end
strip_extension_if_elidable(f) click to toggle source
# File lib/bootsnap/load_path_cache/loaded_features_index.rb, line 139
def strip_extension_if_elidable(f)
  if extension_elidable?(f)
    f.sub(STRIP_EXTENSION, '')
  else
    f
  end
end