bootsnap 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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 {