bootsnap 1.1.1 → 1.3.0

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.
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rake/extensiontask'
2
+ require 'bundler/gem_tasks'
2
3
 
3
4
  gemspec = Gem::Specification.load('bootsnap.gemspec')
4
5
  Rake::ExtensionTask.new do |ext|
data/bin/testunit CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
2
 
3
- if [ $# -eq 0 ]; then
4
- ruby -I"test" -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }' -- "$@"
3
+ if [[ $# -eq 0 ]]; then
4
+ exec ruby -I"test" -w -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }' -- "$@"
5
5
  else
6
6
  path=$1
7
- ruby -I"test" -e "require '${path#test/}'" -- "$@"
7
+ exec ruby -I"test" -w -e "require '${path#test/}'" -- "$@"
8
8
  fi
data/bootsnap.gemspec CHANGED
@@ -11,8 +11,8 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.license = "MIT"
13
13
 
14
- spec.summary = "wip"
15
- spec.description = "wip."
14
+ spec.summary = "Boot large ruby/rails apps faster"
15
+ spec.description = spec.summary
16
16
  spec.homepage = "https://github.com/Shopify/bootsnap"
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
data/dev.yml CHANGED
@@ -5,4 +5,6 @@ up:
5
5
  - ruby: 2.3.3
6
6
  - bundler
7
7
  commands:
8
- test: 'rake compile && bin/testunit'
8
+ build: rake compile
9
+ test: 'rake compile && exec bin/testunit'
10
+ style: 'exec rubocop -D'
@@ -32,7 +32,7 @@
32
32
  /*
33
33
  * An instance of this key is written as the first 64 bytes of each cache file.
34
34
  * The mtime and size members track whether the file contents have changed, and
35
- * the version, os_version, compile_option, and ruby_revision members track
35
+ * the version, ruby_platform, compile_option, and ruby_revision members track
36
36
  * changes to the environment that could invalidate compile results without
37
37
  * file contents having changed. The data_size member is not truly part of the
38
38
  * "key". Really, this could be called a "header" with the first six members
@@ -45,7 +45,7 @@
45
45
  */
46
46
  struct bs_cache_key {
47
47
  uint32_t version;
48
- uint32_t os_version;
48
+ uint32_t ruby_platform;
49
49
  uint32_t compile_option;
50
50
  uint32_t ruby_revision;
51
51
  uint64_t size;
@@ -67,9 +67,9 @@ STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
67
67
  /* Effectively a schema version. Bumping invalidates all previous caches */
68
68
  static const uint32_t current_version = 2;
69
69
 
70
- /* Derived from kernel or libc version; intended to roughly correspond to when
71
- * ABIs have changed, requiring recompilation of native gems. */
72
- static uint32_t current_os_version;
70
+ /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
71
+ * new OS ABI, etc. */
72
+ static uint32_t current_ruby_platform;
73
73
  /* Invalidates cache when switching ruby versions */
74
74
  static uint32_t current_ruby_revision;
75
75
  /* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */
@@ -92,10 +92,9 @@ static void bs_cache_path(const char * cachedir, const char * path, char ** cach
92
92
  static int bs_read_key(int fd, struct bs_cache_key * key);
93
93
  static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
94
94
  static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
95
- static int open_current_file(char * path, struct bs_cache_key * key);
96
- static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag);
97
- static VALUE prot_exception_for_errno(VALUE err);
98
- static uint32_t get_os_version(void);
95
+ static int open_current_file(char * path, struct bs_cache_key * key, char ** errno_provenance);
96
+ static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, char ** errno_provenance);
97
+ static uint32_t get_ruby_platform(void);
99
98
 
100
99
  /*
101
100
  * Helper functions to call ruby methods on handler object without crashing on
@@ -136,7 +135,7 @@ Init_bootsnap(void)
136
135
  rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError);
137
136
 
138
137
  current_ruby_revision = FIX2INT(rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")));
139
- current_os_version = get_os_version();
138
+ current_ruby_platform = get_ruby_platform();
140
139
 
141
140
  uncompilable = rb_intern("__bootsnap_uncompilable__");
142
141
 
@@ -173,10 +172,9 @@ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
173
172
  * - 32 bits doesn't feel collision-resistant enough; 64 is nice.
174
173
  */
175
174
  static uint64_t
176
- fnv1a_64(const char *str)
175
+ fnv1a_64_iter(uint64_t h, const char *str)
177
176
  {
178
177
  unsigned char *s = (unsigned char *)str;
179
- uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
180
178
 
181
179
  while (*s) {
182
180
  h ^= (uint64_t)*s++;
@@ -186,26 +184,42 @@ fnv1a_64(const char *str)
186
184
  return h;
187
185
  }
188
186
 
187
+ static uint64_t
188
+ fnv1a_64(const char *str)
189
+ {
190
+ uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
191
+ return fnv1a_64_iter(h, str);
192
+ }
193
+
189
194
  /*
190
- * The idea here is that we want a cache key member that changes when the OS
191
- * changes in such a way as to make existing compiled ISeqs unloadable.
195
+ * When ruby's version doesn't change, but it's recompiled on a different OS
196
+ * (or OS version), we need to invalidate the cache.
197
+ *
198
+ * We actually factor in some extra information here, to be extra confident
199
+ * that we don't try to re-use caches that will not be compatible, by factoring
200
+ * in utsname.version.
192
201
  */
193
202
  static uint32_t
194
- get_os_version(void)
203
+ get_ruby_platform(void)
195
204
  {
196
- #ifdef _WIN32
197
- return (uint32_t)GetVersion();
198
- #else
199
205
  uint64_t hash;
200
- struct utsname utsname;
206
+ VALUE ruby_platform;
201
207
 
202
- /* Not worth crashing if this fails; lose cache invalidation potential */
203
- if (uname(&utsname) < 0) return 0;
208
+ ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
209
+ hash = fnv1a_64(RSTRING_PTR(ruby_platform));
204
210
 
205
- hash = fnv1a_64(utsname.version);
211
+ #ifdef _WIN32
212
+ return (uint32_t)(hash >> 32) ^ (uint32_t)GetVersion();
213
+ #else
214
+ struct utsname utsname;
215
+
216
+ /* Not worth crashing if this fails; lose extra cache invalidation potential */
217
+ if (uname(&utsname) >= 0) {
218
+ hash = fnv1a_64_iter(hash, utsname.version);
219
+ }
206
220
 
207
221
  return (uint32_t)(hash >> 32);
208
- #endif
222
+ #endif
209
223
  }
210
224
 
211
225
  /*
@@ -239,7 +253,7 @@ cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
239
253
  {
240
254
  return (
241
255
  k1->version == k2->version &&
242
- k1->os_version == k2->os_version &&
256
+ k1->ruby_platform == k2->ruby_platform &&
243
257
  k1->compile_option == k2->compile_option &&
244
258
  k1->ruby_revision == k2->ruby_revision &&
245
259
  k1->size == k2->size &&
@@ -279,24 +293,28 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
279
293
  * was loaded.
280
294
  */
281
295
  static int
282
- open_current_file(char * path, struct bs_cache_key * key)
296
+ open_current_file(char * path, struct bs_cache_key * key, char ** errno_provenance)
283
297
  {
284
298
  struct stat statbuf;
285
299
  int fd;
286
300
 
287
301
  fd = open(path, O_RDONLY);
288
- if (fd < 0) return fd;
302
+ if (fd < 0) {
303
+ *errno_provenance = (char *)"bs_fetch:open_current_file:open";
304
+ return fd;
305
+ }
289
306
  #ifdef _WIN32
290
307
  setmode(fd, O_BINARY);
291
308
  #endif
292
309
 
293
310
  if (fstat(fd, &statbuf) < 0) {
311
+ *errno_provenance = (char *)"bs_fetch:open_current_file:fstat";
294
312
  close(fd);
295
313
  return -1;
296
314
  }
297
315
 
298
316
  key->version = current_version;
299
- key->os_version = current_os_version;
317
+ key->ruby_platform = current_ruby_platform;
300
318
  key->compile_option = current_compile_option_crc32;
301
319
  key->ruby_revision = current_ruby_revision;
302
320
  key->size = (uint64_t)statbuf.st_size;
@@ -336,12 +354,13 @@ bs_read_key(int fd, struct bs_cache_key * key)
336
354
  * - ERROR_WITH_ERRNO (-1, errno is set)
337
355
  */
338
356
  static int
339
- open_cache_file(const char * path, struct bs_cache_key * key)
357
+ open_cache_file(const char * path, struct bs_cache_key * key, char ** errno_provenance)
340
358
  {
341
359
  int fd, res;
342
360
 
343
361
  fd = open(path, O_RDONLY);
344
362
  if (fd < 0) {
363
+ *errno_provenance = (char *)"bs_fetch:open_cache_file:open";
345
364
  if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
346
365
  return ERROR_WITH_ERRNO;
347
366
  }
@@ -351,6 +370,7 @@ open_cache_file(const char * path, struct bs_cache_key * key)
351
370
 
352
371
  res = bs_read_key(fd, key);
353
372
  if (res < 0) {
373
+ *errno_provenance = (char *)"bs_fetch:open_cache_file:read";
354
374
  close(fd);
355
375
  return res;
356
376
  }
@@ -374,7 +394,7 @@ open_cache_file(const char * path, struct bs_cache_key * key)
374
394
  * or exception, will be the final data returnable to the user.
375
395
  */
376
396
  static int
377
- fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag)
397
+ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, char ** errno_provenance)
378
398
  {
379
399
  char * data = NULL;
380
400
  ssize_t nread;
@@ -383,6 +403,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data,
383
403
  VALUE storage_data;
384
404
 
385
405
  if (data_size > 100000000000) {
406
+ *errno_provenance = (char *)"bs_fetch:fetch_cached_data:datasize";
386
407
  errno = EINVAL; /* because wtf? */
387
408
  ret = -1;
388
409
  goto done;
@@ -390,6 +411,7 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data,
390
411
  data = ALLOC_N(char, data_size);
391
412
  nread = read(fd, data, data_size);
392
413
  if (nread < 0) {
414
+ *errno_provenance = (char *)"bs_fetch:fetch_cached_data:read";
393
415
  ret = -1;
394
416
  goto done;
395
417
  }
@@ -441,23 +463,29 @@ mkpath(char * file_path, mode_t mode)
441
463
  * path.
442
464
  */
443
465
  static int
444
- atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
466
+ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, char ** errno_provenance)
445
467
  {
446
468
  char template[MAX_CACHEPATH_SIZE + 20];
447
469
  char * dest;
448
470
  char * tmp_path;
449
- int fd;
471
+ int fd, ret;
450
472
  ssize_t nwrite;
451
473
 
452
474
  dest = strncpy(template, path, MAX_CACHEPATH_SIZE);
453
475
  strcat(dest, ".tmp.XXXXXX");
454
476
 
455
477
  tmp_path = mktemp(template);
456
- fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
478
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0664);
457
479
  if (fd < 0) {
458
- if (mkpath(path, 0755) < 0) return -1;
459
- fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
460
- if (fd < 0) return -1;
480
+ if (mkpath(path, 0775) < 0) {
481
+ *errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:mkpath";
482
+ return -1;
483
+ }
484
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0664);
485
+ if (fd < 0) {
486
+ *errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:open";
487
+ return -1;
488
+ }
461
489
  }
462
490
  #ifdef _WIN32
463
491
  setmode(fd, O_BINARY);
@@ -465,8 +493,12 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
465
493
 
466
494
  key->data_size = RSTRING_LEN(data);
467
495
  nwrite = write(fd, key, KEY_SIZE);
468
- if (nwrite < 0) return -1;
496
+ if (nwrite < 0) {
497
+ *errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:write";
498
+ return -1;
499
+ }
469
500
  if (nwrite != KEY_SIZE) {
501
+ *errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:keysize";
470
502
  errno = EIO; /* Lies but whatever */
471
503
  return -1;
472
504
  }
@@ -474,38 +506,32 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
474
506
  nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
475
507
  if (nwrite < 0) return -1;
476
508
  if (nwrite != RSTRING_LEN(data)) {
509
+ *errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:writelength";
477
510
  errno = EIO; /* Lies but whatever */
478
511
  return -1;
479
512
  }
480
513
 
481
514
  close(fd);
482
- return rename(tmp_path, path);
483
- }
484
-
485
- /*
486
- * Given an errno value (converted to a ruby Fixnum), return the corresponding
487
- * Errno::* constant. If none is found, return StandardError instead.
488
- */
489
- static VALUE
490
- prot_exception_for_errno(VALUE err)
491
- {
492
- if (err != INT2FIX(0)) {
493
- VALUE mErrno = rb_const_get(rb_cObject, rb_intern("Errno"));
494
- VALUE constants = rb_funcall(mErrno, rb_intern("constants"), 0);
495
- VALUE which = rb_funcall(constants, rb_intern("[]"), 1, err);
496
- return rb_funcall(mErrno, rb_intern("const_get"), 1, which);
515
+ ret = rename(tmp_path, path);
516
+ if (ret < 0) {
517
+ *errno_provenance = (char *)"bs_fetch:atomic_write_cache_file:rename";
497
518
  }
498
- return rb_eStandardError;
519
+ return ret;
499
520
  }
500
521
 
501
522
 
502
523
  /* Read contents from an fd, whose contents are asserted to be +size+ bytes
503
524
  * long, into a buffer */
504
525
  static ssize_t
505
- bs_read_contents(int fd, size_t size, char ** contents)
526
+ bs_read_contents(int fd, size_t size, char ** contents, char ** errno_provenance)
506
527
  {
528
+ ssize_t nread;
507
529
  *contents = ALLOC_N(char, size);
508
- return read(fd, *contents, size);
530
+ nread = read(fd, *contents, size);
531
+ if (nread < 0) {
532
+ *errno_provenance = (char *)"bs_fetch:bs_read_contents:read";
533
+ }
534
+ return nread;
509
535
  }
510
536
 
511
537
  /*
@@ -513,24 +539,24 @@ bs_read_contents(int fd, size_t size, char ** contents)
513
539
  * Bootsnap::CompileCache::Native.fetch.
514
540
  *
515
541
  * There are three "formats" in use here:
516
- * 1. "input" fomat, which is what we load from the source file;
542
+ * 1. "input" format, which is what we load from the source file;
517
543
  * 2. "storage" format, which we write to the cache;
518
544
  * 3. "output" format, which is what we return.
519
545
  *
520
546
  * E.g., For ISeq compilation:
521
- * input: ruby source, as text
547
+ * input: ruby source, as text
522
548
  * storage: binary string (RubyVM::InstructionSequence#to_binary)
523
- * output: Instance of RubyVM::InstructionSequence
549
+ * output: Instance of RubyVM::InstructionSequence
524
550
  *
525
551
  * And for YAML:
526
- * input: yaml as text
552
+ * input: yaml as text
527
553
  * storage: MessagePack or Marshal text
528
- * output: ruby object, loaded from yaml/messagepack/marshal
554
+ * output: ruby object, loaded from yaml/messagepack/marshal
529
555
  *
530
- * The handler passed in must support three messages:
531
- * * storage_to_output(s) -> o
532
- * * input_to_output(i) -> o
533
- * * input_to_storage(i) -> s
556
+ * A handler<I,S,O> passed in must support three messages:
557
+ * * storage_to_output(S) -> O
558
+ * * input_to_output(I) -> O
559
+ * * input_to_storage(I) -> S
534
560
  * (input_to_storage may raise Bootsnap::CompileCache::Uncompilable, which
535
561
  * will prevent caching and cause output to be generated with
536
562
  * input_to_output)
@@ -558,7 +584,8 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
558
584
  struct bs_cache_key cached_key, current_key;
559
585
  char * contents = NULL;
560
586
  int cache_fd = -1, current_fd = -1;
561
- int res, valid_cache, exception_tag = 0;
587
+ int res, valid_cache = 0, exception_tag = 0;
588
+ char * errno_provenance = NULL;
562
589
 
563
590
  VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
564
591
  VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
@@ -567,20 +594,27 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
567
594
  VALUE exception; /* ruby exception object to raise instead of returning */
568
595
 
569
596
  /* Open the source file and generate a cache key for it */
570
- current_fd = open_current_file(path, &current_key);
597
+ current_fd = open_current_file(path, &current_key, &errno_provenance);
571
598
  if (current_fd < 0) goto fail_errno;
572
599
 
573
600
  /* Open the cache key if it exists, and read its cache key in */
574
- cache_fd = open_cache_file(cache_path, &cached_key);
575
- if (cache_fd < 0 && cache_fd != CACHE_MISSING_OR_INVALID) goto fail_errno;
576
-
577
- /* True if the cache existed and no invalidating changes have occurred since
578
- * it was generated. */
579
- valid_cache = cache_key_equal(&current_key, &cached_key);
601
+ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
602
+ if (cache_fd == CACHE_MISSING_OR_INVALID) {
603
+ /* This is ok: valid_cache remains false, we re-populate it. */
604
+ } else if (cache_fd < 0) {
605
+ goto fail_errno;
606
+ } else {
607
+ /* True if the cache existed and no invalidating changes have occurred since
608
+ * it was generated. */
609
+ valid_cache = cache_key_equal(&current_key, &cached_key);
610
+ }
580
611
 
581
612
  if (valid_cache) {
582
613
  /* Fetch the cache data and return it if we're able to load it successfully */
583
- res = fetch_cached_data(cache_fd, (ssize_t)cached_key.data_size, handler, &output_data, &exception_tag);
614
+ res = fetch_cached_data(
615
+ cache_fd, (ssize_t)cached_key.data_size, handler,
616
+ &output_data, &exception_tag, &errno_provenance
617
+ );
584
618
  if (exception_tag != 0) goto raise;
585
619
  else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
586
620
  else if (res == ERROR_WITH_ERRNO) goto fail_errno;
@@ -591,7 +625,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
591
625
  /* Cache is stale, invalid, or missing. Regenerate and write it out. */
592
626
 
593
627
  /* Read the contents of the source file into a buffer */
594
- if (bs_read_contents(current_fd, current_key.size, &contents) < 0) goto fail_errno;
628
+ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno;
595
629
  input_data = rb_str_new_static(contents, current_key.size);
596
630
 
597
631
  /* Try to compile the input_data using input_to_storage(input_data) */
@@ -608,7 +642,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
608
642
  if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
609
643
 
610
644
  /* Write the cache key and storage_data to the cache directory */
611
- res = atomic_write_cache_file(cache_path, &current_key, storage_data);
645
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
612
646
  if (res < 0) goto fail_errno;
613
647
 
614
648
  /* Having written the cache, now convert storage_data to output_data */
@@ -618,7 +652,10 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
618
652
  /* If output_data is nil, delete the cache entry and generate the output
619
653
  * using input_to_output */
620
654
  if (NIL_P(output_data)) {
621
- if (unlink(cache_path) < 0) goto fail_errno;
655
+ if (unlink(cache_path) < 0) {
656
+ errno_provenance = (char *)"bs_fetch:unlink";
657
+ goto fail_errno;
658
+ }
622
659
  bs_input_to_output(handler, input_data, &output_data, &exception_tag);
623
660
  if (exception_tag != 0) goto raise;
624
661
  }
@@ -635,8 +672,7 @@ succeed:
635
672
  return output_data;
636
673
  fail_errno:
637
674
  CLEANUP;
638
- exception = rb_protect(prot_exception_for_errno, INT2FIX(errno), &res);
639
- if (res) exception = rb_eStandardError;
675
+ exception = rb_syserr_new(errno, errno_provenance);
640
676
  rb_exc_raise(exception);
641
677
  __builtin_unreachable();
642
678
  raise:
@@ -655,7 +691,17 @@ invalid_type_storage_data:
655
691
  /********************* Handler Wrappers **************************************/
656
692
  /*****************************************************************************
657
693
  * Everything after this point in the file is just wrappers to deal with ruby's
658
- * clunky method of handling exceptions from ruby methods invoked from C.
694
+ * clunky method of handling exceptions from ruby methods invoked from C:
695
+ *
696
+ * In order to call a ruby method from C, while protecting against crashing in
697
+ * the event of an exception, we must call the method with rb_protect().
698
+ *
699
+ * rb_protect takes a C function and precisely one argument; however, we want
700
+ * to pass multiple arguments, so we must create structs to wrap them up.
701
+ *
702
+ * These functions return an exception_tag, which, if non-zero, indicates an
703
+ * exception that should be jumped to with rb_jump_tag after cleaning up
704
+ * allocated resources.
659
705
  */
660
706
 
661
707
  struct s2o_data {