authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-06-30 17:44:50-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-07-01 10:16:22+02:00
log15f00d1756f1503da29151cef5213858f6667f4f
tree2413bbf446f11686fb426d34d79899a94521f2b9
parent580135241b9618fdce6f4acca390e41d14e4b937

std.Build.Cache: make Progress.Node for hits

Occasionally, two different Zig processes are waiting on the same compilation artifacts. For example, if the user runs `zig build` for the first time in two different terminals with the same version of Zig, one of them will win the race on compiler_rt and start building it, while the other process waits. Then they both use the same cache artifacts. Now, the fact that this is happening is communicated to the user by displaying it in a progress node during the checking for cache hit.

15 files changed, 39 insertions(+), 37 deletions(-)

lib/compiler/Maker.zig+1-1
......@@ -1216,7 +1216,7 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
12161216 defer compile_prog_node.end();
12171217
12181218 switch (options.cache_poison) {
1219 .pure, .disallowed, .ignored => if (try config_man.hit()) {
1219 .pure, .disallowed, .ignored => if (try config_man.hit(compile_prog_node)) {
12201220 const digest = config_man.final();
12211221 break :cp .{
12221222 .{
lib/compiler/Maker/Step.zig+6-6
......@@ -711,10 +711,10 @@ pub fn handleChildProcessTerm(s: *Step, maker: *Maker, term: std.process.Child.T
711711 if (!term.success()) return s.fail(maker, "process {f}", .{term});
712712}
713713
714/// Prefer `cacheHitAndWatch` unless you already added watch inputs
714/// Prefer `cacheHitWatched` unless you already added watch inputs
715715/// separately from using the cache system.
716pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool {
717 s.result_cached = man.hit() catch |err| return failWithCacheError(s, maker, man, err);
716pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest, parent_progress_node: std.Progress.Node) !bool {
717 s.result_cached = man.hit(parent_progress_node) catch |err| return failWithCacheError(s, maker, man, err);
718718 return s.result_cached;
719719}
720720
......@@ -722,8 +722,8 @@ pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool {
722722/// the full set of files picked up by the cache manifest.
723723///
724724/// Must be accompanied with `writeManifestAndWatch`.
725pub fn cacheHitAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool {
726 const is_hit = man.hit() catch |err| return failWithCacheError(s, maker, man, err);
725pub fn cacheHitWatched(s: *Step, maker: *Maker, man: *Cache.Manifest, parent_progress_node: std.Progress.Node) !bool {
726 const is_hit = man.hit(parent_progress_node) catch |err| return failWithCacheError(s, maker, man, err);
727727 s.result_cached = is_hit;
728728 // The above call to hit() populates the manifest with files, so in case of
729729 // a hit, we need to populate watch inputs.
......@@ -770,7 +770,7 @@ pub fn writeManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
770770/// Clears previous watch inputs, if any, and then populates watch inputs from
771771/// the full set of files picked up by the cache manifest.
772772///
773/// Must be accompanied with `cacheHitAndWatch`.
773/// Must be accompanied with `cacheHitWatched`.
774774pub fn writeManifestAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
775775 try writeManifest(s, maker, man);
776776 try setWatchInputsFromManifest(s, maker, man);
lib/compiler/Maker/Step/ConfigHeader.zig+1-2
......@@ -27,7 +27,6 @@ pub fn make(
2727 progress_node: std.Progress.Node,
2828) Step.ExtendedMakeError!void {
2929 _ = config_header;
30 _ = progress_node;
3130 const graph = maker.graph;
3231 const step = maker.stepByIndex(step_index);
3332 const io = graph.io;
......@@ -111,7 +110,7 @@ pub fn make(
111110 const output = aw.written();
112111 man.hash.addBytes(output);
113112
114 if (try step.cacheHit(maker, &man)) {
113 if (try step.cacheHit(maker, &man, progress_node)) {
115114 const digest = man.final();
116115 maker.generatedPath(conf_ch.generated_dir).* = .{
117116 .root_dir = cache_root,
lib/compiler/Maker/Step/ObjCopy.zig+1-1
......@@ -46,7 +46,7 @@ pub fn make(
4646
4747 const basename = opt_basename orelse Io.Dir.path.basename(input_path.sub_path);
4848
49 if (try step.cacheHit(maker, &man)) {
49 if (try step.cacheHit(maker, &man, progress_node)) {
5050 // Cache hit, skip subprocess execution.
5151 const digest = man.final();
5252 maker.generatedPath(conf_oc.output_file).* = .{
lib/compiler/Maker/Step/Options.zig+1-4
......@@ -16,9 +16,6 @@ pub fn make(
1616) Step.ExtendedMakeError!void {
1717 _ = options;
1818
19 // This step completes so quickly that no progress reporting is necessary.
20 _ = progress_node;
21
2219 const graph = maker.graph;
2320 const step = maker.stepByIndex(step_index);
2421 const io = graph.io;
......@@ -56,7 +53,7 @@ pub fn make(
5653
5754 const basename = "options.zig";
5855
59 if (try step.cacheHitAndWatch(maker, &man)) {
56 if (try step.cacheHitWatched(maker, &man, progress_node)) {
6057 const digest = man.final();
6158 maker.generatedPath(conf_options.generated_file).* = .{
6259 .root_dir = cache_root,
lib/compiler/Maker/Step/Run.zig+1-1
......@@ -254,7 +254,7 @@ pub fn make(
254254 .check, .zig_test => false,
255255 };
256256
257 if (!has_side_effects and try step.cacheHitAndWatch(maker, &man)) {
257 if (!has_side_effects and try step.cacheHitWatched(maker, &man, progress_node)) {
258258 // Cache hit; skip running command.
259259 const digest = man.final();
260260 try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
lib/compiler/Maker/Step/WriteFile.zig+1-1
......@@ -104,7 +104,7 @@ pub fn make(
104104 }
105105 }
106106
107 if (try step.cacheHit(maker, &man)) {
107 if (try step.cacheHit(maker, &man, progress_node)) {
108108 const digest = man.final();
109109 maker.generatedPath(conf_wf.generated_directory).* = .{
110110 .root_dir = cache_root,
lib/std/Build/Cache.zig+16-10
......@@ -501,7 +501,13 @@ pub const Manifest = struct {
501501 /// The lock on the manifest file is released when `deinit` is called. As another
502502 /// option, one may call `toOwnedLock` to obtain a smaller object which can represent
503503 /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called.
504 pub fn hit(self: *Manifest) HitError!bool {
504 pub fn hit(man: *Manifest, parent_progress_node: std.Progress.Node) HitError!bool {
505 const node = parent_progress_node.start("Reusing Cache Artifacts", 0);
506 defer node.end();
507 return hitInner(man);
508 }
509
510 pub fn hitInner(self: *Manifest) HitError!bool {
505511 assert(self.manifest_file == null);
506512
507513 self.diagnostic = .none;
......@@ -1380,7 +1386,7 @@ test "cache file and then recall it" {
13801386 _ = try ch.addFile(temp_file, null);
13811387
13821388 // There should be nothing in the cache
1383 try testing.expectEqual(false, try ch.hit());
1389 try testing.expectEqual(false, try ch.hit(.none));
13841390
13851391 digest1 = ch.final();
13861392 try ch.writeManifest();
......@@ -1395,7 +1401,7 @@ test "cache file and then recall it" {
13951401 _ = try ch.addFile(temp_file, null);
13961402
13971403 // Cache hit! We just "built" the same file
1398 try testing.expect(try ch.hit());
1404 try testing.expect(try ch.hit(.none));
13991405 digest2 = ch.final();
14001406
14011407 try testing.expectEqual(false, ch.have_exclusive_lock);
......@@ -1448,7 +1454,7 @@ test "check that changing a file makes cache fail" {
14481454 const temp_file_idx = try ch.addFile(temp_file, 100);
14491455
14501456 // There should be nothing in the cache
1451 try testing.expectEqual(false, try ch.hit());
1457 try testing.expectEqual(false, try ch.hit(.none));
14521458
14531459 try testing.expect(mem.eql(u8, original_temp_file_contents, ch.files.keys()[temp_file_idx].contents.?));
14541460
......@@ -1467,7 +1473,7 @@ test "check that changing a file makes cache fail" {
14671473 const temp_file_idx = try ch.addFile(temp_file, 100);
14681474
14691475 // A file that we depend on has been updated, so the cache should not contain an entry for it
1470 try testing.expectEqual(false, try ch.hit());
1476 try testing.expectEqual(false, try ch.hit(.none));
14711477
14721478 // The cache system does not keep the contents of re-hashed input files.
14731479 try testing.expect(ch.files.keys()[temp_file_idx].contents == null);
......@@ -1511,7 +1517,7 @@ test "no file inputs" {
15111517 man.hash.addBytes("1234");
15121518
15131519 // There should be nothing in the cache
1514 try testing.expectEqual(false, try man.hit());
1520 try testing.expectEqual(false, try man.hit(.none));
15151521
15161522 digest1 = man.final();
15171523
......@@ -1523,7 +1529,7 @@ test "no file inputs" {
15231529
15241530 man.hash.addBytes("1234");
15251531
1526 try testing.expect(try man.hit());
1532 try testing.expect(try man.hit(.none));
15271533 digest2 = man.final();
15281534 try testing.expectEqual(false, man.have_exclusive_lock);
15291535 }
......@@ -1575,7 +1581,7 @@ test "Manifest with files added after initial hash work" {
15751581 _ = try ch.addFile(temp_file1, null);
15761582
15771583 // There should be nothing in the cache
1578 try testing.expectEqual(false, try ch.hit());
1584 try testing.expectEqual(false, try ch.hit(.none));
15791585
15801586 _ = try ch.addFilePost(temp_file2);
15811587
......@@ -1589,7 +1595,7 @@ test "Manifest with files added after initial hash work" {
15891595 ch.hash.addBytes("1234");
15901596 _ = try ch.addFile(temp_file1, null);
15911597
1592 try testing.expect(try ch.hit());
1598 try testing.expect(try ch.hit(.none));
15931599 digest2 = ch.final();
15941600
15951601 try testing.expectEqual(false, ch.have_exclusive_lock);
......@@ -1613,7 +1619,7 @@ test "Manifest with files added after initial hash work" {
16131619 _ = try ch.addFile(temp_file1, null);
16141620
16151621 // A file that we depend on has been updated, so the cache should not contain an entry for it
1616 try testing.expectEqual(false, try ch.hit());
1622 try testing.expectEqual(false, try ch.hit(.none));
16171623
16181624 _ = try ch.addFilePost(temp_file2);
16191625
src/Compilation.zig+5-5
......@@ -2755,7 +2755,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) UpdateE
27552755 man.want_shared_lock = false;
27562756 }
27572757
2758 const is_hit = man.hit() catch |err| switch (err) {
2758 const is_hit = man.hit(main_progress_node) catch |err| switch (err) {
27592759 error.CacheCheckFailed => switch (man.diagnostic) {
27602760 .none => unreachable,
27612761 .manifest_create, .manifest_read, .manifest_lock => |e| return comp.setMiscFailure(
......@@ -5435,7 +5435,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
54355435 const target = comp.getTarget();
54365436 assert(target.ofmt != .c);
54375437 const o_ext = target.ofmt.fileExt(target.cpu.arch);
5438 const digest = if (!comp.disable_c_depfile and try man.hit()) man.final() else blk: {
5438 const digest = if (!comp.disable_c_depfile and try man.hit(child_progress_node)) man.final() else blk: {
54395439 var argv: std.array_list.Managed([]const u8) = .init(gpa);
54405440 defer argv.deinit();
54415441
......@@ -5696,7 +5696,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
56965696 }
56975697
56985698 // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
5699 if (comp.disable_c_depfile) _ = try man.hit();
5699 if (comp.disable_c_depfile) _ = try man.hit(child_progress_node);
57005700
57015701 // Rename into place.
57025702 const digest = man.final();
......@@ -5784,7 +5784,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
57845784 const rc_basename = try std.fmt.allocPrint(arena, "{s}.rc", .{src_basename});
57855785 const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename});
57865786
5787 const digest = if (try man.hit()) man.final() else blk: {
5787 const digest = if (try man.hit(child_progress_node)) man.final() else blk: {
57885788 // The digest only depends on the .manifest file, so we can
57895789 // get the digest now and write the .res directly to the cache
57905790 const digest = man.final();
......@@ -5877,7 +5877,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
58775877
58785878 const rc_basename_noext = src_basename[0 .. src_basename.len - fs.path.extension(src_basename).len];
58795879
5880 const digest = if (try man.hit()) man.final() else blk: {
5880 const digest = if (try man.hit(child_progress_node)) man.final() else blk: {
58815881 var zig_cache_tmp_dir = try comp.dirs.local_cache.handle.createDirPathOpen(io, "tmp", .{});
58825882 defer zig_cache_tmp_dir.close(io);
58835883
src/libs/freebsd.zig+1-1
......@@ -462,7 +462,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye
462462 const full_abilists_path = try comp.dirs.zig_lib.join(arena, &.{abilists_path});
463463 const abilists_index = try man.addFile(full_abilists_path, abilists_max_size);
464464
465 if (try man.hit()) {
465 if (try man.hit(prog_node)) {
466466 const digest = man.final();
467467
468468 return queueSharedObjects(comp, .{
src/libs/glibc.zig+1-1
......@@ -703,7 +703,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye
703703 const full_abilists_path = try comp.dirs.zig_lib.join(arena, &.{abilists_path});
704704 const abilists_index = try man.addFile(full_abilists_path, abilists_max_size);
705705
706 if (try man.hit()) {
706 if (try man.hit(prog_node)) {
707707 const digest = man.final();
708708
709709 return queueSharedObjects(comp, .{
src/libs/mingw.zig+1-1
......@@ -255,7 +255,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8, prog_node: std.P
255255 const final_lib_basename = try std.fmt.allocPrint(gpa, "{s}.lib", .{lib_name});
256256 errdefer gpa.free(final_lib_basename);
257257
258 if (try man.hit()) {
258 if (try man.hit(prog_node)) {
259259 const digest = man.final();
260260 const sub_path = try std.fs.path.join(gpa, &.{ "o", &digest, final_lib_basename });
261261 errdefer gpa.free(sub_path);
src/libs/netbsd.zig+1-1
......@@ -409,7 +409,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye
409409 const full_abilists_path = try comp.dirs.zig_lib.join(arena, &.{abilists_path});
410410 const abilists_index = try man.addFile(full_abilists_path, abilists_max_size);
411411
412 if (try man.hit()) {
412 if (try man.hit(prog_node)) {
413413 const digest = man.final();
414414
415415 return queueSharedObjects(comp, .{
src/libs/openbsd.zig+1-1
......@@ -332,7 +332,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye
332332 const full_abilists_path = try comp.dirs.zig_lib.join(arena, &.{abilists_path});
333333 const abilists_index = try man.addFile(full_abilists_path, abilists_max_size);
334334
335 if (try man.hit()) {
335 if (try man.hit(prog_node)) {
336336 const digest = man.final();
337337
338338 return queueSharedObjects(comp, .{
src/main.zig+1-1
......@@ -4792,7 +4792,7 @@ fn cmdTranslateC(
47924792 Compilation.cache_helpers.hashCSource(&man, c_source_file) catch |err|
47934793 fatal("unable to process {q}: {t}", .{ c_source_file.src_path, err });
47944794
4795 const result: Compilation.TranslateCResult = if (try man.hit()) .{
4795 const result: Compilation.TranslateCResult = if (try man.hit(prog_node)) .{
47964796 .digest = man.finalBin(),
47974797 .cache_hit = true,
47984798 .errors = std.zig.ErrorBundle.empty,