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 +5 -5
- data/.travis.yml +11 -2
- data/CHANGELOG.md +13 -0
- data/README.jp.md +229 -0
- data/README.md +4 -1
- data/Rakefile +1 -0
- data/ext/bootsnap/bootsnap.c +1 -22
- data/lib/bootsnap/load_path_cache.rb +7 -1
- data/lib/bootsnap/load_path_cache/cache.rb +1 -1
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +77 -33
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +95 -0
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
- data/lib/bootsnap/version.rb +1 -1
- data/shipit.rubygems.yml +4 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a6aa25eb064ffeb1bd2e7588f6741061c1acd0647782ba830ebc872ce8997ff2
|
4
|
+
data.tar.gz: 981babf7f6d1dc70cd02e5b7373a1a0770d3ebca39e1488503e7aebc1665f8f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5dc67fe4306e38465bbcec301a169e42a7555ce5541b3d2032cfdf157bf4ff47a25b4403a450f57232f2ae27c69fee8f284fdc94588e671b33af9edff5b6fc2d
|
7
|
+
data.tar.gz: de7d7384d6cb6c97b7e32ebf6f00de0e7b9f7a0aa65a76897d0458a1d0a172e8d714ca3d812ed79aef9a34943ef7dcfdbff95413c95dab4d44be13971dd2cf57
|
data/.travis.yml
CHANGED
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
|
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
data/ext/bootsnap/bootsnap.c
CHANGED
@@ -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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
+
require_with_bootsnap_lfi(path)
|
25
37
|
end
|
26
38
|
|
27
|
-
alias_method :
|
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
|
-
|
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
|
-
|
62
|
+
load_without_bootsnap(path, wrap)
|
43
63
|
end
|
44
64
|
end
|
45
65
|
|
46
66
|
class << Kernel
|
47
|
-
alias_method :
|
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
|
-
|
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
|
-
|
86
|
+
require_with_bootsnap_lfi(path)
|
58
87
|
end
|
59
88
|
|
60
|
-
alias_method :
|
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
|
-
|
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
|
-
|
112
|
+
load_without_bootsnap(path, wrap)
|
76
113
|
end
|
77
114
|
end
|
78
115
|
|
79
116
|
class Module
|
80
|
-
alias_method :
|
117
|
+
alias_method :autoload_without_bootsnap, :autoload
|
81
118
|
def autoload(const, path)
|
82
|
-
|
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
|
-
|
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
|
data/lib/bootsnap/version.rb
CHANGED
data/shipit.rubygems.yml
ADDED
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.
|
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-
|
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
|
163
|
+
rubygems_version: 2.7.6
|
160
164
|
signing_key:
|
161
165
|
specification_version: 4
|
162
166
|
summary: Boot large ruby/rails apps faster
|