bootsnap 1.16.0 → 1.18.3

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: 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