bootsnap 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +6 -3
- data/CHANGELOG.md +19 -0
- data/README.jp.md +218 -0
- data/README.md +18 -22
- data/dev.yml +1 -0
- data/ext/bootsnap/bootsnap.c +124 -78
- data/lib/bootsnap/bundler.rb +12 -0
- data/lib/bootsnap/compile_cache/yaml.rb +1 -0
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +4 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +59 -33
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +95 -0
- data/lib/bootsnap/load_path_cache/path.rb +1 -1
- data/lib/bootsnap/load_path_cache/path_scanner.rb +2 -1
- data/lib/bootsnap/load_path_cache/store.rb +7 -2
- data/lib/bootsnap/load_path_cache.rb +4 -1
- data/lib/bootsnap/setup.rb +4 -10
- data/lib/bootsnap/version.rb +1 -1
- data/lib/bootsnap.rb +1 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '059b193c29635dc303b7f72742b404312b172bba723b6396df1d6c83ce7b2155'
|
|
4
|
+
data.tar.gz: 94a7b6ada9cb48574c0820ffe8967f435ee7e7881d71ede14cec5b3d774cfa7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b84ab3c65e266ed2235cc00fc65f2d10c5474d43ebe7813dbf29247a1ae9b04bfd37273f241911a69090233837d2be06bfb434466fced104f5e7511a360b2ce9
|
|
7
|
+
data.tar.gz: 3e582933e441fa2458ff9e7099307345b6aa9f3de5ddbe1af8d2a54adf2508d06e6e8586a311c89bd899909a4cd0aeeb48ecc25e01539f567bece1bf38e0fc88
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# 1.2.0
|
|
2
|
+
|
|
3
|
+
* Add `LoadedFeaturesIndex` to preserve fix a common bug related to `LOAD_PATH` modifications after
|
|
4
|
+
loading bootsnap.
|
|
5
|
+
|
|
6
|
+
# 1.1.8
|
|
7
|
+
|
|
8
|
+
* Don't cache YAML documents with `!ruby/object`
|
|
9
|
+
* Fix cache write mode on Windows
|
|
10
|
+
|
|
11
|
+
# 1.1.7
|
|
12
|
+
|
|
13
|
+
* Create cache entries as 0775/0664 instead of 0755/0644
|
|
14
|
+
* Better handling around cache updates in highly-parallel workloads
|
|
15
|
+
|
|
16
|
+
# 1.1.6
|
|
17
|
+
|
|
18
|
+
* Assortment of minor bugfixes
|
|
19
|
+
|
|
1
20
|
# 1.1.5
|
|
2
21
|
|
|
3
22
|
* bugfix re-release of 1.1.4
|
data/README.jp.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Bootsnap [](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
|
+

|
|
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
|
+

|
|
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
|
+
```
|
data/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Bootsnap [](https://travis-ci.org/Shopify/bootsnap)
|
|
2
2
|
|
|
3
|
-
**Beta-quality. See [the last section of this README](#trustworthiness).**
|
|
4
|
-
|
|
5
3
|
Bootsnap is a library that plugs into Ruby, with optional support for `ActiveSupport` and `YAML`,
|
|
6
4
|
to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work).
|
|
7
5
|
|
|
@@ -20,13 +18,18 @@ Add `bootsnap` to your `Gemfile`:
|
|
|
20
18
|
gem 'bootsnap', require: false
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
If you are using
|
|
21
|
+
If you are using Rails, add this to `config/boot.rb` immediately after `require 'bundler/setup'`:
|
|
24
22
|
|
|
25
23
|
```ruby
|
|
26
24
|
require 'bootsnap/setup'
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
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
|
+
|
|
30
|
+
You can see how this require works [here](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/setup.rb).
|
|
31
|
+
|
|
32
|
+
If you are not using Rails, or if you are but want more control over things, add this to your
|
|
30
33
|
application setup immediately after `require 'bundler/setup'` (i.e. as early as possible: the sooner
|
|
31
34
|
this is loaded, the sooner it can start optimizing things)
|
|
32
35
|
|
|
@@ -48,6 +51,17 @@ Bootsnap.setup(
|
|
|
48
51
|
'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This
|
|
49
52
|
will help optimize boot time further if you have an extremely large `$LOAD_PATH`.
|
|
50
53
|
|
|
54
|
+
Note: Bootsnap and [Spring](https://github.com/rails/spring) are orthogonal tools. While Bootsnap
|
|
55
|
+
speeds up the loading of individual source files, Spring keeps a copy of a pre-booted Rails process
|
|
56
|
+
on hand to completely skip parts of the boot process the next time it's needed. The two tools work
|
|
57
|
+
well together, and are both included in a newly-generated Rails applications by default.
|
|
58
|
+
|
|
59
|
+
### Environments
|
|
60
|
+
|
|
61
|
+
All Bootsnap features are enabled in development, test, production, and all other environments according to the configuration in the setup. At Shopify, we use this gem safely in all environments without issue.
|
|
62
|
+
|
|
63
|
+
If you would like to disable any feature for a certain environment, we suggest changing the configuration to take into account the appropriate ENV var or configuration according to your needs.
|
|
64
|
+
|
|
51
65
|
## How does this work?
|
|
52
66
|
|
|
53
67
|
Bootsnap optimizes methods to cache results of expensive computations, and can be grouped
|
|
@@ -264,21 +278,3 @@ open /c/nope.bundle -> -1
|
|
|
264
278
|
```
|
|
265
279
|
# (nothing!)
|
|
266
280
|
```
|
|
267
|
-
|
|
268
|
-
## Trustworthiness
|
|
269
|
-
|
|
270
|
-
We use the `*_path_cache` features in production and haven't experienced any issues in a long time.
|
|
271
|
-
|
|
272
|
-
The `compile_cache_*` features work well for us in development on macOS. It should work on Linux,
|
|
273
|
-
and we intend to deploy it in production, but we haven't yet.
|
|
274
|
-
|
|
275
|
-
`disable_trace` should be completely safe, but we don't really use it because some people like to
|
|
276
|
-
use tools that make use of `trace` instructions.
|
|
277
|
-
|
|
278
|
-
| feature | where we're using it |
|
|
279
|
-
|-|-|
|
|
280
|
-
| `load_path_cache` | everywhere |
|
|
281
|
-
| `autoload_path_cache` | everywhere |
|
|
282
|
-
| `disable_trace` | nowhere, but it's safe unless you need tracing |
|
|
283
|
-
| `compile_cache_iseq` | development, but probably safe to use everywhere |
|
|
284
|
-
| `compile_cache_yaml` | development, but probably safe to use everywhere |
|
data/dev.yml
CHANGED
data/ext/bootsnap/bootsnap.c
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
/*
|
|
33
33
|
* An instance of this key is written as the first 64 bytes of each cache file.
|
|
34
34
|
* The mtime and size members track whether the file contents have changed, and
|
|
35
|
-
* the version,
|
|
35
|
+
* the version, ruby_platform, compile_option, and ruby_revision members track
|
|
36
36
|
* changes to the environment that could invalidate compile results without
|
|
37
37
|
* file contents having changed. The data_size member is not truly part of the
|
|
38
38
|
* "key". Really, this could be called a "header" with the first six members
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
*/
|
|
46
46
|
struct bs_cache_key {
|
|
47
47
|
uint32_t version;
|
|
48
|
-
uint32_t
|
|
48
|
+
uint32_t ruby_platform;
|
|
49
49
|
uint32_t compile_option;
|
|
50
50
|
uint32_t ruby_revision;
|
|
51
51
|
uint64_t size;
|
|
@@ -67,9 +67,9 @@ STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
|
|
|
67
67
|
/* Effectively a schema version. Bumping invalidates all previous caches */
|
|
68
68
|
static const uint32_t current_version = 2;
|
|
69
69
|
|
|
70
|
-
/*
|
|
71
|
-
*
|
|
72
|
-
static uint32_t
|
|
70
|
+
/* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
|
|
71
|
+
* new OS ABI, etc. */
|
|
72
|
+
static uint32_t current_ruby_platform;
|
|
73
73
|
/* Invalidates cache when switching ruby versions */
|
|
74
74
|
static uint32_t current_ruby_revision;
|
|
75
75
|
/* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */
|
|
@@ -92,10 +92,9 @@ static void bs_cache_path(const char * cachedir, const char * path, char ** cach
|
|
|
92
92
|
static int bs_read_key(int fd, struct bs_cache_key * key);
|
|
93
93
|
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
|
-
static int open_current_file(char * path, struct bs_cache_key * key);
|
|
96
|
-
static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag);
|
|
97
|
-
static
|
|
98
|
-
static uint32_t get_os_version(void);
|
|
95
|
+
static int open_current_file(char * path, struct bs_cache_key * key, char ** errno_provenance);
|
|
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 uint32_t get_ruby_platform(void);
|
|
99
98
|
|
|
100
99
|
/*
|
|
101
100
|
* Helper functions to call ruby methods on handler object without crashing on
|
|
@@ -136,7 +135,7 @@ Init_bootsnap(void)
|
|
|
136
135
|
rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError);
|
|
137
136
|
|
|
138
137
|
current_ruby_revision = FIX2INT(rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")));
|
|
139
|
-
|
|
138
|
+
current_ruby_platform = get_ruby_platform();
|
|
140
139
|
|
|
141
140
|
uncompilable = rb_intern("__bootsnap_uncompilable__");
|
|
142
141
|
|
|
@@ -173,10 +172,9 @@ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
|
|
|
173
172
|
* - 32 bits doesn't feel collision-resistant enough; 64 is nice.
|
|
174
173
|
*/
|
|
175
174
|
static uint64_t
|
|
176
|
-
|
|
175
|
+
fnv1a_64_iter(uint64_t h, const char *str)
|
|
177
176
|
{
|
|
178
177
|
unsigned char *s = (unsigned char *)str;
|
|
179
|
-
uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
|
|
180
178
|
|
|
181
179
|
while (*s) {
|
|
182
180
|
h ^= (uint64_t)*s++;
|
|
@@ -186,26 +184,42 @@ fnv1a_64(const char *str)
|
|
|
186
184
|
return h;
|
|
187
185
|
}
|
|
188
186
|
|
|
187
|
+
static uint64_t
|
|
188
|
+
fnv1a_64(const char *str)
|
|
189
|
+
{
|
|
190
|
+
uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
|
|
191
|
+
return fnv1a_64_iter(h, str);
|
|
192
|
+
}
|
|
193
|
+
|
|
189
194
|
/*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
195
|
+
* When ruby's version doesn't change, but it's recompiled on a different OS
|
|
196
|
+
* (or OS version), we need to invalidate the cache.
|
|
197
|
+
*
|
|
198
|
+
* We actually factor in some extra information here, to be extra confident
|
|
199
|
+
* that we don't try to re-use caches that will not be compatible, by factoring
|
|
200
|
+
* in utsname.version.
|
|
192
201
|
*/
|
|
193
202
|
static uint32_t
|
|
194
|
-
|
|
203
|
+
get_ruby_platform(void)
|
|
195
204
|
{
|
|
196
|
-
#ifdef _WIN32
|
|
197
|
-
return (uint32_t)GetVersion();
|
|
198
|
-
#else
|
|
199
205
|
uint64_t hash;
|
|
200
|
-
|
|
206
|
+
VALUE ruby_platform;
|
|
201
207
|
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
|
|
209
|
+
hash = fnv1a_64(RSTRING_PTR(ruby_platform));
|
|
204
210
|
|
|
205
|
-
|
|
211
|
+
#ifdef _WIN32
|
|
212
|
+
return (uint32_t)(hash >> 32) ^ (uint32_t)GetVersion();
|
|
213
|
+
#else
|
|
214
|
+
struct utsname utsname;
|
|
215
|
+
|
|
216
|
+
/* Not worth crashing if this fails; lose extra cache invalidation potential */
|
|
217
|
+
if (uname(&utsname) >= 0) {
|
|
218
|
+
hash = fnv1a_64_iter(hash, utsname.version);
|
|
219
|
+
}
|
|
206
220
|
|
|
207
221
|
return (uint32_t)(hash >> 32);
|
|
208
|
-
|
|
222
|
+
#endif
|
|
209
223
|
}
|
|
210
224
|
|
|
211
225
|
/*
|
|
@@ -239,7 +253,7 @@ cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
|
|
|
239
253
|
{
|
|
240
254
|
return (
|
|
241
255
|
k1->version == k2->version &&
|
|
242
|
-
k1->
|
|
256
|
+
k1->ruby_platform == k2->ruby_platform &&
|
|
243
257
|
k1->compile_option == k2->compile_option &&
|
|
244
258
|
k1->ruby_revision == k2->ruby_revision &&
|
|
245
259
|
k1->size == k2->size &&
|
|
@@ -279,24 +293,28 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
|
|
|
279
293
|
* was loaded.
|
|
280
294
|
*/
|
|
281
295
|
static int
|
|
282
|
-
open_current_file(char * path, struct bs_cache_key * key)
|
|
296
|
+
open_current_file(char * path, struct bs_cache_key * key, char ** errno_provenance)
|
|
283
297
|
{
|
|
284
298
|
struct stat statbuf;
|
|
285
299
|
int fd;
|
|
286
300
|
|
|
287
301
|
fd = open(path, O_RDONLY);
|
|
288
|
-
if (fd < 0)
|
|
302
|
+
if (fd < 0) {
|
|
303
|
+
*errno_provenance = (char *)"bs_fetch:open_current_file:open";
|
|
304
|
+
return fd;
|
|
305
|
+
}
|
|
289
306
|
#ifdef _WIN32
|
|
290
307
|
setmode(fd, O_BINARY);
|
|
291
308
|
#endif
|
|
292
309
|
|
|
293
310
|
if (fstat(fd, &statbuf) < 0) {
|
|
311
|
+
*errno_provenance = (char *)"bs_fetch:open_current_file:fstat";
|
|
294
312
|
close(fd);
|
|
295
313
|
return -1;
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
key->version = current_version;
|
|
299
|
-
key->
|
|
317
|
+
key->ruby_platform = current_ruby_platform;
|
|
300
318
|
key->compile_option = current_compile_option_crc32;
|
|
301
319
|
key->ruby_revision = current_ruby_revision;
|
|
302
320
|
key->size = (uint64_t)statbuf.st_size;
|
|
@@ -336,12 +354,13 @@ bs_read_key(int fd, struct bs_cache_key * key)
|
|
|
336
354
|
* - ERROR_WITH_ERRNO (-1, errno is set)
|
|
337
355
|
*/
|
|
338
356
|
static int
|
|
339
|
-
open_cache_file(const char * path, struct bs_cache_key * key)
|
|
357
|
+
open_cache_file(const char * path, struct bs_cache_key * key, char ** errno_provenance)
|
|
340
358
|
{
|
|
341
359
|
int fd, res;
|
|
342
360
|
|
|
343
361
|
fd = open(path, O_RDONLY);
|
|
344
362
|
if (fd < 0) {
|
|
363
|
+
*errno_provenance = (char *)"bs_fetch:open_cache_file:open";
|
|
345
364
|
if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
|
|
346
365
|
return ERROR_WITH_ERRNO;
|
|
347
366
|
}
|
|
@@ -351,6 +370,7 @@ open_cache_file(const char * path, struct bs_cache_key * key)
|
|
|
351
370
|
|
|
352
371
|
res = bs_read_key(fd, key);
|
|
353
372
|
if (res < 0) {
|
|
373
|
+
*errno_provenance = (char *)"bs_fetch:open_cache_file:read";
|
|
354
374
|
close(fd);
|
|
355
375
|
return res;
|
|
356
376
|
}
|
|
@@ -374,7 +394,7 @@ open_cache_file(const char * path, struct bs_cache_key * key)
|
|
|
374
394
|
* or exception, will be the final data returnable to the user.
|
|
375
395
|
*/
|
|
376
396
|
static int
|
|
377
|
-
fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag)
|
|
397
|
+
fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, char ** errno_provenance)
|
|
378
398
|
{
|
|
379
399
|
char * data = NULL;
|
|
380
400
|
ssize_t nread;
|
|
@@ -383,6 +403,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data,
|
|
|
383
403
|
VALUE storage_data;
|
|
384
404
|
|
|
385
405
|
if (data_size > 100000000000) {
|
|
406
|
+
*errno_provenance = (char *)"bs_fetch:fetch_cached_data:datasize";
|
|
386
407
|
errno = EINVAL; /* because wtf? */
|
|
387
408
|
ret = -1;
|
|
388
409
|
goto done;
|
|
@@ -390,6 +411,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data,
|
|
|
390
411
|
data = ALLOC_N(char, data_size);
|
|
391
412
|
nread = read(fd, data, data_size);
|
|
392
413
|
if (nread < 0) {
|
|
414
|
+
*errno_provenance = (char *)"bs_fetch:fetch_cached_data:read";
|
|
393
415
|
ret = -1;
|
|
394
416
|
goto done;
|
|
395
417
|
}
|
|
@@ -441,23 +463,29 @@ mkpath(char * file_path, mode_t mode)
|
|
|
441
463
|
* path.
|
|
442
464
|
*/
|
|
443
465
|
static int
|
|
444
|
-
atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
|
|
466
|
+
atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, char ** errno_provenance)
|
|
445
467
|
{
|
|
446
468
|
char template[MAX_CACHEPATH_SIZE + 20];
|
|
447
469
|
char * dest;
|
|
448
470
|
char * tmp_path;
|
|
449
|
-
int fd;
|
|
471
|
+
int fd, ret;
|
|
450
472
|
ssize_t nwrite;
|
|
451
473
|
|
|
452
474
|
dest = strncpy(template, path, MAX_CACHEPATH_SIZE);
|
|
453
475
|
strcat(dest, ".tmp.XXXXXX");
|
|
454
476
|
|
|
455
477
|
tmp_path = mktemp(template);
|
|
456
|
-
fd = open(tmp_path, O_WRONLY | O_CREAT,
|
|
478
|
+
fd = open(tmp_path, O_WRONLY | O_CREAT, 0664);
|
|
457
479
|
if (fd < 0) {
|
|
458
|
-
if (mkpath(path,
|
|
459
|
-
|
|
460
|
-
|
|
480
|
+
if (mkpath(path, 0775) < 0) {
|
|
481
|
+
*errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:mkpath";
|
|
482
|
+
return -1;
|
|
483
|
+
}
|
|
484
|
+
fd = open(tmp_path, O_WRONLY | O_CREAT, 0664);
|
|
485
|
+
if (fd < 0) {
|
|
486
|
+
*errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:open";
|
|
487
|
+
return -1;
|
|
488
|
+
}
|
|
461
489
|
}
|
|
462
490
|
#ifdef _WIN32
|
|
463
491
|
setmode(fd, O_BINARY);
|
|
@@ -465,8 +493,12 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
|
|
|
465
493
|
|
|
466
494
|
key->data_size = RSTRING_LEN(data);
|
|
467
495
|
nwrite = write(fd, key, KEY_SIZE);
|
|
468
|
-
if (nwrite < 0)
|
|
496
|
+
if (nwrite < 0) {
|
|
497
|
+
*errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:write";
|
|
498
|
+
return -1;
|
|
499
|
+
}
|
|
469
500
|
if (nwrite != KEY_SIZE) {
|
|
501
|
+
*errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:keysize";
|
|
470
502
|
errno = EIO; /* Lies but whatever */
|
|
471
503
|
return -1;
|
|
472
504
|
}
|
|
@@ -474,38 +506,32 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
|
|
|
474
506
|
nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
|
|
475
507
|
if (nwrite < 0) return -1;
|
|
476
508
|
if (nwrite != RSTRING_LEN(data)) {
|
|
509
|
+
*errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:writelength";
|
|
477
510
|
errno = EIO; /* Lies but whatever */
|
|
478
511
|
return -1;
|
|
479
512
|
}
|
|
480
513
|
|
|
481
514
|
close(fd);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
/*
|
|
486
|
-
* Given an errno value (converted to a ruby Fixnum), return the corresponding
|
|
487
|
-
* Errno::* constant. If none is found, return StandardError instead.
|
|
488
|
-
*/
|
|
489
|
-
static VALUE
|
|
490
|
-
prot_exception_for_errno(VALUE err)
|
|
491
|
-
{
|
|
492
|
-
if (err != INT2FIX(0)) {
|
|
493
|
-
VALUE mErrno = rb_const_get(rb_cObject, rb_intern("Errno"));
|
|
494
|
-
VALUE constants = rb_funcall(mErrno, rb_intern("constants"), 0);
|
|
495
|
-
VALUE which = rb_funcall(constants, rb_intern("[]"), 1, err);
|
|
496
|
-
return rb_funcall(mErrno, rb_intern("const_get"), 1, which);
|
|
515
|
+
ret = rename(tmp_path, path);
|
|
516
|
+
if (ret < 0) {
|
|
517
|
+
*errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:rename";
|
|
497
518
|
}
|
|
498
|
-
return
|
|
519
|
+
return ret;
|
|
499
520
|
}
|
|
500
521
|
|
|
501
522
|
|
|
502
523
|
/* Read contents from an fd, whose contents are asserted to be +size+ bytes
|
|
503
524
|
* long, into a buffer */
|
|
504
525
|
static ssize_t
|
|
505
|
-
bs_read_contents(int fd, size_t size, char ** contents)
|
|
526
|
+
bs_read_contents(int fd, size_t size, char ** contents, char ** errno_provenance)
|
|
506
527
|
{
|
|
528
|
+
ssize_t nread;
|
|
507
529
|
*contents = ALLOC_N(char, size);
|
|
508
|
-
|
|
530
|
+
nread = read(fd, *contents, size);
|
|
531
|
+
if (nread < 0) {
|
|
532
|
+
*errno_provenance = (char *)"bs_fetch:bs_read_contents:read";
|
|
533
|
+
}
|
|
534
|
+
return nread;
|
|
509
535
|
}
|
|
510
536
|
|
|
511
537
|
/*
|
|
@@ -513,24 +539,24 @@ bs_read_contents(int fd, size_t size, char ** contents)
|
|
|
513
539
|
* Bootsnap::CompileCache::Native.fetch.
|
|
514
540
|
*
|
|
515
541
|
* There are three "formats" in use here:
|
|
516
|
-
* 1. "input"
|
|
542
|
+
* 1. "input" format, which is what we load from the source file;
|
|
517
543
|
* 2. "storage" format, which we write to the cache;
|
|
518
544
|
* 3. "output" format, which is what we return.
|
|
519
545
|
*
|
|
520
546
|
* E.g., For ISeq compilation:
|
|
521
|
-
* input:
|
|
547
|
+
* input: ruby source, as text
|
|
522
548
|
* storage: binary string (RubyVM::InstructionSequence#to_binary)
|
|
523
|
-
* output:
|
|
549
|
+
* output: Instance of RubyVM::InstructionSequence
|
|
524
550
|
*
|
|
525
551
|
* And for YAML:
|
|
526
|
-
* input:
|
|
552
|
+
* input: yaml as text
|
|
527
553
|
* storage: MessagePack or Marshal text
|
|
528
|
-
* output:
|
|
554
|
+
* output: ruby object, loaded from yaml/messagepack/marshal
|
|
529
555
|
*
|
|
530
|
-
*
|
|
531
|
-
* * storage_to_output(
|
|
532
|
-
* * input_to_output(
|
|
533
|
-
* * input_to_storage(
|
|
556
|
+
* A handler<I,S,O> passed in must support three messages:
|
|
557
|
+
* * storage_to_output(S) -> O
|
|
558
|
+
* * input_to_output(I) -> O
|
|
559
|
+
* * input_to_storage(I) -> S
|
|
534
560
|
* (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which
|
|
535
561
|
* will prevent caching and cause output to be generated with
|
|
536
562
|
* input_to_output)
|
|
@@ -558,7 +584,8 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
|
558
584
|
struct bs_cache_key cached_key, current_key;
|
|
559
585
|
char * contents = NULL;
|
|
560
586
|
int cache_fd = -1, current_fd = -1;
|
|
561
|
-
int res, valid_cache, exception_tag = 0;
|
|
587
|
+
int res, valid_cache = 0, exception_tag = 0;
|
|
588
|
+
char * errno_provenance = NULL;
|
|
562
589
|
|
|
563
590
|
VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
|
|
564
591
|
VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
|
|
@@ -567,20 +594,27 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
|
567
594
|
VALUE exception; /* ruby exception object to raise instead of returning */
|
|
568
595
|
|
|
569
596
|
/* Open the source file and generate a cache key for it */
|
|
570
|
-
current_fd = open_current_file(path, ¤t_key);
|
|
597
|
+
current_fd = open_current_file(path, ¤t_key, &errno_provenance);
|
|
571
598
|
if (current_fd < 0) goto fail_errno;
|
|
572
599
|
|
|
573
600
|
/* Open the cache key if it exists, and read its cache key in */
|
|
574
|
-
cache_fd = open_cache_file(cache_path, &cached_key);
|
|
575
|
-
if (cache_fd
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
601
|
+
cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
|
|
602
|
+
if (cache_fd == CACHE_MISSING_OR_INVALID) {
|
|
603
|
+
/* This is ok: valid_cache remains false, we re-populate it. */
|
|
604
|
+
} else if (cache_fd < 0) {
|
|
605
|
+
goto fail_errno;
|
|
606
|
+
} else {
|
|
607
|
+
/* True if the cache existed and no invalidating changes have occurred since
|
|
608
|
+
* it was generated. */
|
|
609
|
+
valid_cache = cache_key_equal(¤t_key, &cached_key);
|
|
610
|
+
}
|
|
580
611
|
|
|
581
612
|
if (valid_cache) {
|
|
582
613
|
/* Fetch the cache data and return it if we're able to load it successfully */
|
|
583
|
-
res = fetch_cached_data(
|
|
614
|
+
res = fetch_cached_data(
|
|
615
|
+
cache_fd, (ssize_t)cached_key.data_size, handler,
|
|
616
|
+
&output_data, &exception_tag, &errno_provenance
|
|
617
|
+
);
|
|
584
618
|
if (exception_tag != 0) goto raise;
|
|
585
619
|
else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
|
|
586
620
|
else if (res == ERROR_WITH_ERRNO) goto fail_errno;
|
|
@@ -591,7 +625,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
|
591
625
|
/* Cache is stale, invalid, or missing. Regenerate and write it out. */
|
|
592
626
|
|
|
593
627
|
/* Read the contents of the source file into a buffer */
|
|
594
|
-
if (bs_read_contents(current_fd, current_key.size, &contents) < 0) goto fail_errno;
|
|
628
|
+
if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno;
|
|
595
629
|
input_data = rb_str_new_static(contents, current_key.size);
|
|
596
630
|
|
|
597
631
|
/* Try to compile the input_data using input_to_storage(input_data) */
|
|
@@ -608,7 +642,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
|
608
642
|
if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
|
|
609
643
|
|
|
610
644
|
/* Write the cache key and storage_data to the cache directory */
|
|
611
|
-
res = atomic_write_cache_file(cache_path, ¤t_key, storage_data);
|
|
645
|
+
res = atomic_write_cache_file(cache_path, ¤t_key, storage_data, &errno_provenance);
|
|
612
646
|
if (res < 0) goto fail_errno;
|
|
613
647
|
|
|
614
648
|
/* Having written the cache, now convert storage_data to output_data */
|
|
@@ -618,7 +652,10 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
|
|
|
618
652
|
/* If output_data is nil, delete the cache entry and generate the output
|
|
619
653
|
* using input_to_output */
|
|
620
654
|
if (NIL_P(output_data)) {
|
|
621
|
-
if (unlink(cache_path) < 0)
|
|
655
|
+
if (unlink(cache_path) < 0) {
|
|
656
|
+
errno_provenance = (char *)"bs_fetch:unlink";
|
|
657
|
+
goto fail_errno;
|
|
658
|
+
}
|
|
622
659
|
bs_input_to_output(handler, input_data, &output_data, &exception_tag);
|
|
623
660
|
if (exception_tag != 0) goto raise;
|
|
624
661
|
}
|
|
@@ -635,8 +672,7 @@ succeed:
|
|
|
635
672
|
return output_data;
|
|
636
673
|
fail_errno:
|
|
637
674
|
CLEANUP;
|
|
638
|
-
exception =
|
|
639
|
-
if (res) exception = rb_eStandardError;
|
|
675
|
+
exception = rb_syserr_new(errno, errno_provenance);
|
|
640
676
|
rb_exc_raise(exception);
|
|
641
677
|
__builtin_unreachable();
|
|
642
678
|
raise:
|
|
@@ -655,7 +691,17 @@ invalid_type_storage_data:
|
|
|
655
691
|
/********************* Handler Wrappers **************************************/
|
|
656
692
|
/*****************************************************************************
|
|
657
693
|
* Everything after this point in the file is just wrappers to deal with ruby's
|
|
658
|
-
* clunky method of handling exceptions from ruby methods invoked from C
|
|
694
|
+
* clunky method of handling exceptions from ruby methods invoked from C:
|
|
695
|
+
*
|
|
696
|
+
* In order to call a ruby method from C, while protecting against crashing in
|
|
697
|
+
* the event of an exception, we must call the method with rb_protect().
|
|
698
|
+
*
|
|
699
|
+
* rb_protect takes a C function and precisely one argument; however, we want
|
|
700
|
+
* to pass multiple arguments, so we must create structs to wrap them up.
|
|
701
|
+
*
|
|
702
|
+
* These functions return an exception_tag, which, if non-zero, indicates an
|
|
703
|
+
* exception that should be jumped to with rb_jump_tag after cleaning up
|
|
704
|
+
* allocated resources.
|
|
659
705
|
*/
|
|
660
706
|
|
|
661
707
|
struct s2o_data {
|
|
@@ -45,6 +45,10 @@ module Bootsnap
|
|
|
45
45
|
# NoMethodError is a NameError, but we only want to handle actual
|
|
46
46
|
# NameError instances.
|
|
47
47
|
raise unless e.class == NameError
|
|
48
|
+
# We can only confidently handle cases when *this* constant fails
|
|
49
|
+
# to load, not other constants referred to by it.
|
|
50
|
+
raise unless e.name == const_name
|
|
51
|
+
# If the constant was actually loaded, something else went wrong?
|
|
48
52
|
raise if from_mod.const_defined?(const_name)
|
|
49
53
|
CoreExt::ActiveSupport.without_bootsnap_cache { super }
|
|
50
54
|
end
|
|
@@ -11,78 +11,104 @@ module Bootsnap
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
module Kernel
|
|
14
|
-
alias_method :
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
require_with_bootsnap_lfi(path)
|
|
25
35
|
end
|
|
26
36
|
|
|
27
|
-
alias_method :
|
|
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
|
-
|
|
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
|
-
|
|
52
|
+
load_without_bootsnap(path, wrap)
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
class << Kernel
|
|
47
|
-
alias_method :
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
require_with_bootsnap_lfi(path)
|
|
58
77
|
end
|
|
59
78
|
|
|
60
|
-
alias_method :
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
load_without_bootsnap(path, wrap)
|
|
76
95
|
end
|
|
77
96
|
end
|
|
78
97
|
|
|
79
98
|
class Module
|
|
80
|
-
alias_method :
|
|
99
|
+
alias_method :autoload_without_bootsnap, :autoload
|
|
81
100
|
def autoload(const, path)
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
@@ -99,7 +99,7 @@ module Bootsnap
|
|
|
99
99
|
@stability ||= begin
|
|
100
100
|
if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
|
|
101
101
|
STABLE
|
|
102
|
-
elsif expanded_path.start_with?(Bundler.bundle_path.to_s)
|
|
102
|
+
elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
|
|
103
103
|
STABLE
|
|
104
104
|
elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
|
|
105
105
|
STABLE
|
|
@@ -15,7 +15,8 @@ module Bootsnap
|
|
|
15
15
|
REQUIRABLES_AND_DIRS = "/{,*/**/}*{#{DOT_RB},#{DL_EXTENSIONS.join(',')},/}"
|
|
16
16
|
NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
|
|
17
17
|
ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
|
|
18
|
-
BUNDLE_PATH =
|
|
18
|
+
BUNDLE_PATH = Bootsnap.bundler? ?
|
|
19
|
+
(Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze : ''.freeze
|
|
19
20
|
|
|
20
21
|
def self.call(path)
|
|
21
22
|
path = path.to_s
|
|
@@ -69,10 +69,15 @@ module Bootsnap
|
|
|
69
69
|
def dump_data
|
|
70
70
|
# Change contents atomically so other processes can't get invalid
|
|
71
71
|
# caches if they read at an inopportune time.
|
|
72
|
-
tmp = "#{@store_path}.#{(rand * 100000).to_i}.tmp"
|
|
72
|
+
tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
|
|
73
73
|
FileUtils.mkpath(File.dirname(tmp))
|
|
74
|
-
File
|
|
74
|
+
exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
|
|
75
|
+
# `encoding:` looks redundant wrt `binwrite`, but necessary on windows
|
|
76
|
+
# because binary is part of mode.
|
|
77
|
+
File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY)
|
|
75
78
|
FileUtils.mv(tmp, @store_path)
|
|
79
|
+
rescue Errno::EEXIST
|
|
80
|
+
retry
|
|
76
81
|
end
|
|
77
82
|
end
|
|
78
83
|
end
|
|
@@ -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'
|
data/lib/bootsnap/setup.rb
CHANGED
|
@@ -4,16 +4,10 @@ env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV']
|
|
|
4
4
|
development_mode = ['', nil, 'development'].include?(env)
|
|
5
5
|
|
|
6
6
|
# only enable on 'ruby' (MRI), POSIX (darin, linux, *bsd), and >= 2.3.0
|
|
7
|
-
enable_cc =
|
|
8
|
-
RUBY_ENGINE == 'ruby' &&
|
|
9
|
-
RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
|
|
10
|
-
RUBY_VERSION
|
|
11
|
-
.split('.') # ["1", "9", "3"]
|
|
12
|
-
.map(&:to_i) # [1, 9, 3]
|
|
13
|
-
.zip([2, 3, -1]) # [[1, 2], [9, 3], [3, -1]]
|
|
14
|
-
.map { |a, b| a <=> b } # [-1, 1, 1]
|
|
15
|
-
.detect { |e| !e.zero? } # -1
|
|
16
|
-
.==(1) # false
|
|
7
|
+
enable_cc =
|
|
8
|
+
RUBY_ENGINE == 'ruby' &&
|
|
9
|
+
RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
|
|
10
|
+
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
|
|
17
11
|
|
|
18
12
|
cache_dir = ENV['BOOTSNAP_CACHE_DIR']
|
|
19
13
|
unless cache_dir
|
data/lib/bootsnap/version.rb
CHANGED
data/lib/bootsnap.rb
CHANGED
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.2.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:
|
|
11
|
+
date: 2018-03-07 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
|
|
@@ -121,6 +122,7 @@ files:
|
|
|
121
122
|
- ext/bootsnap/bootsnap.h
|
|
122
123
|
- ext/bootsnap/extconf.rb
|
|
123
124
|
- lib/bootsnap.rb
|
|
125
|
+
- lib/bootsnap/bundler.rb
|
|
124
126
|
- lib/bootsnap/compile_cache.rb
|
|
125
127
|
- lib/bootsnap/compile_cache/iseq.rb
|
|
126
128
|
- lib/bootsnap/compile_cache/yaml.rb
|
|
@@ -130,6 +132,7 @@ files:
|
|
|
130
132
|
- lib/bootsnap/load_path_cache/change_observer.rb
|
|
131
133
|
- lib/bootsnap/load_path_cache/core_ext/active_support.rb
|
|
132
134
|
- lib/bootsnap/load_path_cache/core_ext/kernel_require.rb
|
|
135
|
+
- lib/bootsnap/load_path_cache/loaded_features_index.rb
|
|
133
136
|
- lib/bootsnap/load_path_cache/path.rb
|
|
134
137
|
- lib/bootsnap/load_path_cache/path_scanner.rb
|
|
135
138
|
- lib/bootsnap/load_path_cache/store.rb
|
|
@@ -155,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
155
158
|
version: '0'
|
|
156
159
|
requirements: []
|
|
157
160
|
rubyforge_project:
|
|
158
|
-
rubygems_version: 2.6
|
|
161
|
+
rubygems_version: 2.7.6
|
|
159
162
|
signing_key:
|
|
160
163
|
specification_version: 4
|
|
161
164
|
summary: Boot large ruby/rails apps faster
|