class Bootsnap::LoadPathCache::LoadedFeaturesIndex
Experimental RBS support (using type sampling data from the type_fusion
project).
# sig/bootsnap/load_path_cache/loaded_features_index.rbs class Bootsnap::LoadPathCache::LoadedFeaturesIndex def key?: (String feature) -> bool end
returning false like ruby.
With bootsnap and with LoadedFeaturesIndex, this skips the second load,
‘a.rb`s.
With bootsnap but with no LoadedFeaturesIndex, this loads two different
Ruby returns false from step 3.
3. `require ’a’‘
2. Prepend a new $LOAD_PATH element containing an `a.rb`
1. `require ’a’‘
If you disable LoadedFeaturesIndex, you can see the problem this solves by:
’/path/to/bundler’.
realize that it has already loaded ‘bundler’, and not just
different absolute path. This class makes bootsnap smart enough to
a new instance of bundler is added to the $LOAD_PATH which resolves to a
has already required a file by its short name (e.g. require ‘bundler’) if
This works around an issue where, without bootsnap, ruby knows that it
we can’t easily obtain an interface to.
LoadedFeaturesIndex partially mirrors an internal structure in ruby that
def cursor(short)
def cursor(short) unless Bootsnap.absolute_path?(short.to_s) $LOADED_FEATURES.size end end
def extension_elidable?(feature)
with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
will _never_ run on MacOS, and therefore think they can get away
(E.g. It's unlikely that someone will know that their code
to name files in a way that assumes otherwise.
will be tried on all platforms, and that people are unlikely
So optimistically pretend that all known elidable extensions
handwavy about what will be tried when and in what order.
This is complex and platform-dependent, and the Ruby docs are a little
will implicitly try 'x.rb' if you ask for 'x'.
someone tries to 'require' the file without it? E.g. Ruby
Might Ruby automatically search for this extension if
def extension_elidable?(feature) feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib") end
def identify(short, cursor)
def identify(short, cursor) $LOADED_FEATURES[cursor..-1].detect do |feat| offset = 0 while (offset = feat.index(short, offset)) if feat.index(".", offset + 1) && !feat.index("/", offset + 2) break true else offset += 1 end end end end
def initialize
def initialize @lfi = {} @mutex = Mutex.new # 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
def key?(feature)
Experimental RBS support (using type sampling data from the type_fusion
project).
def key?: (String feature) -> bool
This signature was generated using 3 samples from 1 application.
def key?(feature) @mutex.synchronize { @lfi.key?(feature) } end
def purge(feature)
If access patterns make this not-okay, we can lazy-invert the LFI on
We've optimized for initialize and register to be fast, and purge to be tolerable.
def purge(feature) @mutex.synchronize do feat_hash = feature.hash @lfi.reject! { |_, hash| hash == feat_hash } end end
def purge_multi(features)
def purge_multi(features) rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true } @mutex.synchronize do @lfi.reject! { |_, hash| rejected_hashes.key?(hash) } end end
def register(short, long)
2. Inspect $LOADED_FEATURES upon return from yield to find the matching
not quite right; or
1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
We could either:
to confidently add the `bundler.rb` form to @lfi.
pass `long` (the full expanded absolute path), then we did are not able
`FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
If the user asked for e.g. `require 'bundler'`, and we went through the
entry:
There is a relatively uncommon case where we could miss adding an
def register(short, long) return if Bootsnap.absolute_path?(short) 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 end
def strip_extension_if_elidable(feature)
def strip_extension_if_elidable(feature) if extension_elidable?(feature) feature.sub(STRIP_EXTENSION, "") else feature end end