compiler: improve progress output

* "Flush" nodes ("LLVM Emit Object", "ELF Flush") appear under "Linking"

* "Code Generation" disappears when all analysis and codegen is done

* We only show one node under "Semantic Analysis" to accurately convey
  that analysis isn't happening in parallel, but rather that we're
  pausing one task to do another
This commit is contained in:
mlugg 2025-06-08 21:47:29 +01:00
parent ba53b14028
commit db5d85b8c8
No known key found for this signature in database
GPG Key ID: 3F5B7DCCBF4AF02E
7 changed files with 126 additions and 41 deletions

View File

@ -234,6 +234,28 @@ pub const Node = struct {
_ = @atomicRmw(u32, &storage.completed_count, .Add, 1, .monotonic);
}
/// Thread-safe. Bytes after '0' in `new_name` are ignored.
pub fn setName(n: Node, new_name: []const u8) void {
const index = n.index.unwrap() orelse return;
const storage = storageByIndex(index);
const name_len = @min(max_name_len, std.mem.indexOfScalar(u8, new_name, 0) orelse new_name.len);
copyAtomicStore(storage.name[0..name_len], new_name[0..name_len]);
if (name_len < storage.name.len)
@atomicStore(u8, &storage.name[name_len], 0, .monotonic);
}
/// Gets the name of this `Node`.
/// A pointer to this array can later be passed to `setName` to restore the name.
pub fn getName(n: Node) [max_name_len]u8 {
var dest: [max_name_len]u8 align(@alignOf(usize)) = undefined;
if (n.index.unwrap()) |index| {
copyAtomicLoad(&dest, &storageByIndex(index).name);
}
return dest;
}
/// Thread-safe.
pub fn setCompletedItems(n: Node, completed_items: usize) void {
const index = n.index.unwrap() orelse return;

View File

@ -255,7 +255,7 @@ test_filters: []const []const u8,
test_name_prefix: ?[]const u8,
link_task_wait_group: WaitGroup = .{},
work_queue_progress_node: std.Progress.Node = .none,
link_prog_node: std.Progress.Node = std.Progress.Node.none,
llvm_opt_bisect_limit: c_int,
@ -2795,6 +2795,17 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
}
}
// The linker progress node is set up here instead of in `performAllTheWork`, because
// we also want it around during `flush`.
const have_link_node = comp.bin_file != null;
if (have_link_node) {
comp.link_prog_node = main_progress_node.start("Linking", 0);
}
defer if (have_link_node) {
comp.link_prog_node.end();
comp.link_prog_node = .none;
};
try comp.performAllTheWork(main_progress_node);
if (comp.zcu) |zcu| {
@ -2843,7 +2854,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
switch (comp.cache_use) {
.none, .incremental => {
try flush(comp, arena, .main, main_progress_node);
try flush(comp, arena, .main);
},
.whole => |whole| {
if (comp.file_system_inputs) |buf| try man.populateFileSystemInputs(buf);
@ -2919,7 +2930,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
}
}
try flush(comp, arena, .main, main_progress_node);
try flush(comp, arena, .main);
// Calling `flush` may have produced errors, in which case the
// cache manifest must not be written.
@ -3009,13 +3020,12 @@ fn flush(
comp: *Compilation,
arena: Allocator,
tid: Zcu.PerThread.Id,
prog_node: std.Progress.Node,
) !void {
if (comp.zcu) |zcu| {
if (zcu.llvm_object) |llvm_object| {
// Emit the ZCU object from LLVM now; it's required to flush the output file.
// If there's an output file, it wants to decide where the LLVM object goes!
const sub_prog_node = prog_node.start("LLVM Emit Object", 0);
const sub_prog_node = comp.link_prog_node.start("LLVM Emit Object", 0);
defer sub_prog_node.end();
try llvm_object.emit(.{
.pre_ir_path = comp.verbose_llvm_ir,
@ -3053,7 +3063,7 @@ fn flush(
}
if (comp.bin_file) |lf| {
// This is needed before reading the error flags.
lf.flush(arena, tid, prog_node) catch |err| switch (err) {
lf.flush(arena, tid, comp.link_prog_node) catch |err| switch (err) {
error.LinkFailure => {}, // Already reported.
error.OutOfMemory => return error.OutOfMemory,
};
@ -4172,28 +4182,15 @@ pub fn addWholeFileError(
}
}
pub fn performAllTheWork(
fn performAllTheWork(
comp: *Compilation,
main_progress_node: std.Progress.Node,
) JobError!void {
comp.work_queue_progress_node = main_progress_node;
defer comp.work_queue_progress_node = .none;
// Regardless of errors, `comp.zcu` needs to update its generation number.
defer if (comp.zcu) |zcu| {
zcu.sema_prog_node.end();
zcu.sema_prog_node = .none;
zcu.codegen_prog_node.end();
zcu.codegen_prog_node = .none;
zcu.generation += 1;
};
try comp.performAllTheWorkInner(main_progress_node);
}
fn performAllTheWorkInner(
comp: *Compilation,
main_progress_node: std.Progress.Node,
) JobError!void {
// Here we queue up all the AstGen tasks first, followed by C object compilation.
// We wait until the AstGen tasks are all completed before proceeding to the
// (at least for now) single-threaded main work queue. However, C object compilation
@ -4513,8 +4510,24 @@ fn performAllTheWorkInner(
}
zcu.sema_prog_node = main_progress_node.start("Semantic Analysis", 0);
zcu.codegen_prog_node = if (comp.bin_file != null) main_progress_node.start("Code Generation", 0) else .none;
if (comp.bin_file != null) {
zcu.codegen_prog_node = main_progress_node.start("Code Generation", 0);
}
// We increment `pending_codegen_jobs` so that it doesn't reach 0 until after analysis finishes.
// That prevents the "Code Generation" node from constantly disappearing and reappearing when
// we're probably going to analyze more functions at some point.
assert(zcu.pending_codegen_jobs.swap(1, .monotonic) == 0); // don't let this become 0 until analysis finishes
}
// When analysis ends, delete the progress nodes for "Semantic Analysis" and possibly "Code Generation".
defer if (comp.zcu) |zcu| {
zcu.sema_prog_node.end();
zcu.sema_prog_node = .none;
if (zcu.pending_codegen_jobs.rmw(.Sub, 1, .monotonic) == 1) {
// Decremented to 0, so all done.
zcu.codegen_prog_node.end();
zcu.codegen_prog_node = .none;
}
};
if (!comp.separateCodegenThreadOk()) {
// Waits until all input files have been parsed.
@ -4583,6 +4596,7 @@ fn processOneJob(tid: usize, comp: *Compilation, job: Job) JobError!void {
.status = .init(.pending),
.value = undefined,
};
assert(zcu.pending_codegen_jobs.rmw(.Add, 1, .monotonic) > 0); // the "Code Generation" node hasn't been ended
if (comp.separateCodegenThreadOk()) {
// `workerZcuCodegen` takes ownership of `air`.
comp.thread_pool.spawnWgId(&comp.link_task_wait_group, workerZcuCodegen, .{ comp, func.func, air, shared_mir });

View File

@ -66,8 +66,18 @@ root_mod: *Package.Module,
/// `root_mod` is the test runner, and `main_mod` is the user's source file which has the tests.
main_mod: *Package.Module,
std_mod: *Package.Module,
sema_prog_node: std.Progress.Node = std.Progress.Node.none,
codegen_prog_node: std.Progress.Node = std.Progress.Node.none,
sema_prog_node: std.Progress.Node = .none,
codegen_prog_node: std.Progress.Node = .none,
/// The number of codegen jobs which are pending or in-progress. Whichever thread drops this value
/// to 0 is responsible for ending `codegen_prog_node`. While semantic analysis is happening, this
/// value bottoms out at 1 instead of 0, to ensure that it can only drop to 0 after analysis is
/// completed (since semantic analysis could trigger more codegen work).
pending_codegen_jobs: std.atomic.Value(u32) = .init(0),
/// This is the progress node *under* `sema_prog_node` which is currently running.
/// When we have to pause to analyze something else, we just temporarily rename this node.
/// Eventually, when we thread semantic analysis, we will want one of these per thread.
cur_sema_prog_node: std.Progress.Node = .none,
/// Used by AstGen worker to load and store ZIR cache.
global_zir_cache: Cache.Directory,
@ -4753,3 +4763,27 @@ fn explainWhyFileIsInModule(
import = importer_ref.import;
}
}
const SemaProgNode = struct {
/// `null` means we created the node, so should end it.
old_name: ?[std.Progress.Node.max_name_len]u8,
pub fn end(spn: SemaProgNode, zcu: *Zcu) void {
if (spn.old_name) |old_name| {
zcu.sema_prog_node.completeOne(); // we're just renaming, but it's effectively completion
zcu.cur_sema_prog_node.setName(&old_name);
} else {
zcu.cur_sema_prog_node.end();
zcu.cur_sema_prog_node = .none;
}
}
};
pub fn startSemaProgNode(zcu: *Zcu, name: []const u8) SemaProgNode {
if (zcu.cur_sema_prog_node.index != .none) {
const old_name = zcu.cur_sema_prog_node.getName();
zcu.cur_sema_prog_node.setName(name);
return .{ .old_name = old_name };
} else {
zcu.cur_sema_prog_node = zcu.sema_prog_node.start(name, 0);
return .{ .old_name = null };
}
}

View File

@ -796,8 +796,8 @@ pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeU
info.deps.clearRetainingCapacity();
}
const unit_prog_node = zcu.sema_prog_node.start("comptime", 0);
defer unit_prog_node.end();
const unit_prog_node = zcu.startSemaProgNode("comptime");
defer unit_prog_node.end(zcu);
return pt.analyzeComptimeUnit(cu_id) catch |err| switch (err) {
error.AnalysisFail => {
@ -976,8 +976,8 @@ pub fn ensureNavValUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu
info.deps.clearRetainingCapacity();
}
const unit_prog_node = zcu.sema_prog_node.start(nav.fqn.toSlice(ip), 0);
defer unit_prog_node.end();
const unit_prog_node = zcu.startSemaProgNode(nav.fqn.toSlice(ip));
defer unit_prog_node.end(zcu);
const invalidate_value: bool, const new_failed: bool = if (pt.analyzeNavVal(nav_id)) |result| res: {
break :res .{
@ -1396,8 +1396,8 @@ pub fn ensureNavTypeUpToDate(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zc
info.deps.clearRetainingCapacity();
}
const unit_prog_node = zcu.sema_prog_node.start(nav.fqn.toSlice(ip), 0);
defer unit_prog_node.end();
const unit_prog_node = zcu.startSemaProgNode(nav.fqn.toSlice(ip));
defer unit_prog_node.end(zcu);
const invalidate_type: bool, const new_failed: bool = if (pt.analyzeNavType(nav_id)) |result| res: {
break :res .{
@ -1617,8 +1617,8 @@ pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, maybe_coerced_func_index: Inter
info.deps.clearRetainingCapacity();
}
const func_prog_node = zcu.sema_prog_node.start(ip.getNav(func.owner_nav).fqn.toSlice(ip), 0);
defer func_prog_node.end();
const func_prog_node = zcu.startSemaProgNode(ip.getNav(func.owner_nav).fqn.toSlice(ip));
defer func_prog_node.end(zcu);
const ies_outdated, const new_failed = if (pt.analyzeFuncBody(func_index)) |result|
.{ prev_failed or result.ies_outdated, false }
@ -3360,6 +3360,7 @@ pub fn populateTestFunctions(
ip.mutateVarInit(test_fns_val.toIntern(), new_init);
}
{
assert(zcu.codegen_prog_node.index == .none);
zcu.codegen_prog_node = main_progress_node.start("Code Generation", 0);
defer {
zcu.codegen_prog_node.end();
@ -4393,6 +4394,11 @@ pub fn runCodegen(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air, ou
},
}
zcu.comp.link_task_queue.mirReady(zcu.comp, out);
if (zcu.pending_codegen_jobs.rmw(.Sub, 1, .monotonic) == 1) {
// Decremented to 0, so all done.
zcu.codegen_prog_node.end();
zcu.codegen_prog_node = .none;
}
}
fn runCodegenInner(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) error{
OutOfMemory,

View File

@ -1074,7 +1074,7 @@ pub const File = struct {
/// Called when all linker inputs have been sent via `loadInput`. After
/// this, `loadInput` will not be called anymore.
pub fn prelink(base: *File, prog_node: std.Progress.Node) FlushError!void {
pub fn prelink(base: *File) FlushError!void {
assert(!base.post_prelink);
// In this case, an object file is created by the LLVM backend, so
@ -1085,7 +1085,7 @@ pub const File = struct {
switch (base.tag) {
inline .wasm => |tag| {
dev.check(tag.devFeature());
return @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(prog_node);
return @as(*tag.Type(), @fieldParentPtr("base", base)).prelink(base.comp.link_prog_node);
},
else => {},
}
@ -1293,7 +1293,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
const base = comp.bin_file orelse return;
switch (task) {
.load_explicitly_provided => {
const prog_node = comp.work_queue_progress_node.start("Parse Linker Inputs", comp.link_inputs.len);
const prog_node = comp.link_prog_node.start("Parse Inputs", comp.link_inputs.len);
defer prog_node.end();
for (comp.link_inputs) |input| {
base.loadInput(input) catch |err| switch (err) {
@ -1310,7 +1310,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
}
},
.load_host_libc => {
const prog_node = comp.work_queue_progress_node.start("Linker Parse Host libc", 0);
const prog_node = comp.link_prog_node.start("Parse Host libc", 0);
defer prog_node.end();
const target = comp.root_mod.resolved_target.result;
@ -1369,7 +1369,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
}
},
.load_object => |path| {
const prog_node = comp.work_queue_progress_node.start("Linker Parse Object", 0);
const prog_node = comp.link_prog_node.start("Parse Object", 0);
defer prog_node.end();
base.openLoadObject(path) catch |err| switch (err) {
error.LinkFailure => return, // error reported via diags
@ -1377,7 +1377,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
};
},
.load_archive => |path| {
const prog_node = comp.work_queue_progress_node.start("Linker Parse Archive", 0);
const prog_node = comp.link_prog_node.start("Parse Archive", 0);
defer prog_node.end();
base.openLoadArchive(path, null) catch |err| switch (err) {
error.LinkFailure => return, // error reported via link_diags
@ -1385,7 +1385,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
};
},
.load_dso => |path| {
const prog_node = comp.work_queue_progress_node.start("Linker Parse Shared Library", 0);
const prog_node = comp.link_prog_node.start("Parse Shared Library", 0);
defer prog_node.end();
base.openLoadDso(path, .{
.preferred_mode = .dynamic,
@ -1396,7 +1396,7 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void {
};
},
.load_input => |input| {
const prog_node = comp.work_queue_progress_node.start("Linker Parse Input", 0);
const prog_node = comp.link_prog_node.start("Parse Input", 0);
defer prog_node.end();
base.loadInput(input) catch |err| switch (err) {
error.LinkFailure => return, // error reported via link_diags
@ -1418,6 +1418,9 @@ pub fn doZcuTask(comp: *Compilation, tid: usize, task: ZcuTask) void {
const zcu = comp.zcu.?;
const pt: Zcu.PerThread = .activate(zcu, @enumFromInt(tid));
defer pt.deactivate();
const fqn_slice = zcu.intern_pool.getNav(nav_index).fqn.toSlice(&zcu.intern_pool);
const nav_prog_node = comp.link_prog_node.start(fqn_slice, 0);
defer nav_prog_node.end();
if (zcu.llvm_object) |llvm_object| {
llvm_object.updateNav(pt, nav_index) catch |err| switch (err) {
error.OutOfMemory => diags.setAllocFailure(),
@ -1441,6 +1444,9 @@ pub fn doZcuTask(comp: *Compilation, tid: usize, task: ZcuTask) void {
const nav = zcu.funcInfo(func.func).owner_nav;
const pt: Zcu.PerThread = .activate(zcu, @enumFromInt(tid));
defer pt.deactivate();
const fqn_slice = zcu.intern_pool.getNav(nav).fqn.toSlice(&zcu.intern_pool);
const nav_prog_node = comp.link_prog_node.start(fqn_slice, 0);
defer nav_prog_node.end();
switch (func.mir.status.load(.monotonic)) {
.pending => unreachable,
.ready => {},

View File

@ -267,6 +267,9 @@ pub fn flush(
const comp = lld.base.comp;
const result = if (comp.config.output_mode == .Lib and comp.config.link_mode == .static) r: {
if (!@import("build_options").have_llvm or !comp.config.use_lib_llvm) {
return lld.base.comp.link_diags.fail("using lld without libllvm not implemented", .{});
}
break :r linkAsArchive(lld, arena);
} else switch (lld.ofmt) {
.coff => coffLink(lld, arena),

View File

@ -180,7 +180,7 @@ fn flushTaskQueue(tid: usize, q: *Queue, comp: *Compilation) void {
// We've finished the prelink tasks, so run prelink if necessary.
if (comp.bin_file) |lf| {
if (!lf.post_prelink) {
if (lf.prelink(comp.work_queue_progress_node)) |_| {
if (lf.prelink()) |_| {
lf.post_prelink = true;
} else |err| switch (err) {
error.OutOfMemory => comp.link_diags.setAllocFailure(),