bootsnap 1.1.8 → 1.3.0

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: a6aa25eb064ffeb1bd2e7588f6741061c1acd0647782ba830ebc872ce8997ff2
4
+ data.tar.gz: 981babf7f6d1dc70cd02e5b7373a1a0770d3ebca39e1488503e7aebc1665f8f2
5
5
  SHA512:
6
- metadata.gz: fa366476bf324d6eda9415e7170d67e31eb4a01d08758ff51d9e83c7661db8160cfa334ef9df67777ef059887de24a3f8a7da49b1675f3a1b0bf7704ecf42620
7
- data.tar.gz: '06758fb4f8445d041816330983799684a3b79e6ec82ac1e21cf087544d377744b0bb8bc20012146d529876fac6398e1053821eea2468be60c8194c6c07855312'
6
+ metadata.gz: 5dc67fe4306e38465bbcec301a169e42a7555ce5541b3d2032cfdf157bf4ff47a25b4403a450f57232f2ae27c69fee8f284fdc94588e671b33af9edff5b6fc2d
7
+ data.tar.gz: de7d7384d6cb6c97b7e32ebf6f00de0e7b9f7a0aa65a76897d0458a1d0a172e8d714ca3d812ed79aef9a34943ef7dcfdbff95413c95dab4d44be13971dd2cf57
data/.travis.yml CHANGED
@@ -1,4 +1,13 @@
1
1
  language: ruby
2
- rvm: ruby-2.4.1
2
+ sudo: false
3
+
4
+ os:
5
+ - linux
6
+ - osx
7
+
8
+ rvm:
9
+ - ruby-2.4
10
+ - ruby-2.5
11
+
3
12
  before_script: rake
4
- script: bin/testunit
13
+ script: bundle exec bin/testunit
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 1.3.0
2
+
3
+ * Handle cases where load path entries are symlinked (https://github.com/Shopify/bootsnap/pull/136)
4
+
5
+ # 1.2.1
6
+
7
+ * Fix method visibility of `Kernel#require`.
8
+
9
+ # 1.2.0
10
+
11
+ * Add `LoadedFeaturesIndex` to preserve fix a common bug related to `LOAD_PATH` modifications after
12
+ loading bootsnap.
13
+
1
14
  # 1.1.8
2
15
 
3
16
  * Don't cache YAML documents with `!ruby/object`
data/README.jp.md ADDED
@@ -0,0 +1,229 @@
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](https://github.com/discourse/discourse) では、約6秒から3秒まで、約50%の起動時間短縮が確認されています。
10
+ * 小さなアプリケーションでも、50%の改善(3.6秒から1.8秒)が確認されています。
11
+ * 非常に巨大でモノリシックなアプリである Shopify のプラットフォームでは、約25秒から6.5秒へと約75%短縮されました。
12
+
13
+ ## 使用方法
14
+
15
+ この gem は MacOS と Linux で作動します。まずは、`bootsnap` を `Gemfile` に追加します:
16
+
17
+ ```ruby
18
+ gem 'bootsnap', require: false
19
+ ```
20
+
21
+ Rails を使用している場合は、以下のコードを、`config/boot.rb` 内にある `require 'bundler/setup'` の直後に追加してください。
22
+
23
+ ```ruby
24
+ require 'bootsnap/setup'
25
+ ```
26
+
27
+ この require の仕組みは[こちら](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/setup.rb)で確認できます。
28
+
29
+ Rails を使用していない場合、または、より多くの設定を変更したい場合は、以下のコードを `require 'bundler/setup'` の直後に追加してください(早く読み込まれるほど、より多くのものを最適化することができます)。
30
+
31
+ ```ruby
32
+ require 'bootsnap'
33
+ env = ENV['RAILS_ENV'] || "development"
34
+ Bootsnap.setup(
35
+  cache_dir:           'tmp/cache',         # キャッシュファイルを保存する path
36
+  development_mode:     env == 'development', # 現在の作業環境、例えば RACK_ENV, RAILS_ENV など。
37
+ load_path_cache: true, # キャッシュで LOAD_PATH を最適化する。
38
+  autoload_paths_cache: true,                 # キャッシュで ActiveSupport による autoload を行う。
39
+  disable_trace:       true,                 # (アルファ) `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`をセットする。
40
+  compile_cache_iseq:   true,                 # ISeq キャッシュをコンパイルする
41
+  compile_cache_yaml:   true                 # YAML キャッシュをコンパイルする
42
+ )
43
+ ```
44
+
45
+ **ヒント**: `require 'bootsnap'` を `BootLib::Require.from_gem('bootsnap', 'bootsnap')` で、 [こちらのトリック](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require)を使って置き換えることができます。こうすると、巨大な`$LOAD_PATH`がある場合でも、起動時間を最短化するのに役立ちます。
46
+
47
+ 注意: Bootsnap と [Spring](https://github.com/rails/spring) は別領域の問題を扱うツールです。Bootsnap は個々のソースファイルの読み込みを高速化します。一方で、Spring は起動されたRailsプロセスのコピーを保持して次回の起動時に起動プロセスの一部を完全にスキップします。2つのツールはうまく連携しており、どちらも新しく生成された Rails アプリケーションにデフォルトで含まれています。
48
+
49
+ ### 環境
50
+ Bootsnapのすべての機能はセットアップ時の設定に従って開発、テスト、プロダクション、および他のすべての環境で有効化されます。Shopify では、この gem を問題なくすべての環境で安全に使用しています。
51
+
52
+ 特定の環境で機能を無効にする場合は、必要に応じて適切な ENV 変数または設定を考慮して設定を変更することをおすすめします。
53
+
54
+ ## 内部動作
55
+
56
+ Bootsnap は、処理に時間のかかるメソッドの結果をキャッシュすることで最適化しています。これは、大きく分けて2つのカテゴリに分けられます。
57
+
58
+ * [Path Pre-Scanning](#path-pre-scanning)
59
+ * `Kernel#require` と `Kernel#load` を `$LOAD_PATH` フルスキャンを行わないように変更します。
60
+   * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` を `ActiveSupport::Dependencies.autoload_paths` のフルスキャンを行わないようにオーバーライドします。
61
+ * [Compilation caching](#compilation-caching)
62
+  * Ruby バイトコードのコンパイル結果をキャッシュするためのメソッド `RubyVM::InstructionSequence.load_iseq` が実装されています。
63
+   * `YAML.load_file` を YAML オブジェクトのロード結果を MessagePack でキャッシュするように変更します。 MessagePack でサポートされていないタイプが使われている場合は Marshal が使われます。
64
+
65
+ ### Path Pre-Scanning
66
+
67
+ _(このライブラリは [bootscale](https://github.com/byroot/bootscale) という別のライブラリを元に開発されました)_
68
+
69
+ Bootsnap の初期化時、あるいはパス(例えば、`$LOAD_PATH`)の変更時に、`Bootsnap::LoadPathCache` がキャッシュから必要なエントリーのリストを読み込みます。または、必要に応じてフルスキャンを実行し結果をキャッシュします。
70
+ その後、たとえば `require 'foo'` を評価する場合, Ruby は `$LOAD_PATH` `['x', 'y', ...]` のすべてのエントリーを繰り返し評価することで `x/foo.rb`, `y/foo.rb` などを探索します。これに対して Bootsnap は、キャッシュされた reuiqre 可能なファイルと `$LOAD_PATH` を見ることで、Rubyが最終的に選択するであろうパスで置き換えます。
71
+
72
+ この動作によって生成された syscall を見ると、最終的な結果は以前なら次のようになります。
73
+
74
+ ```
75
+ open x/foo.rb # (fail)
76
+ # (imagine this with 500 $LOAD_PATH entries instead of two)
77
+ open y/foo.rb # (success)
78
+ close y/foo.rb
79
+ open y/foo.rb
80
+ ...
81
+ ```
82
+
83
+ これが、次のようになります:
84
+
85
+ ```
86
+ open y/foo.rb
87
+ ...
88
+ ```
89
+
90
+ `autoload_paths_cache` オプションが `Bootsnap.setup` に与えられている場合、`ActiveSupport::Dependencies.autoload_paths` をトラバースする方法にはまったく同じ最適化が使用されます。
91
+
92
+ `*_path_cache` を機能させるオーバーライドを図にすると、次のようになります。
93
+
94
+ ![Bootsnapの説明図](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png)
95
+
96
+ Bootsnap は、 `$LOAD_PATH` エントリを安定エントリと不安定エントリの2つのカテゴリに分類します。不安定エントリはアプリケーションが起動するたびにスキャンされ、そのキャッシュは30秒間だけ有効になります。安定エントリーに期限切れはありません。コンテンツがスキャンされると、決して変更されないものとみなされます。
97
+
98
+ 安定していると考えられる唯一のディレクトリは、Rubyのインストールプレフィックス (`RbConfig::CONFIG['prefix']`, または `/usr/local/ruby` や `~/.rubies/x.y.z`)下にあるものと、`Gem.path` (たとえば `~/.gem/ruby/x.y.z`) や `Bundler.bundle_path` 下にあるものです。他のすべては不安定エントリと分類されます。
99
+
100
+ [`Bootsnap::LoadPathCache::Cache`](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb) に加えて次の図では、エントリの解決がどのように機能するかを理解するのに役立つかもしれません。経路探索は以下のようになります。
101
+
102
+ ![パス探索の仕組み](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png)
103
+
104
+ また、`LoadError` のスキャンがどれほど重いかに注意を払うことも大切です。もし Ruby が `require 'something'` を評価し、そのファイルが `$LOAD_PATH` にない場合は、それを知るために `2 * $LOAD_PATH.length` のファイルシステムアスセスが必要になります。Bootsnap は、ファイルシステムにまったく触れずに `LoadError` を投げ、この結果をキャッシュします。
105
+
106
+ ## Compilation Caching
107
+
108
+ *(このコンセプトのより分かりやすい解説は [yomikomu](https://github.com/ko1/yomikomu) をお読み下さい。)*
109
+
110
+ Ruby には複雑な文法が実装されており、構文解析は簡単なオペレーションではありません。1.9以降、Ruby は Ruby ソースを内部のバイトコードに変換した後、Ruby VM によって実行してきました。2.3.0 以降、[RubyはAPIを公開し](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html)、そのバイトコードをキャッシュすることができるようになりました。これにより、同じファイルが複数ロードされた時の、比較的時間のかかる部分をバイパスすることができます。
111
+
112
+ また、アプリケーションの起動時に YAML ドキュメントの読み込みに多くの時間を費やしていることを発見しました。そして、 MessagePack と Marshal は deserialization にあたって YAML よりもはるかに高速であるということに気付きました。そこで、YAML ドキュメントを、Ruby バイトコードと同じコンパイルキャッシングの最適化を施すことで、高速化しています。Ruby の "バイトコード" フォーマットに相当するものは MessagePack ドキュメント (あるいは、MessagePack をサポートしていないタイプの YAML ドキュメントの場合は、Marshal stream)になります。
113
+
114
+ これらのコンパイル結果は、入力ファイル(FNV1a-64)のフルパスのハッシュを取って生成されたファイル名で、キャッシュディレクトリに保存されます。
115
+
116
+ Bootsnap 無しでは、ファイルを `require` するために生成された syscall の順序は次のようになっていました:
117
+
118
+ ```
119
+ open /c/foo.rb -> m
120
+ fstat64 m
121
+ close m
122
+ open /c/foo.rb -> o
123
+ fstat64 o
124
+ fstat64 o
125
+ read o
126
+ read o
127
+ ...
128
+ close o
129
+ ```
130
+
131
+ しかし Bootsnap では、次のようになります:
132
+
133
+ ```
134
+ open /c/foo.rb -> n
135
+ fstat64 n
136
+ close n
137
+ open /c/foo.rb -> n
138
+ fstat64 n
139
+ open (cache) -> m
140
+ read m
141
+ read m
142
+ close m
143
+ close n
144
+ ```
145
+
146
+ これは一見劣化していると思われるかもしれませんが、性能に大きな違いがあります。
147
+
148
+ *(両方のリストの最初の3つの syscalls -- `open`, `fstat64`, `close` -- は本質的に有用ではありません。[このRubyパッチ](https://bugs.ruby-lang.org/issues/13378)は、Boosnap と組み合わせることによって、それらを最適化しています)*
149
+
150
+ Bootsnap は、64バイトのヘッダーとそれに続くキャッシュの内容を含んだキャッシュファイルを書き込みます。ヘッダーは、次のいくつかのフィールドで構成されるキャッシュキーです。
151
+
152
+ - `version`、Bootsnapにハードコードされる基本的なスキーマのバージョン
153
+ - `os_version`、(macOS, BSDの) 現在のカーネルバージョンか 、(Linuxの) glibc のバージョンのハッシュ
154
+ - `compile_option`、`RubyVM::InstructionSequence.compile_option` の返り値
155
+ - `ruby_revision`、コンパイルされたRubyのバージョン
156
+ - `size`、ソースファイルのサイズ
157
+ - `mtime`、コンパイル時のソースファイルの最終変更タイムスタンプ
158
+ - `data_size`、バッファに読み込む必要のあるヘッダーに続くバイト数。
159
+
160
+ キーが有効な場合、キャッシュがファイルからロードされます。そうでない場合、キャッシュは再生成され、現在のキャッシュを破棄します。
161
+
162
+ # 最終的なキャッシュ結果
163
+
164
+ 次のファイル構造があるとします。
165
+
166
+ ```
167
+ /
168
+ ├── a
169
+ ├── b
170
+ └── c
171
+ └── foo.rb
172
+ ```
173
+
174
+ そして、このような `$LOAD_PATH` があるとします。
175
+
176
+ ```
177
+ ["/a", "/b", "/c"]
178
+ ```
179
+
180
+ Bootsnap なしで `require 'foo'` を呼び出すと、Ruby は次の順序で syscalls を生成します:
181
+
182
+ ```
183
+ open /a/foo.rb -> -1
184
+ open /b/foo.rb -> -1
185
+ open /c/foo.rb -> n
186
+ close n
187
+ open /c/foo.rb -> m
188
+ fstat64 m
189
+ close m
190
+ open /c/foo.rb -> o
191
+ fstat64 o
192
+ fstat64 o
193
+ read o
194
+ read o
195
+ ...
196
+ close o
197
+ ```
198
+
199
+ しかし Bootsnap では、次のようになります:
200
+
201
+ ```
202
+ open /c/foo.rb -> n
203
+ fstat64 n
204
+ close n
205
+ open /c/foo.rb -> n
206
+ fstat64 n
207
+ open (cache) -> m
208
+ read m
209
+ read m
210
+ close m
211
+ close n
212
+ ```
213
+
214
+ Bootsnap なしで `require 'nope'` を呼び出すと、次のようになります:
215
+
216
+ ```
217
+ open /a/nope.rb -> -1
218
+ open /b/nope.rb -> -1
219
+ open /c/nope.rb -> -1
220
+ open /a/nope.bundle -> -1
221
+ open /b/nope.bundle -> -1
222
+ open /c/nope.bundle -> -1
223
+ ```
224
+
225
+ ...そして、Bootsnap で `require 'nope'` を呼び出すと、次のようになります...
226
+
227
+ ```
228
+ # (nothing!)
229
+ ```
data/README.md CHANGED
@@ -10,7 +10,7 @@ to optimize and cache expensive computations. See [How Does This Work](#how-does
10
10
 
11
11
  ## Usage
12
12
 
13
- This gem works on MacOS and Linux.
13
+ This gem works on macOS and Linux.
14
14
 
15
15
  Add `bootsnap` to your `Gemfile`:
16
16
 
@@ -24,6 +24,9 @@ If you are using Rails, add this to `config/boot.rb` immediately after `require
24
24
  require 'bootsnap/setup'
25
25
  ```
26
26
 
27
+ It's technically possible to simply specify `gem 'bootsnap', require: 'bootsnap/setup'`, but it's
28
+ important to load Bootsnap as early as possible to get maximum performance improvement.
29
+
27
30
  You can see how this require works [here](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/setup.rb).
28
31
 
29
32
  If you are not using Rails, or if you are but want more control over things, add this to your
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rake/extensiontask'
2
+ require 'bundler/gem_tasks'
2
3
 
3
4
  gemspec = Gem::Specification.load('bootsnap.gemspec')
4
5
  Rake::ExtensionTask.new do |ext|
@@ -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,15 @@ 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,
25
+ :loaded_features_index, :realpath_cache
25
26
 
26
27
  def setup(cache_path:, development_mode:, active_support: true)
27
28
  store = Store.new(cache_path)
28
29
 
30
+ @loaded_features_index = LoadedFeaturesIndex.new
31
+ @realpath_cache = RealpathCache.new
32
+
29
33
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
30
34
  require_relative 'load_path_cache/core_ext/kernel_require'
31
35
 
@@ -50,3 +54,5 @@ require_relative 'load_path_cache/path'
50
54
  require_relative 'load_path_cache/cache'
51
55
  require_relative 'load_path_cache/store'
52
56
  require_relative 'load_path_cache/change_observer'
57
+ require_relative 'load_path_cache/loaded_features_index'
58
+ require_relative 'load_path_cache/realpath_cache'
@@ -9,7 +9,7 @@ module Bootsnap
9
9
  @development_mode = development_mode
10
10
  @store = store
11
11
  @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
12
- @path_obj = path_obj
12
+ @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
13
13
  @has_relative_paths = nil
14
14
  reinitialize
15
15
  end
@@ -11,78 +11,122 @@ module Bootsnap
11
11
  end
12
12
 
13
13
  module Kernel
14
- alias_method :require_without_cache, :require
14
+ private
15
+
16
+ alias_method :require_without_bootsnap, :require
17
+
18
+ # Note that require registers to $LOADED_FEATURES while load does not.
19
+ def require_with_bootsnap_lfi(path, resolved = nil)
20
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
21
+ require_without_bootsnap(resolved || path)
22
+ end
23
+ end
24
+
15
25
  def require(path)
26
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
27
+
16
28
  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)
29
+ return require_with_bootsnap_lfi(path, resolved)
20
30
  end
31
+
32
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
21
33
  rescue Bootsnap::LoadPathCache::ReturnFalse
22
34
  return false
23
35
  rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
36
+ require_with_bootsnap_lfi(path)
25
37
  end
26
38
 
27
- alias_method :load_without_cache, :load
39
+ alias_method :require_relative_without_bootsnap, :require_relative
40
+ def require_relative(path)
41
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
42
+ caller_locations(1..1).first.absolute_path, path
43
+ )
44
+ require(realpath)
45
+ end
46
+
47
+ alias_method :load_without_bootsnap, :load
28
48
  def load(path, wrap = false)
29
49
  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)
50
+ return load_without_bootsnap(resolved, wrap)
38
51
  end
52
+
53
+ # load also allows relative paths from pwd even when not in $:
54
+ if File.exist?(relative = File.expand_path(path))
55
+ return load_without_bootsnap(relative, wrap)
56
+ end
57
+
58
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
39
59
  rescue Bootsnap::LoadPathCache::ReturnFalse
40
60
  return false
41
61
  rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
62
+ load_without_bootsnap(path, wrap)
43
63
  end
44
64
  end
45
65
 
46
66
  class << Kernel
47
- alias_method :require_without_cache, :require
67
+ alias_method :require_without_bootsnap, :require
68
+
69
+ def require_with_bootsnap_lfi(path, resolved = nil)
70
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
71
+ require_without_bootsnap(resolved || path)
72
+ end
73
+ end
74
+
48
75
  def require(path)
76
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
77
+
49
78
  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)
79
+ return require_with_bootsnap_lfi(path, resolved)
53
80
  end
81
+
82
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
54
83
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
84
  return false
56
85
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
86
+ require_with_bootsnap_lfi(path)
58
87
  end
59
88
 
60
- alias_method :load_without_cache, :load
89
+ alias_method :require_relative_without_bootsnap, :require_relative
90
+ def require_relative(path)
91
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
92
+ caller_locations(1..1).first.absolute_path, path
93
+ )
94
+ require(realpath)
95
+ end
96
+
97
+ alias_method :load_without_bootsnap, :load
61
98
  def load(path, wrap = false)
62
99
  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)
100
+ return load_without_bootsnap(resolved, wrap)
71
101
  end
102
+
103
+ # load also allows relative paths from pwd even when not in $:
104
+ if File.exist?(relative = File.expand_path(path))
105
+ return load_without_bootsnap(relative, wrap)
106
+ end
107
+
108
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
72
109
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
110
  return false
74
111
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
112
+ load_without_bootsnap(path, wrap)
76
113
  end
77
114
  end
78
115
 
79
116
  class Module
80
- alias_method :autoload_without_cache, :autoload
117
+ alias_method :autoload_without_bootsnap, :autoload
81
118
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
119
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
120
+ # obvious how to make it work. This feels like a pretty niche case, unclear
121
+ # if it will ever burn anyone.
122
+ #
123
+ # The challenge is that we don't control the point at which the entry gets
124
+ # added to $LOADED_FEATURES and won't be able to hook that modification
125
+ # since it's done in C-land.
126
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
127
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
128
  return false
85
129
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
130
+ autoload_without_bootsnap(const, path)
87
131
  end
88
132
  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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ class RealpathCache
6
+ def initialize
7
+ @cache = Hash.new { |h, k| h[k] = realpath(*k) }
8
+ end
9
+
10
+ def call(*key)
11
+ @cache[key]
12
+ end
13
+
14
+ private
15
+
16
+ def realpath(caller_location, path)
17
+ base = File.dirname(caller_location)
18
+ file = find_file(File.expand_path(path, base))
19
+ dir = File.dirname(file)
20
+ File.join(dir, File.basename(file))
21
+ end
22
+
23
+ def find_file(name)
24
+ ['', *CACHED_EXTENSIONS].each do |ext|
25
+ filename = "#{name}#{ext}"
26
+ return File.realpath(filename) if File.exist?(filename)
27
+ end
28
+ name
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module Bootsnap
2
- VERSION = "1.1.8"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -0,0 +1,4 @@
1
+ dependencies:
2
+ bundler:
3
+ without:
4
+ - development
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.3.0
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-04-09 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,11 +132,14 @@ 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
138
+ - lib/bootsnap/load_path_cache/realpath_cache.rb
136
139
  - lib/bootsnap/load_path_cache/store.rb
137
140
  - lib/bootsnap/setup.rb
138
141
  - lib/bootsnap/version.rb
142
+ - shipit.rubygems.yml
139
143
  homepage: https://github.com/Shopify/bootsnap
140
144
  licenses:
141
145
  - MIT
@@ -156,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
160
  version: '0'
157
161
  requirements: []
158
162
  rubyforge_project:
159
- rubygems_version: 2.6.14
163
+ rubygems_version: 2.7.6
160
164
  signing_key:
161
165
  specification_version: 4
162
166
  summary: Boot large ruby/rails apps faster