authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-03-21 19:53:24-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-03-21 19:56:30-07:00
log2f4bbd6c637782eb985860255cf70011bbadd452
tree8baa0a2c003b66c3e449af9d73bc33985d13d0f3
parentebec7336e23404ab091d34303055cd3b8a0088a5

std.Build.Cache: use an array hash map for files

Rather than an ArrayList. Provides deduplication.

4 files changed, 152 insertions(+), 72 deletions(-)

lib/std/Build/Cache.zig+148-68
......@@ -55,7 +55,15 @@ pub fn prefixes(cache: *const Cache) []const Directory {
5555
5656const PrefixedPath = struct {
5757 prefix: u8,
58 sub_path: []u8,
58 sub_path: []const u8,
59
60 fn eql(a: PrefixedPath, b: PrefixedPath) bool {
61 return a.prefix == b.prefix and std.mem.eql(u8, a.sub_path, b.sub_path);
62 }
63
64 fn hash(pp: PrefixedPath) u32 {
65 return @truncate(std.hash.Wyhash.hash(pp.prefix, pp.sub_path));
66 }
5967};
6068
6169fn findPrefix(cache: *const Cache, file_path: []const u8) !PrefixedPath {
......@@ -132,7 +140,7 @@ pub const hasher_init: Hasher = Hasher.init(&[_]u8{
132140});
133141
134142pub const File = struct {
135 prefixed_path: ?PrefixedPath,
143 prefixed_path: PrefixedPath,
136144 max_file_size: ?usize,
137145 stat: Stat,
138146 bin_digest: BinDigest,
......@@ -145,16 +153,18 @@ pub const File = struct {
145153 };
146154
147155 pub fn deinit(self: *File, gpa: Allocator) void {
148 if (self.prefixed_path) |pp| {
149 gpa.free(pp.sub_path);
150 self.prefixed_path = null;
151 }
156 gpa.free(self.prefixed_path.sub_path);
152157 if (self.contents) |contents| {
153158 gpa.free(contents);
154159 self.contents = null;
155160 }
156161 self.* = undefined;
157162 }
163
164 pub fn updateMaxSize(file: *File, new_max_size: ?usize) void {
165 const new = new_max_size orelse return;
166 file.max_file_size = if (file.max_file_size) |old| @max(old, new) else new;
167 }
158168};
159169
160170pub const HashHelper = struct {
......@@ -296,7 +306,7 @@ pub const Manifest = struct {
296306 // order to obtain a problematic timestamp for the next call. Calls after that
297307 // will then use the same timestamp, to avoid unnecessary filesystem writes.
298308 want_refresh_timestamp: bool = true,
299 files: std.ArrayListUnmanaged(File) = .{},
309 files: Files = .{},
300310 hex_digest: HexDigest,
301311 /// Populated when hit() returns an error because of one
302312 /// of the files listed in the manifest.
......@@ -305,6 +315,34 @@ pub const Manifest = struct {
305315 /// what time the file system thinks it is, according to its own granularity.
306316 recent_problematic_timestamp: i128 = 0,
307317
318 pub const Files = std.ArrayHashMapUnmanaged(File, void, FilesContext, false);
319
320 pub const FilesContext = struct {
321 pub fn hash(fc: FilesContext, file: File) u32 {
322 _ = fc;
323 return file.prefixed_path.hash();
324 }
325
326 pub fn eql(fc: FilesContext, a: File, b: File, b_index: usize) bool {
327 _ = fc;
328 _ = b_index;
329 return a.prefixed_path.eql(b.prefixed_path);
330 }
331 };
332
333 const FilesAdapter = struct {
334 pub fn eql(context: @This(), a: PrefixedPath, b: File, b_index: usize) bool {
335 _ = context;
336 _ = b_index;
337 return a.eql(b.prefixed_path);
338 }
339
340 pub fn hash(context: @This(), key: PrefixedPath) u32 {
341 _ = context;
342 return key.hash();
343 }
344 };
345
308346 /// Add a file as a dependency of process being cached. When `hit` is
309347 /// called, the file's contents will be checked to ensure that it matches
310348 /// the contents from previous times.
......@@ -317,7 +355,7 @@ pub const Manifest = struct {
317355 /// to access the contents of the file after calling `hit()` like so:
318356 ///
319357 /// ```
320 /// var file_contents = cache_hash.files.items[file_index].contents.?;
358 /// var file_contents = cache_hash.files.keys()[file_index].contents.?;
321359 /// ```
322360 pub fn addFile(self: *Manifest, file_path: []const u8, max_file_size: ?usize) !usize {
323361 assert(self.manifest_file == null);
......@@ -327,7 +365,12 @@ pub const Manifest = struct {
327365 const prefixed_path = try self.cache.findPrefix(file_path);
328366 errdefer gpa.free(prefixed_path.sub_path);
329367
330 self.files.addOneAssumeCapacity().* = .{
368 const gop = self.files.getOrPutAssumeCapacityAdapted(prefixed_path, FilesAdapter{});
369 if (gop.found_existing) {
370 gop.key_ptr.updateMaxSize(max_file_size);
371 return gop.index;
372 }
373 gop.key_ptr.* = .{
331374 .prefixed_path = prefixed_path,
332375 .contents = null,
333376 .max_file_size = max_file_size,
......@@ -338,7 +381,7 @@ pub const Manifest = struct {
338381 self.hash.add(prefixed_path.prefix);
339382 self.hash.addBytes(prefixed_path.sub_path);
340383
341 return self.files.items.len - 1;
384 return gop.index;
342385 }
343386
344387 pub fn addOptionalFile(self: *Manifest, optional_file_path: ?[]const u8) !void {
......@@ -418,7 +461,7 @@ pub const Manifest = struct {
418461
419462 self.want_refresh_timestamp = true;
420463
421 const input_file_count = self.files.items.len;
464 const input_file_count = self.files.entries.len;
422465 while (true) : (self.unhit(bin_digest, input_file_count)) {
423466 const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max);
424467 defer gpa.free(file_contents);
......@@ -430,7 +473,7 @@ pub const Manifest = struct {
430473 if (try self.upgradeToExclusiveLock()) continue;
431474 self.manifest_dirty = true;
432475 while (idx < input_file_count) : (idx += 1) {
433 const ch_file = &self.files.items[idx];
476 const ch_file = &self.files.keys()[idx];
434477 self.populateFileHash(ch_file) catch |err| {
435478 self.failed_file_index = idx;
436479 return err;
......@@ -441,18 +484,6 @@ pub const Manifest = struct {
441484 while (line_iter.next()) |line| {
442485 defer idx += 1;
443486
444 const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: {
445 const new = try self.files.addOne(gpa);
446 new.* = .{
447 .prefixed_path = null,
448 .contents = null,
449 .max_file_size = null,
450 .stat = undefined,
451 .bin_digest = undefined,
452 };
453 break :blk new;
454 };
455
456487 var iter = mem.tokenizeScalar(u8, line, ' ');
457488 const size = iter.next() orelse return error.InvalidFormat;
458489 const inode = iter.next() orelse return error.InvalidFormat;
......@@ -461,30 +492,61 @@ pub const Manifest = struct {
461492 const prefix_str = iter.next() orelse return error.InvalidFormat;
462493 const file_path = iter.rest();
463494
464 cache_hash_file.stat.size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
465 cache_hash_file.stat.inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
466 cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
467 _ = fmt.hexToBytes(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat;
495 const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
496 const stat_inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat;
497 const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
498 const file_bin_digest = b: {
499 if (digest_str.len != hex_digest_len) return error.InvalidFormat;
500 var bd: BinDigest = undefined;
501 _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat;
502 break :b bd;
503 };
504
468505 const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat;
469506 if (prefix >= self.cache.prefixes_len) return error.InvalidFormat;
470507
471 if (file_path.len == 0) {
472 return error.InvalidFormat;
473 }
474 if (cache_hash_file.prefixed_path) |pp| {
475 if (pp.prefix != prefix or !mem.eql(u8, file_path, pp.sub_path)) {
476 return error.InvalidFormat;
477 }
478 }
508 if (file_path.len == 0) return error.InvalidFormat;
479509
480 if (cache_hash_file.prefixed_path == null) {
481 cache_hash_file.prefixed_path = .{
510 const cache_hash_file = f: {
511 const prefixed_path: PrefixedPath = .{
482512 .prefix = prefix,
483 .sub_path = try gpa.dupe(u8, file_path),
513 .sub_path = file_path, // expires with file_contents
484514 };
485 }
515 if (idx < input_file_count) {
516 const file = &self.files.keys()[idx];
517 if (!file.prefixed_path.eql(prefixed_path))
518 return error.InvalidFormat;
519
520 file.stat = .{
521 .size = stat_size,
522 .inode = stat_inode,
523 .mtime = stat_mtime,
524 };
525 file.bin_digest = file_bin_digest;
526 break :f file;
527 }
528 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
529 errdefer assert(self.files.popOrNull() != null);
530 if (!gop.found_existing) {
531 gop.key_ptr.* = .{
532 .prefixed_path = .{
533 .prefix = prefix,
534 .sub_path = try gpa.dupe(u8, file_path),
535 },
536 .contents = null,
537 .max_file_size = null,
538 .stat = .{
539 .size = stat_size,
540 .inode = stat_inode,
541 .mtime = stat_mtime,
542 },
543 .bin_digest = file_bin_digest,
544 };
545 }
546 break :f gop.key_ptr;
547 };
486548
487 const pp = cache_hash_file.prefixed_path.?;
549 const pp = cache_hash_file.prefixed_path;
488550 const dir = self.cache.prefixes()[pp.prefix].handle;
489551 const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {
490552 error.FileNotFound => {
......@@ -548,7 +610,7 @@ pub const Manifest = struct {
548610 if (try self.upgradeToExclusiveLock()) continue;
549611 self.manifest_dirty = true;
550612 while (idx < input_file_count) : (idx += 1) {
551 const ch_file = &self.files.items[idx];
613 const ch_file = &self.files.keys()[idx];
552614 self.populateFileHash(ch_file) catch |err| {
553615 self.failed_file_index = idx;
554616 return err;
......@@ -571,12 +633,12 @@ pub const Manifest = struct {
571633 self.hash.hasher.update(&bin_digest);
572634
573635 // Remove files not in the initial hash.
574 for (self.files.items[input_file_count..]) |*file| {
636 for (self.files.keys()[input_file_count..]) |*file| {
575637 file.deinit(self.cache.gpa);
576638 }
577639 self.files.shrinkRetainingCapacity(input_file_count);
578640
579 for (self.files.items) |file| {
641 for (self.files.keys()) |file| {
580642 self.hash.hasher.update(&file.bin_digest);
581643 }
582644 }
......@@ -616,7 +678,7 @@ pub const Manifest = struct {
616678 }
617679
618680 fn populateFileHash(self: *Manifest, ch_file: *File) !void {
619 const pp = ch_file.prefixed_path.?;
681 const pp = ch_file.prefixed_path;
620682 const dir = self.cache.prefixes()[pp.prefix].handle;
621683 const file = try dir.openFile(pp.sub_path, .{});
622684 defer file.close();
......@@ -682,7 +744,7 @@ pub const Manifest = struct {
682744 .bin_digest = undefined,
683745 .contents = null,
684746 };
685 errdefer self.files.shrinkRetainingCapacity(self.files.items.len - 1);
747 errdefer self.files.shrinkRetainingCapacity(self.files.entries.len - 1);
686748
687749 try self.populateFileHash(new_ch_file);
688750
......@@ -690,9 +752,11 @@ pub const Manifest = struct {
690752 }
691753
692754 /// Add a file as a dependency of process being cached, after the initial hash has been
693 /// calculated. This is useful for processes that don't know the all the files that
694 /// are depended on ahead of time. For example, a source file that can import other files
695 /// will need to be recompiled if the imported file is changed.
755 /// calculated.
756 ///
757 /// This is useful for processes that don't know the all the files that are
758 /// depended on ahead of time. For example, a source file that can import
759 /// other files will need to be recompiled if the imported file is changed.
696760 pub fn addFilePost(self: *Manifest, file_path: []const u8) !void {
697761 assert(self.manifest_file != null);
698762
......@@ -700,17 +764,26 @@ pub const Manifest = struct {
700764 const prefixed_path = try self.cache.findPrefix(file_path);
701765 errdefer gpa.free(prefixed_path.sub_path);
702766
703 const new_ch_file = try self.files.addOne(gpa);
704 new_ch_file.* = .{
767 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
768 errdefer assert(self.files.popOrNull() != null);
769
770 if (gop.found_existing) {
771 gpa.free(prefixed_path.sub_path);
772 return;
773 }
774
775 gop.key_ptr.* = .{
705776 .prefixed_path = prefixed_path,
706777 .max_file_size = null,
707778 .stat = undefined,
708779 .bin_digest = undefined,
709780 .contents = null,
710781 };
711 errdefer self.files.shrinkRetainingCapacity(self.files.items.len - 1);
712782
713 try self.populateFileHash(new_ch_file);
783 self.files.lockPointers();
784 defer self.files.unlockPointers();
785
786 try self.populateFileHash(gop.key_ptr);
714787 }
715788
716789 /// Like `addFilePost` but when the file contents have already been loaded from disk.
......@@ -724,13 +797,20 @@ pub const Manifest = struct {
724797 assert(self.manifest_file != null);
725798 const gpa = self.cache.gpa;
726799
727 const ch_file = try self.files.addOne(gpa);
728 errdefer self.files.shrinkRetainingCapacity(self.files.items.len - 1);
729
730800 const prefixed_path = try self.cache.findPrefixResolved(resolved_path);
731801 errdefer gpa.free(prefixed_path.sub_path);
732802
733 ch_file.* = .{
803 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
804 errdefer assert(self.files.popOrNull() != null);
805
806 if (gop.found_existing) {
807 gpa.free(prefixed_path.sub_path);
808 return;
809 }
810
811 const new_file = gop.key_ptr;
812
813 new_file.* = .{
734814 .prefixed_path = prefixed_path,
735815 .max_file_size = null,
736816 .stat = stat,
......@@ -738,19 +818,19 @@ pub const Manifest = struct {
738818 .contents = null,
739819 };
740820
741 if (self.isProblematicTimestamp(ch_file.stat.mtime)) {
821 if (self.isProblematicTimestamp(new_file.stat.mtime)) {
742822 // The actual file has an unreliable timestamp, force it to be hashed
743 ch_file.stat.mtime = 0;
744 ch_file.stat.inode = 0;
823 new_file.stat.mtime = 0;
824 new_file.stat.inode = 0;
745825 }
746826
747827 {
748828 var hasher = hasher_init;
749829 hasher.update(bytes);
750 hasher.final(&ch_file.bin_digest);
830 hasher.final(&new_file.bin_digest);
751831 }
752832
753 self.hash.hasher.update(&ch_file.bin_digest);
833 self.hash.hasher.update(&new_file.bin_digest);
754834 }
755835
756836 pub fn addDepFilePost(self: *Manifest, dir: fs.Dir, dep_file_basename: []const u8) !void {
......@@ -816,14 +896,14 @@ pub const Manifest = struct {
816896
817897 const writer = contents.writer();
818898 try writer.writeAll(manifest_header ++ "\n");
819 for (self.files.items) |file| {
899 for (self.files.keys()) |file| {
820900 try writer.print("{d} {d} {d} {} {d} {s}\n", .{
821901 file.stat.size,
822902 file.stat.inode,
823903 file.stat.mtime,
824904 fmt.fmtSliceHexLower(&file.bin_digest),
825 file.prefixed_path.?.prefix,
826 file.prefixed_path.?.sub_path,
905 file.prefixed_path.prefix,
906 file.prefixed_path.sub_path,
827907 });
828908 }
829909
......@@ -892,7 +972,7 @@ pub const Manifest = struct {
892972
893973 file.close();
894974 }
895 for (self.files.items) |*file| {
975 for (self.files.keys()) |*file| {
896976 file.deinit(self.cache.gpa);
897977 }
898978 self.files.deinit(self.cache.gpa);
......@@ -1061,7 +1141,7 @@ test "check that changing a file makes cache fail" {
10611141 // There should be nothing in the cache
10621142 try testing.expectEqual(false, try ch.hit());
10631143
1064 try testing.expect(mem.eql(u8, original_temp_file_contents, ch.files.items[temp_file_idx].contents.?));
1144 try testing.expect(mem.eql(u8, original_temp_file_contents, ch.files.keys()[temp_file_idx].contents.?));
10651145
10661146 digest1 = ch.final();
10671147
......@@ -1081,7 +1161,7 @@ test "check that changing a file makes cache fail" {
10811161 try testing.expectEqual(false, try ch.hit());
10821162
10831163 // The cache system does not keep the contents of re-hashed input files.
1084 try testing.expect(ch.files.items[temp_file_idx].contents == null);
1164 try testing.expect(ch.files.keys()[temp_file_idx].contents == null);
10851165
10861166 digest2 = ch.final();
10871167
lib/std/Build/Step.zig+1-1
......@@ -544,7 +544,7 @@ pub fn cacheHit(s: *Step, man: *std.Build.Cache.Manifest) !bool {
544544
545545fn failWithCacheError(s: *Step, man: *const std.Build.Cache.Manifest, err: anyerror) anyerror {
546546 const i = man.failed_file_index orelse return err;
547 const pp = man.files.items[i].prefixed_path orelse return err;
547 const pp = man.files.keys()[i].prefixed_path;
548548 const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
549549 return s.fail("{s}: {s}/{s}", .{ @errorName(err), prefix, pp.sub_path });
550550}
src/Compilation.zig+2-2
......@@ -1999,7 +1999,7 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
19991999
20002000 const is_hit = man.hit() catch |err| {
20012001 const i = man.failed_file_index orelse return err;
2002 const pp = man.files.items[i].prefixed_path orelse return err;
2002 const pp = man.files.keys()[i].prefixed_path;
20032003 const prefix = man.cache.prefixes()[pp.prefix];
20042004 return comp.setMiscFailure(
20052005 .check_whole_cache,
......@@ -4147,7 +4147,7 @@ pub fn cImport(comp: *Compilation, c_src: []const u8, owner_mod: *Package.Module
41474147 const prev_hash_state = man.hash.peekBin();
41484148 const actual_hit = hit: {
41494149 _ = try man.hit();
4150 if (man.files.items.len == 0) {
4150 if (man.files.entries.len == 0) {
41514151 man.unhit(prev_hash_state, 0);
41524152 break :hit false;
41534153 }
src/glibc.zig+1-1
......@@ -713,7 +713,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: *std.Progress.Node) !vo
713713 };
714714 defer o_directory.handle.close();
715715
716 const abilists_contents = man.files.items[abilists_index].contents.?;
716 const abilists_contents = man.files.keys()[abilists_index].contents.?;
717717 const metadata = try loadMetaData(comp.gpa, abilists_contents);
718718 defer metadata.destroy(comp.gpa);
719719