bootsnap 1.1.8 → 1.2.0.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a6f66d96dbadca08e5c50985c45319350f1e85ca
4
- data.tar.gz: c1227f8f8c6ed542abe582c221ccf45318655e87
2
+ SHA256:
3
+ metadata.gz: ae101ea55aec450002c9322cc752960c5b6367c3a4bbddd32241919291adc1dc
4
+ data.tar.gz: 617173fa3e608e774f3730e6a8c3ca2d10030e2ec419a96ff8e90b84979dc736
5
5
  SHA512:
6
- metadata.gz: fa366476bf324d6eda9415e7170d67e31eb4a01d08758ff51d9e83c7661db8160cfa334ef9df67777ef059887de24a3f8a7da49b1675f3a1b0bf7704ecf42620
7
- data.tar.gz: '06758fb4f8445d041816330983799684a3b79e6ec82ac1e21cf087544d377744b0bb8bc20012146d529876fac6398e1053821eea2468be60c8194c6c07855312'
6
+ metadata.gz: f7488865e503a7f1d7feadcd05b54e0718531c3ec36923db9c6dd7fc1dcea3bbdc112ed8cd0ce75688185554ae256ed8a304c14f83f87125c3bcb31ae57f6036
7
+ data.tar.gz: af929fd81f41657a17e0710c2fc3403d5e37a943f968f363a12b9c4c5326dc1405fed0f02caa0104619846688dd3d59b15cb171b8119cec39c3e49fc72517493
@@ -1,4 +1,8 @@
1
1
  language: ruby
2
- rvm: ruby-2.4.1
2
+
3
+ rvm:
4
+ - ruby-2.4
5
+ - ruby-2.5
6
+
3
7
  before_script: rake
4
- script: bin/testunit
8
+ script: bundle exec bin/testunit
@@ -0,0 +1,218 @@
1
+ # Bootsnap [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](https://travis-ci.org/Shopify/bootsnap)
2
+
3
+ Bootsnap は RubyVM におけるバイトコード生成やファイルルックアップ等の時間のかかる処理を最適化するためのライブラリです。ActiveSupport や YAML もサポートしています。
4
+
5
+ 注意書き: このライブラリは英語話者によって管理されています。この README は日本語ですが、日本語でのサポートはしておらず、リクエストにお答えすることもできません。バイリンガルの方がサポートをサポートしてくださる場合はお知らせください!:)
6
+
7
+ ## パフォーマンス
8
+
9
+ Discourse では、約6秒から3秒まで、約50%の起動時間短縮が確認されています。小さなアプリケーションでも、50%の改善(3.6秒から1.8秒)が確認されています。非常に巨大でモノリシックなアプリである Shopify のプラットフォームでは、約25秒から6.5秒へと約75%短縮されました。
10
+
11
+ ## 使用方法
12
+
13
+ この gem は MacOS と Linux で作動します。まずは、`bootsnap` を `Gemfile` に追加します:
14
+
15
+ ```ruby
16
+ gem 'bootsnap', require: false
17
+ ```
18
+
19
+ Rails を使用している場合は、以下のコードを、`config/boot.rb` 内にある `require 'bundler/setup'` の後に追加してください。
20
+
21
+ ```ruby
22
+ require 'bootsnap/setup'
23
+ ```
24
+
25
+ Rails を使用していない場合、または、より多くの設定を変更したい場合は、以下のコードを `require 'bundler/setup'` の直後に追加してください(早く読み込まれるほど、より多くのものを最適化することができます)。
26
+
27
+ ```ruby
28
+ require 'bootsnap'
29
+ env = ENV['RAILS_ENV'] || "development"
30
+ Bootsnap.setup(
31
+  cache_dir:           'tmp/cache',         # キャッシュファイルを保存する path
32
+  development_mode:     env == 'development', # 現在の作業環境、例えば RACK_ENV, RAILS_ENV など。
33
+ load_path_cache: true, # キャッシュで LOAD_PATH を最適化する。
34
+  autoload_paths_cache: true,                 # キャッシュで ActiveSupport による autoload を行う。
35
+  disable_trace:       true,                 # (アルファ) `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`をセットする。
36
+  compile_cache_iseq:   true,                 # ISeq キャッシュをコンパイルする
37
+  compile_cache_yaml:   true                 # YAML キャッシュをコンパイルする
38
+ )
39
+ ```
40
+
41
+ ヒント: `require 'bootsnap'` を `BootLib::Require.from_gem('bootsnap', 'bootsnap')` で、 [こちらのトリック](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require)を使って置き換えることができます。こうすると、巨大な`$LOAD_PATH`がある場合でも、起動時間を最短化するのに役立ちます。
42
+
43
+ ## Bootsnap の内部動作
44
+
45
+ Bootsnap は、処理に時間のかかるメソッドの結果をキャッシュすることで最適化しています。これには、大きく分けて2つのカテゴリに分けられます。
46
+
47
+ * [Path Pre-Scanning](#path-pre-scanning)
48
+ * `Kernel#require` と `Kernel#load` を `$LOAD_PATH` のフルスキャンを行わないようにオーバーライドします。
49
+   * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` を `ActiveSupport::Dependencies.autoload_paths` のフルスキャンを行わないようにオーバーライドします。
50
+ * [Compilation caching](#compilation-caching)
51
+  * Ruby バイトコードのコンパイル結果をキャッシュするためのメソッド `RubyVM::InstructionSequence.load_iseq` が実装されています。
52
+   * `YAML.load_file` を YAML オブジェクトのロード結果を MessagePack でキャッシュするようにオーバーライドします。 MessagePack でサポートされていないタイプが使われている場合は Marshal が使われます。
53
+
54
+ ### Path Pre-Scanning
55
+
56
+ _(このライブラリは [bootscale](https://github.com/byroot/bootscale) という別のライブラリを元に開発されました)_
57
+
58
+ Bootsnap の始動時、あるいはパス(例えば、`$LOAD_PATH`)の変更時に、`Bootsnap::LoadPathCache` がキャッシュから必要なエントリーのリストを読み込みます。または、必要に応じてフルスキャンを実行し結果をキャッシュします。
59
+ その後、たとえば `require 'foo'` を評価する場合, Ruby は `$LOAD_PATH` `['x', 'y', ...]` のすべてのエントリーを繰り返し評価することで `x/foo.rb`, `y/foo.rb` などを探索します。これに対して Bootsnap は、キャッシュされた reuiqre 可能なファイルと `$LOAD_PATH` を見ることで、Rubyが最終的に選択するであろうパスで置き換えます。
60
+
61
+ この動作によって生成された syscall を見ると、最終的な結果は以前なら次のようになります。
62
+
63
+ ```
64
+ open x/foo.rb # (fail)
65
+ # (imagine this with 500 $LOAD_PATH entries instead of two)
66
+ open y/foo.rb # (success)
67
+ close y/foo.rb
68
+ open y/foo.rb
69
+ ...
70
+ ```
71
+
72
+ これが、次のようになります:
73
+
74
+ ```
75
+ open y/foo.rb
76
+ ...
77
+ ```
78
+
79
+ `autoload_paths_cache` オプションが `Bootsnap.setup` に与えられている場合、`ActiveSupport::Dependencies.autoload_paths` をトラバースするメソッドにはまったく同じ最適化が使用されます。
80
+
81
+ `*_path_cache` 機能を埋め込むオーバーライドを図にすると、次のようになります。
82
+
83
+ ![Bootsnapの説明図](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png)
84
+
85
+ Bootsnap は、 `$LOAD_PATH` エントリを安定エントリと不安定エントリの2つのカテゴリに分類します。不安定エントリはアプリケーションが起動するたびにスキャンされ、そのキャッシュは30秒間だけ有効になります。安定エントリーに期限切れはありません。コンテンツがスキャンされると、決して変更されないものとみなされます。
86
+
87
+ 安定していると考えられる唯一のディレクトリは、Rubyのインストールプレフィックス (`RbConfig::CONFIG['prefix']`, または `/usr/local/ruby` や `~/.rubies/x.y.z`)下にあるものと、`Gem.path` (たとえば `~/.gem/ruby/x.y.z`) や `Bundler.bundle_path` 下にあるものです。他のすべては不安定エントリと分類されます。
88
+
89
+ [`Bootsnap::LoadPathCache::Cache`](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb) に加えて次の図では、エントリの解決がどのように機能するかを理解するのに役立つかもしれません。経路探索は以下のようになります。
90
+
91
+ ![パス探索の仕組み](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png)
92
+
93
+ また、`LoadError` のスキャンがどれほど重いかに注意を払うことも大切です。もし Ruby が `require 'something'` を評価し、そのファイルが `$LOAD_PATH` にない場合は、それを知るために `2 * $LOAD_PATH.length` のファイルシステムアスセスが必要になります。Bootsnap は、ファイルシステムにまったく触れずに `LoadError` を投げ、この結果をキャッシュします。
94
+
95
+ ## Compilation Caching
96
+
97
+ このコンセプトのより分かりやすい解説は [yomikomu](https://github.com/ko1/yomikomu) をお読み下さい。
98
+
99
+ Ruby には複雑な文法が実装されており、構文解析は簡単なオペレーションではありません。1.9以降、Ruby は Ruby ソースを内部のバイトコードに変換した後、Ruby VM によって実行してきました。2.3.0 以降、[RubyはAPIを公開し](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html)、そのバイトコードをキャッシュすることができるようになりました。これにより、同じファイルが複数ロードされた時の、比較的時間のかかる部分をバイパスすることができます。
100
+
101
+ また、Shopify のアプリケーションでは、アプリケーションの起動時に YAML ドキュメントの読み込みに多くの時間を費やしていることを発見しました。そして、 MessagePack と Marshal は deserialization にあたって YAML よりもはるかに高速であるということに気付きました。そこで、YAML ドキュメントを、Ruby バイトコードと同じコンパイルキャッシングの最適化を施すことで、高速化しています。Ruby の "バイトコード" フォーマットに相当するものは MessagePack ドキュメント (あるいは、MessagePack をサポートしていないタイプの YAML ドキュメントの場合は、Marshal stream)になります。
102
+
103
+ これらのコンパイル結果は、入力ファイル(FNV1a-64)のフルパスのハッシュを取って生成されたファイル名で、キャッシュディレクトリに保存されます。
104
+
105
+ Bootsnap 無しでは、ファイルを `require` するために生成された syscall の順序は次のようになっていました:
106
+
107
+ ```
108
+ open /c/foo.rb -> m
109
+ fstat64 m
110
+ close m
111
+ open /c/foo.rb -> o
112
+ fstat64 o
113
+ fstat64 o
114
+ read o
115
+ read o
116
+ ...
117
+ close o
118
+ ```
119
+
120
+ しかし Bootsnap では、次のようになります:
121
+
122
+ ```
123
+ open /c/foo.rb -> n
124
+ fstat64 n
125
+ close n
126
+ open /c/foo.rb -> n
127
+ fstat64 n
128
+ open (cache) -> m
129
+ read m
130
+ read m
131
+ close m
132
+ close n
133
+ ```
134
+
135
+ これは一目見るだけでは劣化していると思われるかもしれませんが、性能に大きな違いがあります。
136
+
137
+ (両方のリストの最初の3つの syscalls -- `open`, `fstat64`, `close` -- は本質的に有用ではありません。[このRubyパッチ](https://bugs.ruby-lang.org/issues/13378)は、Boosnap と組み合わせることによって、それらを最適化しています)
138
+
139
+ Bootsnap は、64バイトのヘッダーとそれに続くキャッシュの内容を含んだキャッシュファイルを書き込みます。ヘッダーは、次のいくつかのフィールドで構成されるキャッシュキーです。
140
+
141
+ - `version`、Bootsnapにハードコードされる基本的なスキーマのバージョン
142
+ - `os_version`、(macOS, BSDの) 現在のカーネルバージョンか 、(Linuxの) glibc のバージョンのハッシュ
143
+ - `compile_option`、`RubyVM::InstructionSequence.compile_option` の返り値
144
+ - `ruby_revision`、コンパイルされたRubyのバージョン
145
+ - `size`、ソースファイルのサイズ
146
+ - `mtime`、コンパイル時のソースファイルの最終変更タイムスタンプ
147
+ - `data_size`、バッファに読み込む必要のあるヘッダーに続くバイト数。
148
+
149
+ キーが有効な場合、キャッシュがファイルからロードされます。そうでない場合、キャッシュは再生成され、現在のキャッシュを破棄します。
150
+
151
+ # 最終的なキャッシュ結果
152
+
153
+ 次のファイル構造があるとします。
154
+
155
+ ```
156
+ /
157
+ ├── a
158
+ ├── b
159
+ └── c
160
+ └── foo.rb
161
+ ```
162
+
163
+ そして、このような `$LOAD_PATH` があるとします。
164
+
165
+ ```
166
+ ["/a", "/b", "/c"]
167
+ ```
168
+
169
+ Bootsnap なしで `require 'foo'` を呼び出すと、Ruby は次の順序で syscalls を生成します:
170
+
171
+ ```
172
+ open /a/foo.rb -> -1
173
+ open /b/foo.rb -> -1
174
+ open /c/foo.rb -> n
175
+ close n
176
+ open /c/foo.rb -> m
177
+ fstat64 m
178
+ close m
179
+ open /c/foo.rb -> o
180
+ fstat64 o
181
+ fstat64 o
182
+ read o
183
+ read o
184
+ ...
185
+ close o
186
+ ```
187
+
188
+ しかし Bootsnap では、次のようになります:
189
+
190
+ ```
191
+ open /c/foo.rb -> n
192
+ fstat64 n
193
+ close n
194
+ open /c/foo.rb -> n
195
+ fstat64 n
196
+ open (cache) -> m
197
+ read m
198
+ read m
199
+ close m
200
+ close n
201
+ ```
202
+
203
+ Bootsnap なしで `require 'nope'` を呼び出すと、次のようになります:
204
+
205
+ ```
206
+ open /a/nope.rb -> -1
207
+ open /b/nope.rb -> -1
208
+ open /c/nope.rb -> -1
209
+ open /a/nope.bundle -> -1
210
+ open /b/nope.bundle -> -1
211
+ open /c/nope.bundle -> -1
212
+ ```
213
+
214
+ ...そして、Bootsnap で `require 'nope'` を呼び出すと、次のようになります...
215
+
216
+ ```
217
+ # (nothing!)
218
+ ```
@@ -94,7 +94,6 @@ static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
94
94
  static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
95
95
  static int open_current_file(char * path, struct bs_cache_key * key, char ** errno_provenance);
96
96
  static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, char ** errno_provenance);
97
- static VALUE prot_exception_for_errno(VALUE err);
98
97
  static uint32_t get_ruby_platform(void);
99
98
 
100
99
  /*
@@ -520,22 +519,6 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, char
520
519
  return ret;
521
520
  }
522
521
 
523
- /*
524
- * Given an errno value (converted to a ruby Fixnum), return the corresponding
525
- * Errno::* constant. If none is found, return StandardError instead.
526
- */
527
- static VALUE
528
- prot_exception_for_errno(VALUE err)
529
- {
530
- if (err != INT2FIX(0)) {
531
- VALUE mErrno = rb_const_get(rb_cObject, rb_intern("Errno"));
532
- VALUE constants = rb_funcall(mErrno, rb_intern("constants"), 0);
533
- VALUE which = rb_funcall(constants, rb_intern("[]"), 1, err);
534
- return rb_funcall(mErrno, rb_intern("const_get"), 1, which);
535
- }
536
- return rb_eStandardError;
537
- }
538
-
539
522
 
540
523
  /* Read contents from an fd, whose contents are asserted to be +size+ bytes
541
524
  * long, into a buffer */
@@ -689,11 +672,7 @@ succeed:
689
672
  return output_data;
690
673
  fail_errno:
691
674
  CLEANUP;
692
- exception = rb_protect(prot_exception_for_errno, INT2FIX(errno), &res);
693
- if (res) exception = rb_eStandardError;
694
- if (errno_provenance != NULL) {
695
- exception = rb_exc_new_str(exception, rb_str_new2(errno_provenance));
696
- }
675
+ exception = rb_syserr_new(errno, errno_provenance);
697
676
  rb_exc_raise(exception);
698
677
  __builtin_unreachable();
699
678
  raise:
@@ -21,11 +21,13 @@ module Bootsnap
21
21
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
22
22
 
23
23
  class << self
24
- attr_reader :load_path_cache, :autoload_paths_cache
24
+ attr_reader :load_path_cache, :autoload_paths_cache, :loaded_features_index
25
25
 
26
26
  def setup(cache_path:, development_mode:, active_support: true)
27
27
  store = Store.new(cache_path)
28
28
 
29
+ @loaded_features_index = LoadedFeaturesIndex.new
30
+
29
31
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
30
32
  require_relative 'load_path_cache/core_ext/kernel_require'
31
33
 
@@ -50,3 +52,4 @@ require_relative 'load_path_cache/path'
50
52
  require_relative 'load_path_cache/cache'
51
53
  require_relative 'load_path_cache/store'
52
54
  require_relative 'load_path_cache/change_observer'
55
+ require_relative 'load_path_cache/loaded_features_index'
@@ -11,78 +11,104 @@ module Bootsnap
11
11
  end
12
12
 
13
13
  module Kernel
14
- alias_method :require_without_cache, :require
14
+ alias_method :require_without_bootsnap, :require
15
+
16
+ # Note that require registers to $LOADED_FEATURES while load does not.
17
+ def require_with_bootsnap_lfi(path, resolved = nil)
18
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
19
+ require_without_bootsnap(resolved || path)
20
+ end
21
+ end
22
+
15
23
  def require(path)
24
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
25
+
16
26
  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)
27
+ return require_with_bootsnap_lfi(path, resolved)
20
28
  end
29
+
30
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
21
31
  rescue Bootsnap::LoadPathCache::ReturnFalse
22
32
  return false
23
33
  rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
34
+ require_with_bootsnap_lfi(path)
25
35
  end
26
36
 
27
- alias_method :load_without_cache, :load
37
+ alias_method :load_without_bootsnap, :load
28
38
  def load(path, wrap = false)
29
39
  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)
40
+ return load_without_bootsnap(resolved, wrap)
38
41
  end
42
+
43
+ # load also allows relative paths from pwd even when not in $:
44
+ if File.exist?(relative = File.expand_path(path))
45
+ return load_without_bootsnap(relative, wrap)
46
+ end
47
+
48
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
39
49
  rescue Bootsnap::LoadPathCache::ReturnFalse
40
50
  return false
41
51
  rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
52
+ load_without_bootsnap(path, wrap)
43
53
  end
44
54
  end
45
55
 
46
56
  class << Kernel
47
- alias_method :require_without_cache, :require
57
+ alias_method :require_without_bootsnap, :require
58
+
59
+ def require_with_bootsnap_lfi(path, resolved = nil)
60
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
61
+ require_without_bootsnap(resolved || path)
62
+ end
63
+ end
64
+
48
65
  def require(path)
66
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
67
+
49
68
  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)
69
+ return require_with_bootsnap_lfi(path, resolved)
53
70
  end
71
+
72
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
54
73
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
74
  return false
56
75
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
76
+ require_with_bootsnap_lfi(path)
58
77
  end
59
78
 
60
- alias_method :load_without_cache, :load
79
+ alias_method :load_without_bootsnap, :load
61
80
  def load(path, wrap = false)
62
81
  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)
82
+ return load_without_bootsnap(resolved, wrap)
71
83
  end
84
+
85
+ # load also allows relative paths from pwd even when not in $:
86
+ if File.exist?(relative = File.expand_path(path))
87
+ return load_without_bootsnap(relative, wrap)
88
+ end
89
+
90
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
72
91
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
92
  return false
74
93
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
94
+ load_without_bootsnap(path, wrap)
76
95
  end
77
96
  end
78
97
 
79
98
  class Module
80
- alias_method :autoload_without_cache, :autoload
99
+ alias_method :autoload_without_bootsnap, :autoload
81
100
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
101
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
102
+ # obvious how to make it work. This feels like a pretty niche case, unclear
103
+ # if it will ever burn anyone.
104
+ #
105
+ # The challenge is that we don't control the point at which the entry gets
106
+ # added to $LOADED_FEATURES and won't be able to hook that modification
107
+ # since it's done in C-land.
108
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
109
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
110
  return false
85
111
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
112
+ autoload_without_bootsnap(const, path)
87
113
  end
88
114
  end
@@ -0,0 +1,95 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
4
+ # we can't easily obtain an interface to.
5
+ #
6
+ # This works around an issue where, without bootsnap, *ruby* knows that it
7
+ # has already required a file by its short name (e.g. require 'bundler') if
8
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
9
+ # different absolute path. This class makes bootsnap smart enough to
10
+ # realize that it has already loaded 'bundler', and not just
11
+ # '/path/to/bundler'.
12
+ #
13
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
14
+ #
15
+ # 1. `require 'a'`
16
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
17
+ # 3. `require 'a'`
18
+ #
19
+ # Ruby returns false from step 3.
20
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
21
+ # `a.rb`s.
22
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
23
+ # returning false like ruby.
24
+ class LoadedFeaturesIndex
25
+ def initialize
26
+ @lfi = {}
27
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
28
+
29
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
30
+ # cache. If this ever comes up in practice — or if you, the
31
+ # enterprising reader, feels inclined to solve this problem — we could
32
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
33
+ # updates to our @lfi.
34
+ $LOADED_FEATURES.each do |feat|
35
+ $LOAD_PATH.each do |lpe|
36
+ next unless feat.start_with?(lpe)
37
+ # /a/b/lib/my/foo.rb
38
+ # ^^^^^^^^^
39
+ short = feat[(lpe.length + 1)..-1]
40
+ @lfi[short] = true
41
+ @lfi[strip_extension(short)] = true
42
+ end
43
+ end
44
+ end
45
+
46
+ def key?(feature)
47
+ @mutex.synchronize { @lfi.key?(feature) }
48
+ end
49
+
50
+ # There is a relatively uncommon case where we could miss adding an
51
+ # entry:
52
+ #
53
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
54
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
55
+ # pass `long` (the full expanded absolute path), then we did are not able
56
+ # to confidently add the `bundler.rb` form to @lfi.
57
+ #
58
+ # We could either:
59
+ #
60
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
61
+ # not quite right; or
62
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
63
+ # entry.
64
+ def register(short, long = nil)
65
+ ret = yield
66
+
67
+ # do we have 'bundler' or 'bundler.rb'?
68
+ altname = if File.extname(short) != ''
69
+ # strip the path from 'bundler.rb' -> 'bundler'
70
+ strip_extension(short)
71
+ elsif long && ext = File.extname(long)
72
+ # get the extension from the expanded path if given
73
+ # 'bundler' + '.rb'
74
+ short + ext
75
+ end
76
+
77
+ @mutex.synchronize do
78
+ @lfi[short] = true
79
+ (@lfi[altname] = true) if altname
80
+ end
81
+
82
+ ret
83
+ end
84
+
85
+ private
86
+
87
+ STRIP_EXTENSION = /\..*?$/
88
+ private_constant :STRIP_EXTENSION
89
+
90
+ def strip_extension(f)
91
+ f.sub(STRIP_EXTENSION, '')
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,3 +1,3 @@
1
1
  module Bootsnap
2
- VERSION = "1.1.8"
2
+ VERSION = "1.2.0.pre"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.8
4
+ version: 1.2.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-05 00:00:00.000000000 Z
11
+ date: 2018-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -110,6 +110,7 @@ files:
110
110
  - CONTRIBUTING.md
111
111
  - Gemfile
112
112
  - LICENSE.txt
113
+ - README.jp.md
113
114
  - README.md
114
115
  - Rakefile
115
116
  - bin/console
@@ -131,6 +132,7 @@ files:
131
132
  - lib/bootsnap/load_path_cache/change_observer.rb
132
133
  - lib/bootsnap/load_path_cache/core_ext/active_support.rb
133
134
  - lib/bootsnap/load_path_cache/core_ext/kernel_require.rb
135
+ - lib/bootsnap/load_path_cache/loaded_features_index.rb
134
136
  - lib/bootsnap/load_path_cache/path.rb
135
137
  - lib/bootsnap/load_path_cache/path_scanner.rb
136
138
  - lib/bootsnap/load_path_cache/store.rb
@@ -151,12 +153,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
153
  version: 2.0.0
152
154
  required_rubygems_version: !ruby/object:Gem::Requirement
153
155
  requirements:
154
- - - ">="
156
+ - - ">"
155
157
  - !ruby/object:Gem::Version
156
- version: '0'
158
+ version: 1.3.1
157
159
  requirements: []
158
160
  rubyforge_project:
159
- rubygems_version: 2.6.14
161
+ rubygems_version: 2.7.6
160
162
  signing_key:
161
163
  specification_version: 4
162
164
  summary: Boot large ruby/rails apps faster