bootsnap 1.4.6 → 1.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +45 -14
  4. data/exe/bootsnap +5 -0
  5. data/ext/bootsnap/bootsnap.c +229 -65
  6. data/ext/bootsnap/extconf.rb +19 -14
  7. data/lib/bootsnap.rb +90 -15
  8. data/lib/bootsnap/cli.rb +246 -0
  9. data/lib/bootsnap/cli/worker_pool.rb +131 -0
  10. data/lib/bootsnap/compile_cache.rb +2 -2
  11. data/lib/bootsnap/compile_cache/iseq.rb +21 -7
  12. data/lib/bootsnap/compile_cache/yaml.rb +109 -40
  13. data/lib/bootsnap/load_path_cache.rb +3 -16
  14. data/lib/bootsnap/load_path_cache/cache.rb +23 -6
  15. data/lib/bootsnap/load_path_cache/change_observer.rb +1 -1
  16. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +16 -4
  17. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +3 -3
  18. data/lib/bootsnap/load_path_cache/path.rb +2 -2
  19. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -26
  20. data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
  21. data/lib/bootsnap/load_path_cache/store.rb +17 -9
  22. data/lib/bootsnap/setup.rb +1 -36
  23. data/lib/bootsnap/version.rb +1 -1
  24. metadata +16 -30
  25. data/.github/CODEOWNERS +0 -2
  26. data/.github/probots.yml +0 -2
  27. data/.gitignore +0 -17
  28. data/.rubocop.yml +0 -20
  29. data/.travis.yml +0 -21
  30. data/CODE_OF_CONDUCT.md +0 -74
  31. data/CONTRIBUTING.md +0 -21
  32. data/Gemfile +0 -9
  33. data/README.jp.md +0 -231
  34. data/Rakefile +0 -13
  35. data/bin/ci +0 -10
  36. data/bin/console +0 -15
  37. data/bin/setup +0 -8
  38. data/bin/test-minimal-support +0 -7
  39. data/bin/testunit +0 -8
  40. data/bootsnap.gemspec +0 -46
  41. data/dev.yml +0 -10
  42. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -107
  43. data/shipit.rubygems.yml +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d4f38db9a609c2adb0a0ede991bd993dff7ae59885cb1722eb699658211fd96
4
- data.tar.gz: 9f363c21a154e123693f18e48073451c6cfe6c05ec378c980e6ef770f01e658c
3
+ metadata.gz: b7dac6bb1ac2018e46d1cdac77a243cc1e2d46d8dc24deb0a9248320451549ba
4
+ data.tar.gz: 9cd95e8c52993daffd79f1d21961c5754f88642250c9552881f30f26f9dcc462
5
5
  SHA512:
6
- metadata.gz: 925f595e21911c61ff7cf3a86cb055d25e56bb65a8a6437c513f25bfea3aec6c086259808b5aed3289e5d82f66706f98314f31d2ff3886d85edeb47085d6a918
7
- data.tar.gz: 31507ba8393d47361f8332064a9a39220392a4242e1f3f3c3a88c4de032b51eb8aab8d3769869fec7f9da55400ce773c42aecc52604d5293c9ff7ea3f9f40e54
6
+ metadata.gz: e6cee7f1ed6c30d819e576fa79d1e627585996d139dd4add684fc22d4bf720ae1c0f778bb1ee23c2a7819d788c7956b9632f03b5861bb7f6e507f3562687b70e
7
+ data.tar.gz: 505de0bcbfa90ab03fde256bea4dec4f5728482b61d452887da7a49c84894daff61c1aa0b16a2a114ddab142ae2a75d09f4447de80a6b33068ea0c15ec0fdd8f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,82 @@
1
+ # Unreleased
2
+
3
+ # 1.7.5
4
+
5
+ * Handle a regression of Ruby 2.7.3 causing Bootsnap to call the deprecated `untaint` method. (#360)
6
+ * Gracefully handle read-only file system as well as other errors preventing to persist the load path cache. (#358)
7
+
8
+ # 1.7.4
9
+
10
+ * Stop raising errors when encoutering various file system errors. The cache is now best effort,
11
+ if somehow it can't be saved, bootsnapp will gracefully fallback to the original operation (e.g. `Kernel.require`).
12
+ (#353, #177, #262)
13
+
14
+ # 1.7.3
15
+
16
+ * Disable YAML precompilation when encountering YAML tags. (#351)
17
+
18
+ # 1.7.2
19
+
20
+ * Fix compatibility with msgpack < 1. (#349)
21
+
22
+ # 1.7.1
23
+
24
+ * Warn Ruby 2.5 users if they turn ISeq caching on. (#327, #244)
25
+ * Disable ISeq caching for the whole 2.5.x series again.
26
+ * Better handle hashing of Ruby strings. (#318)
27
+
28
+ # 1.7.0
29
+
30
+ * Fix detection of YAML files in gems.
31
+ * Adds an instrumentation API to monitor cache misses.
32
+ * Allow to control the behavior of `require 'bootsnap/setup'` using environment variables.
33
+ * Deprecate the `disable_trace` option.
34
+ * Deprecate the `ActiveSupport::Dependencies` (AKA Classic autoloader) integration. (#344)
35
+
36
+ # 1.6.0
37
+
38
+ * Fix a Ruby 2.7/3.0 issue with `YAML.load_file` keyword arguments. (#342)
39
+ * `bootsnap precompile` CLI use multiple processes to complete faster. (#341)
40
+ * `bootsnap precompile` CLI also precompile YAML files. (#340)
41
+ * Changed the load path cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-load-path-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/load-path-cache` for ease of use. (#334)
42
+ * Changed the compile cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-compile-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/compile-cache` for ease of use. (#334)
43
+
44
+ # 1.5.1
45
+
46
+ * Workaround a Ruby bug in InstructionSequence.compile_file. (#332)
47
+
48
+ # 1.5.0
49
+
50
+ * Add a command line to statically precompile the ISeq cache. (#326)
51
+
52
+ # 1.4.9
53
+
54
+ * [Windows support](https://github.com/Shopify/bootsnap/pull/319)
55
+ * [Fix potential crash](https://github.com/Shopify/bootsnap/pull/322)
56
+
57
+ # 1.4.8
58
+
59
+ * [Prevent FallbackScan from polluting exception cause](https://github.com/Shopify/bootsnap/pull/314)
60
+
61
+ # 1.4.7
62
+
63
+ * Various performance enhancements
64
+ * Fix race condition in heavy concurrent load scenarios that would cause bootsnap to raise
65
+
66
+ # 1.4.6
67
+
68
+ * Fix bug that was erroneously considering that files containing `.` in the names were being
69
+ required if a different file with the same name was already being required
70
+
71
+ Example:
72
+
73
+ require 'foo'
74
+ require 'foo.en'
75
+
76
+ Before bootsnap was considering `foo.en` to be the same file as `foo`
77
+
78
+ * Use glibc as part of the ruby_platform cache key
79
+
1
80
  # 1.4.5
2
81
 
3
82
  * MRI 2.7 support
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Bootsnap [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](https://travis-ci.org/Shopify/bootsnap)
1
+ # Bootsnap [![Actions Status](https://github.com/Shopify/bootsnap/workflows/ci/badge.svg)](https://github.com/Shopify/bootsnap/actions)
2
2
 
3
- Bootsnap is a library that plugs into Ruby, with optional support for `ActiveSupport` and `YAML`,
3
+ Bootsnap is a library that plugs into Ruby, with optional support for `YAML`,
4
4
  to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work).
5
5
 
6
6
  #### Performance
@@ -11,7 +11,7 @@ to optimize and cache expensive computations. See [How Does This Work](#how-does
11
11
  - The core Shopify platform -- a rather large monolithic application -- boots about 75% faster,
12
12
  dropping from around 25s to 6.5s.
13
13
  * In Shopify core (a large app), about 25% of this gain can be attributed to `compile_cache_*`
14
- features; 75% to path caching, and ~1% to `disable_trace`. This is fairly representative.
14
+ features; 75% to path caching. This is fairly representative.
15
15
 
16
16
  ## Usage
17
17
 
@@ -29,7 +29,8 @@ If you are using Rails, add this to `config/boot.rb` immediately after `require
29
29
  require 'bootsnap/setup'
30
30
  ```
31
31
 
32
- Note that bootsnap writes to `tmp/cache`, and that directory *must* be writable. Rails will fail to
32
+ Note that bootsnap writes to `tmp/cache` (or the path specified by `ENV['BOOTSNAP_CACHE_DIR']`),
33
+ and that directory *must* be writable. Rails will fail to
33
34
  boot if it is not. If this is unacceptable (e.g. you are running in a read-only container and
34
35
  unwilling to mount in a writable tmpdir), you should remove this line or wrap it in a conditional.
35
36
 
@@ -53,15 +54,11 @@ Bootsnap.setup(
53
54
  cache_dir: 'tmp/cache', # Path to your cache
54
55
  development_mode: env == 'development', # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc
55
56
  load_path_cache: true, # Optimize the LOAD_PATH with a cache
56
- autoload_paths_cache: true, # Optimize ActiveSupport autoloads with cache
57
- disable_trace: true, # Set `RubyVM::InstructionSequence.compile_option = { trace_instruction: false }`
58
57
  compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting.
59
58
  compile_cache_yaml: true # Compile YAML into a cache
60
59
  )
61
60
  ```
62
61
 
63
- **Note that `disable_trace` will break debuggers and tracing.**
64
-
65
62
  **Protip:** You can replace `require 'bootsnap'` with `BootLib::Require.from_gem('bootsnap',
66
63
  'bootsnap')` using [this trick](https://github.com/Shopify/bootsnap/wiki/Bootlib::Require). This
67
64
  will help optimize boot time further if you have an extremely large `$LOAD_PATH`.
@@ -71,12 +68,39 @@ speeds up the loading of individual source files, Spring keeps a copy of a pre-b
71
68
  on hand to completely skip parts of the boot process the next time it's needed. The two tools work
72
69
  well together, and are both included in a newly-generated Rails applications by default.
73
70
 
71
+ ### Environment variables
72
+
73
+ `require 'bootsnap/setup'` behavior can be changed using environment variables:
74
+
75
+ - `BOOTSNAP_CACHE_DIR` allows to define the cache location.
76
+ - `DISABLE_BOOTSNAP` allows to entirely disable bootsnap.
77
+ - `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching.
78
+ - `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
79
+ - `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR.
80
+
74
81
  ### Environments
75
82
 
76
83
  All Bootsnap features are enabled in development, test, production, and all other environments according to the configuration in the setup. At Shopify, we use this gem safely in all environments without issue.
77
84
 
78
85
  If you would like to disable any feature for a certain environment, we suggest changing the configuration to take into account the appropriate ENV var or configuration according to your needs.
79
86
 
87
+ ### Instrumentation
88
+
89
+ Bootsnap cache misses can be monitored though a callback:
90
+
91
+ ```ruby
92
+ Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
93
+ ```
94
+
95
+ `event` is either `:miss` or `:stale`. You can also call `Bootsnap.log!` as a shortcut to
96
+ log all events to STDERR.
97
+
98
+ To turn instrumentation back off you can set it to nil:
99
+
100
+ ```ruby
101
+ Bootsnap.instrumentation = nil
102
+ ```
103
+
80
104
  ## How does this work?
81
105
 
82
106
  Bootsnap optimizes methods to cache results of expensive computations, and can be grouped
@@ -84,8 +108,6 @@ into two broad categories:
84
108
 
85
109
  * [Path Pre-Scanning](#path-pre-scanning)
86
110
  * `Kernel#require` and `Kernel#load` are modified to eliminate `$LOAD_PATH` scans.
87
- * `ActiveSupport::Dependencies.{autoloadable_module?,load_missing_constant,depend_on}` are
88
- overridden to eliminate scans of `ActiveSupport::Dependencies.autoload_paths`.
89
111
  * [Compilation caching](#compilation-caching)
90
112
  * `RubyVM::InstructionSequence.load_iseq` is implemented to cache the result of ruby bytecode
91
113
  compilation.
@@ -124,10 +146,6 @@ open y/foo.rb
124
146
  ...
125
147
  ```
126
148
 
127
- Exactly the same strategy is employed for methods that traverse
128
- `ActiveSupport::Dependencies.autoload_paths` if the `autoload_paths_cache` option is given to
129
- `Bootsnap.setup`.
130
-
131
149
  The following diagram flowcharts the overrides that make the `*_path_cache` features work.
132
150
 
133
151
  ![Flowchart explaining
@@ -294,6 +312,19 @@ open /c/nope.bundle -> -1
294
312
  # (nothing!)
295
313
  ```
296
314
 
315
+ ## Precompilation
316
+
317
+ In development environments the bootsnap compilation cache is generated on the fly when source files are loaded.
318
+ But in production environments, such as docker images, you might need to precompile the cache.
319
+
320
+ To do so you can use the `bootsnap precompile` command.
321
+
322
+ Example:
323
+
324
+ ```bash
325
+ $ bundle exec bootsnap precompile --gemfile app/ lib/
326
+ ```
327
+
297
328
  ## When not to use Bootsnap
298
329
 
299
330
  *Alternative engines*: Bootsnap is pretty reliant on MRI features, and parts are disabled entirely on alternative ruby
data/exe/bootsnap ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bootsnap/cli'
5
+ exit Bootsnap::CLI.new(ARGV).run
@@ -14,6 +14,7 @@
14
14
  #include "bootsnap.h"
15
15
  #include "ruby.h"
16
16
  #include <stdint.h>
17
+ #include <stdbool.h>
17
18
  #include <sys/types.h>
18
19
  #include <errno.h>
19
20
  #include <fcntl.h>
@@ -32,6 +33,12 @@
32
33
 
33
34
  #define KEY_SIZE 64
34
35
 
36
+ #define MAX_CREATE_TEMPFILE_ATTEMPT 3
37
+
38
+ #ifndef RB_UNLIKELY
39
+ #define RB_UNLIKELY(x) (x)
40
+ #endif
41
+
35
42
  /*
36
43
  * An instance of this key is written as the first 64 bytes of each cache file.
37
44
  * The mtime and size members track whether the file contents have changed, and
@@ -68,7 +75,7 @@ struct bs_cache_key {
68
75
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
69
76
 
70
77
  /* Effectively a schema version. Bumping invalidates all previous caches */
71
- static const uint32_t current_version = 2;
78
+ static const uint32_t current_version = 3;
72
79
 
73
80
  /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
74
81
  * new OS ABI, etc. */
@@ -86,19 +93,25 @@ static VALUE rb_mBootsnap_CompileCache;
86
93
  static VALUE rb_mBootsnap_CompileCache_Native;
87
94
  static VALUE rb_eBootsnap_CompileCache_Uncompilable;
88
95
  static ID uncompilable;
96
+ static ID instrumentation_method;
97
+ static VALUE sym_miss;
98
+ static VALUE sym_stale;
99
+ static bool instrumentation_enabled = false;
89
100
 
90
101
  /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
102
+ static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
91
103
  static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
92
- static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
104
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
105
+ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
93
106
 
94
107
  /* Helpers */
95
- static uint64_t fnv1a_64(const char *str);
96
- static void bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
108
+ static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
97
109
  static int bs_read_key(int fd, struct bs_cache_key * key);
98
110
  static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
99
- static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
111
+ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
112
+ static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
100
113
  static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance);
101
- static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
114
+ static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
102
115
  static uint32_t get_ruby_revision(void);
103
116
  static uint32_t get_ruby_platform(void);
104
117
 
@@ -106,12 +119,12 @@ static uint32_t get_ruby_platform(void);
106
119
  * Helper functions to call ruby methods on handler object without crashing on
107
120
  * exception.
108
121
  */
109
- static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data);
122
+ static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data);
110
123
  static VALUE prot_storage_to_output(VALUE arg);
111
124
  static VALUE prot_input_to_output(VALUE arg);
112
- static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag);
125
+ static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag);
113
126
  static VALUE prot_input_to_storage(VALUE arg);
114
- static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data);
127
+ static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data);
115
128
  struct s2o_data;
116
129
  struct i2o_data;
117
130
  struct i2s_data;
@@ -144,15 +157,31 @@ Init_bootsnap(void)
144
157
  current_ruby_platform = get_ruby_platform();
145
158
 
146
159
  uncompilable = rb_intern("__bootsnap_uncompilable__");
160
+ instrumentation_method = rb_intern("_instrument");
161
+
162
+ sym_miss = ID2SYM(rb_intern("miss"));
163
+ rb_global_variable(&sym_miss);
147
164
 
165
+ sym_stale = ID2SYM(rb_intern("stale"));
166
+ rb_global_variable(&sym_stale);
167
+
168
+ rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
148
169
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
149
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 3);
170
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
171
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
150
172
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
151
173
 
152
174
  current_umask = umask(0777);
153
175
  umask(current_umask);
154
176
  }
155
177
 
178
+ static VALUE
179
+ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
180
+ {
181
+ instrumentation_enabled = RTEST(enabled);
182
+ return enabled;
183
+ }
184
+
156
185
  /*
157
186
  * Bootsnap's ruby code registers a hook that notifies us via this function
158
187
  * when compile_option changes. These changes invalidate all existing caches.
@@ -181,7 +210,7 @@ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
181
210
  * - 32 bits doesn't feel collision-resistant enough; 64 is nice.
182
211
  */
183
212
  static uint64_t
184
- fnv1a_64_iter(uint64_t h, const char *str)
213
+ fnv1a_64_iter_cstr(uint64_t h, const char *str)
185
214
  {
186
215
  unsigned char *s = (unsigned char *)str;
187
216
 
@@ -194,7 +223,21 @@ fnv1a_64_iter(uint64_t h, const char *str)
194
223
  }
195
224
 
196
225
  static uint64_t
197
- fnv1a_64(const char *str)
226
+ fnv1a_64_iter(uint64_t h, const VALUE str)
227
+ {
228
+ unsigned char *s = (unsigned char *)RSTRING_PTR(str);
229
+ unsigned char *str_end = (unsigned char *)RSTRING_PTR(str) + RSTRING_LEN(str);
230
+
231
+ while (s < str_end) {
232
+ h ^= (uint64_t)*s++;
233
+ h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40);
234
+ }
235
+
236
+ return h;
237
+ }
238
+
239
+ static uint64_t
240
+ fnv1a_64(const VALUE str)
198
241
  {
199
242
  uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
200
243
  return fnv1a_64_iter(h, str);
@@ -215,7 +258,7 @@ get_ruby_revision(void)
215
258
  } else {
216
259
  uint64_t hash;
217
260
 
218
- hash = fnv1a_64(StringValueCStr(ruby_revision));
261
+ hash = fnv1a_64(ruby_revision);
219
262
  return (uint32_t)(hash >> 32);
220
263
  }
221
264
  }
@@ -235,19 +278,19 @@ get_ruby_platform(void)
235
278
  VALUE ruby_platform;
236
279
 
237
280
  ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
238
- hash = fnv1a_64(RSTRING_PTR(ruby_platform));
281
+ hash = fnv1a_64(ruby_platform);
239
282
 
240
283
  #ifdef _WIN32
241
284
  return (uint32_t)(hash >> 32) ^ (uint32_t)GetVersion();
242
285
  #elif defined(__GLIBC__)
243
- hash = fnv1a_64_iter(hash, gnu_get_libc_version());
286
+ hash = fnv1a_64_iter_cstr(hash, gnu_get_libc_version());
244
287
  return (uint32_t)(hash >> 32);
245
288
  #else
246
289
  struct utsname utsname;
247
290
 
248
291
  /* Not worth crashing if this fails; lose extra cache invalidation potential */
249
292
  if (uname(&utsname) >= 0) {
250
- hash = fnv1a_64_iter(hash, utsname.version);
293
+ hash = fnv1a_64_iter_cstr(hash, utsname.version);
251
294
  }
252
295
 
253
296
  return (uint32_t)(hash >> 32);
@@ -262,14 +305,13 @@ get_ruby_platform(void)
262
305
  * The path will look something like: <cachedir>/12/34567890abcdef
263
306
  */
264
307
  static void
265
- bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE])
308
+ bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE])
266
309
  {
267
310
  uint64_t hash = fnv1a_64(path);
268
-
269
311
  uint8_t first_byte = (hash >> (64 - 8));
270
312
  uint64_t remainder = hash & 0x00ffffffffffffff;
271
313
 
272
- sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder);
314
+ sprintf(*cache_path, "%s/%02"PRIx8"/%014"PRIx64, cachedir, first_byte, remainder);
273
315
  }
274
316
 
275
317
  /*
@@ -299,7 +341,7 @@ cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
299
341
  * conversions on the ruby VALUE arguments before passing them along.
300
342
  */
301
343
  static VALUE
302
- bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
344
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args)
303
345
  {
304
346
  FilePathValue(path_v);
305
347
 
@@ -315,11 +357,37 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
315
357
  char cache_path[MAX_CACHEPATH_SIZE];
316
358
 
317
359
  /* generate cache path to cache_path */
318
- bs_cache_path(cachedir, path, &cache_path);
360
+ bs_cache_path(cachedir, path_v, &cache_path);
319
361
 
320
- return bs_fetch(path, path_v, cache_path, handler);
362
+ return bs_fetch(path, path_v, cache_path, handler, args);
321
363
  }
322
364
 
365
+ /*
366
+ * Entrypoint for Bootsnap::CompileCache::Native.precompile.
367
+ * Similar to fetch, but it only generate the cache if missing
368
+ * and doesn't return the content.
369
+ */
370
+ static VALUE
371
+ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
372
+ {
373
+ FilePathValue(path_v);
374
+
375
+ Check_Type(cachedir_v, T_STRING);
376
+ Check_Type(path_v, T_STRING);
377
+
378
+ if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
379
+ rb_raise(rb_eArgError, "cachedir too long");
380
+ }
381
+
382
+ char * cachedir = RSTRING_PTR(cachedir_v);
383
+ char * path = RSTRING_PTR(path_v);
384
+ char cache_path[MAX_CACHEPATH_SIZE];
385
+
386
+ /* generate cache path to cache_path */
387
+ bs_cache_path(cachedir, path_v, &cache_path);
388
+
389
+ return bs_precompile(path, path_v, cache_path, handler);
390
+ }
323
391
  /*
324
392
  * Open the file we want to load/cache and generate a cache key for it if it
325
393
  * was loaded.
@@ -356,7 +424,8 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
356
424
  }
357
425
 
358
426
  #define ERROR_WITH_ERRNO -1
359
- #define CACHE_MISSING_OR_INVALID -2
427
+ #define CACHE_MISS -2
428
+ #define CACHE_STALE -3
360
429
 
361
430
  /*
362
431
  * Read the cache key from the given fd, which must have position 0 (e.g.
@@ -364,15 +433,16 @@ open_current_file(char * path, struct bs_cache_key * key, const char ** errno_pr
364
433
  *
365
434
  * Possible return values:
366
435
  * - 0 (OK, key was loaded)
367
- * - CACHE_MISSING_OR_INVALID (-2)
368
436
  * - ERROR_WITH_ERRNO (-1, errno is set)
437
+ * - CACHE_MISS (-2)
438
+ * - CACHE_STALE (-3)
369
439
  */
370
440
  static int
371
441
  bs_read_key(int fd, struct bs_cache_key * key)
372
442
  {
373
443
  ssize_t nread = read(fd, key, KEY_SIZE);
374
444
  if (nread < 0) return ERROR_WITH_ERRNO;
375
- if (nread < KEY_SIZE) return CACHE_MISSING_OR_INVALID;
445
+ if (nread < KEY_SIZE) return CACHE_STALE;
376
446
  return 0;
377
447
  }
378
448
 
@@ -382,7 +452,8 @@ bs_read_key(int fd, struct bs_cache_key * key)
382
452
  *
383
453
  * Possible return values:
384
454
  * - 0 (OK, key was loaded)
385
- * - CACHE_MISSING_OR_INVALID (-2)
455
+ * - CACHE_MISS (-2)
456
+ * - CACHE_STALE (-3)
386
457
  * - ERROR_WITH_ERRNO (-1, errno is set)
387
458
  */
388
459
  static int
@@ -393,8 +464,7 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
393
464
  fd = open(path, O_RDONLY);
394
465
  if (fd < 0) {
395
466
  *errno_provenance = "bs_fetch:open_cache_file:open";
396
- if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
397
- return ERROR_WITH_ERRNO;
467
+ return CACHE_MISS;
398
468
  }
399
469
  #ifdef _WIN32
400
470
  setmode(fd, O_BINARY);
@@ -426,7 +496,7 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
426
496
  * or exception, will be the final data returnable to the user.
427
497
  */
428
498
  static int
429
- fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
499
+ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
430
500
  {
431
501
  char * data = NULL;
432
502
  ssize_t nread;
@@ -448,13 +518,13 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data,
448
518
  goto done;
449
519
  }
450
520
  if (nread != data_size) {
451
- ret = CACHE_MISSING_OR_INVALID;
521
+ ret = CACHE_STALE;
452
522
  goto done;
453
523
  }
454
524
 
455
- storage_data = rb_str_new_static(data, data_size);
525
+ storage_data = rb_str_new(data, data_size);
456
526
 
457
- *exception_tag = bs_storage_to_output(handler, storage_data, output_data);
527
+ *exception_tag = bs_storage_to_output(handler, args, storage_data, output_data);
458
528
  ret = 0;
459
529
  done:
460
530
  if (data != NULL) xfree(data);
@@ -499,25 +569,32 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
499
569
  {
500
570
  char template[MAX_CACHEPATH_SIZE + 20];
501
571
  char * tmp_path;
502
- int fd, ret;
572
+ int fd, ret, attempt;
503
573
  ssize_t nwrite;
504
574
 
505
- tmp_path = strncpy(template, path, MAX_CACHEPATH_SIZE);
506
- strcat(tmp_path, ".tmp.XXXXXX");
575
+ for (attempt = 0; attempt < MAX_CREATE_TEMPFILE_ATTEMPT; ++attempt) {
576
+ tmp_path = strncpy(template, path, MAX_CACHEPATH_SIZE);
577
+ strcat(tmp_path, ".tmp.XXXXXX");
507
578
 
508
- // mkstemp modifies the template to be the actual created path
509
- fd = mkstemp(tmp_path);
510
- if (fd < 0) {
511
- if (mkpath(tmp_path, 0775) < 0) {
579
+ // mkstemp modifies the template to be the actual created path
580
+ fd = mkstemp(tmp_path);
581
+ if (fd > 0) break;
582
+
583
+ if (attempt == 0 && mkpath(tmp_path, 0775) < 0) {
512
584
  *errno_provenance = "bs_fetch:atomic_write_cache_file:mkpath";
513
585
  return -1;
514
586
  }
515
- fd = open(tmp_path, O_WRONLY | O_CREAT, 0664);
516
- if (fd < 0) {
517
- *errno_provenance = "bs_fetch:atomic_write_cache_file:open";
518
- return -1;
519
- }
520
587
  }
588
+ if (fd < 0) {
589
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:mkstemp";
590
+ return -1;
591
+ }
592
+
593
+ if (chmod(tmp_path, 0644) < 0) {
594
+ *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod";
595
+ return -1;
596
+ }
597
+
521
598
  #ifdef _WIN32
522
599
  setmode(fd, O_BINARY);
523
600
  #endif
@@ -615,7 +692,7 @@ bs_read_contents(int fd, size_t size, char ** contents, const char ** errno_prov
615
692
  * - Return storage_to_output(storage_data)
616
693
  */
617
694
  static VALUE
618
- bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
695
+ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args)
619
696
  {
620
697
  struct bs_cache_key cached_key, current_key;
621
698
  char * contents = NULL;
@@ -635,26 +712,34 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
635
712
 
636
713
  /* Open the cache key if it exists, and read its cache key in */
637
714
  cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
638
- if (cache_fd == CACHE_MISSING_OR_INVALID) {
715
+ if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
639
716
  /* This is ok: valid_cache remains false, we re-populate it. */
717
+ if (RB_UNLIKELY(instrumentation_enabled)) {
718
+ rb_funcall(rb_mBootsnap, instrumentation_method, 2, cache_fd == CACHE_MISS ? sym_miss : sym_stale, path_v);
719
+ }
640
720
  } else if (cache_fd < 0) {
641
721
  goto fail_errno;
642
722
  } else {
643
723
  /* True if the cache existed and no invalidating changes have occurred since
644
724
  * it was generated. */
645
725
  valid_cache = cache_key_equal(&current_key, &cached_key);
726
+ if (RB_UNLIKELY(instrumentation_enabled)) {
727
+ if (!valid_cache) {
728
+ rb_funcall(rb_mBootsnap, instrumentation_method, 2, sym_stale, path_v);
729
+ }
730
+ }
646
731
  }
647
732
 
648
733
  if (valid_cache) {
649
734
  /* Fetch the cache data and return it if we're able to load it successfully */
650
735
  res = fetch_cached_data(
651
- cache_fd, (ssize_t)cached_key.data_size, handler,
736
+ cache_fd, (ssize_t)cached_key.data_size, handler, args,
652
737
  &output_data, &exception_tag, &errno_provenance
653
738
  );
654
- if (exception_tag != 0) goto raise;
655
- else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
656
- else if (res == ERROR_WITH_ERRNO) goto fail_errno;
657
- else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
739
+ if (exception_tag != 0) goto raise;
740
+ else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
741
+ else if (res == ERROR_WITH_ERRNO) goto fail_errno;
742
+ else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
658
743
  }
659
744
  close(cache_fd);
660
745
  cache_fd = -1;
@@ -662,27 +747,29 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
662
747
 
663
748
  /* Read the contents of the source file into a buffer */
664
749
  if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno;
665
- input_data = rb_str_new_static(contents, current_key.size);
750
+ input_data = rb_str_new(contents, current_key.size);
666
751
 
667
752
  /* Try to compile the input_data using input_to_storage(input_data) */
668
- exception_tag = bs_input_to_storage(handler, input_data, path_v, &storage_data);
753
+ exception_tag = bs_input_to_storage(handler, args, input_data, path_v, &storage_data);
669
754
  if (exception_tag != 0) goto raise;
670
755
  /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
671
756
  * to cache anything; just return input_to_output(input_data) */
672
757
  if (storage_data == uncompilable) {
673
- bs_input_to_output(handler, input_data, &output_data, &exception_tag);
758
+ bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
674
759
  if (exception_tag != 0) goto raise;
675
760
  goto succeed;
676
761
  }
677
762
  /* If storage_data isn't a string, we can't cache it */
678
763
  if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
679
764
 
680
- /* Write the cache key and storage_data to the cache directory */
681
- res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
682
- if (res < 0) goto fail_errno;
765
+ /* Attempt to write the cache key and storage_data to the cache directory.
766
+ * We do however ignore any failures to persist the cache, as it's better
767
+ * to move along, than to interrupt the process.
768
+ */
769
+ atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
683
770
 
684
771
  /* Having written the cache, now convert storage_data to output_data */
685
- exception_tag = bs_storage_to_output(handler, storage_data, &output_data);
772
+ exception_tag = bs_storage_to_output(handler, args, storage_data, &output_data);
686
773
  if (exception_tag != 0) goto raise;
687
774
 
688
775
  /* If output_data is nil, delete the cache entry and generate the output
@@ -692,7 +779,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
692
779
  errno_provenance = "bs_fetch:unlink";
693
780
  goto fail_errno;
694
781
  }
695
- bs_input_to_output(handler, input_data, &output_data, &exception_tag);
782
+ bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
696
783
  if (exception_tag != 0) goto raise;
697
784
  }
698
785
 
@@ -723,6 +810,79 @@ invalid_type_storage_data:
723
810
  #undef CLEANUP
724
811
  }
725
812
 
813
+ static VALUE
814
+ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
815
+ {
816
+ struct bs_cache_key cached_key, current_key;
817
+ char * contents = NULL;
818
+ int cache_fd = -1, current_fd = -1;
819
+ int res, valid_cache = 0, exception_tag = 0;
820
+ const char * errno_provenance = NULL;
821
+
822
+ VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
823
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
824
+
825
+ /* Open the source file and generate a cache key for it */
826
+ current_fd = open_current_file(path, &current_key, &errno_provenance);
827
+ if (current_fd < 0) goto fail;
828
+
829
+ /* Open the cache key if it exists, and read its cache key in */
830
+ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
831
+ if (cache_fd == CACHE_MISS || cache_fd == CACHE_STALE) {
832
+ /* This is ok: valid_cache remains false, we re-populate it. */
833
+ } else if (cache_fd < 0) {
834
+ goto fail;
835
+ } else {
836
+ /* True if the cache existed and no invalidating changes have occurred since
837
+ * it was generated. */
838
+ valid_cache = cache_key_equal(&current_key, &cached_key);
839
+ }
840
+
841
+ if (valid_cache) {
842
+ goto succeed;
843
+ }
844
+
845
+ close(cache_fd);
846
+ cache_fd = -1;
847
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
848
+
849
+ /* Read the contents of the source file into a buffer */
850
+ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail;
851
+ input_data = rb_str_new(contents, current_key.size);
852
+
853
+ /* Try to compile the input_data using input_to_storage(input_data) */
854
+ exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
855
+ if (exception_tag != 0) goto fail;
856
+
857
+ /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
858
+ * to cache anything; just return false */
859
+ if (storage_data == uncompilable) {
860
+ goto fail;
861
+ }
862
+ /* If storage_data isn't a string, we can't cache it */
863
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
864
+
865
+ /* Write the cache key and storage_data to the cache directory */
866
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
867
+ if (res < 0) goto fail;
868
+
869
+ goto succeed;
870
+
871
+ #define CLEANUP \
872
+ if (contents != NULL) xfree(contents); \
873
+ if (current_fd >= 0) close(current_fd); \
874
+ if (cache_fd >= 0) close(cache_fd);
875
+
876
+ succeed:
877
+ CLEANUP;
878
+ return Qtrue;
879
+ fail:
880
+ CLEANUP;
881
+ return Qfalse;
882
+ #undef CLEANUP
883
+ }
884
+
885
+
726
886
  /*****************************************************************************/
727
887
  /********************* Handler Wrappers **************************************/
728
888
  /*****************************************************************************
@@ -742,11 +902,13 @@ invalid_type_storage_data:
742
902
 
743
903
  struct s2o_data {
744
904
  VALUE handler;
905
+ VALUE args;
745
906
  VALUE storage_data;
746
907
  };
747
908
 
748
909
  struct i2o_data {
749
910
  VALUE handler;
911
+ VALUE args;
750
912
  VALUE input_data;
751
913
  };
752
914
 
@@ -760,15 +922,16 @@ static VALUE
760
922
  prot_storage_to_output(VALUE arg)
761
923
  {
762
924
  struct s2o_data * data = (struct s2o_data *)arg;
763
- return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data);
925
+ return rb_funcall(data->handler, rb_intern("storage_to_output"), 2, data->storage_data, data->args);
764
926
  }
765
927
 
766
928
  static int
767
- bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
929
+ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data)
768
930
  {
769
931
  int state;
770
932
  struct s2o_data s2o_data = {
771
933
  .handler = handler,
934
+ .args = args,
772
935
  .storage_data = storage_data,
773
936
  };
774
937
  *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
@@ -776,10 +939,11 @@ bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
776
939
  }
777
940
 
778
941
  static void
779
- bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag)
942
+ bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag)
780
943
  {
781
944
  struct i2o_data i2o_data = {
782
945
  .handler = handler,
946
+ .args = args,
783
947
  .input_data = input_data,
784
948
  };
785
949
  *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
@@ -789,7 +953,7 @@ static VALUE
789
953
  prot_input_to_output(VALUE arg)
790
954
  {
791
955
  struct i2o_data * data = (struct i2o_data *)arg;
792
- return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data);
956
+ return rb_funcall(data->handler, rb_intern("input_to_output"), 2, data->input_data, data->args);
793
957
  }
794
958
 
795
959
  static VALUE
@@ -800,7 +964,7 @@ try_input_to_storage(VALUE arg)
800
964
  }
801
965
 
802
966
  static VALUE
803
- rescue_input_to_storage(VALUE arg)
967
+ rescue_input_to_storage(VALUE arg, VALUE e)
804
968
  {
805
969
  return uncompilable;
806
970
  }
@@ -816,7 +980,7 @@ prot_input_to_storage(VALUE arg)
816
980
  }
817
981
 
818
982
  static int
819
- bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data)
983
+ bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data)
820
984
  {
821
985
  int state;
822
986
  struct i2s_data i2s_data = {