Class: Bootsnap::LoadPathCache::LoadedFeaturesIndex

Inherits:
Object
  • Object
show all
Defined in:
lib/bootsnap/load_path_cache/loaded_features_index.rb

Overview

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.

Instance Method Summary collapse

Constructor Details

#initializeLoadedFeaturesIndex

Returns a new instance of LoadedFeaturesIndex.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 27

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)..]
      stripped = strip_extension_if_elidable(short)
      @lfi[short] = hash
      @lfi[stripped] = hash
    end
  end
end

Instance Method Details

#cursor(short) ⇒ Object



72
73
74
75
76
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 72

def cursor(short)
  unless Bootsnap.absolute_path?(short.to_s)
    $LOADED_FEATURES.size
  end
end

#identify(short, cursor) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 78

def identify(short, cursor)
  $LOADED_FEATURES[cursor..].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

#key?(feature) ⇒ Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 68

def key?(feature)
  @mutex.synchronize { @lfi.key?(feature) }
end

#purge(feature) ⇒ Object

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.



54
55
56
57
58
59
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 54

def purge(feature)
  @mutex.synchronize do
    feat_hash = feature.hash
    @lfi.reject! { |_, hash| hash == feat_hash }
  end
end

#purge_multi(features) ⇒ Object



61
62
63
64
65
66
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 61

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

#register(short, long) ⇒ Object

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 `FALLBACK_SCAN` 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.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/bootsnap/load_path_cache/loaded_features_index.rb', line 105

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