bootsnap 1.5.1 → 1.7.5

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
  SHA256:
3
- metadata.gz: 9a15b298603bbdda820fa4aa3d37e32c72f181aa49aac240a02f076de2dd17eb
4
- data.tar.gz: bbce00645395d42d30cb3c89b35dcb910003e3f9c3b65afd3a94b7e9019b868a
3
+ metadata.gz: b7dac6bb1ac2018e46d1cdac77a243cc1e2d46d8dc24deb0a9248320451549ba
4
+ data.tar.gz: 9cd95e8c52993daffd79f1d21961c5754f88642250c9552881f30f26f9dcc462
5
5
  SHA512:
6
- metadata.gz: 982d29eb952ba2c053fd78d244493fc2c75d148f11bed24c67e657c0b59d1d6cb4cfc964583087dfababe0b3f977aa9d183c67c69949a31b1dca65c5d51886a4
7
- data.tar.gz: 9e862ebb9a2ddb6cebdfec4835e78c9fcf14dd1451af73a6f04bf38edb362111f27cb70a04e381cb9550cdbdf4cf83d838aaf514126a41e1c9f1c1a12ab96417
6
+ metadata.gz: e6cee7f1ed6c30d819e576fa79d1e627585996d139dd4add684fc22d4bf720ae1c0f778bb1ee23c2a7819d788c7956b9632f03b5861bb7f6e507f3562687b70e
7
+ data.tar.gz: 505de0bcbfa90ab03fde256bea4dec4f5728482b61d452887da7a49c84894daff61c1aa0b16a2a114ddab142ae2a75d09f4447de80a6b33068ea0c15ec0fdd8f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,46 @@
1
+ # Unreleased
2
+
3
+ # 1.7.5
4
+
5
+ * Handle a regression of Ruby 2.7.3 causing Bootsnap to call the deprecated `untaint` method. (#360)
6
+ * Gracefully handle read-only file system as well as other errors preventing to persist the load path cache. (#358)
7
+
8
+ # 1.7.4
9
+
10
+ * Stop raising errors when encoutering various file system errors. The cache is now best effort,
11
+ if somehow it can't be saved, bootsnapp will gracefully fallback to the original operation (e.g. `Kernel.require`).
12
+ (#353, #177, #262)
13
+
14
+ # 1.7.3
15
+
16
+ * Disable YAML precompilation when encountering YAML tags. (#351)
17
+
18
+ # 1.7.2
19
+
20
+ * Fix compatibility with msgpack < 1. (#349)
21
+
22
+ # 1.7.1
23
+
24
+ * Warn Ruby 2.5 users if they turn ISeq caching on. (#327, #244)
25
+ * Disable ISeq caching for the whole 2.5.x series again.
26
+ * Better handle hashing of Ruby strings. (#318)
27
+
28
+ # 1.7.0
29
+
30
+ * Fix detection of YAML files in gems.
31
+ * Adds an instrumentation API to monitor cache misses.
32
+ * Allow to control the behavior of `require 'bootsnap/setup'` using environment variables.
33
+ * Deprecate the `disable_trace` option.
34
+ * Deprecate the `ActiveSupport::Dependencies` (AKA Classic autoloader) integration. (#344)
35
+
36
+ # 1.6.0
37
+
38
+ * Fix a Ruby 2.7/3.0 issue with `YAML.load_file` keyword arguments. (#342)
39
+ * `bootsnap precompile` CLI use multiple processes to complete faster. (#341)
40
+ * `bootsnap precompile` CLI also precompile YAML files. (#340)
41
+ * Changed the load path cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-load-path-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/load-path-cache` for ease of use. (#334)
42
+ * Changed the compile cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-compile-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/compile-cache` for ease of use. (#334)
43
+
1
44
  # 1.5.1
2
45
 
3
46
  * Workaround a Ruby bug in InstructionSequence.compile_file. (#332)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Bootsnap [![Actions Status](https://github.com/Shopify/bootsnap/workflows/ci/badge.svg)](https://github.com/Shopify/bootsnap/actions)
2
2
 
3
- Bootsnap is a library that plugs into Ruby, with optional support for `ActiveSupport` and `YAML`,
3
+ Bootsnap is a library that plugs into Ruby, with optional support for `YAML`,
4
4
  to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work).
5
5
 
6
6
  #### Performance
@@ -11,7 +11,7 @@ to optimize and cache expensive computations. See [How Does This Work](#how-does
11
11
  - The core Shopify platform -- a rather large monolithic application -- boots about 75% faster,
12
12
  dropping from around 25s to 6.5s.
13
13
  * In Shopify core (a large app), about 25% of this gain can be attributed to `compile_cache_*`
14
- features; 75% to path caching, and ~1% to `disable_trace`. This is fairly representative.
14
+ features; 75% to path caching. This is fairly representative.
15
15
 
16
16
  ## Usage
17
17
 
@@ -29,7 +29,8 @@ If you are using Rails, add this to `config/boot.rb` immediately after `require
29
29
  require 'bootsnap/setup'
30
30
  ```
31
31
 
32
- Note that bootsnap writes to `tmp/cache`, and that directory *must* be writable. Rails will fail to
32
+ Note that bootsnap writes to `tmp/cache` (or the path specified by `ENV['BOOTSNAP_CACHE_DIR']`),
33
+ and that directory *must* be writable. Rails will fail to
33
34
  boot if it is not. If this is unacceptable (e.g. you are running in a read-only container and
34
35
  unwilling to mount in a writable tmpdir), you should remove this line or wrap it in a conditional.
35
36
 
@@ -53,15 +54,11 @@ Bootsnap.setup(
53
54
  cache_dir: 'tmp/cache', # Path to your cache
54
55
  development_mode: env == 'development', # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc
55
56
  load_path_cache: true, # Optimize the LOAD_PATH with a cache
56
- autoload_paths_cache: true, # Optimize ActiveSupport autoloads with cache
57
- disable_trace: true, # Set `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`
58
57
  compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting.
59
58
  compile_cache_yaml: true # Compile YAML into a cache
60
59
  )
61
60
  ```
62
61
 
63
- **Note that `disable_trace` will break debuggers and tracing.**
64
-
65
62
  **Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap',
66
63
  'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This
67
64
  will help optimize boot time further if you have an extremely large `$LOAD_PATH`.
@@ -71,12 +68,39 @@ speeds up the loading of individual source files, Spring keeps a copy of a pre-b
71
68
  on hand to completely skip parts of the boot process the next time it's needed. The two tools work
72
69
  well together, and are both included in a newly-generated Rails applications by default.
73
70
 
71
+ ### Environment variables
72
+
73
+ `require 'bootsnap/setup'` behavior can be changed using environment variables:
74
+
75
+ - `BOOTSNAP_CACHE_DIR` allows to define the cache location.
76
+ - `DISABLE_BOOTSNAP` allows to entirely disable bootsnap.
77
+ - `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching.
78
+ - `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
79
+ - `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR.
80
+
74
81
  ### Environments
75
82
 
76
83
  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.
77
84
 
78
85
  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.
79
86
 
87
+ ### Instrumentation
88
+
89
+ Bootsnap cache misses can be monitored though a callback:
90
+
91
+ ```ruby
92
+ Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
93
+ ```
94
+
95
+ `event` is either `:miss` or `:stale`. You can also call `Bootsnap.log!` as a shortcut to
96
+ log all events to STDERR.
97
+
98
+ To turn instrumentation back off you can set it to nil:
99
+
100
+ ```ruby
101
+ Bootsnap.instrumentation = nil
102
+ ```
103
+
80
104
  ## How does this work?
81
105
 
82
106
  Bootsnap optimizes methods to cache results of expensive computations, and can be grouped
@@ -84,8 +108,6 @@ into two broad categories:
84
108
 
85
109
  * [Path Pre-Scanning](#path-pre-scanning)
86
110
  * `Kernel#require` and `Kernel#load` are modified to eliminate `$LOAD_PATH` scans.
87
- * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` are
88
- overridden to eliminate scans of `ActiveSupport::Dependencies.autoload_paths`.
89
111
  * [Compilation caching](#compilation-caching)
90
112
  * `RubyVM::InstructionSequence.load_iseq` is implemented to cache the result of ruby bytecode
91
113
  compilation.
@@ -124,10 +146,6 @@ open y/foo.rb
124
146
  ...
125
147
  ```
126
148
 
127
- Exactly the same strategy is employed for methods that traverse
128
- `ActiveSupport::Dependencies.autoload_paths` if the `autoload_paths_cache` option is given to
129
- `Bootsnap.setup`.
130
-
131
149
  The following diagram flowcharts the overrides that make the `*_path_cache` features work.
132
150
 
133
151
  ![Flowchart explaining
@@ -14,6 +14,7 @@
14
14
  #include "bootsnap.h"
15
15
  #include "ruby.h"
16
16
  #include <stdint.h>
17
+ #include <stdbool.h>
17
18
  #include <sys/types.h>
18
19
  #include <errno.h>
19
20
  #include <fcntl.h>
@@ -34,6 +35,10 @@
34
35
 
35
36
  #define MAX_CREATE_TEMPFILE_ATTEMPT 3
36
37
 
38
+ #ifndef RB_UNLIKELY
39
+ #define RB_UNLIKELY(x) (x)
40
+ #endif
41
+
37
42
  /*
38
43
  * An instance of this key is written as the first 64 bytes of each cache file.
39
44
  * The mtime and size members track whether the file contents have changed, and
@@ -70,7 +75,7 @@ struct bs_cache_key {
70
75
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
71
76
 
72
77
  /* Effectively a schema version. Bumping invalidates all previous caches */
73
- static const uint32_t current_version = 2;
78
+ static const uint32_t current_version = 3;
74
79
 
75
80
  /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
76
81
  * new OS ABI, etc. */
@@ -88,17 +93,23 @@ static VALUE rb_mBootsnap_CompileCache;
88
93
  static VALUE rb_mBootsnap_CompileCache_Native;
89
94
  static VALUE rb_eBootsnap_CompileCache_Uncompilable;
90
95
  static ID uncompilable;
96
+ static ID instrumentation_method;
97
+ static VALUE sym_miss;
98
+ static VALUE sym_stale;
99
+ static bool instrumentation_enabled = false;
91
100
 
92
101
  /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
102
+ static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
93
103
  static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
94
104
  static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
105
+ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
95
106
 
96
107
  /* Helpers */
97
- static uint64_t fnv1a_64(const char *str);
98
- static void bs_cache_path(const char * cachedir, const char * path, const char * extra, char (* cache_path)[MAX_CACHEPATH_SIZE]);
108
+ static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
99
109
  static int bs_read_key(int fd, struct bs_cache_key * key);
100
110
  static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
101
111
  static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
112
+ static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
102
113
  static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance);
103
114
  static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
104
115
  static uint32_t get_ruby_revision(void);
@@ -146,15 +157,31 @@ Init_bootsnap(void)
146
157
  current_ruby_platform = get_ruby_platform();
147
158
 
148
159
  uncompilable = rb_intern("__bootsnap_uncompilable__");
160
+ instrumentation_method = rb_intern("_instrument");
161
+
162
+ sym_miss = ID2SYM(rb_intern("miss"));
163
+ rb_global_variable(&sym_miss);
164
+
165
+ sym_stale = ID2SYM(rb_intern("stale"));
166
+ rb_global_variable(&sym_stale);
149
167
 
168
+ rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
150
169
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
151
170
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
171
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
152
172
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
153
173
 
154
174
  current_umask = umask(0777);
155
175
  umask(current_umask);
156
176
  }
157
177
 
178
+ static VALUE
179
+ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
180
+ {
181
+ instrumentation_enabled = RTEST(enabled);
182
+ return enabled;
183
+ }
184
+
158
185
  /*
159
186
  * Bootsnap's ruby code registers a hook that notifies us via this function
160
187
  * when compile_option changes. These changes invalidate all existing caches.
@@ -183,7 +210,7 @@ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
183
210
  * - 32 bits doesn't feel collision-resistant enough; 64 is nice.
184
211
  */
185
212
  static uint64_t
186
- fnv1a_64_iter(uint64_t h, const char *str)
213
+ fnv1a_64_iter_cstr(uint64_t h, const char *str)
187
214
  {
188
215
  unsigned char *s = (unsigned char *)str;
189
216
 
@@ -196,7 +223,21 @@ fnv1a_64_iter(uint64_t h, const char *str)
196
223
  }
197
224
 
198
225
  static uint64_t
199
- fnv1a_64(const char *str)
226
+ fnv1a_64_iter(uint64_t h, const VALUE str)
227
+ {
228
+ unsigned char *s = (unsigned char *)RSTRING_PTR(str);
229
+ unsigned char *str_end = (unsigned char *)RSTRING_PTR(str) + RSTRING_LEN(str);
230
+
231
+ while (s < str_end) {
232
+ h ^= (uint64_t)*s++;
233
+ h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40);
234
+ }
235
+
236
+ return h;
237
+ }
238
+
239
+ static uint64_t
240
+ fnv1a_64(const VALUE str)
200
241
  {
201
242
  uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
202
243
  return fnv1a_64_iter(h, str);
@@ -217,7 +258,7 @@ get_ruby_revision(void)
217
258
  } else {
218
259
  uint64_t hash;
219
260
 
220
- hash = fnv1a_64(StringValueCStr(ruby_revision));
261
+ hash = fnv1a_64(ruby_revision);
221
262
  return (uint32_t)(hash >> 32);
222
263
  }
223
264
  }
@@ -237,19 +278,19 @@ get_ruby_platform(void)
237
278
  VALUE ruby_platform;
238
279
 
239
280
  ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
240
- hash = fnv1a_64(RSTRING_PTR(ruby_platform));
281
+ hash = fnv1a_64(ruby_platform);
241
282
 
242
283
  #ifdef _WIN32
243
284
  return (uint32_t)(hash >> 32) ^ (uint32_t)GetVersion();
244
285
  #elif defined(__GLIBC__)
245
- hash = fnv1a_64_iter(hash, gnu_get_libc_version());
286
+ hash = fnv1a_64_iter_cstr(hash, gnu_get_libc_version());
246
287
  return (uint32_t)(hash >> 32);
247
288
  #else
248
289
  struct utsname utsname;
249
290
 
250
291
  /* Not worth crashing if this fails; lose extra cache invalidation potential */
251
292
  if (uname(&utsname) >= 0) {
252
- hash = fnv1a_64_iter(hash, utsname.version);
293
+ hash = fnv1a_64_iter_cstr(hash, utsname.version);
253
294
  }
254
295
 
255
296
  return (uint32_t)(hash >> 32);
@@ -264,17 +305,13 @@ get_ruby_platform(void)
264
305
  * The path will look something like: <cachedir>/12/34567890abcdef
265
306
  */
266
307
  static void
267
- bs_cache_path(const char * cachedir, const char * path, const char * extra, char (* cache_path)[MAX_CACHEPATH_SIZE])
308
+ bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE])
268
309
  {
269
310
  uint64_t hash = fnv1a_64(path);
270
- if (extra) {
271
- hash ^= fnv1a_64(extra);
272
- }
273
-
274
311
  uint8_t first_byte = (hash >> (64 - 8));
275
312
  uint64_t remainder = hash & 0x00ffffffffffffff;
276
313
 
277
- sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder);
314
+ sprintf(*cache_path, "%s/%02"PRIx8"/%014"PRIx64, cachedir, first_byte, remainder);
278
315
  }
279
316
 
280
317
  /*
@@ -318,18 +355,39 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE arg
318
355
  char * cachedir = RSTRING_PTR(cachedir_v);
319
356
  char * path = RSTRING_PTR(path_v);
320
357
  char cache_path[MAX_CACHEPATH_SIZE];
321
- char * extra = NULL;
322
- if (!NIL_P(args)) {
323
- VALUE args_serial = rb_marshal_dump(args, Qnil);
324
- extra = RSTRING_PTR(args_serial);
325
- }
326
358
 
327
359
  /* generate cache path to cache_path */
328
- bs_cache_path(cachedir, path, extra, &cache_path);
360
+ bs_cache_path(cachedir, path_v, &cache_path);
329
361
 
330
362
  return bs_fetch(path, path_v, cache_path, handler, args);
331
363
  }
332
364
 
365
+ /*
366
+ * Entrypoint for Bootsnap::CompileCache::Native.precompile.
367
+ * Similar to fetch, but it only generate the cache if missing
368
+ * and doesn't return the content.
369
+ */
370
+ static VALUE
371
+ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
372
+ {
373
+ FilePathValue(path_v);
374
+
375
+ Check_Type(cachedir_v, T_STRING);
376
+ Check_Type(path_v, T_STRING);
377
+
378
+ if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
379
+ rb_raise(rb_eArgError, "cachedir too long");
380
+ }
381
+
382
+ char * cachedir = RSTRING_PTR(cachedir_v);
383
+ char * path = RSTRING_PTR(path_v);
384
+ char cache_path[MAX_CACHEPATH_SIZE];
385
+
386
+ /* generate cache path to cache_path */
387
+ bs_cache_path(cachedir, path_v, &cache_path);
388
+
389
+ return bs_precompile(path, path_v, cache_path, handler);
390
+ }
333
391
  /*
334
392
  * Open the file we want to load/cache and generate a cache key for it if it
335
393
  * was loaded.
@@ -366,7 +424,8 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
366
424
  }
367
425
 
368
426
  #define ERROR_WITH_ERRNO -1
369
- #define CACHE_MISSING_OR_INVALID -2
427
+ #define CACHE_MISS -2
428
+ #define CACHE_STALE -3
370
429
 
371
430
  /*
372
431
  * Read the cache key from the given fd, which must have position 0 (e.g.
@@ -374,15 +433,16 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
374
433
  *
375
434
  * Possible return values:
376
435
  * - 0 (OK, key was loaded)
377
- * - CACHE_MISSING_OR_INVALID (-2)
378
436
  * - ERROR_WITH_ERRNO (-1, errno is set)
437
+ * - CACHE_MISS (-2)
438
+ * - CACHE_STALE (-3)
379
439
  */
380
440
  static int
381
441
  bs_read_key(int fd, struct bs_cache_key * key)
382
442
  {
383
443
  ssize_t nread = read(fd, key, KEY_SIZE);
384
444
  if (nread < 0) return ERROR_WITH_ERRNO;
385
- if (nread < KEY_SIZE) return CACHE_MISSING_OR_INVALID;
445
+ if (nread < KEY_SIZE) return CACHE_STALE;
386
446
  return 0;
387
447
  }
388
448
 
@@ -392,7 +452,8 @@ bs_read_key(int fd, struct bs_cache_key * key)
392
452
  *
393
453
  * Possible return values:
394
454
  * - 0 (OK, key was loaded)
395
- * - CACHE_MISSING_OR_INVALID (-2)
455
+ * - CACHE_MISS (-2)
456
+ * - CACHE_STALE (-3)
396
457
  * - ERROR_WITH_ERRNO (-1, errno is set)
397
458
  */
398
459
  static int
@@ -403,8 +464,7 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
403
464
  fd = open(path, O_RDONLY);
404
465
  if (fd < 0) {
405
466
  *errno_provenance = "bs_fetch:open_cache_file:open";
406
- if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
407
- return ERROR_WITH_ERRNO;
467
+ return CACHE_MISS;
408
468
  }
409
469
  #ifdef _WIN32
410
470
  setmode(fd, O_BINARY);
@@ -458,7 +518,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
458
518
  goto done;
459
519
  }
460
520
  if (nread != data_size) {
461
- ret = CACHE_MISSING_OR_INVALID;
521
+ ret = CACHE_STALE;
462
522
  goto done;
463
523
  }
464
524
 
@@ -652,14 +712,22 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
652
712
 
653
713
  /* Open the cache key if it exists, and read its cache key in */
654
714
  cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
655
- if (cache_fd == CACHE_MISSING_OR_INVALID) {
715
+ if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
656
716
  /* This is ok: valid_cache remains false, we re-populate it. */
717
+ if (RB_UNLIKELY(instrumentation_enabled)) {
718
+ rb_funcall(rb_mBootsnap, instrumentation_method, 2, cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
719
+ }
657
720
  } else if (cache_fd < 0) {
658
721
  goto fail_errno;
659
722
  } else {
660
723
  /* True if the cache existed and no invalidating changes have occurred since
661
724
  * it was generated. */
662
725
  valid_cache = cache_key_equal(&current_key, &cached_key);
726
+ if (RB_UNLIKELY(instrumentation_enabled)) {
727
+ if (!valid_cache) {
728
+ rb_funcall(rb_mBootsnap, instrumentation_method, 2, sym_stale, path_v);
729
+ }
730
+ }
663
731
  }
664
732
 
665
733
  if (valid_cache) {
@@ -668,10 +736,10 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
668
736
  cache_fd, (ssize_t)cached_key.data_size, handler, args,
669
737
  &output_data, &exception_tag, &errno_provenance
670
738
  );
671
- if (exception_tag != 0) goto raise;
672
- else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
673
- else if (res == ERROR_WITH_ERRNO) goto fail_errno;
674
- else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
739
+ if (exception_tag != 0) goto raise;
740
+ else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
741
+ else if (res == ERROR_WITH_ERRNO) goto fail_errno;
742
+ else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
675
743
  }
676
744
  close(cache_fd);
677
745
  cache_fd = -1;
@@ -694,9 +762,11 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
694
762
  /* If storage_data isn't a string, we can't cache it */
695
763
  if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
696
764
 
697
- /* Write the cache key and storage_data to the cache directory */
698
- res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
699
- if (res < 0) goto fail_errno;
765
+ /* Attempt to write the cache key and storage_data to the cache directory.
766
+ * We do however ignore any failures to persist the cache, as it's better
767
+ * to move along, than to interrupt the process.
768
+ */
769
+ atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
700
770
 
701
771
  /* Having written the cache, now convert storage_data to output_data */
702
772
  exception_tag = bs_storage_to_output(handler, args, storage_data, &output_data);
@@ -740,6 +810,79 @@ invalid_type_storage_data:
740
810
  #undef CLEANUP
741
811
  }
742
812
 
813
+ static VALUE
814
+ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
815
+ {
816
+ struct bs_cache_key cached_key, current_key;
817
+ char * contents = NULL;
818
+ int cache_fd = -1, current_fd = -1;
819
+ int res, valid_cache = 0, exception_tag = 0;
820
+ const char * errno_provenance = NULL;
821
+
822
+ VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
823
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
824
+
825
+ /* Open the source file and generate a cache key for it */
826
+ current_fd = open_current_file(path, &current_key, &errno_provenance);
827
+ if (current_fd < 0) goto fail;
828
+
829
+ /* Open the cache key if it exists, and read its cache key in */
830
+ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
831
+ if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
832
+ /* This is ok: valid_cache remains false, we re-populate it. */
833
+ } else if (cache_fd < 0) {
834
+ goto fail;
835
+ } else {
836
+ /* True if the cache existed and no invalidating changes have occurred since
837
+ * it was generated. */
838
+ valid_cache = cache_key_equal(&current_key, &cached_key);
839
+ }
840
+
841
+ if (valid_cache) {
842
+ goto succeed;
843
+ }
844
+
845
+ close(cache_fd);
846
+ cache_fd = -1;
847
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
848
+
849
+ /* Read the contents of the source file into a buffer */
850
+ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail;
851
+ input_data = rb_str_new(contents, current_key.size);
852
+
853
+ /* Try to compile the input_data using input_to_storage(input_data) */
854
+ exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
855
+ if (exception_tag != 0) goto fail;
856
+
857
+ /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
858
+ * to cache anything; just return false */
859
+ if (storage_data == uncompilable) {
860
+ goto fail;
861
+ }
862
+ /* If storage_data isn't a string, we can't cache it */
863
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
864
+
865
+ /* Write the cache key and storage_data to the cache directory */
866
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
867
+ if (res < 0) goto fail;
868
+
869
+ goto succeed;
870
+
871
+ #define CLEANUP \
872
+ if (contents != NULL) xfree(contents); \
873
+ if (current_fd >= 0) close(current_fd); \
874
+ if (cache_fd >= 0) close(cache_fd);
875
+
876
+ succeed:
877
+ CLEANUP;
878
+ return Qtrue;
879
+ fail:
880
+ CLEANUP;
881
+ return Qfalse;
882
+ #undef CLEANUP
883
+ }
884
+
885
+
743
886
  /*****************************************************************************/
744
887
  /********************* Handler Wrappers **************************************/
745
888
  /*****************************************************************************
@@ -771,7 +914,6 @@ struct i2o_data {
771
914
 
772
915
  struct i2s_data {
773
916
  VALUE handler;
774
- VALUE args;
775
917
  VALUE input_data;
776
918
  VALUE pathval;
777
919
  };
@@ -789,7 +931,7 @@ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * outp
789
931
  int state;
790
932
  struct s2o_data s2o_data = {
791
933
  .handler = handler,
792
- .args = args,
934
+ .args = args,
793
935
  .storage_data = storage_data,
794
936
  };
795
937
  *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
@@ -818,7 +960,7 @@ static VALUE
818
960
  try_input_to_storage(VALUE arg)
819
961
  {
820
962
  struct i2s_data * data = (struct i2s_data *)arg;
821
- return rb_funcall(data->handler, rb_intern("input_to_storage"), 3, data->input_data, data->pathval, data->args);
963
+ return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval);
822
964
  }
823
965
 
824
966
  static VALUE
@@ -843,7 +985,6 @@ bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval,
843
985
  int state;
844
986
  struct i2s_data i2s_data = {
845
987
  .handler = handler,
846
- .args = args,
847
988
  .input_data = input_data,
848
989
  .pathval = pathval,
849
990
  };