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 {...@@ -1365,6 +1365,8 @@ pub const Manifest = struct {
1365 if (mem.eql(u8, &disk_file.digest, &input_file.digest))1365 if (mem.eql(u8, &disk_file.digest, &input_file.digest))
1366 return .hit;1366 return .hit;
1367 }1367 }
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
1369 if (input_path.have_stat) {1371 if (input_path.have_stat) {
1370 const changed = try disk_file.setStatChanged(m, input_file.stat());1372 const changed = try disk_file.setStatChanged(m, input_file.stat());
...@@ -1418,10 +1420,14 @@ pub const Manifest = struct {...@@ -1418,10 +1420,14 @@ pub const Manifest = struct {
1418 if (input_path.contents.get(m.all_input_content.items)) |file_or_dir_contents| {1420 if (input_path.contents.get(m.all_input_content.items)) |file_or_dir_contents| {
1419 var hasher = hasher_init;1421 var hasher = hasher_init;
1420 hasher.update(file_or_dir_contents);1422 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;1425 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) {
1424 return .{ .contents_changed = file_off };1426 return .hit;
1427 } else {
1428 @memcpy(&disk_file.digest, &input_file.digest);
1429 return .{ .contents_changed = file_off };
1430 }
1425 }1431 }
14261432
1427 if (disk_file.flags.is_directory) {1433 if (disk_file.flags.is_directory) {
...@@ -1455,7 +1461,7 @@ pub const Manifest = struct {...@@ -1455,7 +1461,7 @@ pub const Manifest = struct {
1455 }1461 }
14561462
1457 const dir_contents_start = m.all_input_content.items.len;1463 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) {
1459 error.Canceled, error.OutOfMemory => |e| return e,1465 error.Canceled, error.OutOfMemory => |e| return e,
1460 else => |e| return fail(diag, .{ .file_read = .{1466 else => |e| return fail(diag, .{ .file_read = .{
1461 .file_offset = file_off,1467 .file_offset = file_off,
...@@ -1471,8 +1477,12 @@ pub const Manifest = struct {...@@ -1471,8 +1477,12 @@ pub const Manifest = struct {
1471 .populated => unreachable,1477 .populated => unreachable,
1472 };1478 };
14731479
1474 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) return .hit;1480 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) {
1475 return .{ .contents_changed = file_off };1481 return .hit;
1482 } else {
1483 @memcpy(&disk_file.digest, &input_file.digest);
1484 return .{ .contents_changed = file_off };
1485 }
1476 } else {1486 } else {
1477 const opened_file = if (input_path.have_handle)1487 const opened_file = if (input_path.have_handle)
1478 input_path.handle.file1488 input_path.handle.file
...@@ -1503,7 +1513,7 @@ pub const Manifest = struct {...@@ -1503,7 +1513,7 @@ pub const Manifest = struct {
1503 switch (input_path.contents.unwrap()) {1513 switch (input_path.contents.unwrap()) {
1504 .requested => {1514 .requested => {
1505 const start = m.all_input_content.items.len;1515 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) {
1507 error.Canceled, error.OutOfMemory => |e| return e,1517 error.Canceled, error.OutOfMemory => |e| return e,
1508 else => |e| return fail(diag, .{ .file_read = .{1518 else => |e| return fail(diag, .{ .file_read = .{
1509 .file_offset = file_off,1519 .file_offset = file_off,
...@@ -1515,7 +1525,7 @@ pub const Manifest = struct {...@@ -1515,7 +1525,7 @@ pub const Manifest = struct {
1515 .len = @intCast(m.all_input_content.items.len - start),1525 .len = @intCast(m.all_input_content.items.len - start),
1516 });1526 });
1517 },1527 },
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) {
1519 error.Canceled => |e| return e,1529 error.Canceled => |e| return e,
1520 else => |e| return fail(diag, .{ .file_read = .{1530 else => |e| return fail(diag, .{ .file_read = .{
1521 .file_offset = file_off,1531 .file_offset = file_off,
...@@ -1525,8 +1535,12 @@ pub const Manifest = struct {...@@ -1525,8 +1535,12 @@ pub const Manifest = struct {
1525 .populated => unreachable,1535 .populated => unreachable,
1526 }1536 }
15271537
1528 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) return .hit;1538 if (mem.eql(u8, &disk_file.digest, &input_file.digest)) {
1529 return .{ .contents_changed = file_off };1539 return .hit;
1540 } else {
1541 @memcpy(&disk_file.digest, &input_file.digest);
1542 return .{ .contents_changed = file_off };
1543 }
1530 }1544 }
1531 }1545 }
15321546
...@@ -2313,6 +2327,13 @@ fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp {...@@ -2313,6 +2327,13 @@ fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp {
2313 return (try file.stat(io)).mtime;2327 return (try file.stat(io)).mtime;
2314}2328}
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
2316test "cache file and then recall it" {2337test "cache file and then recall it" {
2317 const io = testing.io;2338 const io = testing.io;
23182339
...@@ -2332,11 +2353,7 @@ test "cache file and then recall it" {...@@ -2332,11 +2353,7 @@ test "cache file and then recall it" {
2332 };2353 };
2333 defer testing.allocator.free(tmp_directory.path.?);2354 defer testing.allocator.free(tmp_directory.path.?);
23342355
2335 // Wait for file timestamps to tick2356 try testWaitForFileTimestampTick(io, tmp.dir);
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 }
23402357
2341 var digest1: HexDigest = undefined;2358 var digest1: HexDigest = undefined;
2342 var digest2: HexDigest = undefined;2359 var digest2: HexDigest = undefined;
...@@ -2415,11 +2432,7 @@ test "check that changing a file causes cache miss" {...@@ -2415,11 +2432,7 @@ test "check that changing a file causes cache miss" {
2415 };2432 };
2416 defer testing.allocator.free(tmp_directory.path.?);2433 defer testing.allocator.free(tmp_directory.path.?);
24172434
2418 // Wait for file timestamps to tick2435 try testWaitForFileTimestampTick(io, tmp.dir);
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 }
24232436
2424 var digest1: HexDigest = undefined;2437 var digest1: HexDigest = undefined;
2425 var digest2: HexDigest = undefined;2438 var digest2: HexDigest = undefined;
...@@ -2485,6 +2498,91 @@ test "check that changing a file causes cache miss" {...@@ -2485,6 +2498,91 @@ test "check that changing a file causes cache miss" {
2485 }2498 }
2486}2499}
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
2488test "no file inputs" {2586test "no file inputs" {
2489 const io = testing.io;2587 const io = testing.io;
24902588
...@@ -2557,11 +2655,7 @@ test "Manifest with files added after initial hash" {...@@ -2557,11 +2655,7 @@ test "Manifest with files added after initial hash" {
2557 };2655 };
2558 defer testing.allocator.free(tmp_directory.path.?);2656 defer testing.allocator.free(tmp_directory.path.?);
25592657
2560 // Wait for file timestamps to tick2658 try testWaitForFileTimestampTick(io, tmp.dir);
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 }
25652659
2566 var digest1: HexDigest = undefined;2660 var digest1: HexDigest = undefined;
2567 var digest2: HexDigest = undefined;2661 var digest2: HexDigest = undefined;
...@@ -2620,11 +2714,7 @@ test "Manifest with files added after initial hash" {...@@ -2620,11 +2714,7 @@ test "Manifest with files added after initial hash" {
2620 // Modify the file added after initial hash2714 // Modify the file added after initial hash
2621 try tmp.dir.writeFile(io, .{ .sub_path = temp_file2, .data = "Hello world the second, updated\n" });2715 try tmp.dir.writeFile(io, .{ .sub_path = temp_file2, .data = "Hello world the second, updated\n" });
26222716
2623 // Wait for file timestamps to tick2717 try testWaitForFileTimestampTick(io, tmp.dir);
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 }
26282718
2629 {2719 {
2630 var man = cache.obtain();2720 var man = cache.obtain();