std.Build.Watch: make dirty steps invalidate each other

and make failed steps always be invalidated
and make steps that don't need to be reevaluated marked as cached
This commit is contained in:
Andrew Kelley 2024-07-09 20:05:37 -07:00
parent 6f89824c22
commit 001ff7b3b2
3 changed files with 82 additions and 40 deletions

View file

@ -500,9 +500,10 @@ pub fn main() !void {
const events_len = try std.posix.poll(&poll_fds, timeout); const events_len = try std.posix.poll(&poll_fds, timeout);
if (events_len == 0) { if (events_len == 0) {
debouncing_node.end(); debouncing_node.end();
Watch.markFailedStepsDirty(gpa, run.step_stack.keys());
continue :rebuild; continue :rebuild;
} }
if (try markDirtySteps(&w)) { if (try w.markDirtySteps(gpa)) {
if (!debouncing) { if (!debouncing) {
debouncing = true; debouncing = true;
debouncing_node.end(); debouncing_node.end();
@ -513,44 +514,6 @@ pub fn main() !void {
} }
} }
fn markDirtySteps(w: *Watch) !bool {
const fanotify = std.os.linux.fanotify;
const M = fanotify.event_metadata;
var events_buf: [256 + 4096]u8 = undefined;
var any_dirty = false;
while (true) {
var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) {
error.WouldBlock => return any_dirty,
else => |e| return e,
};
var meta: [*]align(1) M = @ptrCast(&events_buf);
while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
len -= meta[0].event_len;
meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
}) {
assert(meta[0].vers == M.VERSION);
const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
switch (fid.hdr.info_type) {
.DFID_NAME => {
const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
const file_name = mem.span(file_name_z);
const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle };
if (w.handle_table.getPtr(lfh)) |reaction_set| {
if (reaction_set.getPtr(file_name)) |step_set| {
for (step_set.keys()) |step| {
step.state = .precheck_done;
any_dirty = true;
}
}
}
},
else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
}
}
}
}
const Run = struct { const Run = struct {
max_rss: u64, max_rss: u64,
max_rss_is_default: bool, max_rss_is_default: bool,
@ -1319,7 +1282,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {
\\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
\\ --fetch Exit after fetching dependency tree \\ --fetch Exit after fetching dependency tree
\\ --watch Continuously rebuild when source files are modified \\ --watch Continuously rebuild when source files are modified
\\ --debounce <ms> Delay before rebuilding after watched file detection \\ --debounce <ms> Delay before rebuilding after changed file detected
\\ \\
\\Project-Specific Options: \\Project-Specific Options:
\\ \\

View file

@ -637,6 +637,31 @@ fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const
try gop.value_ptr.append(gpa, basename); try gop.value_ptr.append(gpa, basename);
} }
fn reset(step: *Step, gpa: Allocator) void {
assert(step.state == .precheck_done);
step.result_error_msgs.clearRetainingCapacity();
step.result_stderr = "";
step.result_cached = false;
step.result_duration_ns = null;
step.result_peak_rss = 0;
step.test_results = .{};
step.result_error_bundle.deinit(gpa);
step.result_error_bundle = std.zig.ErrorBundle.empty;
}
/// Implementation detail of file watching. Prepares the step for being re-evaluated.
pub fn recursiveReset(step: *Step, gpa: Allocator) void {
assert(step.state != .precheck_done);
step.state = .precheck_done;
step.reset(gpa);
for (step.dependants.items) |dep| {
if (dep.state == .precheck_done) continue;
dep.recursiveReset(gpa);
}
}
test { test {
_ = CheckFile; _ = CheckFile;
_ = CheckObject; _ = CheckObject;

View file

@ -2,6 +2,7 @@ const std = @import("../std.zig");
const Watch = @This(); const Watch = @This();
const Step = std.Build.Step; const Step = std.Build.Step;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
dir_table: DirTable, dir_table: DirTable,
/// Keyed differently but indexes correspond 1:1 with `dir_table`. /// Keyed differently but indexes correspond 1:1 with `dir_table`.
@ -117,3 +118,56 @@ pub fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !LinuxFileHandle
const stack_lfh: LinuxFileHandle = .{ .handle = stack_ptr }; const stack_lfh: LinuxFileHandle = .{ .handle = stack_ptr };
return stack_lfh.clone(gpa); return stack_lfh.clone(gpa);
} }
pub fn markDirtySteps(w: *Watch, gpa: Allocator) !bool {
const fanotify = std.os.linux.fanotify;
const M = fanotify.event_metadata;
var events_buf: [256 + 4096]u8 = undefined;
var any_dirty = false;
while (true) {
var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) {
error.WouldBlock => return any_dirty,
else => |e| return e,
};
var meta: [*]align(1) M = @ptrCast(&events_buf);
while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
len -= meta[0].event_len;
meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
}) {
assert(meta[0].vers == M.VERSION);
const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
switch (fid.hdr.info_type) {
.DFID_NAME => {
const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
const file_name = std.mem.span(file_name_z);
const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle };
if (w.handle_table.getPtr(lfh)) |reaction_set| {
if (reaction_set.getPtr(file_name)) |step_set| {
for (step_set.keys()) |step| {
if (step.state != .precheck_done) {
step.recursiveReset(gpa);
any_dirty = true;
}
}
}
}
},
else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
}
}
}
}
pub fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
for (all_steps) |step| switch (step.state) {
.dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
else => continue,
};
// Now that all dirty steps have been found, the remaining steps that
// succeeded from last run shall be marked "cached".
for (all_steps) |step| switch (step.state) {
.success => step.result_cached = true,
else => continue,
};
}