authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-14 12:48:17-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-15 08:17:27+02:00
log9343d0fe4bb700cf58bc861596cf0d9f2093b494
tree62c01309c261b21e568e0f3bf57a9236581d1870
parentd8938120130dbe48ee8764b502b18c6224ab9340

std.Build.Cache: fix cache miss when metadata changes

adds test for "changed metadata of input file but same digest causes hit" closes #37200

1 files changed, 120 insertions(+), 30 deletions(-)

lib/std/Build/Cache.zig+120-30
......@@ -1365,6 +1365,8 @@ pub const Manifest = struct {
13651365 if (mem.eql(u8, &disk_file.digest, &input_file.digest))
13661366 return .hit;
13671367 }
1368 // From this point in the function, since `input_file.digest` is not populated, it is used as a scratch
1369 // space to compute the digest in case stat changed, in order to compare with `disk_file.digest`.
13681370
13691371 if (input_path.have_stat) {
13701372 const changed = try disk_file.setStatChanged(m, input_file.stat());
......@@ -1418,10 +1420,14 @@ pub const Manifest = struct {
14181420 if (input_path.contents.get(m.all_input_content.items)) |file_or_dir_contents| {
14191421 var hasher = hasher_init;
14201422 hasher.update(file_or_dir_contents);
1421 hasher.final(&disk_file.digest);
1423 hasher.final(&input_file.digest);
14221424
1423 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) return .hit;
1424 return .{ .contents_changed = file_off };
1425 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) {
1426 return .hit;
1427 } else {
1428 @memcpy(&disk_file.digest, &input_file.digest);
1429 return .{ .contents_changed = file_off };
1430 }
14251431 }
14261432
14271433 if (disk_file.flags.is_directory) {
......@@ -1455,7 +1461,7 @@ pub const Manifest = struct {
14551461 }
14561462
14571463 const dir_contents_start = m.all_input_content.items.len;
1458 hashDir(gpa, io, opened_dir, &disk_file.digest, &m.all_input_content) catch |err| switch (err) {
1464 hashDir(gpa, io, opened_dir, &input_file.digest, &m.all_input_content) catch |err| switch (err) {
14591465 error.Canceled, error.OutOfMemory => |e| return e,
14601466 else => |e| return fail(diag, .{ .file_read = .{
14611467 .file_offset = file_off,
......@@ -1471,8 +1477,12 @@ pub const Manifest = struct {
14711477 .populated => unreachable,
14721478 };
14731479
1474 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) return .hit;
1475 return .{ .contents_changed = file_off };
1480 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) {
1481 return .hit;
1482 } else {
1483 @memcpy(&disk_file.digest, &input_file.digest);
1484 return .{ .contents_changed = file_off };
1485 }
14761486 } else {
14771487 const opened_file = if (input_path.have_handle)
14781488 input_path.handle.file
......@@ -1503,7 +1513,7 @@ pub const Manifest = struct {
15031513 switch (input_path.contents.unwrap()) {
15041514 .requested => {
15051515 const start = m.all_input_content.items.len;
1506 hashFileAppend(io, opened_file, &disk_file.digest, &m.all_input_content, gpa) catch |err| switch (err) {
1516 hashFileAppend(io, opened_file, &input_file.digest, &m.all_input_content, gpa) catch |err| switch (err) {
15071517 error.Canceled, error.OutOfMemory => |e| return e,
15081518 else => |e| return fail(diag, .{ .file_read = .{
15091519 .file_offset = file_off,
......@@ -1515,7 +1525,7 @@ pub const Manifest = struct {
15151525 .len = @intCast(m.all_input_content.items.len - start),
15161526 });
15171527 },
1518 .unrequested => hashFile(io, opened_file, &disk_file.digest) catch |err| switch (err) {
1528 .unrequested => hashFile(io, opened_file, &input_file.digest) catch |err| switch (err) {
15191529 error.Canceled => |e| return e,
15201530 else => |e| return fail(diag, .{ .file_read = .{
15211531 .file_offset = file_off,
......@@ -1525,8 +1535,12 @@ pub const Manifest = struct {
15251535 .populated => unreachable,
15261536 }
15271537
1528 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) return .hit;
1529 return .{ .contents_changed = file_off };
1538 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) {
1539 return .hit;
1540 } else {
1541 @memcpy(&disk_file.digest, &input_file.digest);
1542 return .{ .contents_changed = file_off };
1543 }
15301544 }
15311545 }
15321546
......@@ -2313,6 +2327,13 @@ fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp {
23132327 return (try file.stat(io)).mtime;
23142328}
23152329
2330fn testWaitForFileTimestampTick(io: Io, dir: Io.Dir) !void {
2331 const initial_time = try testGetCurrentFileTimestamp(io, dir);
2332 while ((try testGetCurrentFileTimestamp(io, dir)).nanoseconds == initial_time.nanoseconds) {
2333 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
2334 }
2335}
2336
23162337test "cache file and then recall it" {
23172338 const io = testing.io;
23182339
......@@ -2332,11 +2353,7 @@ test "cache file and then recall it" {
23322353 };
23332354 defer testing.allocator.free(tmp_directory.path.?);
23342355
2335 // Wait for file timestamps to tick
2336 const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
2337 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
2338 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
2339 }
2356 try testWaitForFileTimestampTick(io, tmp.dir);
23402357
23412358 var digest1: HexDigest = undefined;
23422359 var digest2: HexDigest = undefined;
......@@ -2415,11 +2432,7 @@ test "check that changing a file causes cache miss" {
24152432 };
24162433 defer testing.allocator.free(tmp_directory.path.?);
24172434
2418 // Wait for file timestamps to tick
2419 const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
2420 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
2421 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
2422 }
2435 try testWaitForFileTimestampTick(io, tmp.dir);
24232436
24242437 var digest1: HexDigest = undefined;
24252438 var digest2: HexDigest = undefined;
......@@ -2485,6 +2498,91 @@ test "check that changing a file causes cache miss" {
24852498 }
24862499}
24872500
2501test "changed metadata of input file but same digest causes hit" {
2502 const io = testing.io;
2503 const gpa = testing.allocator;
2504
2505 var tmp = testing.tmpDir(.{});
2506 defer tmp.cleanup();
2507
2508 const cwd = try std.process.currentPathAlloc(io, gpa);
2509 defer gpa.free(cwd);
2510
2511 const input_file_name = "input.txt";
2512 const input_file_contents = "blah blah blah";
2513 const temp_manifest_dir = "cache_hash_change_file_manifest_dir";
2514
2515 try tmp.dir.writeFile(io, .{ .sub_path = input_file_name, .data = input_file_contents });
2516 const tmp_directory: Directory = .{
2517 .path = try std.fs.path.join(gpa, &.{std.testing.TmpDir.parent_dir_path}),
2518 .handle = tmp.dir,
2519 };
2520 defer gpa.free(tmp_directory.path.?);
2521
2522 try testWaitForFileTimestampTick(io, tmp.dir);
2523
2524 var digest1: HexDigest = undefined;
2525 var digest2: HexDigest = undefined;
2526
2527 {
2528 var cache: Cache = .{
2529 .io = io,
2530 .gpa = gpa,
2531 .manifest_dir = try tmp.dir.createDirPathOpen(io, temp_manifest_dir, .{}),
2532 .cwd = cwd,
2533 };
2534 cache.addPrefix(.{ .path = null, .handle = Io.Dir.cwd() });
2535 cache.addPrefix(tmp_directory);
2536 defer cache.manifest_dir.close(io);
2537
2538 {
2539 var man = cache.obtain();
2540 defer man.deinit();
2541
2542 man.hash.addBytes("hHHR7dRt");
2543 try testing.expectEqual(
2544 @as(Manifest.InputPath.Index, @fromBackingInt(0)),
2545 try man.addInputPath(.{
2546 .root_dir = tmp_directory,
2547 .sub_path = input_file_name,
2548 }, .{}),
2549 );
2550
2551 var diag: Manifest.CheckDiagnostic = undefined;
2552 try testing.expectEqual(.incomplete_manifest, try man.check(&diag, .none));
2553
2554 digest1 = man.missDigestHex();
2555
2556 try man.finalize();
2557 }
2558
2559 // Same contents, but updates the metadata.
2560 try tmp.dir.writeFile(io, .{ .sub_path = input_file_name, .data = input_file_contents });
2561
2562 {
2563 var man = cache.obtain();
2564 defer man.deinit();
2565
2566 man.hash.addBytes("hHHR7dRt");
2567 try testing.expectEqual(
2568 @as(Manifest.InputPath.Index, @fromBackingInt(0)),
2569 try man.addInputPath(.{
2570 .root_dir = tmp_directory,
2571 .sub_path = input_file_name,
2572 }, .{}),
2573 );
2574
2575 var diag: Manifest.CheckDiagnostic = undefined;
2576 try testing.expectEqual(.hit, try man.check(&diag, .none));
2577
2578 digest2 = man.hitDigestHex();
2579 try testing.expectEqual(false, man.have_exclusive_lock);
2580 }
2581
2582 try testing.expectEqualSlices(u8, &digest1, &digest2);
2583 }
2584}
2585
24882586test "no file inputs" {
24892587 const io = testing.io;
24902588
......@@ -2557,11 +2655,7 @@ test "Manifest with files added after initial hash" {
25572655 };
25582656 defer testing.allocator.free(tmp_directory.path.?);
25592657
2560 // Wait for file timestamps to tick
2561 const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
2562 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
2563 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
2564 }
2658 try testWaitForFileTimestampTick(io, tmp.dir);
25652659
25662660 var digest1: HexDigest = undefined;
25672661 var digest2: HexDigest = undefined;
......@@ -2620,11 +2714,7 @@ test "Manifest with files added after initial hash" {
26202714 // Modify the file added after initial hash
26212715 try tmp.dir.writeFile(io, .{ .sub_path = temp_file2, .data = "Hello world the second, updated\n" });
26222716
2623 // Wait for file timestamps to tick
2624 const initial_time2 = try testGetCurrentFileTimestamp(io, tmp.dir);
2625 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time2.nanoseconds) {
2626 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
2627 }
2717 try testWaitForFileTimestampTick(io, tmp.dir);
26282718
26292719 {
26302720 var man = cache.obtain();