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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c3e149a8eb8efa3469c157ec2858ccfbe1613a84
4
- data.tar.gz: eb7b1180fc432ffd80b88849b7ffe7df50fd409e
2
+ SHA256:
3
+ metadata.gz: '059b193c29635dc303b7f72742b404312b172bba723b6396df1d6c83ce7b2155'
4
+ data.tar.gz: 94a7b6ada9cb48574c0820ffe8967f435ee7e7881d71ede14cec5b3d774cfa7f
5
5
  SHA512:
6
- metadata.gz: dc8848fdac6603869affe7f7329bdf3bf691233fd4031d0d4481cee6190623b4d388a6d868bca7d7f49ff9da6bd622f8b98a9056314a344ee980103d2ae2d2dc
7
- data.tar.gz: 91326a624801bd3c24053bd789a1157d497c35e44a7c5993f06c11f51eadebf3fed8305195e57e3d8e907415875763c592e6772842692c6155ff0f60d4f6dbf0
6
+ metadata.gz: b84ab3c65e266ed2235cc00fc65f2d10c5474d43ebe7813dbf29247a1ae9b04bfd37273f241911a69090233837d2be06bfb434466fced104f5e7511a360b2ce9
7
+ data.tar.gz: 3e582933e441fa2458ff9e7099307345b6aa9f3de5ddbe1af8d2a54adf2508d06e6e8586a311c89bd899909a4cd0aeeb48ecc25e01539f567bece1bf38e0fc88
data/.travis.yml CHANGED
@@ -1,5 +1,8 @@
1
- os: osx
2
1
  language: ruby
3
- rvm: ruby-2.4.0
2
+
3
+ rvm:
4
+ - ruby-2.4
5
+ - ruby-2.5
6
+
4
7
  before_script: rake
5
- script: bin/testunit
8
+ script: bundle exec bin/testunit
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 [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](https://travis-ci.org/Shopify/bootsnap)
2
+
3
+ Bootsnap は RubyVM におけるバイトコード生成やファイルルックアップ等の時間のかかる処理を最適化するためのライブラリです。ActiveSupport や YAML もサポートしています。
4
+
5
+ 注意書き: このライブラリは英語話者によって管理されています。この README は日本語ですが、日本語でのサポートはしておらず、リクエストにお答えすることもできません。バイリンガルの方がサポートをサポートしてくださる場合はお知らせください!:)
6
+
7
+ ## パフォーマンス
8
+
9
+ Discourse では、約6秒から3秒まで、約50%の起動時間短縮が確認されています。小さなアプリケーションでも、50%の改善(3.6秒から1.8秒)が確認されています。非常に巨大でモノリシックなアプリである Shopify のプラットフォームでは、約25秒から6.5秒へと約75%短縮されました。
10
+
11
+ ## 使用方法
12
+
13
+ この gem は MacOS と Linux で作動します。まずは、`bootsnap` を `Gemfile` に追加します:
14
+
15
+ ```ruby
16
+ gem 'bootsnap', require: false
17
+ ```
18
+
19
+ Rails を使用している場合は、以下のコードを、`config/boot.rb` 内にある `require 'bundler/setup'` の後に追加してください。
20
+
21
+ ```ruby
22
+ require 'bootsnap/setup'
23
+ ```
24
+
25
+ Rails を使用していない場合、または、より多くの設定を変更したい場合は、以下のコードを `require 'bundler/setup'` の直後に追加してください(早く読み込まれるほど、より多くのものを最適化することができます)。
26
+
27
+ ```ruby
28
+ require 'bootsnap'
29
+ env = ENV['RAILS_ENV'] || "development"
30
+ Bootsnap.setup(
31
+  cache_dir:           'tmp/cache',         # キャッシュファイルを保存する path
32
+  development_mode:     env == 'development', # 現在の作業環境、例えば RACK_ENV, RAILS_ENV など。
33
+ load_path_cache: true, # キャッシュで LOAD_PATH を最適化する。
34
+  autoload_paths_cache: true,                 # キャッシュで ActiveSupport による autoload を行う。
35
+  disable_trace:       true,                 # (アルファ) `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`をセットする。
36
+  compile_cache_iseq:   true,                 # ISeq キャッシュをコンパイルする
37
+  compile_cache_yaml:   true                 # YAML キャッシュをコンパイルする
38
+ )
39
+ ```
40
+
41
+ ヒント: `require 'bootsnap'` を `BootLib::Require.from_gem('bootsnap', 'bootsnap')` で、 [こちらのトリック](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require)を使って置き換えることができます。こうすると、巨大な`$LOAD_PATH`がある場合でも、起動時間を最短化するのに役立ちます。
42
+
43
+ ## Bootsnap の内部動作
44
+
45
+ Bootsnap は、処理に時間のかかるメソッドの結果をキャッシュすることで最適化しています。これには、大きく分けて2つのカテゴリに分けられます。
46
+
47
+ * [Path Pre-Scanning](#path-pre-scanning)
48
+ * `Kernel#require` と `Kernel#load` を `$LOAD_PATH` のフルスキャンを行わないようにオーバーライドします。
49
+   * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` を `ActiveSupport::Dependencies.autoload_paths` のフルスキャンを行わないようにオーバーライドします。
50
+ * [Compilation caching](#compilation-caching)
51
+  * Ruby バイトコードのコンパイル結果をキャッシュするためのメソッド `RubyVM::InstructionSequence.load_iseq` が実装されています。
52
+   * `YAML.load_file` を YAML オブジェクトのロード結果を MessagePack でキャッシュするようにオーバーライドします。 MessagePack でサポートされていないタイプが使われている場合は Marshal が使われます。
53
+
54
+ ### Path Pre-Scanning
55
+
56
+ _(このライブラリは [bootscale](https://github.com/byroot/bootscale) という別のライブラリを元に開発されました)_
57
+
58
+ Bootsnap の始動時、あるいはパス(例えば、`$LOAD_PATH`)の変更時に、`Bootsnap::LoadPathCache` がキャッシュから必要なエントリーのリストを読み込みます。または、必要に応じてフルスキャンを実行し結果をキャッシュします。
59
+ その後、たとえば `require 'foo'` を評価する場合, Ruby は `$LOAD_PATH` `['x', 'y', ...]` のすべてのエントリーを繰り返し評価することで `x/foo.rb`, `y/foo.rb` などを探索します。これに対して Bootsnap は、キャッシュされた reuiqre 可能なファイルと `$LOAD_PATH` を見ることで、Rubyが最終的に選択するであろうパスで置き換えます。
60
+
61
+ この動作によって生成された syscall を見ると、最終的な結果は以前なら次のようになります。
62
+
63
+ ```
64
+ open x/foo.rb # (fail)
65
+ # (imagine this with 500 $LOAD_PATH entries instead of two)
66
+ open y/foo.rb # (success)
67
+ close y/foo.rb
68
+ open y/foo.rb
69
+ ...
70
+ ```
71
+
72
+ これが、次のようになります:
73
+
74
+ ```
75
+ open y/foo.rb
76
+ ...
77
+ ```
78
+
79
+ `autoload_paths_cache` オプションが `Bootsnap.setup` に与えられている場合、`ActiveSupport::Dependencies.autoload_paths` をトラバースするメソッドにはまったく同じ最適化が使用されます。
80
+
81
+ `*_path_cache` 機能を埋め込むオーバーライドを図にすると、次のようになります。
82
+
83
+ ![Bootsnapの説明図](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png)
84
+
85
+ Bootsnap は、 `$LOAD_PATH` エントリを安定エントリと不安定エントリの2つのカテゴリに分類します。不安定エントリはアプリケーションが起動するたびにスキャンされ、そのキャッシュは30秒間だけ有効になります。安定エントリーに期限切れはありません。コンテンツがスキャンされると、決して変更されないものとみなされます。
86
+
87
+ 安定していると考えられる唯一のディレクトリは、Rubyのインストールプレフィックス (`RbConfig::CONFIG['prefix']`, または `/usr/local/ruby` や `~/.rubies/x.y.z`)下にあるものと、`Gem.path` (たとえば `~/.gem/ruby/x.y.z`) や `Bundler.bundle_path` 下にあるものです。他のすべては不安定エントリと分類されます。
88
+
89
+ [`Bootsnap::LoadPathCache::Cache`](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb) に加えて次の図では、エントリの解決がどのように機能するかを理解するのに役立つかもしれません。経路探索は以下のようになります。
90
+
91
+ ![パス探索の仕組み](https://cloud.githubusercontent.com/assets/3074765/25388270/670b5652-299b-11e7-87fb-975647f68981.png)
92
+
93
+ また、`LoadError` のスキャンがどれほど重いかに注意を払うことも大切です。もし Ruby が `require 'something'` を評価し、そのファイルが `$LOAD_PATH` にない場合は、それを知るために `2 * $LOAD_PATH.length` のファイルシステムアスセスが必要になります。Bootsnap は、ファイルシステムにまったく触れずに `LoadError` を投げ、この結果をキャッシュします。
94
+
95
+ ## Compilation Caching
96
+
97
+ このコンセプトのより分かりやすい解説は [yomikomu](https://github.com/ko1/yomikomu) をお読み下さい。
98
+
99
+ Ruby には複雑な文法が実装されており、構文解析は簡単なオペレーションではありません。1.9以降、Ruby は Ruby ソースを内部のバイトコードに変換した後、Ruby VM によって実行してきました。2.3.0 以降、[RubyはAPIを公開し](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html)、そのバイトコードをキャッシュすることができるようになりました。これにより、同じファイルが複数ロードされた時の、比較的時間のかかる部分をバイパスすることができます。
100
+
101
+ また、Shopify のアプリケーションでは、アプリケーションの起動時に YAML ドキュメントの読み込みに多くの時間を費やしていることを発見しました。そして、 MessagePack と Marshal は deserialization にあたって YAML よりもはるかに高速であるということに気付きました。そこで、YAML ドキュメントを、Ruby バイトコードと同じコンパイルキャッシングの最適化を施すことで、高速化しています。Ruby の "バイトコード" フォーマットに相当するものは MessagePack ドキュメント (あるいは、MessagePack をサポートしていないタイプの YAML ドキュメントの場合は、Marshal stream)になります。
102
+
103
+ これらのコンパイル結果は、入力ファイル(FNV1a-64)のフルパスのハッシュを取って生成されたファイル名で、キャッシュディレクトリに保存されます。
104
+
105
+ Bootsnap 無しでは、ファイルを `require` するために生成された syscall の順序は次のようになっていました:
106
+
107
+ ```
108
+ open /c/foo.rb -> m
109
+ fstat64 m
110
+ close m
111
+ open /c/foo.rb -> o
112
+ fstat64 o
113
+ fstat64 o
114
+ read o
115
+ read o
116
+ ...
117
+ close o
118
+ ```
119
+
120
+ しかし Bootsnap では、次のようになります:
121
+
122
+ ```
123
+ open /c/foo.rb -> n
124
+ fstat64 n
125
+ close n
126
+ open /c/foo.rb -> n
127
+ fstat64 n
128
+ open (cache) -> m
129
+ read m
130
+ read m
131
+ close m
132
+ close n
133
+ ```
134
+
135
+ これは一目見るだけでは劣化していると思われるかもしれませんが、性能に大きな違いがあります。
136
+
137
+ (両方のリストの最初の3つの syscalls -- `open`, `fstat64`, `close` -- は本質的に有用ではありません。[このRubyパッチ](https://bugs.ruby-lang.org/issues/13378)は、Boosnap と組み合わせることによって、それらを最適化しています)
138
+
139
+ Bootsnap は、64バイトのヘッダーとそれに続くキャッシュの内容を含んだキャッシュファイルを書き込みます。ヘッダーは、次のいくつかのフィールドで構成されるキャッシュキーです。
140
+
141
+ - `version`、Bootsnapにハードコードされる基本的なスキーマのバージョン
142
+ - `os_version`、(macOS, BSDの) 現在のカーネルバージョンか 、(Linuxの) glibc のバージョンのハッシュ
143
+ - `compile_option`、`RubyVM::InstructionSequence.compile_option` の返り値
144
+ - `ruby_revision`、コンパイルされたRubyのバージョン
145
+ - `size`、ソースファイルのサイズ
146
+ - `mtime`、コンパイル時のソースファイルの最終変更タイムスタンプ
147
+ - `data_size`、バッファに読み込む必要のあるヘッダーに続くバイト数。
148
+
149
+ キーが有効な場合、キャッシュがファイルからロードされます。そうでない場合、キャッシュは再生成され、現在のキャッシュを破棄します。
150
+
151
+ # 最終的なキャッシュ結果
152
+
153
+ 次のファイル構造があるとします。
154
+
155
+ ```
156
+ /
157
+ ├── a
158
+ ├── b
159
+ └── c
160
+ └── foo.rb
161
+ ```
162
+
163
+ そして、このような `$LOAD_PATH` があるとします。
164
+
165
+ ```
166
+ ["/a", "/b", "/c"]
167
+ ```
168
+
169
+ Bootsnap なしで `require 'foo'` を呼び出すと、Ruby は次の順序で syscalls を生成します:
170
+
171
+ ```
172
+ open /a/foo.rb -> -1
173
+ open /b/foo.rb -> -1
174
+ open /c/foo.rb -> n
175
+ close n
176
+ open /c/foo.rb -> m
177
+ fstat64 m
178
+ close m
179
+ open /c/foo.rb -> o
180
+ fstat64 o
181
+ fstat64 o
182
+ read o
183
+ read o
184
+ ...
185
+ close o
186
+ ```
187
+
188
+ しかし Bootsnap では、次のようになります:
189
+
190
+ ```
191
+ open /c/foo.rb -> n
192
+ fstat64 n
193
+ close n
194
+ open /c/foo.rb -> n
195
+ fstat64 n
196
+ open (cache) -> m
197
+ read m
198
+ read m
199
+ close m
200
+ close n
201
+ ```
202
+
203
+ Bootsnap なしで `require 'nope'` を呼び出すと、次のようになります:
204
+
205
+ ```
206
+ open /a/nope.rb -> -1
207
+ open /b/nope.rb -> -1
208
+ open /c/nope.rb -> -1
209
+ open /a/nope.bundle -> -1
210
+ open /b/nope.bundle -> -1
211
+ open /c/nope.bundle -> -1
212
+ ```
213
+
214
+ ...そして、Bootsnap で `require 'nope'` を呼び出すと、次のようになります...
215
+
216
+ ```
217
+ # (nothing!)
218
+ ```
data/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # Bootsnap [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](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 rails, add this to `config/boot.rb` immediately after `require 'bundler/setup'`:
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
- If you are not using rails, or if you are but want more control over things, add this to your
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
@@ -5,5 +5,6 @@ up:
5
5
  - ruby: 2.3.3
6
6
  - bundler
7
7
  commands:
8
+ build: rake compile
8
9
  test: 'rake compile && exec bin/testunit'
9
10
  style: 'exec rubocop -D'
@@ -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, os_version, compile_option, and ruby_revision members track
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 os_version;
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
- /* Derived from kernel or libc version; intended to roughly correspond to when
71
- * ABIs have changed, requiring recompilation of native gems. */
72
- static uint32_t current_os_version;
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 VALUE prot_exception_for_errno(VALUE err);
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
- current_os_version = get_os_version();
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
- fnv1a_64(const char *str)
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
- * The idea here is that we want a cache key member that changes when the OS
191
- * changes in such a way as to make existing compiled ISeqs unloadable.
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
- get_os_version(void)
203
+ get_ruby_platform(void)
195
204
  {
196
- #ifdef _WIN32
197
- return (uint32_t)GetVersion();
198
- #else
199
205
  uint64_t hash;
200
- struct utsname utsname;
206
+ VALUE ruby_platform;
201
207
 
202
- /* Not worth crashing if this fails; lose cache invalidation potential */
203
- if (uname(&utsname) < 0) return 0;
208
+ ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
209
+ hash = fnv1a_64(RSTRING_PTR(ruby_platform));
204
210
 
205
- hash = fnv1a_64(utsname.version);
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
- #endif
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->os_version == k2->os_version &&
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) return fd;
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->os_version = current_os_version;
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, 0644);
478
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0664);
457
479
  if (fd < 0) {
458
- if (mkpath(path, 0755) < 0) return -1;
459
- fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
460
- if (fd < 0) return -1;
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) return -1;
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
- return rename(tmp_path, path);
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 rb_eStandardError;
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
- return read(fd, *contents, size);
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" fomat, which is what we load from the source file;
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: ruby source, as text
547
+ * input: ruby source, as text
522
548
  * storage: binary string (RubyVM::InstructionSequence#to_binary)
523
- * output: Instance of RubyVM::InstructionSequence
549
+ * output: Instance of RubyVM::InstructionSequence
524
550
  *
525
551
  * And for YAML:
526
- * input: yaml as text
552
+ * input: yaml as text
527
553
  * storage: MessagePack or Marshal text
528
- * output: ruby object, loaded from yaml/messagepack/marshal
554
+ * output: ruby object, loaded from yaml/messagepack/marshal
529
555
  *
530
- * The handler passed in must support three messages:
531
- * * storage_to_output(s) -> o
532
- * * input_to_output(i) -> o
533
- * * input_to_storage(i) -> s
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, &current_key);
597
+ current_fd = open_current_file(path, &current_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 < 0 && cache_fd != CACHE_MISSING_OR_INVALID) goto fail_errno;
576
-
577
- /* True if the cache existed and no invalidating changes have occurred since
578
- * it was generated. */
579
- valid_cache = cache_key_equal(&current_key, &cached_key);
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(&current_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(cache_fd, (ssize_t)cached_key.data_size, handler, &output_data, &exception_tag);
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, &current_key, storage_data);
645
+ res = atomic_write_cache_file(cache_path, &current_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) goto fail_errno;
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 = rb_protect(prot_exception_for_errno, INT2FIX(errno), &res);
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 {
@@ -0,0 +1,12 @@
1
+ module Bootsnap
2
+ module_function
3
+
4
+ def bundler?
5
+ # Bundler environment variable
6
+ ['BUNDLE_BIN_PATH', 'BUNDLE_GEMFILE'].each do |current|
7
+ return true if ENV.key?(current)
8
+ end
9
+
10
+ false
11
+ end
12
+ end
@@ -8,6 +8,7 @@ module Bootsnap
8
8
  end
9
9
 
10
10
  def self.input_to_storage(contents, _)
11
+ raise Uncompilable if contents.index("!ruby/object")
11
12
  obj = ::YAML.load(contents)
12
13
  msgpack_factory.packer.write(obj).to_s
13
14
  rescue NoMethodError, RangeError
@@ -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 :require_without_cache, :require
14
+ alias_method :require_without_bootsnap, :require
15
+
16
+ # Note that require registers to $LOADED_FEATURES while load does not.
17
+ def require_with_bootsnap_lfi(path, resolved = nil)
18
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
19
+ require_without_bootsnap(resolved || path)
20
+ end
21
+ end
22
+
15
23
  def require(path)
24
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
25
+
16
26
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
17
- require_without_cache(resolved)
18
- else
19
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
27
+ return require_with_bootsnap_lfi(path, resolved)
20
28
  end
29
+
30
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
21
31
  rescue Bootsnap::LoadPathCache::ReturnFalse
22
32
  return false
23
33
  rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
34
+ require_with_bootsnap_lfi(path)
25
35
  end
26
36
 
27
- alias_method :load_without_cache, :load
37
+ alias_method :load_without_bootsnap, :load
28
38
  def load(path, wrap = false)
29
39
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
30
- load_without_cache(resolved, wrap)
31
- else
32
- # load also allows relative paths from pwd even when not in $:
33
- relative = File.expand_path(path)
34
- if File.exist?(File.expand_path(path))
35
- return load_without_cache(relative, wrap)
36
- end
37
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
40
+ return load_without_bootsnap(resolved, wrap)
38
41
  end
42
+
43
+ # load also allows relative paths from pwd even when not in $:
44
+ if File.exist?(relative = File.expand_path(path))
45
+ return load_without_bootsnap(relative, wrap)
46
+ end
47
+
48
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
39
49
  rescue Bootsnap::LoadPathCache::ReturnFalse
40
50
  return false
41
51
  rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
52
+ load_without_bootsnap(path, wrap)
43
53
  end
44
54
  end
45
55
 
46
56
  class << Kernel
47
- alias_method :require_without_cache, :require
57
+ alias_method :require_without_bootsnap, :require
58
+
59
+ def require_with_bootsnap_lfi(path, resolved = nil)
60
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
61
+ require_without_bootsnap(resolved || path)
62
+ end
63
+ end
64
+
48
65
  def require(path)
66
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
67
+
49
68
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
50
- require_without_cache(resolved)
51
- else
52
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
69
+ return require_with_bootsnap_lfi(path, resolved)
53
70
  end
71
+
72
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
54
73
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
74
  return false
56
75
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
76
+ require_with_bootsnap_lfi(path)
58
77
  end
59
78
 
60
- alias_method :load_without_cache, :load
79
+ alias_method :load_without_bootsnap, :load
61
80
  def load(path, wrap = false)
62
81
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
63
- load_without_cache(resolved, wrap)
64
- else
65
- # load also allows relative paths from pwd even when not in $:
66
- relative = File.expand_path(path)
67
- if File.exist?(relative)
68
- return load_without_cache(relative, wrap)
69
- end
70
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
82
+ return load_without_bootsnap(resolved, wrap)
71
83
  end
84
+
85
+ # load also allows relative paths from pwd even when not in $:
86
+ if File.exist?(relative = File.expand_path(path))
87
+ return load_without_bootsnap(relative, wrap)
88
+ end
89
+
90
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
72
91
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
92
  return false
74
93
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
94
+ load_without_bootsnap(path, wrap)
76
95
  end
77
96
  end
78
97
 
79
98
  class Module
80
- alias_method :autoload_without_cache, :autoload
99
+ alias_method :autoload_without_bootsnap, :autoload
81
100
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
101
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
102
+ # obvious how to make it work. This feels like a pretty niche case, unclear
103
+ # if it will ever burn anyone.
104
+ #
105
+ # The challenge is that we don't control the point at which the entry gets
106
+ # added to $LOADED_FEATURES and won't be able to hook that modification
107
+ # since it's done in C-land.
108
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
109
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
110
  return false
85
111
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
112
+ autoload_without_bootsnap(const, path)
87
113
  end
88
114
  end
@@ -0,0 +1,95 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
4
+ # we can't easily obtain an interface to.
5
+ #
6
+ # This works around an issue where, without bootsnap, *ruby* knows that it
7
+ # has already required a file by its short name (e.g. require 'bundler') if
8
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
9
+ # different absolute path. This class makes bootsnap smart enough to
10
+ # realize that it has already loaded 'bundler', and not just
11
+ # '/path/to/bundler'.
12
+ #
13
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
14
+ #
15
+ # 1. `require 'a'`
16
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
17
+ # 3. `require 'a'`
18
+ #
19
+ # Ruby returns false from step 3.
20
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
21
+ # `a.rb`s.
22
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
23
+ # returning false like ruby.
24
+ class LoadedFeaturesIndex
25
+ def initialize
26
+ @lfi = {}
27
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
28
+
29
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
30
+ # cache. If this ever comes up in practice — or if you, the
31
+ # enterprising reader, feels inclined to solve this problem — we could
32
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
33
+ # updates to our @lfi.
34
+ $LOADED_FEATURES.each do |feat|
35
+ $LOAD_PATH.each do |lpe|
36
+ next unless feat.start_with?(lpe)
37
+ # /a/b/lib/my/foo.rb
38
+ # ^^^^^^^^^
39
+ short = feat[(lpe.length + 1)..-1]
40
+ @lfi[short] = true
41
+ @lfi[strip_extension(short)] = true
42
+ end
43
+ end
44
+ end
45
+
46
+ def key?(feature)
47
+ @mutex.synchronize { @lfi.key?(feature) }
48
+ end
49
+
50
+ # There is a relatively uncommon case where we could miss adding an
51
+ # entry:
52
+ #
53
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
54
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
55
+ # pass `long` (the full expanded absolute path), then we did are not able
56
+ # to confidently add the `bundler.rb` form to @lfi.
57
+ #
58
+ # We could either:
59
+ #
60
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
61
+ # not quite right; or
62
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
63
+ # entry.
64
+ def register(short, long = nil)
65
+ ret = yield
66
+
67
+ # do we have 'bundler' or 'bundler.rb'?
68
+ altname = if File.extname(short) != ''
69
+ # strip the path from 'bundler.rb' -> 'bundler'
70
+ strip_extension(short)
71
+ elsif long && ext = File.extname(long)
72
+ # get the extension from the expanded path if given
73
+ # 'bundler' + '.rb'
74
+ short + ext
75
+ end
76
+
77
+ @mutex.synchronize do
78
+ @lfi[short] = true
79
+ (@lfi[altname] = true) if altname
80
+ end
81
+
82
+ ret
83
+ end
84
+
85
+ private
86
+
87
+ STRIP_EXTENSION = /\..*?$/
88
+ private_constant :STRIP_EXTENSION
89
+
90
+ def strip_extension(f)
91
+ f.sub(STRIP_EXTENSION, '')
92
+ end
93
+ end
94
+ end
95
+ end
@@ -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 = (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
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.binwrite(tmp, MessagePack.dump(@data))
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'
@@ -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 # "1.9.3"
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
@@ -1,3 +1,3 @@
1
1
  module Bootsnap
2
- VERSION = "1.1.5"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/bootsnap.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'bootsnap/version'
2
+ require_relative 'bootsnap/bundler'
2
3
  require_relative 'bootsnap/load_path_cache'
3
4
  require_relative 'bootsnap/compile_cache'
4
5
 
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.5
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: 2017-10-06 00:00:00.000000000 Z
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.13
161
+ rubygems_version: 2.7.6
159
162
  signing_key:
160
163
  specification_version: 4
161
164
  summary: Boot large ruby/rails apps faster