bootsnap 1.16.0 → 1.18.3

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
2
  SHA256:
3
- metadata.gz: 1bf1eca00971c561ca1e15941903dcf193ec8fc84c68736177a3aa72558c536b
4
- data.tar.gz: bcd5596d1b4b00905af82cf25693fb72e71897685e024b27311353eef6f588bf
3
+ metadata.gz: 4fa4ab785277ee01a1c8ee75b43f0efb93db42bffcdacc1c8505a65efa03dede
4
+ data.tar.gz: 8aaaca48ae257b563580023c8fa36a59463f4c30f5463c14f6b8b94bf5fe27df
5
5
  SHA512:
6
- metadata.gz: 0d3e37e56d994647ac88a1c9b83f087f137d32acfc09bfba1e71a85ce254697b1dcf6c9e1c90b5c71728ce0f3c0a63ee86680455a8f95003d237671435042859
7
- data.tar.gz: 270bf8fc609981d25441c9c4bd975348130177564f2e1a0744f18720d7f59cda3c3da24bea0c07f2498a185308647b05715e1b1dd5a6045e653600ab2f5907a1
6
+ metadata.gz: 27b48d27d3330c8565952a2fbb979e71013b1e9585bcb3284656192808c304f2874c32a135b14895eec61a7ef038fa71fa111964a56e7aaedc9ff507ef307686
7
+ data.tar.gz: c3d83a0b068f2908a6298c7cd8e1a660f1228a7ddbfb9409cb3f6c174319f3974388ce73756c04062b17b97a37e199a07bccbb0b8dc1c6224998c58c51194b27
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.18.3
4
+
5
+ * Fix the cache corruption issue in the revalidation feature. See #474.
6
+ The cache revalidation feature remains opt-in for now, until it is more battle tested.
7
+
8
+ # 1.18.2
9
+
10
+ * Disable stale cache entries revalidation by default as it seems to cause cache corruption issues. See #471 and #474.
11
+ Will be re-enabled in a future version once the root cause is identified.
12
+ * Fix a potential compilation issue on some systems. See #470.
13
+
14
+ # 1.18.1
15
+
16
+ * Handle `EPERM` errors when opening files with `O_NOATIME`.
17
+
18
+ # 1.18.0
19
+
20
+ * `Bootsnap.instrumentation` now receive `:hit` events.
21
+ * Add `Bootsnap.log_stats!` to print hit rate statistics on process exit. Can also be enabled with `BOOTSNAP_STATS=1`.
22
+ * Revalidate stale cache entries by digesting the source content.
23
+ This should significantly improve performance in environments where `mtime` isn't preserved (e.g. CI systems doing a git clone, etc).
24
+ See #468.
25
+ * Open source files and cache entries with `O_NOATIME` when available to reduce disk accesses. See #469.
26
+ * `bootsnap precompile --gemfile` now look for `.rb` files in the whole gem and not just the `lib/` directory. See #466.
27
+
28
+ # 1.17.1
29
+
30
+ * Fix a compatibility issue with the `prism` library that ships with Ruby 3.3. See #463.
31
+ * Improved the `Kernel#require` decorator to not cause a method redefinition warning. See #461.
32
+
33
+ # 1.17.0
34
+
35
+ * Ensure `$LOAD_PATH.dup` is Ractor shareable to fix an conflict with `did_you_mean`.
36
+ * Allow to ignore directories using absolute paths.
37
+ * Support YAML and JSON CompileCache on TruffleRuby.
38
+ * Support LoadPathCache on TruffleRuby.
39
+
3
40
  # 1.16.0
4
41
 
5
42
  * Use `RbConfig::CONFIG["rubylibdir"]` instead of `RbConfig::CONFIG["libdir"]` to check for stdlib files. See #431.
@@ -17,7 +54,7 @@
17
54
  * Add a way to skip directories during load path scanning.
18
55
  If you have large non-ruby directories in the middle of your load path, it can severely slow down scanning.
19
56
  Typically this is a problem with `node_modules`. See #277.
20
- * Fix `Bootsnap.unload_cache!`, it simply wouldn't work at all becaue of a merge mistake. See #421.
57
+ * Fix `Bootsnap.unload_cache!`, it simply wouldn't work at all because of a merge mistake. See #421.
21
58
 
22
59
  # 1.13.0
23
60
 
@@ -36,7 +73,7 @@
36
73
 
37
74
  * Stop decorating `Module#autoload` as it was only useful for supporting Ruby 2.2 and older.
38
75
 
39
- * Remove `uname` and other patform specific version from the cache keys. `RUBY_PLATFORM + RUBY_REVISION` should be
76
+ * Remove `uname` and other platform specific version from the cache keys. `RUBY_PLATFORM + RUBY_REVISION` should be
40
77
  enough to ensure bytecode compatibility. This should improve caching for alpine based setups. See #409.
41
78
 
42
79
  # 1.11.1
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 `YAML`,
3
+ Bootsnap is a library that plugs into Ruby, with optional support for `YAML` and `JSON`,
4
4
  to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work).
5
5
 
6
6
  #### Performance
@@ -57,6 +57,7 @@ Bootsnap.setup(
57
57
  load_path_cache: true, # Optimize the LOAD_PATH with a cache
58
58
  compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting.
59
59
  compile_cache_yaml: true, # Compile YAML into a cache
60
+ compile_cache_json: true, # Compile JSON into a cache
60
61
  readonly: true, # Use the caches but don't update them on miss or stale entries.
61
62
  )
62
63
  ```
@@ -80,6 +81,7 @@ well together.
80
81
  - `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
81
82
  - `BOOTSNAP_READONLY` configure bootsnap to not update the cache on miss or stale entries.
82
83
  - `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR.
84
+ - `BOOTSNAP_STATS` log hit rate statistics on exit. Can't be used if `BOOTSNAP_LOG` is enabled.
83
85
  - `BOOTSNAP_IGNORE_DIRECTORIES` a comma separated list of directories that shouldn't be scanned.
84
86
  Useful when you have large directories of non-ruby files inside `$LOAD_PATH`.
85
87
  It defaults to ignore any directory named `node_modules`.
@@ -98,8 +100,8 @@ Bootsnap cache misses can be monitored though a callback:
98
100
  Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
99
101
  ```
100
102
 
101
- `event` is either `:miss` or `:stale`. You can also call `Bootsnap.log!` as a shortcut to
102
- log all events to STDERR.
103
+ `event` is either `:hit`, `:miss`, `:stale` or `:revalidated`.
104
+ You can also call `Bootsnap.log!` as a shortcut to log all events to STDERR.
103
105
 
104
106
  To turn instrumentation back off you can set it to nil:
105
107
 
@@ -119,6 +121,7 @@ into two broad categories:
119
121
  compilation.
120
122
  * `YAML.load_file` is modified to cache the result of loading a YAML object in MessagePack format
121
123
  (or Marshal, if the message uses types unsupported by MessagePack).
124
+ * `JSON.load_file` is modified to cache the result of loading a JSON object in MessagePack format
122
125
 
123
126
  ### Path Pre-Scanning
124
127
 
@@ -189,9 +192,9 @@ translated ruby source to an internal bytecode format, which is then executed by
189
192
  allows caching that bytecode. This allows us to bypass the relatively-expensive compilation step on
190
193
  subsequent loads of the same file.
191
194
 
192
- We also noticed that we spend a lot of time loading YAML documents during our application boot, and
193
- that MessagePack and Marshal are *much* faster at deserialization than YAML, even with a fast
194
- implementation. We use the same strategy of compilation caching for YAML documents, with the
195
+ We also noticed that we spend a lot of time loading YAML and JSON documents during our application boot, and
196
+ that MessagePack and Marshal are *much* faster at deserialization than YAML and JSON, even with a fast
197
+ implementation. We use the same strategy of compilation caching for YAML and JSON documents, with the
195
198
  equivalent of Ruby's "bytecode" format being a MessagePack document (or, in the case of YAML
196
199
  documents with types unsupported by MessagePack, a Marshal stream).
197
200
 
@@ -18,8 +18,19 @@
18
18
  #include <sys/types.h>
19
19
  #include <errno.h>
20
20
  #include <fcntl.h>
21
+ #include <unistd.h>
21
22
  #include <sys/stat.h>
22
23
 
24
+ #ifdef __APPLE__
25
+ // The symbol is present, however not in the headers
26
+ // See: https://github.com/Shopify/bootsnap/issues/470
27
+ extern int fdatasync(int);
28
+ #endif
29
+
30
+ #ifndef O_NOATIME
31
+ #define O_NOATIME 0
32
+ #endif
33
+
23
34
  /* 1000 is an arbitrary limit; FNV64 plus some slashes brings the cap down to
24
35
  * 981 for the cache dir */
25
36
  #define MAX_CACHEPATH_SIZE 1000
@@ -30,7 +41,7 @@
30
41
  #define MAX_CREATE_TEMPFILE_ATTEMPT 3
31
42
 
32
43
  #ifndef RB_UNLIKELY
33
- #define RB_UNLIKELY(x) (x)
44
+ #define RB_UNLIKELY(x) (x)
34
45
  #endif
35
46
 
36
47
  /*
@@ -54,8 +65,10 @@ struct bs_cache_key {
54
65
  uint32_t ruby_revision;
55
66
  uint64_t size;
56
67
  uint64_t mtime;
57
- uint64_t data_size; /* not used for equality */
58
- uint8_t pad[24];
68
+ uint64_t data_size; //
69
+ uint64_t digest;
70
+ uint8_t digest_set;
71
+ uint8_t pad[15];
59
72
  } __attribute__((packed));
60
73
 
61
74
  /*
@@ -69,7 +82,7 @@ struct bs_cache_key {
69
82
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
70
83
 
71
84
  /* Effectively a schema version. Bumping invalidates all previous caches */
72
- static const uint32_t current_version = 4;
85
+ static const uint32_t current_version = 5;
73
86
 
74
87
  /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
75
88
  * new OS ABI, etc. */
@@ -87,25 +100,36 @@ static VALUE rb_mBootsnap_CompileCache;
87
100
  static VALUE rb_mBootsnap_CompileCache_Native;
88
101
  static VALUE rb_cBootsnap_CompileCache_UNCOMPILABLE;
89
102
  static ID instrumentation_method;
90
- static VALUE sym_miss;
91
- static VALUE sym_stale;
103
+ static VALUE sym_hit, sym_miss, sym_stale, sym_revalidated;
92
104
  static bool instrumentation_enabled = false;
93
105
  static bool readonly = false;
106
+ static bool revalidation = false;
107
+ static bool perm_issue = false;
94
108
 
95
109
  /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
96
110
  static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
97
111
  static VALUE bs_readonly_set(VALUE self, VALUE enabled);
112
+ static VALUE bs_revalidation_set(VALUE self, VALUE enabled);
98
113
  static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
99
114
  static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
100
115
  static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
101
116
 
102
117
  /* Helpers */
118
+ enum cache_status {
119
+ miss,
120
+ hit,
121
+ stale,
122
+ };
103
123
  static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
104
124
  static int bs_read_key(int fd, struct bs_cache_key * key);
105
- static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
125
+ static enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2);
126
+ static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data);
127
+ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance);
128
+
129
+ static void bs_cache_key_digest(struct bs_cache_key * key, const VALUE input_data);
106
130
  static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
107
131
  static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
108
- static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance);
132
+ static int open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance);
109
133
  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);
110
134
  static uint32_t get_ruby_revision(void);
111
135
  static uint32_t get_ruby_platform(void);
@@ -161,14 +185,14 @@ Init_bootsnap(void)
161
185
 
162
186
  instrumentation_method = rb_intern("_instrument");
163
187
 
188
+ sym_hit = ID2SYM(rb_intern("hit"));
164
189
  sym_miss = ID2SYM(rb_intern("miss"));
165
- rb_global_variable(&sym_miss);
166
-
167
190
  sym_stale = ID2SYM(rb_intern("stale"));
168
- rb_global_variable(&sym_stale);
191
+ sym_revalidated = ID2SYM(rb_intern("revalidated"));
169
192
 
170
193
  rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
171
194
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
195
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
172
196
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
173
197
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
174
198
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
@@ -185,6 +209,14 @@ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
185
209
  return enabled;
186
210
  }
187
211
 
212
+ static inline void
213
+ bs_instrumentation(VALUE event, VALUE path)
214
+ {
215
+ if (RB_UNLIKELY(instrumentation_enabled)) {
216
+ rb_funcall(rb_mBootsnap, instrumentation_method, 2, event, path);
217
+ }
218
+ }
219
+
188
220
  static VALUE
189
221
  bs_readonly_set(VALUE self, VALUE enabled)
190
222
  {
@@ -192,6 +224,13 @@ bs_readonly_set(VALUE self, VALUE enabled)
192
224
  return enabled;
193
225
  }
194
226
 
227
+ static VALUE
228
+ bs_revalidation_set(VALUE self, VALUE enabled)
229
+ {
230
+ revalidation = RTEST(enabled);
231
+ return enabled;
232
+ }
233
+
195
234
  /*
196
235
  * Bootsnap's ruby code registers a hook that notifies us via this function
197
236
  * when compile_option changes. These changes invalidate all existing caches.
@@ -290,17 +329,59 @@ bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_C
290
329
  * The data_size member is not compared, as it serves more of a "header"
291
330
  * function.
292
331
  */
293
- static int
294
- cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
332
+ static enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1,
333
+ struct bs_cache_key *k2) {
334
+ if (k1->version == k2->version &&
335
+ k1->ruby_platform == k2->ruby_platform &&
336
+ k1->compile_option == k2->compile_option &&
337
+ k1->ruby_revision == k2->ruby_revision && k1->size == k2->size) {
338
+ if (k1->mtime == k2->mtime) {
339
+ return hit;
340
+ }
341
+ if (revalidation) {
342
+ return stale;
343
+ }
344
+ }
345
+ return miss;
346
+ }
347
+
348
+ static int cache_key_equal_slow_path(struct bs_cache_key *current_key,
349
+ struct bs_cache_key *cached_key,
350
+ const VALUE input_data)
351
+ {
352
+ bs_cache_key_digest(current_key, input_data);
353
+ return current_key->digest == cached_key->digest;
354
+ }
355
+
356
+ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance)
295
357
  {
296
- return (
297
- k1->version == k2->version &&
298
- k1->ruby_platform == k2->ruby_platform &&
299
- k1->compile_option == k2->compile_option &&
300
- k1->ruby_revision == k2->ruby_revision &&
301
- k1->size == k2->size &&
302
- k1->mtime == k2->mtime
303
- );
358
+ old_key->mtime = current_key->mtime;
359
+ lseek(cache_fd, 0, SEEK_SET);
360
+ ssize_t nwrite = write(cache_fd, old_key, KEY_SIZE);
361
+ if (nwrite < 0) {
362
+ *errno_provenance = "update_cache_key:write";
363
+ return -1;
364
+ }
365
+
366
+ #ifdef HAVE_FDATASYNC
367
+ if (fdatasync(cache_fd) < 0) {
368
+ *errno_provenance = "update_cache_key:fdatasync";
369
+ return -1;
370
+ }
371
+ #endif
372
+
373
+ return 0;
374
+ }
375
+
376
+ /*
377
+ * Fills the cache key digest.
378
+ */
379
+ static void bs_cache_key_digest(struct bs_cache_key *key,
380
+ const VALUE input_data) {
381
+ if (key->digest_set)
382
+ return;
383
+ key->digest = fnv1a_64(input_data);
384
+ key->digest_set = 1;
304
385
  }
305
386
 
306
387
  /*
@@ -356,17 +437,34 @@ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
356
437
 
357
438
  return bs_precompile(path, path_v, cache_path, handler);
358
439
  }
440
+
441
+ static int bs_open_noatime(const char *path, int flags) {
442
+ int fd = 1;
443
+ if (!perm_issue) {
444
+ fd = open(path, flags | O_NOATIME);
445
+ if (fd < 0 && errno == EPERM) {
446
+ errno = 0;
447
+ perm_issue = true;
448
+ }
449
+ }
450
+
451
+ if (perm_issue) {
452
+ fd = open(path, flags);
453
+ }
454
+ return fd;
455
+ }
456
+
359
457
  /*
360
458
  * Open the file we want to load/cache and generate a cache key for it if it
361
459
  * was loaded.
362
460
  */
363
461
  static int
364
- open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance)
462
+ open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance)
365
463
  {
366
464
  struct stat statbuf;
367
465
  int fd;
368
466
 
369
- fd = open(path, O_RDONLY);
467
+ fd = bs_open_noatime(path, O_RDONLY);
370
468
  if (fd < 0) {
371
469
  *errno_provenance = "bs_fetch:open_current_file:open";
372
470
  return fd;
@@ -377,7 +475,9 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
377
475
 
378
476
  if (fstat(fd, &statbuf) < 0) {
379
477
  *errno_provenance = "bs_fetch:open_current_file:fstat";
478
+ int previous_errno = errno;
380
479
  close(fd);
480
+ errno = previous_errno;
381
481
  return -1;
382
482
  }
383
483
 
@@ -387,6 +487,7 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
387
487
  key->ruby_revision = current_ruby_revision;
388
488
  key->size = (uint64_t)statbuf.st_size;
389
489
  key->mtime = (uint64_t)statbuf.st_mtime;
490
+ key->digest_set = false;
390
491
 
391
492
  return fd;
392
493
  }
@@ -430,7 +531,12 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
430
531
  {
431
532
  int fd, res;
432
533
 
433
- fd = open(path, O_RDONLY);
534
+ if (readonly || !revalidation) {
535
+ fd = bs_open_noatime(path, O_RDONLY);
536
+ } else {
537
+ fd = bs_open_noatime(path, O_RDWR);
538
+ }
539
+
434
540
  if (fd < 0) {
435
541
  *errno_provenance = "bs_fetch:open_cache_file:open";
436
542
  return CACHE_MISS;
@@ -467,7 +573,6 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
467
573
  static int
468
574
  fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
469
575
  {
470
- char * data = NULL;
471
576
  ssize_t nread;
472
577
  int ret;
473
578
 
@@ -479,8 +584,8 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
479
584
  ret = ERROR_WITH_ERRNO;
480
585
  goto done;
481
586
  }
482
- data = ALLOC_N(char, data_size);
483
- nread = read(fd, data, data_size);
587
+ storage_data = rb_str_buf_new(data_size);
588
+ nread = read(fd, RSTRING_PTR(storage_data), data_size);
484
589
  if (nread < 0) {
485
590
  *errno_provenance = "bs_fetch:fetch_cached_data:read";
486
591
  ret = ERROR_WITH_ERRNO;
@@ -491,7 +596,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
491
596
  goto done;
492
597
  }
493
598
 
494
- storage_data = rb_str_new(data, data_size);
599
+ rb_str_set_len(storage_data, nread);
495
600
 
496
601
  *exception_tag = bs_storage_to_output(handler, args, storage_data, output_data);
497
602
  if (*output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
@@ -500,7 +605,6 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE *
500
605
  }
501
606
  ret = 0;
502
607
  done:
503
- if (data != NULL) xfree(data);
504
608
  return ret;
505
609
  }
506
610
 
@@ -607,17 +711,22 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
607
711
 
608
712
 
609
713
  /* Read contents from an fd, whose contents are asserted to be +size+ bytes
610
- * long, into a buffer */
611
- static ssize_t
612
- bs_read_contents(int fd, size_t size, char ** contents, const char ** errno_provenance)
714
+ * long, returning a Ruby string on success and Qfalse on failure */
715
+ static VALUE
716
+ bs_read_contents(int fd, size_t size, const char ** errno_provenance)
613
717
  {
718
+ VALUE contents;
614
719
  ssize_t nread;
615
- *contents = ALLOC_N(char, size);
616
- nread = read(fd, *contents, size);
720
+ contents = rb_str_buf_new(size);
721
+ nread = read(fd, RSTRING_PTR(contents), size);
722
+
617
723
  if (nread < 0) {
618
724
  *errno_provenance = "bs_fetch:bs_read_contents:read";
725
+ return Qfalse;
726
+ } else {
727
+ rb_str_set_len(contents, nread);
728
+ return contents;
619
729
  }
620
- return nread;
621
730
  }
622
731
 
623
732
  /*
@@ -668,38 +777,67 @@ static VALUE
668
777
  bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args)
669
778
  {
670
779
  struct bs_cache_key cached_key, current_key;
671
- char * contents = NULL;
672
780
  int cache_fd = -1, current_fd = -1;
673
781
  int res, valid_cache = 0, exception_tag = 0;
674
782
  const char * errno_provenance = NULL;
675
783
 
676
- VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
784
+ VALUE status = Qfalse;
785
+ VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
677
786
  VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
678
787
  VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
679
788
 
680
789
  VALUE exception; /* ruby exception object to raise instead of returning */
790
+ VALUE exception_message; /* ruby exception string to use instead of errno_provenance */
681
791
 
682
792
  /* Open the source file and generate a cache key for it */
683
793
  current_fd = open_current_file(path, &current_key, &errno_provenance);
684
- if (current_fd < 0) goto fail_errno;
794
+ if (current_fd < 0) {
795
+ exception_message = path_v;
796
+ goto fail_errno;
797
+ }
685
798
 
686
799
  /* Open the cache key if it exists, and read its cache key in */
687
800
  cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
688
801
  if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
689
802
  /* This is ok: valid_cache remains false, we re-populate it. */
690
- if (RB_UNLIKELY(instrumentation_enabled)) {
691
- rb_funcall(rb_mBootsnap, instrumentation_method, 2, cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
692
- }
803
+ bs_instrumentation(cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
693
804
  } else if (cache_fd < 0) {
805
+ exception_message = rb_str_new_cstr(cache_path);
694
806
  goto fail_errno;
695
807
  } else {
696
808
  /* True if the cache existed and no invalidating changes have occurred since
697
809
  * it was generated. */
698
- valid_cache = cache_key_equal(&current_key, &cached_key);
699
- if (RB_UNLIKELY(instrumentation_enabled)) {
700
- if (!valid_cache) {
701
- rb_funcall(rb_mBootsnap, instrumentation_method, 2, sym_stale, path_v);
810
+
811
+ switch(cache_key_equal_fast_path(&current_key, &cached_key)) {
812
+ case hit:
813
+ status = sym_hit;
814
+ valid_cache = true;
815
+ break;
816
+ case miss:
817
+ valid_cache = false;
818
+ break;
819
+ case stale:
820
+ valid_cache = false;
821
+ if ((input_data = bs_read_contents(current_fd, current_key.size,
822
+ &errno_provenance)) == Qfalse) {
823
+ exception_message = path_v;
824
+ goto fail_errno;
702
825
  }
826
+ valid_cache = cache_key_equal_slow_path(&current_key, &cached_key, input_data);
827
+ if (valid_cache) {
828
+ if (!readonly) {
829
+ if (update_cache_key(&current_key, &cached_key, cache_fd, &errno_provenance)) {
830
+ exception_message = path_v;
831
+ goto fail_errno;
832
+ }
833
+ }
834
+ status = sym_revalidated;
835
+ }
836
+ break;
837
+ };
838
+
839
+ if (!valid_cache) {
840
+ status = sym_stale;
703
841
  }
704
842
  }
705
843
 
@@ -713,13 +851,18 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
713
851
  else if (res == CACHE_UNCOMPILABLE) {
714
852
  /* If fetch_cached_data returned `Uncompilable` we fallback to `input_to_output`
715
853
  This happens if we have say, an unsafe YAML cache, but try to load it in safe mode */
716
- if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno;
717
- input_data = rb_str_new(contents, current_key.size);
854
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
855
+ exception_message = path_v;
856
+ goto fail_errno;
857
+ }
718
858
  bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
719
859
  if (exception_tag != 0) goto raise;
720
860
  goto succeed;
721
861
  } else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
722
- else if (res == ERROR_WITH_ERRNO) goto fail_errno;
862
+ else if (res == ERROR_WITH_ERRNO){
863
+ exception_message = rb_str_new_cstr(cache_path);
864
+ goto fail_errno;
865
+ }
723
866
  else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
724
867
  }
725
868
  close(cache_fd);
@@ -727,8 +870,10 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
727
870
  /* Cache is stale, invalid, or missing. Regenerate and write it out. */
728
871
 
729
872
  /* Read the contents of the source file into a buffer */
730
- if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno;
731
- input_data = rb_str_new(contents, current_key.size);
873
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
874
+ exception_message = path_v;
875
+ goto fail_errno;
876
+ }
732
877
 
733
878
  /* Try to compile the input_data using input_to_storage(input_data) */
734
879
  exception_tag = bs_input_to_storage(handler, args, input_data, path_v, &storage_data);
@@ -747,6 +892,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
747
892
  * We do however ignore any failures to persist the cache, as it's better
748
893
  * to move along, than to interrupt the process.
749
894
  */
895
+ bs_cache_key_digest(&current_key, input_data);
750
896
  atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
751
897
 
752
898
  /* Having written the cache, now convert storage_data to output_data */
@@ -765,6 +911,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
765
911
  * No point raising an error */
766
912
  if (errno != ENOENT) {
767
913
  errno_provenance = "bs_fetch:unlink";
914
+ exception_message = rb_str_new_cstr(cache_path);
768
915
  goto fail_errno;
769
916
  }
770
917
  }
@@ -775,7 +922,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
775
922
  goto succeed; /* output_data is now the correct return. */
776
923
 
777
924
  #define CLEANUP \
778
- if (contents != NULL) xfree(contents); \
925
+ if (status != Qfalse) bs_instrumentation(status, path_v); \
779
926
  if (current_fd >= 0) close(current_fd); \
780
927
  if (cache_fd >= 0) close(cache_fd);
781
928
 
@@ -784,7 +931,13 @@ succeed:
784
931
  return output_data;
785
932
  fail_errno:
786
933
  CLEANUP;
787
- exception = rb_syserr_new(errno, errno_provenance);
934
+ if (errno_provenance) {
935
+ exception_message = rb_str_concat(
936
+ rb_str_new_cstr(errno_provenance),
937
+ rb_str_concat(rb_str_new_cstr(": "), exception_message)
938
+ );
939
+ }
940
+ exception = rb_syserr_new_str(errno, exception_message);
788
941
  rb_exc_raise(exception);
789
942
  __builtin_unreachable();
790
943
  raise:
@@ -802,13 +955,16 @@ invalid_type_storage_data:
802
955
  static VALUE
803
956
  bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
804
957
  {
958
+ if (readonly) {
959
+ return Qfalse;
960
+ }
961
+
805
962
  struct bs_cache_key cached_key, current_key;
806
- char * contents = NULL;
807
963
  int cache_fd = -1, current_fd = -1;
808
964
  int res, valid_cache = 0, exception_tag = 0;
809
965
  const char * errno_provenance = NULL;
810
966
 
811
- VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
967
+ VALUE input_data = Qfalse; /* data read from source file, e.g. YAML or ruby source */
812
968
  VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
813
969
 
814
970
  /* Open the source file and generate a cache key for it */
@@ -824,7 +980,26 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
824
980
  } else {
825
981
  /* True if the cache existed and no invalidating changes have occurred since
826
982
  * it was generated. */
827
- valid_cache = cache_key_equal(&current_key, &cached_key);
983
+ switch(cache_key_equal_fast_path(&current_key, &cached_key)) {
984
+ case hit:
985
+ valid_cache = true;
986
+ break;
987
+ case miss:
988
+ valid_cache = false;
989
+ break;
990
+ case stale:
991
+ valid_cache = false;
992
+ if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) {
993
+ goto fail;
994
+ }
995
+ valid_cache = cache_key_equal_slow_path(&current_key, &cached_key, input_data);
996
+ if (valid_cache) {
997
+ if (update_cache_key(&current_key, &cached_key, cache_fd, &errno_provenance)) {
998
+ goto fail;
999
+ }
1000
+ }
1001
+ break;
1002
+ };
828
1003
  }
829
1004
 
830
1005
  if (valid_cache) {
@@ -836,8 +1011,7 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
836
1011
  /* Cache is stale, invalid, or missing. Regenerate and write it out. */
837
1012
 
838
1013
  /* Read the contents of the source file into a buffer */
839
- if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail;
840
- input_data = rb_str_new(contents, current_key.size);
1014
+ if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) goto fail;
841
1015
 
842
1016
  /* Try to compile the input_data using input_to_storage(input_data) */
843
1017
  exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
@@ -852,13 +1026,13 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
852
1026
  if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
853
1027
 
854
1028
  /* Write the cache key and storage_data to the cache directory */
1029
+ bs_cache_key_digest(&current_key, input_data);
855
1030
  res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
856
1031
  if (res < 0) goto fail;
857
1032
 
858
1033
  goto succeed;
859
1034
 
860
1035
  #define CLEANUP \
861
- if (contents != NULL) xfree(contents); \
862
1036
  if (current_fd >= 0) close(current_fd); \
863
1037
  if (cache_fd >= 0) close(cache_fd);
864
1038
 
@@ -1,23 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("mkmf")
3
+ require "mkmf"
4
4
 
5
- if RUBY_ENGINE == "ruby"
6
- $CFLAGS << " -O3 "
7
- $CFLAGS << " -std=c99"
5
+ if %w[ruby truffleruby].include?(RUBY_ENGINE)
6
+ have_func "fdatasync", "unistd.h"
7
+
8
+ unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
9
+ append_cppflags ["-D_GNU_SOURCE"] # Needed of O_NOATIME
10
+ end
11
+
12
+ append_cflags ["-O3", "-std=c99"]
8
13
 
9
14
  # ruby.h has some -Wpedantic fails in some cases
10
15
  # (e.g. https://github.com/Shopify/bootsnap/issues/15)
11
16
  unless ["0", "", nil].include?(ENV["BOOTSNAP_PEDANTIC"])
12
- $CFLAGS << " -Wall"
13
- $CFLAGS << " -Werror"
14
- $CFLAGS << " -Wextra"
15
- $CFLAGS << " -Wpedantic"
17
+ append_cflags([
18
+ "-Wall",
19
+ "-Werror",
20
+ "-Wextra",
21
+ "-Wpedantic",
16
22
 
17
- $CFLAGS << " -Wno-unused-parameter" # VALUE self has to be there but we don't care what it is.
18
- $CFLAGS << " -Wno-keyword-macro" # hiding return
19
- $CFLAGS << " -Wno-gcc-compat" # ruby.h 2.6.0 on macos 10.14, dunno
20
- $CFLAGS << " -Wno-compound-token-split-by-macro"
23
+ "-Wno-unused-parameter", # VALUE self has to be there but we don't care what it is.
24
+ "-Wno-keyword-macro", # hiding return
25
+ "-Wno-gcc-compat", # ruby.h 2.6.0 on macos 10.14, dunno
26
+ "-Wno-compound-token-split-by-macro",
27
+ ])
21
28
  end
22
29
 
23
30
  create_makefile("bootsnap/bootsnap")
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- extend(self)
4
+ extend self
5
5
 
6
6
  def bundler?
7
7
  return false unless defined?(::Bundler)
data/lib/bootsnap/cli.rb CHANGED
@@ -60,14 +60,16 @@ module Bootsnap
60
60
  precompile_json_files(main_sources)
61
61
 
62
62
  if compile_gemfile
63
- # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
64
- gem_exclude = Regexp.union([exclude, "/spec/", "/test/"].compact)
65
- precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
66
-
67
63
  # Gems that include JSON or YAML files usually don't put them in `lib/`.
68
64
  # So we look at the gem root.
65
+ # Similarly, gems that include Rails engines generally file Ruby files in `app/`.
66
+ # However some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
67
+ gem_exclude = Regexp.union([exclude, "/spec/", "/test/", "/features/"].compact)
68
+
69
69
  gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems/[^/]+}
70
- gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
70
+ gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] || p }.uniq
71
+
72
+ precompile_ruby_files(gem_paths, exclude: gem_exclude)
71
73
  precompile_yaml_files(gem_paths, exclude: gem_exclude)
72
74
  precompile_json_files(gem_paths, exclude: gem_exclude)
73
75
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("bootsnap/bootsnap")
4
- require("zlib")
3
+ require "bootsnap/bootsnap"
4
+ require "zlib"
5
5
 
6
6
  module Bootsnap
7
7
  module CompileCache
@@ -12,6 +12,10 @@ module Bootsnap
12
12
  def cache_dir=(cache_dir)
13
13
  @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}iseq" : "#{cache_dir}-iseq"
14
14
  end
15
+
16
+ def supported?
17
+ CompileCache.supported? && defined?(RubyVM)
18
+ end
15
19
  end
16
20
 
17
21
  has_ruby_bug_18250 = begin # https://bugs.ruby-lang.org/issues/18250
@@ -83,8 +87,6 @@ module Bootsnap
83
87
  return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
84
88
 
85
89
  Bootsnap::CompileCache::ISeq.fetch(path.to_s)
86
- rescue Errno::EACCES
87
- Bootsnap::CompileCache.permission_error(path)
88
90
  rescue RuntimeError => error
89
91
  if error.message =~ /unmatched platform/
90
92
  puts("unmatched platform for file #{path}")
@@ -103,11 +105,15 @@ module Bootsnap
103
105
  crc = Zlib.crc32(option.inspect)
104
106
  Bootsnap::CompileCache::Native.compile_option_crc32 = crc
105
107
  end
106
- compile_option_updated
108
+ compile_option_updated if supported?
107
109
 
108
110
  def self.install!(cache_dir)
109
111
  Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
112
+
113
+ return unless supported?
114
+
110
115
  Bootsnap::CompileCache::ISeq.compile_option_updated
116
+
111
117
  class << RubyVM::InstructionSequence
112
118
  prepend(InstructionSequenceMixin)
113
119
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("bootsnap/bootsnap")
3
+ require "bootsnap/bootsnap"
4
4
 
5
5
  module Bootsnap
6
6
  module CompileCache
@@ -46,8 +46,8 @@ module Bootsnap
46
46
  end
47
47
 
48
48
  def init!
49
- require("json")
50
- require("msgpack")
49
+ require "json"
50
+ require "msgpack"
51
51
 
52
52
  self.msgpack_factory = MessagePack::Factory.new
53
53
  self.supported_options = [:symbolize_names]
@@ -74,16 +74,12 @@ module Bootsnap
74
74
  return super unless (kwargs.keys - ::Bootsnap::CompileCache::JSON.supported_options).empty?
75
75
  end
76
76
 
77
- begin
78
- ::Bootsnap::CompileCache::Native.fetch(
79
- Bootsnap::CompileCache::JSON.cache_dir,
80
- File.realpath(path),
81
- ::Bootsnap::CompileCache::JSON,
82
- kwargs,
83
- )
84
- rescue Errno::EACCES
85
- ::Bootsnap::CompileCache.permission_error(path)
86
- end
77
+ ::Bootsnap::CompileCache::Native.fetch(
78
+ Bootsnap::CompileCache::JSON.cache_dir,
79
+ File.realpath(path),
80
+ ::Bootsnap::CompileCache::JSON,
81
+ kwargs,
82
+ )
87
83
  end
88
84
 
89
85
  ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("bootsnap/bootsnap")
3
+ require "bootsnap/bootsnap"
4
4
 
5
5
  module Bootsnap
6
6
  module CompileCache
@@ -55,9 +55,9 @@ module Bootsnap
55
55
  end
56
56
 
57
57
  def init!
58
- require("yaml")
59
- require("msgpack")
60
- require("date")
58
+ require "yaml"
59
+ require "msgpack"
60
+ require "date"
61
61
 
62
62
  @implementation = ::YAML::VERSION >= "4" ? Psych4 : Psych3
63
63
  if @implementation::Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
@@ -229,16 +229,12 @@ module Bootsnap
229
229
  return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
230
230
  end
231
231
 
232
- begin
233
- CompileCache::Native.fetch(
234
- CompileCache::YAML.cache_dir,
235
- File.realpath(path),
236
- CompileCache::YAML::Psych4::SafeLoad,
237
- kwargs,
238
- )
239
- rescue Errno::EACCES
240
- CompileCache.permission_error(path)
241
- end
232
+ CompileCache::Native.fetch(
233
+ CompileCache::YAML.cache_dir,
234
+ File.realpath(path),
235
+ CompileCache::YAML::Psych4::SafeLoad,
236
+ kwargs,
237
+ )
242
238
  end
243
239
 
244
240
  ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
@@ -253,16 +249,12 @@ module Bootsnap
253
249
  return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
254
250
  end
255
251
 
256
- begin
257
- CompileCache::Native.fetch(
258
- CompileCache::YAML.cache_dir,
259
- File.realpath(path),
260
- CompileCache::YAML::Psych4::UnsafeLoad,
261
- kwargs,
262
- )
263
- rescue Errno::EACCES
264
- CompileCache.permission_error(path)
265
- end
252
+ CompileCache::Native.fetch(
253
+ CompileCache::YAML.cache_dir,
254
+ File.realpath(path),
255
+ CompileCache::YAML::Psych4::UnsafeLoad,
256
+ kwargs,
257
+ )
266
258
  end
267
259
 
268
260
  ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
@@ -309,16 +301,12 @@ module Bootsnap
309
301
  return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
310
302
  end
311
303
 
312
- begin
313
- CompileCache::Native.fetch(
314
- CompileCache::YAML.cache_dir,
315
- File.realpath(path),
316
- CompileCache::YAML::Psych3,
317
- kwargs,
318
- )
319
- rescue Errno::EACCES
320
- CompileCache.permission_error(path)
321
- end
304
+ CompileCache::Native.fetch(
305
+ CompileCache::YAML.cache_dir,
306
+ File.realpath(path),
307
+ CompileCache::YAML::Psych3,
308
+ kwargs,
309
+ )
322
310
  end
323
311
 
324
312
  ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
@@ -333,16 +321,12 @@ module Bootsnap
333
321
  return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
334
322
  end
335
323
 
336
- begin
337
- CompileCache::Native.fetch(
338
- CompileCache::YAML.cache_dir,
339
- File.realpath(path),
340
- CompileCache::YAML::Psych3,
341
- kwargs,
342
- )
343
- rescue Errno::EACCES
344
- CompileCache.permission_error(path)
345
- end
324
+ CompileCache::Native.fetch(
325
+ CompileCache::YAML.cache_dir,
326
+ File.realpath(path),
327
+ CompileCache::YAML::Psych3,
328
+ kwargs,
329
+ )
346
330
  end
347
331
 
348
332
  ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
@@ -8,12 +8,11 @@ module Bootsnap
8
8
  end
9
9
 
10
10
  Error = Class.new(StandardError)
11
- PermissionError = Class.new(Error)
12
11
 
13
- def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false)
12
+ def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false, revalidation: false)
14
13
  if iseq
15
14
  if supported?
16
- require_relative("compile_cache/iseq")
15
+ require_relative "compile_cache/iseq"
17
16
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
18
17
  elsif $VERBOSE
19
18
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -22,7 +21,7 @@ module Bootsnap
22
21
 
23
22
  if yaml
24
23
  if supported?
25
- require_relative("compile_cache/yaml")
24
+ require_relative "compile_cache/yaml"
26
25
  Bootsnap::CompileCache::YAML.install!(cache_dir)
27
26
  elsif $VERBOSE
28
27
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
@@ -31,7 +30,7 @@ module Bootsnap
31
30
 
32
31
  if json
33
32
  if supported?
34
- require_relative("compile_cache/json")
33
+ require_relative "compile_cache/json"
35
34
  Bootsnap::CompileCache::JSON.install!(cache_dir)
36
35
  elsif $VERBOSE
37
36
  warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
@@ -40,21 +39,14 @@ module Bootsnap
40
39
 
41
40
  if supported? && defined?(Bootsnap::CompileCache::Native)
42
41
  Bootsnap::CompileCache::Native.readonly = readonly
42
+ Bootsnap::CompileCache::Native.revalidation = revalidation
43
43
  end
44
44
  end
45
45
 
46
- def self.permission_error(path)
47
- cpath = Bootsnap::CompileCache::ISeq.cache_dir
48
- raise(
49
- PermissionError,
50
- "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
51
- "(or, less likely, doesn't have permission to read '#{path}')",
52
- )
53
- end
54
-
55
46
  def self.supported?
56
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
57
- RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
47
+ # only enable on 'ruby' (MRI) and TruffleRuby for POSIX (darwin, linux, *bsd), Windows (RubyInstaller2)
48
+ %w[ruby truffleruby].include?(RUBY_ENGINE) &&
49
+ RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
58
50
  end
59
51
  end
60
52
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -24,8 +24,16 @@ module Bootsnap
24
24
  @mutex.synchronize { @dirs[dir] }
25
25
  end
26
26
 
27
+ TRUFFLERUBY_LIB_DIR_PREFIX = if RUBY_ENGINE == "truffleruby"
28
+ "#{File.join(RbConfig::CONFIG['libdir'], 'truffle')}#{File::SEPARATOR}"
29
+ end
30
+
27
31
  # { 'enumerator' => nil, 'enumerator.so' => nil, ... }
28
32
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
33
+ if TRUFFLERUBY_LIB_DIR_PREFIX && feat.start_with?(TRUFFLERUBY_LIB_DIR_PREFIX)
34
+ feat = feat.byteslice(TRUFFLERUBY_LIB_DIR_PREFIX.bytesize..-1)
35
+ end
36
+
29
37
  # Builtin features are of the form 'enumerator.so'.
30
38
  # All others include paths.
31
39
  next unless feat.size < 20 && !feat.include?("/")
@@ -54,6 +54,12 @@ module Bootsnap
54
54
  ret
55
55
  end
56
56
  end
57
+
58
+ def dup
59
+ [] + self
60
+ end
61
+
62
+ alias_method :clone, :dup
57
63
  end
58
64
 
59
65
  def self.register(arr, observer)
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kernel
4
- module_function
4
+ alias_method :require_without_bootsnap, :require
5
5
 
6
- alias_method(:require_without_bootsnap, :require)
6
+ alias_method :require, :require # Avoid method redefinition warnings
7
7
 
8
- def require(path)
8
+ def require(path) # rubocop:disable Lint/DuplicateMethods
9
9
  return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
10
10
 
11
11
  string_path = Bootsnap.rb_get_path(path)
@@ -24,9 +24,7 @@ module Kernel
24
24
  elsif false == resolved
25
25
  return false
26
26
  elsif resolved.nil?
27
- error = LoadError.new(+"cannot load such file -- #{path}")
28
- error.instance_variable_set(:@path, path)
29
- raise error
27
+ return require_without_bootsnap(path)
30
28
  else
31
29
  # Note that require registers to $LOADED_FEATURES while load does not.
32
30
  ret = require_without_bootsnap(resolved)
@@ -34,4 +32,6 @@ module Kernel
34
32
  return ret
35
33
  end
36
34
  end
35
+
36
+ private :require
37
37
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("path_scanner")
3
+ require_relative "path_scanner"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -54,7 +54,7 @@ module Bootsnap
54
54
 
55
55
  absolute_path = "#{absolute_dir_path}/#{name}"
56
56
  if File.directory?(absolute_path)
57
- next if ignored_directories.include?(name)
57
+ next if ignored_directories.include?(name) || ignored_directories.include?(absolute_path)
58
58
 
59
59
  if yield relative_path, absolute_path, true
60
60
  walk(absolute_path, relative_path, &block)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
- Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require "msgpack" }
6
6
 
7
7
  module Bootsnap
8
8
  module LoadPathCache
@@ -38,11 +38,11 @@ module Bootsnap
38
38
 
39
39
  @loaded_features_index = LoadedFeaturesIndex.new
40
40
 
41
- @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
42
41
  PathScanner.ignored_directories = ignore_directories if ignore_directories
42
+ @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
43
43
  @enabled = true
44
- require_relative("load_path_cache/core_ext/kernel_require")
45
- require_relative("load_path_cache/core_ext/loaded_features")
44
+ require_relative "load_path_cache/core_ext/kernel_require"
45
+ require_relative "load_path_cache/core_ext/loaded_features"
46
46
  end
47
47
 
48
48
  def unload!
@@ -50,22 +50,31 @@ module Bootsnap
50
50
  @loaded_features_index = nil
51
51
  @realpath_cache = nil
52
52
  @load_path_cache = nil
53
- ChangeObserver.unregister($LOAD_PATH)
53
+ ChangeObserver.unregister($LOAD_PATH) if supported?
54
54
  end
55
55
 
56
56
  def supported?
57
- RUBY_ENGINE == "ruby" &&
58
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
57
+ if RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
58
+ case RUBY_ENGINE
59
+ when "truffleruby"
60
+ # https://github.com/oracle/truffleruby/issues/3131
61
+ RUBY_ENGINE_VERSION >= "23.1.0"
62
+ when "ruby"
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
59
68
  end
60
69
  end
61
70
  end
62
71
  end
63
72
 
64
73
  if Bootsnap::LoadPathCache.supported?
65
- require_relative("load_path_cache/path_scanner")
66
- require_relative("load_path_cache/path")
67
- require_relative("load_path_cache/cache")
68
- require_relative("load_path_cache/store")
69
- require_relative("load_path_cache/change_observer")
70
- require_relative("load_path_cache/loaded_features_index")
74
+ require_relative "load_path_cache/path_scanner"
75
+ require_relative "load_path_cache/path"
76
+ require_relative "load_path_cache/cache"
77
+ require_relative "load_path_cache/store"
78
+ require_relative "load_path_cache/change_observer"
79
+ require_relative "load_path_cache/loaded_features_index"
71
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../bootsnap")
3
+ require_relative "../bootsnap"
4
4
 
5
5
  Bootsnap.default_setup
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- VERSION = "1.16.0"
4
+ VERSION = "1.18.3"
5
5
  end
data/lib/bootsnap.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("bootsnap/version")
4
- require_relative("bootsnap/bundler")
5
- require_relative("bootsnap/load_path_cache")
6
- require_relative("bootsnap/compile_cache")
3
+ require_relative "bootsnap/version"
4
+ require_relative "bootsnap/bundler"
5
+ require_relative "bootsnap/load_path_cache"
6
+ require_relative "bootsnap/compile_cache"
7
7
 
8
8
  module Bootsnap
9
9
  InvalidConfiguration = Class.new(StandardError)
@@ -11,6 +11,16 @@ module Bootsnap
11
11
  class << self
12
12
  attr_reader :logger
13
13
 
14
+ def log_stats!
15
+ stats = {hit: 0, revalidated: 0, miss: 0, stale: 0}
16
+ self.instrumentation = ->(event, _path) { stats[event] += 1 }
17
+ Kernel.at_exit do
18
+ stats.each do |event, count|
19
+ $stderr.puts "bootsnap #{event}: #{count}"
20
+ end
21
+ end
22
+ end
23
+
14
24
  def log!
15
25
  self.logger = $stderr.method(:puts)
16
26
  end
@@ -18,9 +28,9 @@ module Bootsnap
18
28
  def logger=(logger)
19
29
  @logger = logger
20
30
  self.instrumentation = if logger.respond_to?(:debug)
21
- ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
31
+ ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") unless event == :hit }
22
32
  else
23
- ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
33
+ ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") unless event == :hit }
24
34
  end
25
35
  end
26
36
 
@@ -41,6 +51,7 @@ module Bootsnap
41
51
  load_path_cache: true,
42
52
  ignore_directories: nil,
43
53
  readonly: false,
54
+ revalidation: false,
44
55
  compile_cache_iseq: true,
45
56
  compile_cache_yaml: true,
46
57
  compile_cache_json: true
@@ -60,6 +71,7 @@ module Bootsnap
60
71
  yaml: compile_cache_yaml,
61
72
  json: compile_cache_json,
62
73
  readonly: readonly,
74
+ revalidation: revalidation,
63
75
  )
64
76
  end
65
77
 
@@ -110,6 +122,8 @@ module Bootsnap
110
122
 
111
123
  if ENV["BOOTSNAP_LOG"]
112
124
  log!
125
+ elsif ENV["BOOTSNAP_STATS"]
126
+ log_stats!
113
127
  end
114
128
  end
115
129
  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: 1.16.0
4
+ version: 1.18.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-25 00:00:00.000000000 Z
11
+ date: 2024-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -83,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  requirements: []
86
- rubygems_version: 3.3.3
86
+ rubygems_version: 3.5.5
87
87
  signing_key:
88
88
  specification_version: 4
89
89
  summary: Boot large ruby/rails apps faster