bootsnap 0.2.0 → 0.2.1
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 +4 -4
- data/LICENSE +20 -0
- data/README.md +225 -20
- data/lib/bootsnap/load_path_cache/cache.rb +1 -2
- data/lib/bootsnap/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81a66a32929d05c86f053bc76267669c86b21223
|
4
|
+
data.tar.gz: 02fa45efd635633dc2faf44793b22c99b2a9b966
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee53dc7664efb6727862745fcece02da6661db07d2dfba9370e78fcc196f4699bf99d6b1beb718638d208e0e3452d8d12ae5bab1c12b20a811af36afafd31bd6
|
7
|
+
data.tar.gz: 64273db79d1d0b4ff5dd1796141772f478520cffa4bd5a5f3e6351cea88211d9a0bffe2a53a586d97b7c98fabc4373da17fc8278effd0c9708ea7fcbc1e3f811
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017 Shopify
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,29 +1,213 @@
|
|
1
1
|
# Bootsnap
|
2
2
|
|
3
|
-
|
3
|
+
**Beta-quality. See [the last section of this README](#trustworthiness).**
|
4
4
|
|
5
|
-
Bootsnap is a library that
|
5
|
+
Bootsnap is a library that plugs into a number of ruby and (optionally) `ActiveSupport` and `YAML`
|
6
|
+
methods. These methods are modified to cache results of expensive computations, and can be grouped
|
7
|
+
into two broad categories:
|
6
8
|
|
7
|
-
|
9
|
+
* [Path Pre-Scanning](#path-pre-scanning)
|
10
|
+
* `Kernel#require` and `Kernel#load` are modified to eliminate `$LOAD_PATH` scans.
|
11
|
+
* `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` are
|
12
|
+
overridden to eliminate scans of `ActiveSupport::Dependencies.autoload_paths`.
|
13
|
+
* [Compilation caching](#compilation-caching)
|
14
|
+
* `RubyVM::InstructionSequence.load_iseq` is implemented to cache the result of ruby bytecode
|
15
|
+
compilation.
|
16
|
+
* `YAML.load_file` is modified to cache the result of loading a YAML object in MessagePack format
|
17
|
+
(or Marshal, if the message uses types unsupported by MessagePack).
|
8
18
|
|
9
|
-
|
19
|
+
### Path Pre-Scanning
|
10
20
|
|
11
|
-
|
21
|
+
*(This work is a minor evolution of [bootscale](https://github.com/byroot/bootscale)).*
|
12
22
|
|
13
|
-
|
23
|
+
Upon initialization of bootsnap or modification of the path (e.g. `$LOAD_PATH`),
|
24
|
+
`Bootsnap::LoadPathCache` will fetch a list of requirable entries from a cache, or, if necessary,
|
25
|
+
perform a full scan and cache the result.
|
14
26
|
|
15
|
-
|
27
|
+
Later, when we run (e.g.) `require 'foo'`, ruby *would* iterate through every item on our
|
28
|
+
`$LOAD_PATH` `['x', 'y', ...]`, looking for `x/foo.rb`, `y/foo.rb`, and so on. Bootsnap instead
|
29
|
+
looks at all the cached requirables for each `$LOAD_PATH` entry and substitutes the full expanded
|
30
|
+
path of the match ruby would have eventually chosen.
|
16
31
|
|
17
|
-
|
32
|
+
If you look at the syscalls generated by this behaviour, the net effect is that what would
|
33
|
+
previously look like this:
|
18
34
|
|
19
|
-
|
20
|
-
|
35
|
+
```
|
36
|
+
open x/foo.rb # (fail)
|
37
|
+
# (imagine this with 500 $LOAD_PATH entries instead of two)
|
38
|
+
open y/foo.rb # (success)
|
39
|
+
close y/foo.rb
|
40
|
+
open y/foo.rb
|
41
|
+
...
|
42
|
+
```
|
43
|
+
|
44
|
+
becomes this:
|
45
|
+
|
46
|
+
```
|
47
|
+
open y/foo.rb
|
48
|
+
...
|
49
|
+
```
|
50
|
+
|
51
|
+
Exactly the same strategy is employed for methods that traverse
|
52
|
+
`ActiveSupport::Dependencies.autoload_paths` if the `autoload_paths_cache` option is given to
|
53
|
+
`Bootsnap.setup`.
|
54
|
+
|
55
|
+
The following diagram flowcharts the overrides that make the `*_path_cache` features work.
|
21
56
|
|
22
|
-
|
57
|
+

|
59
|
+
|
60
|
+
Bootsnap classifies path entries into two categories: stable and volatile. Volatile entries are
|
61
|
+
scanned each time the application boots, and their caches are only valid for 30 seconds. Stable
|
62
|
+
entries do not expire -- once their contents has been scanned, it is assumed to never change.
|
63
|
+
|
64
|
+
The only directories considered "stable" are things under the Ruby install prefix
|
65
|
+
(`RbConfig::CONFIG['prefix']`, e.g. `/usr/local/ruby` or `~/.rubies/x.y.z`), and things under the
|
66
|
+
`Gem.path` (e.g. `~/.gem/ruby/x.y.z`). Everything else is considered "volatile".
|
67
|
+
|
68
|
+
In addition to the [`Bootsnap::LoadPathCache::Cache`
|
69
|
+
source](https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/cache.rb),
|
70
|
+
this diagram may help clarify how entry resolution works:
|
23
71
|
|
24
72
|

|
25
73
|
|
26
|
-
|
74
|
+
It's also important to note how expensive `LoadError`s can be. If ruby invokes
|
75
|
+
`require 'something'`, but that file isn't on `$LOAD_PATH`, it takes `2 *
|
76
|
+
$LOAD_PATH.length` filesystem accesses to determine that. Bootsnap caches this
|
77
|
+
result too, raising a `LoadError` without touching the filesystem at all.
|
78
|
+
|
79
|
+
### Compilation Caching
|
80
|
+
|
81
|
+
*(A simpler implementation of this concept can be found in [yomikomu](https://github.com/ko1/yomikomu)).*
|
82
|
+
|
83
|
+
Ruby has complex grammar and parsing it is not a particularly cheap operation. Since 1.9, Ruby has
|
84
|
+
translated ruby source to an internal bytecode format, which is then executed by the Ruby VM. Since
|
85
|
+
2.2, Ruby [exposes an API](https://ruby-doc.org/core-2.3.0/RubyVM/InstructionSequence.html) that
|
86
|
+
allows caching that bytecode. This allows us to bypass the relatively-expensive compilation step on
|
87
|
+
subsequent loads of the same file.
|
88
|
+
|
89
|
+
We also noticed that we spend a lot of time loading YAML documents during our application boot, and
|
90
|
+
that MessagePack and Marshal are *much* faster at deserialization than YAML, even with a fast
|
91
|
+
implementation. We use the same strategy of compilation caching for YAML documents, with the
|
92
|
+
equivalent of Ruby's "bytecode" format being a MessagePack document (or, in the case of YAML
|
93
|
+
documents with types unsupported by MessagePack, a Marshal stream).
|
94
|
+
|
95
|
+
These compilation results are stored using `xattr`s on the source files. This is likely to change in
|
96
|
+
the future, as it has some limitations (notably precluding Linux support except where the user feels
|
97
|
+
like changing mount flags). However, this is a very performant implementation.
|
98
|
+
|
99
|
+
Whereas before, the sequence of syscalls generated to `require` a file would look like:
|
100
|
+
|
101
|
+
```
|
102
|
+
open /c/foo.rb -> m
|
103
|
+
fstat64 m
|
104
|
+
close m
|
105
|
+
open /c/foo.rb -> o
|
106
|
+
fstat64 o
|
107
|
+
fstat64 o
|
108
|
+
read o
|
109
|
+
read o
|
110
|
+
...
|
111
|
+
close o
|
112
|
+
```
|
113
|
+
|
114
|
+
With bootsnap, we get:
|
115
|
+
|
116
|
+
```
|
117
|
+
open /c/foo.rb -> n
|
118
|
+
fstat64 n
|
119
|
+
fgetxattr n
|
120
|
+
fgetxattr n
|
121
|
+
close n
|
122
|
+
```
|
123
|
+
|
124
|
+
Bootsnap writes two `xattrs` attached to each file read:
|
125
|
+
|
126
|
+
* `user.aotcc.value`, the binary compilation result; and
|
127
|
+
* `user.aotcc.key`, a cache key to determine whether `user.aotcc.value` is still valid.
|
128
|
+
|
129
|
+
The key includes several fields:
|
130
|
+
|
131
|
+
* `version`, hardcoded in bootsnap. Essentially a schema version;
|
132
|
+
* `compile_option`, which changes with `RubyVM::InstructionSequence.compile_option` does;
|
133
|
+
* `data_size`, the number of bytes in `user.aotcc.value`, which we need to read it into a buffer
|
134
|
+
using `fgetxattr(2)`;
|
135
|
+
* `ruby_revision`, the version of Ruby this was compiled with; and
|
136
|
+
* `mtime`, the last-modification timestamp of the source file when it was compiled.
|
137
|
+
|
138
|
+
If the key is valid, the result is loaded from the value. Otherwise, it is regenerated and clobbers
|
139
|
+
the current cache.
|
140
|
+
|
141
|
+
This diagram may help illustrate how it works:
|
142
|
+
|
143
|
+

|
144
|
+
|
145
|
+
### Putting it all together
|
146
|
+
|
147
|
+
Imagine we have this file structure:
|
148
|
+
|
149
|
+
```
|
150
|
+
/
|
151
|
+
├── a
|
152
|
+
├── b
|
153
|
+
└── c
|
154
|
+
└── foo.rb
|
155
|
+
```
|
156
|
+
|
157
|
+
And this `$LOAD_PATH`:
|
158
|
+
|
159
|
+
```
|
160
|
+
["/a", "/b", "/c"]
|
161
|
+
```
|
162
|
+
|
163
|
+
When we call `require 'foo'` without bootsnap, Ruby would generate this sequence of syscalls:
|
164
|
+
|
165
|
+
|
166
|
+
```
|
167
|
+
open /a/foo.rb -> -1
|
168
|
+
open /b/foo.rb -> -1
|
169
|
+
open /c/foo.rb -> n
|
170
|
+
close n
|
171
|
+
open /c/foo.rb -> m
|
172
|
+
fstat64 m
|
173
|
+
close m
|
174
|
+
open /c/foo.rb -> o
|
175
|
+
fstat64 o
|
176
|
+
fstat64 o
|
177
|
+
read o
|
178
|
+
read o
|
179
|
+
...
|
180
|
+
close o
|
181
|
+
```
|
182
|
+
|
183
|
+
With bootsnap, we get:
|
184
|
+
|
185
|
+
```
|
186
|
+
open /c/foo.rb -> n
|
187
|
+
fstat64 n
|
188
|
+
fgetxattr n
|
189
|
+
fgetxattr n
|
190
|
+
close n
|
191
|
+
```
|
192
|
+
|
193
|
+
If we call `require 'nope'` without bootsnap, we get:
|
194
|
+
|
195
|
+
```
|
196
|
+
open /a/nope.rb -> -1
|
197
|
+
open /b/nope.rb -> -1
|
198
|
+
open /c/nope.rb -> -1
|
199
|
+
open /a/nope.bundle -> -1
|
200
|
+
open /b/nope.bundle -> -1
|
201
|
+
open /c/nope.bundle -> -1
|
202
|
+
```
|
203
|
+
|
204
|
+
...and if we call `require 'nope'` *with* bootsnap, we get...
|
205
|
+
|
206
|
+
```
|
207
|
+
# (nothing!)
|
208
|
+
```
|
209
|
+
|
210
|
+
## Usage
|
27
211
|
|
28
212
|
Add `bootsnap` to your `Gemfile`:
|
29
213
|
|
@@ -31,19 +215,40 @@ Add `bootsnap` to your `Gemfile`:
|
|
31
215
|
gem 'bootsnap'
|
32
216
|
```
|
33
217
|
|
34
|
-
Next, add this to your boot setup after `require 'bundler/setup'`
|
218
|
+
Next, add this to your boot setup immediately after `require 'bundler/setup'` (i.e. as early as
|
219
|
+
possible: the sooner this is loaded, the sooner it can start optimizing things)
|
35
220
|
|
36
221
|
```ruby
|
37
222
|
require 'bootsnap'
|
38
223
|
Bootsnap.setup(
|
39
|
-
cache_dir: 'tmp/cache',
|
224
|
+
cache_dir: 'tmp/cache', # Path to your cache
|
40
225
|
development_mode: ENV['MY_ENV'] == 'development',
|
41
|
-
load_path_cache: true,
|
42
|
-
autoload_paths_cache: true,
|
43
|
-
disable_trace: false,
|
44
|
-
compile_cache_iseq: true,
|
45
|
-
compile_cache_yaml: true
|
226
|
+
load_path_cache: true, # Should we optimize the LOAD_PATH with a cache?
|
227
|
+
autoload_paths_cache: true, # Should we optimize ActiveSupport autoloads with cache?
|
228
|
+
disable_trace: false, # Sets `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`
|
229
|
+
compile_cache_iseq: true, # Should compile Ruby code into ISeq cache?
|
230
|
+
compile_cache_yaml: true # Should compile YAML into a cache?
|
46
231
|
)
|
47
232
|
```
|
48
233
|
|
49
|
-
**Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap',
|
234
|
+
**Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap',
|
235
|
+
'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This
|
236
|
+
will help optimize boot time further if you have an extremely large `$LOAD_PATH`.
|
237
|
+
|
238
|
+
## Trustworthiness
|
239
|
+
|
240
|
+
We use the `*_path_cache` features in production and haven't experienced any issues in a long time.
|
241
|
+
|
242
|
+
The `compile_cache_*` features work well for us in development on macOS, but probably don't work on
|
243
|
+
Linux at all.
|
244
|
+
|
245
|
+
`disable_trace` should be completely safe, but we don't really use it because some people like to
|
246
|
+
use tools that make use of `trace` instructions.
|
247
|
+
|
248
|
+
| feature | where we're using it |
|
249
|
+
|-|-|
|
250
|
+
| `load_path_cache` | everywhere |
|
251
|
+
| `autoload_path_cache` | everywhere |
|
252
|
+
| `disable_trace` | nowhere, but it's safe unless you need tracing |
|
253
|
+
| `compile_cache_iseq` | development, unlikely to work on Linux |
|
254
|
+
| `compile_cache_yaml` | development, unlikely to work on Linux |
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require_relative '../load_path_cache'
|
2
2
|
require_relative '../explicit_require'
|
3
|
-
Bootsnap::ExplicitRequire.from_archdir('thread')
|
4
3
|
|
5
4
|
module Bootsnap
|
6
5
|
module LoadPathCache
|
@@ -49,7 +48,7 @@ module Bootsnap
|
|
49
48
|
# doesn't correspond to any entry on the filesystem. Ruby lies. So we
|
50
49
|
# lie too, forcing our monkeypatch to return false like ruby would.
|
51
50
|
when ""
|
52
|
-
raise LoadPathCache::ReturnFalse if feature == 'enumerator'
|
51
|
+
raise LoadPathCache::ReturnFalse if feature == 'enumerator' || feature == 'thread'
|
53
52
|
nil
|
54
53
|
# Ruby allows specifying native extensions as '.so' even when DLEXT
|
55
54
|
# is '.bundle'. This is where we handle that case.
|
data/lib/bootsnap/version.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: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Burke Libbey
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03
|
11
|
+
date: 2017-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -120,6 +120,7 @@ files:
|
|
120
120
|
- ".rubocop.yml"
|
121
121
|
- CONTRIBUTING.md
|
122
122
|
- Gemfile
|
123
|
+
- LICENSE
|
123
124
|
- README.md
|
124
125
|
- Rakefile
|
125
126
|
- bin/console
|