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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 021403c5730eaf97a0e8e8a1b31f4b5fd0a24af3
4
- data.tar.gz: 63951a0464d36619d7695cfeceb2a6e806db6a9e
3
+ metadata.gz: 81a66a32929d05c86f053bc76267669c86b21223
4
+ data.tar.gz: 02fa45efd635633dc2faf44793b22c99b2a9b966
5
5
  SHA512:
6
- metadata.gz: 0f152d961214408b1c75e5f7288f411c70fc0eba8697e2b7646b767fa0cc7238c58ad2b68e5e63b9b2fac37f26d73b2093a9ad77bb8a77e57cb1495e3fa37e73
7
- data.tar.gz: 6b0b6812411caefaf659bbd1375009a2b3b55a37428ce6d7a29115c40b30a3047b7bdb41710519ba4b6db9b4d889c1473ca87bba121243f21ab7b63e507a0b53
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
- > Beta version
3
+ **Beta-quality. See [the last section of this README](#trustworthiness).**
4
4
 
5
- Bootsnap is a library that overrides `Kernel#require`, `Kernel#load`, `Module#autoload` and in the case that `ActiveSupport` is used, it will also override a number of `ActiveSupport` methods.
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
- Bootsnap creates 2 kinds of caches, a stable, long lived cache out of Ruby and Gem directories. These are assumed to never change and so we can cache more aggresively. Application code is expected to change frequently, so it is cached with little aggression (short lived bursts that should last only as long as the app takes to boot). This is the “volatile” cache.
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
- Below is a diagram explaining how the overrides work.
19
+ ### Path Pre-Scanning
10
20
 
11
- ![Flowchart explaining Bootsnap](https://cloud.githubusercontent.com/assets/3074765/24532120/eed94e64-158b-11e7-9137-438d759b2ac8.png)
21
+ *(This work is a minor evolution of [bootscale](https://github.com/byroot/bootscale)).*
12
22
 
13
- In this diagram, you might notice that we refer to cache and autoload_path_cache as the main points of override.
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
- # How it works
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
- Caching paths is the main function of bootsnap. There are 2 types of caches:
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
- - Stable: For Gems and Rubies since these are highly unlikely to change
20
- - Volatile: For everything else, like your app code, since this is likely to change
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
- This path is shown in the flowchart below. In a number of instances, scan is mentioned.
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
- # Usage
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'` but before the end.
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', ## Path to your cache
224
+ cache_dir: 'tmp/cache', # Path to your cache
40
225
  development_mode: ENV['MY_ENV'] == 'development',
41
- load_path_cache: true, ## Should we optimize the LOAD_PATH with a cache?
42
- autoload_paths_cache: true, ## Should we optimize the AUTOLOAD_PATH with a cache?
43
- disable_trace: false, ## Sets `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`
44
- compile_cache_iseq: true, ## Should compile Ruby code into iSeq cache?
45
- compile_cache_yaml: true ## Should compile YAML into a cache?
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', 'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This will help optimize boot time.
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.
@@ -1,3 +1,3 @@
1
1
  module Bootsnap
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
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.0
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-31 00:00:00.000000000 Z
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