feature. See also
. The project being documented here (as the example) is the Zig library itself.
File
Code
const WebServer = @This();
const builtin = @import("builtin");
const std = @import("std");
const Allocator = std.mem.Allocator;
const Cache = std.Build.Cache;
const Configuration = std.Build.Configuration;
const Io = std.Io;
const abi = std.Build.abi;
const assert = std.debug.assert;
const http = std.http;
const log = std.log.scoped(.web_server);
const mem = std.mem;
const net = std.Io.net;
const Maker = @import("../Maker.zig");
const Fuzz = @import("Fuzz.zig");
const Graph = @import("Graph.zig");
const Step = @import("Step.zig");
graph: *const Graph,
listen_address: net.IpAddress,
root_prog_node: std.Progress.Node,
tcp_server: ?net.Server,
serve_task: ?Io.Future(Io.Cancelable!void),
base_timestamp: Io.Timestamp,
fuzz: ?Fuzz,
build_status: std.atomic.Value(abi.BuildStatus),
update_id: std.atomic.Value(u32),
runner_request_mutex: Io.Mutex,
runner_request_ready_cond: Io.Condition,
runner_request_empty_cond: Io.Condition,
runner_request: ?RunnerRequest,
configured: ?Configured,
const Configured = struct {
maker: *Maker,
step_names_trailing: []u8,
step_status_bits: []u8,
time_report_mutex: Io.Mutex,
time_report_msgs: [][]u8,
time_report_update_times: []i64,
};
const default_update_interval_ms = 500;
pub const base_clock: Io.Clock = .awake;
pub fn notifyUpdate(ws: *WebServer) void {
const io = ws.graph.io;
_ = ws.update_id.rmw(.Add, 1, .release);
io.futexWake(u32, &ws.update_id.raw, 16);
}
pub const Options = struct {
graph: *const Graph,
root_prog_node: std.Progress.Node,
listen_address: net.IpAddress,
base_timestamp: Io.Clock.Timestamp,
};
pub fn init(opts: Options) WebServer {
// instead of threads, so that the web server can function in single-threaded builds.
comptime assert(!builtin.single_threaded);
assert(opts.base_timestamp.clock == base_clock);
return .{
.graph = opts.graph,
.listen_address = opts.listen_address,
.root_prog_node = opts.root_prog_node,
.tcp_server = null,
.serve_task = null,
.base_timestamp = opts.base_timestamp.raw,
.fuzz = null,
.build_status = .init(.idle),
.update_id = .init(0),
.runner_request_mutex = .init,
.runner_request_ready_cond = .init,
.runner_request_empty_cond = .init,
.runner_request = null,
.configured = null,
};
}
pub fn deinit(ws: *WebServer) void {
const graph = ws.graph;
const io = graph.io;
if (ws.fuzz) |*f| f.deinit();
ws.releaseConfigured();
if (ws.serve_task) |t| {
if (ws.tcp_server) |*s| s.stream.close(io);
t.await();
}
if (ws.tcp_server) |*s| s.deinit();
}
fn releaseConfigured(ws: *WebServer) void {
if (ws.configured) |*configured| {
const gpa = configured.maker.gpa;
gpa.free(configured.step_names_trailing);
gpa.free(configured.step_status_bits);
for (configured.time_report_msgs) |msg| gpa.free(msg);
gpa.free(configured.time_report_msgs);
gpa.free(configured.time_report_update_times);
gpa.free(configured.step_names_trailing);
ws.configured = null;
}
}
pub fn updateConfiguration(ws: *WebServer, maker: *Maker) !void {
const graph = ws.graph;
const gpa = maker.gpa;
const all_steps = maker.step_stack.keys();
const c = &maker.scanned_config.configuration;
const step_names_trailing = try gpa.alloc(u8, len: {
var name_bytes: usize = 0;
for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len;
break :len name_bytes + all_steps.len * 4;
});
errdefer gpa.free(step_names_trailing);
{
const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
var idx: usize = all_steps.len * 4;
for (all_steps, step_name_lens) |step_index, *name_len| {
const step_name = step_index.ptr(c).name.slice(c);
name_len.* = @intCast(step_name.len);
@memcpy(step_names_trailing[idx..][0..step_name.len], step_name);
idx += step_name.len;
}
assert(idx == step_names_trailing.len);
}
const step_status_bits = try gpa.alloc(u8, @divCeil(all_steps.len, 4));
errdefer gpa.free(step_status_bits);
@memset(step_status_bits, 0);
const time_reports_len: usize = if (graph.time_report) all_steps.len else 0;
const time_report_msgs = try gpa.alloc([]u8, time_reports_len);
errdefer gpa.free(time_report_msgs);
const time_report_update_times = try gpa.alloc(i64, time_reports_len);
errdefer gpa.free(time_report_update_times);
@memset(time_report_msgs, &.{});
@memset(time_report_update_times, std.math.minInt(i64));
ws.releaseConfigured();
ws.configured = .{
.maker = maker,
.step_names_trailing = step_names_trailing,
.step_status_bits = step_status_bits,
.time_report_mutex = .init,
.time_report_msgs = time_report_msgs,
.time_report_update_times = time_report_update_times,
};
}
pub fn start(ws: *WebServer) error{AlreadyReported}!void {
assert(ws.tcp_server == null);
assert(ws.serve_task == null);
const graph = ws.graph;
const io = graph.io;
ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
return error.AlreadyReported;
};
ws.serve_task = io.concurrent(serve, .{ws}) catch |err| {
log.err("unable to spawn web server thread: {t}", .{err});
ws.tcp_server.?.deinit(io);
ws.tcp_server = null;
return error.AlreadyReported;
};
log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
if (ws.listen_address.getPort() == 0) {
log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
}
}
fn serve(ws: *WebServer) Io.Cancelable!void {
const graph = ws.graph;
const io = graph.io;
var group: Io.Group = .init;
defer group.cancel(io);
while (true) {
var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err("failed to accept connection: {t}", .{e});
return;
},
};
group.concurrent(io, accept, .{ ws, stream }) catch |err| {
log.err("unable to spawn connection thread: {t}", .{err});
stream.close(io);
continue;
};
}
}
pub fn startBuild(ws: *WebServer) void {
if (ws.fuzz) |*fuzz| {
fuzz.deinit();
ws.fuzz = null;
}
const configured = &ws.configured.?;
for (configured.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
ws.build_status.store(.running, .monotonic);
ws.notifyUpdate();
}
pub fn updateStepStatus(
ws: *WebServer,
step_index: Configuration.Step.Index,
new_status: abi.StepUpdate.Status,
) void {
const configured = &ws.configured.?;
const maker = configured.maker;
const all_steps = maker.step_stack.keys();
const step_idx: u32 = for (all_steps, 0..) |s, i| {
if (s == step_index) break @intCast(i);
} else unreachable;
const ptr = &configured.step_status_bits[step_idx / 4];
const bit_offset: u3 = @intCast((step_idx % 4) * 2);
const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
const mask = @as(u8, @backingInt(new_status) ^ old_bits) << bit_offset;
_ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
ws.notifyUpdate();
}
pub fn finishBuild(ws: *WebServer, opts: struct {
fuzz: bool,
}) void {
const configured = &ws.configured.?;
const maker = configured.maker;
const all_steps = maker.step_stack.keys();
if (opts.fuzz) {
switch (builtin.os.tag) {
// * Memory-mapping to share data between the fuzzer and build runner.
// * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
// many addresses to source locations).
.windows => std.process.fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
else => {},
}
if (@bitSizeOf(usize) != 64) {
// parameter, `length: usize`, being compatible with file system's
// u64 return value. This is not the case on 32-bit platforms.
// Affects or affected by issues #5185, #22523, and #22464.
std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
}
assert(ws.fuzz == null);
ws.build_status.store(.fuzz_init, .monotonic);
ws.notifyUpdate();
ws.fuzz = Fuzz.init(maker, all_steps, ws.root_prog_node, .{ .forever = .{ .ws = ws } }) catch |err|
std.process.fatal("failed to start fuzzer: {t}", .{err});
ws.fuzz.?.start();
}
ws.build_status.store(if (maker.watch) .watching else .idle, .monotonic);
ws.notifyUpdate();
}
pub fn now(ws: *const WebServer) i64 {
const graph = ws.graph;
const io = graph.io;
const ts = base_clock.now(io);
return @intCast(ws.base_timestamp.durationTo(ts).toNanoseconds());
}
fn accept(ws: *WebServer, stream: net.Stream) void {
const graph = ws.graph;
const io = graph.io;
defer {
// `undefined`, but it cannot do so since it is an immutable parameter.
var copy = stream;
copy.close(io);
}
var send_buffer: [4096]u8 = undefined;
var recv_buffer: [4096]u8 = undefined;
var connection_reader = stream.reader(io, &recv_buffer);
var connection_writer = stream.writer(io, &send_buffer);
var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
while (true) {
var request = server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => return,
else => return log.err("failed to receive http request: {t}", .{err}),
};
switch (request.upgradeRequested()) {
.websocket => |opt_key| {
const key = opt_key orelse return log.err("missing websocket key", .{});
var web_socket = request.respondWebSocket(.{ .key = key }) catch {
return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
};
ws.serveWebSocket(&web_socket) catch |err| {
log.err("failed to serve websocket: {t}", .{err});
return;
};
comptime unreachable;
},
.other => |name| return log.err("unknown upgrade request: {s}", .{name}),
.none => {
ws.serveRequest(&request) catch |err| switch (err) {
error.AlreadyReported => return,
else => {
log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
return;
},
};
},
}
}
}
fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
const graph = ws.graph;
const gpa = graph.cache.gpa;
const io = graph.io;
log.err("TODO serve a different message when the configuration changes", .{});
const configured = &ws.configured.?;
const maker = configured.maker;
const all_steps = maker.step_stack.keys();
var prev_build_status = ws.build_status.load(.monotonic);
const prev_step_status_bits = try gpa.alloc(u8, configured.step_status_bits.len);
defer gpa.free(prev_step_status_bits);
for (prev_step_status_bits, configured.step_status_bits) |*copy, *shared| {
copy.* = @atomicLoad(u8, shared, .monotonic);
}
var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock });
defer recv_thread.cancel(io);
{
const hello_header: abi.Hello = .{
.status = prev_build_status,
.flags = .{
.time_report = graph.time_report,
},
.timestamp = ws.now(),
.steps_len = @intCast(all_steps.len),
};
var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), configured.step_names_trailing, prev_step_status_bits };
try sock.writeMessageVec(&bufs, .binary);
}
var prev_fuzz: Fuzz.Previous = .init;
var prev_time: i64 = std.math.minInt(i64);
while (true) {
const start_time = ws.now();
const start_update_id = ws.update_id.load(.acquire);
if (ws.fuzz) |*fuzz| {
try fuzz.sendUpdate(sock, &prev_fuzz);
}
{
try configured.time_report_mutex.lock(io);
defer configured.time_report_mutex.unlock(io);
for (configured.time_report_msgs, configured.time_report_update_times) |msg, update_time| {
if (update_time <= prev_time) continue;
// that we don't hold up the build system on the client accepting this packet.
const owned_msg = try gpa.dupe(u8, msg);
defer gpa.free(owned_msg);
configured.time_report_mutex.unlock(io);
defer configured.time_report_mutex.lockUncancelable(io);
try sock.writeMessage(owned_msg, .binary);
}
}
{
const build_status = ws.build_status.load(.monotonic);
if (build_status != prev_build_status) {
prev_build_status = build_status;
const msg: abi.StatusUpdate = .{ .new = build_status };
try sock.writeMessage(@ptrCast(&msg), .binary);
}
}
for (prev_step_status_bits, configured.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
const cur_byte = @atomicLoad(u8, shared, .monotonic);
if (prev_byte.* == cur_byte) continue;
const cur: [4]abi.StepUpdate.Status = .{
@fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 0)))),
@fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 2)))),
@fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 4)))),
@fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 6)))),
};
const prev: [4]abi.StepUpdate.Status = .{
@fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 0)))),
@fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 2)))),
@fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 4)))),
@fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 6)))),
};
for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
}
prev_byte.* = cur_byte;
}
prev_time = start_time;
const old_cp = io.swapCancelProtection(.blocked);
defer _ = io.swapCancelProtection(old_cp);
io.futexWaitTimeout(
u32,
&ws.update_id.raw,
start_update_id,
.{ .duration = .{
.clock = .awake,
.raw = .fromMilliseconds(default_update_interval_ms),
} },
) catch |err| switch (err) {
error.Canceled => unreachable,
};
}
}
fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
const graph = ws.graph;
const io = graph.io;
while (true) {
const msg = sock.readSmallMessage() catch return;
if (msg.opcode != .binary) continue;
if (msg.data.len == 0) continue;
const tag: abi.ToServerTag = @fromBackingInt(@intCast(msg.data[0]));
switch (tag) {
_ => continue,
.rebuild => while (true) {
ws.runner_request_mutex.lock(io) catch |err| switch (err) {
error.Canceled => return,
};
defer ws.runner_request_mutex.unlock(io);
if (ws.runner_request == null) {
ws.runner_request = .rebuild;
ws.runner_request_ready_cond.signal(io);
break;
}
ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
},
}
}
}
fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
const target: []const u8, const debug: bool = target: {
if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
break :target .{ req.head.target, false };
};
if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .debug else .fast);
if (ws.fuzz) |*fuzz| {
if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
}
try req.respond("not found", .{
.status = .not_found,
.extra_headers = &.{
.{ .name = "Content-Type", .value = "text/plain" },
},
});
}
fn serveLibFile(
ws: *WebServer,
request: *http.Server.Request,
sub_path: []const u8,
content_type: []const u8,
) !void {
const graph = ws.graph;
return serveFile(ws, request, .{
.root_dir = graph.zig_lib_directory,
.sub_path = sub_path,
}, content_type);
}
fn serveClientWasm(
ws: *WebServer,
req: *http.Server.Request,
optimize_mode: std.builtin.OptimizeMode,
) !void {
const gpa = ws.graph.cache.gpa;
var arena_state: std.heap.ArenaAllocator = .init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
const bin_path = try buildClientWasm(ws, arena, optimize_mode);
return serveFile(ws, req, bin_path, "application/wasm");
}
pub fn serveFile(
ws: *WebServer,
request: *http.Server.Request,
path: Cache.Path,
content_type: []const u8,
) !void {
const graph = ws.graph;
const gpa = graph.cache.gpa;
const io = graph.io;
// We load the file with every request so that the user can make changes to the file
// and refresh the HTML page without restarting this server.
const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
log.err("failed to read '{f}': {t}", .{ path, err });
return error.AlreadyReported;
};
defer gpa.free(file_contents);
try request.respond(file_contents, .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = content_type },
cache_control_header,
},
});
}
pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
const graph = ws.graph;
const io = graph.io;
var send_buffer: [0x4000]u8 = undefined;
var response = try request.respondStreaming(&send_buffer, .{
.respond_options = .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "application/x-tar" },
cache_control_header,
},
},
});
var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
for (paths) |path| {
var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| {
log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
continue;
};
defer file.close(io);
const stat = try file.stat(io);
var read_buffer: [1024]u8 = undefined;
var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size);
archiver.prefix = path.root_dir.path orelse graph.cache.cwd;
try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
}
try response.end();
}
fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path {
const root_name = "build-web";
const arch_os_abi = "wasm32-freestanding";
const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
const graph = ws.graph;
const gpa = graph.cache.gpa;
const io = graph.io;
const main_src_path: Cache.Path = .{
.root_dir = graph.zig_lib_directory,
.sub_path = "build-web/main.zig",
};
const walk_src_path: Cache.Path = .{
.root_dir = graph.zig_lib_directory,
.sub_path = "docs/wasm/Walk.zig",
};
const html_render_src_path: Cache.Path = .{
.root_dir = graph.zig_lib_directory,
.sub_path = "docs/wasm/html_render.zig",
};
var argv: std.ArrayList([]const u8) = .empty;
try argv.appendSlice(arena, &.{
graph.zig_exe, "build-exe",
"-fno-entry",
"-O", @tagName(optimize),
"-target", arch_os_abi,
"-mcpu", cpu_features,
"--cache-dir", graph.global_cache_root.path orelse ".",
"--global-cache-dir", graph.global_cache_root.path orelse ".",
"--zig-lib-dir", graph.zig_lib_directory.path orelse ".",
"--name", root_name,
"-rdynamic",
"-fsingle-threaded",
"--dep", "Walk",
"--dep", "html_render",
try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}),
try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}),
"--dep", "Walk",
try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}),
"--listen=-",
});
const compile_prog_node = ws.root_prog_node.start("Compile WebAssembly Component", 0);
defer compile_prog_node.end();
const result = try std.zig.buildExeSubprocess(gpa, io, .{
.argv = argv.items,
.cache_root = graph.global_cache_root,
.root_name = root_name,
.arch_os_abi = arch_os_abi,
.cpu_features = cpu_features,
.progress_node = compile_prog_node,
});
if (!result.cache_hit) log.info("source changes detected; rebuilt wasm component", .{});
return result.path;
}
pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
compile_step: Configuration.Step.Index,
use_llvm: bool,
stats: abi.time_report.CompileResult.Stats,
ns_total: u64,
llvm_pass_timings_len: u32,
files_len: u32,
decls_len: u32,
trailing: []const u8,
}) void {
const graph = ws.graph;
const io = graph.io;
const configured = &ws.configured.?;
const maker = configured.maker;
const gpa = maker.gpa;
const all_steps = maker.step_stack.keys();
const step_idx: u32 = for (all_steps, 0..) |s, i| {
if (s == opts.compile_step) break @intCast(i);
} else unreachable;
const old_buf = old: {
configured.time_report_mutex.lock(io) catch return;
defer configured.time_report_mutex.unlock(io);
const old = configured.time_report_msgs[step_idx];
configured.time_report_msgs[step_idx] = &.{};
break :old old;
};
const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
out_header.* = .{
.step_idx = step_idx,
.flags = .{
.use_llvm = opts.use_llvm,
},
.stats = opts.stats,
.ns_total = opts.ns_total,
.llvm_pass_timings_len = opts.llvm_pass_timings_len,
.files_len = opts.files_len,
.decls_len = opts.decls_len,
};
@memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
{
configured.time_report_mutex.lock(io) catch return;
defer configured.time_report_mutex.unlock(io);
assert(configured.time_report_msgs[step_idx].len == 0);
configured.time_report_msgs[step_idx] = buf;
configured.time_report_update_times[step_idx] = ws.now();
}
ws.notifyUpdate();
}
pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void {
const graph = ws.graph;
const io = graph.io;
const configured = &ws.configured.?;
const maker = configured.maker;
const gpa = maker.gpa;
const all_steps = maker.step_stack.keys();
const step_idx: u32 = for (all_steps, 0..) |s, i| {
if (s == step_index) break @intCast(i);
} else unreachable;
const old_buf = old: {
configured.time_report_mutex.lock(io) catch return;
defer configured.time_report_mutex.unlock(io);
const old = configured.time_report_msgs[step_idx];
configured.time_report_msgs[step_idx] = &.{};
break :old old;
};
const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
out.* = .{
.step_idx = step_idx,
.ns_total = @intCast(duration.toNanoseconds()),
};
{
configured.time_report_mutex.lock(io) catch return;
defer configured.time_report_mutex.unlock(io);
assert(configured.time_report_msgs[step_idx].len == 0);
configured.time_report_msgs[step_idx] = buf;
configured.time_report_update_times[step_idx] = ws.now();
}
ws.notifyUpdate();
}
pub fn updateTimeReportRunTest(
ws: *WebServer,
run_step_index: Configuration.Step.Index,
tests: *const Step.Run.CachedTestMetadata,
ns_per_test: []const u64,
) void {
const graph = ws.graph;
const io = graph.io;
const configured = &ws.configured.?;
const maker = configured.maker;
const gpa = maker.gpa;
const all_steps = maker.step_stack.keys();
const step_idx: u32 = for (all_steps, 0..) |s, i| {
if (s == run_step_index) break @intCast(i);
} else unreachable;
assert(tests.names.len == ns_per_test.len);
const tests_len: u32 = @intCast(tests.names.len);
const new_len: usize = len: {
var names_len: usize = 0;
for (0..tests_len) |i| {
names_len += tests.testName(@intCast(i)).len + 1;
}
break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
};
const old_buf = old: {
configured.time_report_mutex.lock(io) catch return;
defer configured.time_report_mutex.unlock(io);
const old = configured.time_report_msgs[step_idx];
configured.time_report_msgs[step_idx] = &.{};
break :old old;
};
const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
out_header.* = .{
.step_idx = step_idx,
.tests_len = tests_len,
};
var offset: usize = @sizeOf(abi.time_report.RunTestResult);
const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
@memcpy(ns_per_test_out, ns_per_test);
offset += tests_len * 8;
for (0..tests_len) |i| {
const name = tests.testName(@intCast(i));
@memcpy(buf[offset..][0..name.len], name);
buf[offset..][name.len] = 0;
offset += name.len + 1;
}
assert(offset == buf.len);
{
configured.time_report_mutex.lock(io) catch return;
defer configured.time_report_mutex.unlock(io);
assert(configured.time_report_msgs[step_idx].len == 0);
configured.time_report_msgs[step_idx] = buf;
configured.time_report_update_times[step_idx] = ws.now();
}
ws.notifyUpdate();
}
const RunnerRequest = union(enum) {
rebuild,
};
pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
const io = ws.graph.io;
ws.runner_request_mutex.lock(io) catch return;
defer ws.runner_request_mutex.unlock(io);
if (ws.runner_request) |req| {
ws.runner_request = null;
ws.runner_request_empty_cond.signal();
return req;
}
return null;
}
pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
const io = ws.graph.io;
try ws.runner_request_mutex.lock(io);
defer ws.runner_request_mutex.unlock(io);
while (true) {
if (ws.runner_request) |req| {
ws.runner_request = null;
ws.runner_request_empty_cond.signal(io);
return req;
}
try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
}
}
const cache_control_header: http.Header = .{
.name = "Cache-Control",
.value = "max-age=0, must-revalidate",
};