bootsnap 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Flowchart explaining
|
58
|
+
Bootsnap](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png)
|
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
|
![How path searching works](https://cloud.githubusercontent.com/assets/3074765/24532143/18278cd6-158c-11e7-8250-78d831df70db.png)
|
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
|
+
![Compilation Caching](https://burkelibbey.s3.amazonaws.com/bootsnap-compile-cache.png)
|
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
|