zig/lib/build-web/time_report.zig
mlugg 7e7d7875b9
std.Build: implement unit test timeouts
For now, there is a flag to `zig build` called `--test-timeout-ms` which
accepts a value in milliseconds. If the execution time of any individual
unit test exceeds that number of milliseconds, the test is terminated
and marked as timed out.

In the future, we may want to increase the granularity of this feature
by allowing timeouts to be specified per-step or even per-test. However,
a global option is actually very useful. In particular, it can be used
in CI scripts to ensure that no individual unit test exceeds some
reasonable limit (e.g. 60 seconds) without having to assign limits to
every individual test step in the build script.

Also, individual unit test durations are now shown in the time report
web interface -- this was fairly trivial to add since we're timing tests
(to check for timeouts) anyway.

This commit makes progress on #19821, but does not close it, because
that proposal includes a more sophisticated mechanism for setting
timeouts.

Co-Authored-By: David Rubin <david@vortan.dev>
2025-10-18 09:28:39 +01:00

280 lines
11 KiB
Zig

const std = @import("std");
const gpa = std.heap.wasm_allocator;
const abi = std.Build.abi.time_report;
const fmtEscapeHtml = @import("root").fmtEscapeHtml;
const step_list = &@import("root").step_list;
const js = struct {
extern "time_report" fn updateGeneric(
/// The index of the step.
step_idx: u32,
// The HTML which will be used to populate the template slots.
inner_html_ptr: [*]const u8,
inner_html_len: usize,
) void;
extern "time_report" fn updateCompile(
/// The index of the step.
step_idx: u32,
// The HTML which will be used to populate the template slots.
inner_html_ptr: [*]const u8,
inner_html_len: usize,
// The HTML which will populate the <tbody> of the file table.
file_table_html_ptr: [*]const u8,
file_table_html_len: usize,
// The HTML which will populate the <tbody> of the decl table.
decl_table_html_ptr: [*]const u8,
decl_table_html_len: usize,
/// Whether the LLVM backend was used. If not, LLVM-specific statistics are hidden.
use_llvm: bool,
) void;
extern "time_report" fn updateRunTest(
/// The index of the step.
step_idx: u32,
// The HTML which will populate the <tbody> of the test table.
table_html_ptr: [*]const u8,
table_html_len: usize,
) void;
};
pub fn genericResultMessage(msg_bytes: []u8) error{OutOfMemory}!void {
if (msg_bytes.len != @sizeOf(abi.GenericResult)) @panic("malformed GenericResult message");
const msg: *const abi.GenericResult = @ptrCast(msg_bytes);
if (msg.step_idx >= step_list.*.len) @panic("malformed GenericResult message");
const inner_html = try std.fmt.allocPrint(gpa,
\\<code slot="step-name">{[step_name]f}</code>
\\<span slot="stat-total-time">{[stat_total_time]D}</span>
, .{
.step_name = fmtEscapeHtml(step_list.*[msg.step_idx].name),
.stat_total_time = msg.ns_total,
});
defer gpa.free(inner_html);
js.updateGeneric(msg.step_idx, inner_html.ptr, inner_html.len);
}
pub fn compileResultMessage(msg_bytes: []u8) error{ OutOfMemory, WriteFailed }!void {
const max_table_rows = 500;
if (msg_bytes.len < @sizeOf(abi.CompileResult)) @panic("malformed CompileResult message");
const hdr: *const abi.CompileResult = @ptrCast(msg_bytes[0..@sizeOf(abi.CompileResult)]);
if (hdr.step_idx >= step_list.*.len) @panic("malformed CompileResult message");
var trailing = msg_bytes[@sizeOf(abi.CompileResult)..];
const llvm_pass_timings = trailing[0..hdr.llvm_pass_timings_len];
trailing = trailing[hdr.llvm_pass_timings_len..];
const FileTimeReport = struct {
name: []const u8,
ns_sema: u64,
ns_codegen: u64,
ns_link: u64,
};
const DeclTimeReport = struct {
file_name: []const u8,
name: []const u8,
sema_count: u32,
ns_sema: u64,
ns_codegen: u64,
ns_link: u64,
};
const slowest_files = try gpa.alloc(FileTimeReport, hdr.files_len);
defer gpa.free(slowest_files);
const slowest_decls = try gpa.alloc(DeclTimeReport, hdr.decls_len);
defer gpa.free(slowest_decls);
for (slowest_files) |*file_out| {
const i = std.mem.indexOfScalar(u8, trailing, 0) orelse @panic("malformed CompileResult message");
file_out.* = .{
.name = trailing[0..i],
.ns_sema = 0,
.ns_codegen = 0,
.ns_link = 0,
};
trailing = trailing[i + 1 ..];
}
for (slowest_decls) |*decl_out| {
const i = std.mem.indexOfScalar(u8, trailing, 0) orelse @panic("malformed CompileResult message");
const file_idx = std.mem.readInt(u32, trailing[i..][1..5], .little);
const sema_count = std.mem.readInt(u32, trailing[i..][5..9], .little);
const sema_ns = std.mem.readInt(u64, trailing[i..][9..17], .little);
const codegen_ns = std.mem.readInt(u64, trailing[i..][17..25], .little);
const link_ns = std.mem.readInt(u64, trailing[i..][25..33], .little);
const file = &slowest_files[file_idx];
decl_out.* = .{
.file_name = file.name,
.name = trailing[0..i],
.sema_count = sema_count,
.ns_sema = sema_ns,
.ns_codegen = codegen_ns,
.ns_link = link_ns,
};
trailing = trailing[i + 33 ..];
file.ns_sema += sema_ns;
file.ns_codegen += codegen_ns;
file.ns_link += link_ns;
}
const S = struct {
fn fileLessThan(_: void, lhs: FileTimeReport, rhs: FileTimeReport) bool {
const lhs_ns = lhs.ns_sema + lhs.ns_codegen + lhs.ns_link;
const rhs_ns = rhs.ns_sema + rhs.ns_codegen + rhs.ns_link;
return lhs_ns > rhs_ns; // flipped to sort in reverse order
}
fn declLessThan(_: void, lhs: DeclTimeReport, rhs: DeclTimeReport) bool {
//if (true) return lhs.sema_count > rhs.sema_count;
const lhs_ns = lhs.ns_sema + lhs.ns_codegen + lhs.ns_link;
const rhs_ns = rhs.ns_sema + rhs.ns_codegen + rhs.ns_link;
return lhs_ns > rhs_ns; // flipped to sort in reverse order
}
};
std.mem.sort(FileTimeReport, slowest_files, {}, S.fileLessThan);
std.mem.sort(DeclTimeReport, slowest_decls, {}, S.declLessThan);
const stats = hdr.stats;
const inner_html = try std.fmt.allocPrint(gpa,
\\<code slot="step-name">{[step_name]f}</code>
\\<span slot="stat-reachable-files">{[stat_reachable_files]d}</span>
\\<span slot="stat-imported-files">{[stat_imported_files]d}</span>
\\<span slot="stat-generic-instances">{[stat_generic_instances]d}</span>
\\<span slot="stat-inline-calls">{[stat_inline_calls]d}</span>
\\<span slot="stat-compilation-time">{[stat_compilation_time]D}</span>
\\<span slot="cpu-time-parse">{[cpu_time_parse]D}</span>
\\<span slot="cpu-time-astgen">{[cpu_time_astgen]D}</span>
\\<span slot="cpu-time-sema">{[cpu_time_sema]D}</span>
\\<span slot="cpu-time-codegen">{[cpu_time_codegen]D}</span>
\\<span slot="cpu-time-link">{[cpu_time_link]D}</span>
\\<span slot="real-time-files">{[real_time_files]D}</span>
\\<span slot="real-time-decls">{[real_time_decls]D}</span>
\\<span slot="real-time-llvm-emit">{[real_time_llvm_emit]D}</span>
\\<span slot="real-time-link-flush">{[real_time_link_flush]D}</span>
\\<pre slot="llvm-pass-timings"><code>{[llvm_pass_timings]f}</code></pre>
\\
, .{
.step_name = fmtEscapeHtml(step_list.*[hdr.step_idx].name),
.stat_reachable_files = stats.n_reachable_files,
.stat_imported_files = stats.n_imported_files,
.stat_generic_instances = stats.n_generic_instances,
.stat_inline_calls = stats.n_inline_calls,
.stat_compilation_time = hdr.ns_total,
.cpu_time_parse = stats.cpu_ns_parse,
.cpu_time_astgen = stats.cpu_ns_astgen,
.cpu_time_sema = stats.cpu_ns_sema,
.cpu_time_codegen = stats.cpu_ns_codegen,
.cpu_time_link = stats.cpu_ns_link,
.real_time_files = stats.real_ns_files,
.real_time_decls = stats.real_ns_decls,
.real_time_llvm_emit = stats.real_ns_llvm_emit,
.real_time_link_flush = stats.real_ns_link_flush,
.llvm_pass_timings = fmtEscapeHtml(llvm_pass_timings),
});
defer gpa.free(inner_html);
var file_table_html: std.Io.Writer.Allocating = .init(gpa);
defer file_table_html.deinit();
for (slowest_files[0..@min(max_table_rows, slowest_files.len)]) |file| {
try file_table_html.writer.print(
\\<tr>
\\ <th scope="row"><code>{f}</code></th>
\\ <td>{D}</td>
\\ <td>{D}</td>
\\ <td>{D}</td>
\\ <td>{D}</td>
\\</tr>
\\
, .{
fmtEscapeHtml(file.name),
file.ns_sema,
file.ns_codegen,
file.ns_link,
file.ns_sema + file.ns_codegen + file.ns_link,
});
}
if (slowest_files.len > max_table_rows) {
try file_table_html.writer.print(
\\<tr><td colspan="4">{d} more rows omitted</td></tr>
\\
, .{slowest_files.len - max_table_rows});
}
var decl_table_html: std.Io.Writer.Allocating = .init(gpa);
defer decl_table_html.deinit();
for (slowest_decls[0..@min(max_table_rows, slowest_decls.len)]) |decl| {
try decl_table_html.writer.print(
\\<tr>
\\ <th scope="row"><code>{f}</code></th>
\\ <th scope="row"><code>{f}</code></th>
\\ <td>{d}</td>
\\ <td>{D}</td>
\\ <td>{D}</td>
\\ <td>{D}</td>
\\ <td>{D}</td>
\\</tr>
\\
, .{
fmtEscapeHtml(decl.file_name),
fmtEscapeHtml(decl.name),
decl.sema_count,
decl.ns_sema,
decl.ns_codegen,
decl.ns_link,
decl.ns_sema + decl.ns_codegen + decl.ns_link,
});
}
if (slowest_decls.len > max_table_rows) {
try decl_table_html.writer.print(
\\<tr><td colspan="6">{d} more rows omitted</td></tr>
\\
, .{slowest_decls.len - max_table_rows});
}
js.updateCompile(
hdr.step_idx,
inner_html.ptr,
inner_html.len,
file_table_html.written().ptr,
file_table_html.written().len,
decl_table_html.written().ptr,
decl_table_html.written().len,
hdr.flags.use_llvm,
);
}
pub fn runTestResultMessage(msg_bytes: []u8) error{OutOfMemory}!void {
if (msg_bytes.len < @sizeOf(abi.RunTestResult)) @panic("malformed RunTestResult message");
const hdr: *const abi.RunTestResult = @ptrCast(msg_bytes[0..@sizeOf(abi.RunTestResult)]);
if (hdr.step_idx >= step_list.*.len) @panic("malformed RunTestResult message");
const trailing = msg_bytes[@sizeOf(abi.RunTestResult)..];
const durations: []align(1) const u64 = @ptrCast(trailing[0 .. hdr.tests_len * 8]);
var offset: usize = hdr.tests_len * 8;
var table_html: std.ArrayListUnmanaged(u8) = .empty;
defer table_html.deinit(gpa);
for (durations) |test_ns| {
const test_name_len = std.mem.indexOfScalar(u8, trailing[offset..], 0) orelse @panic("malformed RunTestResult message");
const test_name = trailing[offset..][0..test_name_len];
offset += test_name_len + 1;
try table_html.print(gpa, "<tr><th scope=\"row\"><code>{f}</code></th>", .{fmtEscapeHtml(test_name)});
if (test_ns == std.math.maxInt(u64)) {
try table_html.appendSlice(gpa, "<td class=\"empty-cell\"></td>"); // didn't run
} else {
try table_html.print(gpa, "<td>{D}</td>", .{test_ns});
}
try table_html.appendSlice(gpa, "</tr>\n");
}
if (offset != trailing.len) @panic("malformed RunTestResult message");
js.updateRunTest(
hdr.step_idx,
table_html.items.ptr,
table_html.items.len,
);
}