zig/lib/std/Build/WebServer.zig
mlugg dcc3e6e1dd build system: replace fuzzing UI with build UI, add time report
This commit replaces the "fuzzer" UI, previously accessed with the
`--fuzz` and `--port` flags, with a more interesting web UI which allows
more interactions with the Zig build system. Most notably, it allows
accessing the data emitted by a new "time report" system, which allows
users to see which parts of Zig programs take the longest to compile.

The option to expose the web UI is `--webui`. By default, it will listen
on `[::1]` on a random port, but any IPv6 or IPv4 address can be
specified with e.g. `--webui=[::1]:8000` or `--webui=127.0.0.1:8000`.
The options `--fuzz` and `--time-report` both imply `--webui` if not
given. Currently, `--webui` is incompatible with `--watch`; specifying
both will cause `zig build` to exit with a fatal error.

When the web UI is enabled, the build runner spawns the web server as
soon as the configure phase completes. The frontend code consists of one
HTML file, one JavaScript file, two CSS files, and a few Zig source
files which are built into a WASM blob on-demand -- this is all very
similar to the old fuzzer UI. Also inherited from the fuzzer UI is that
the build system communicates with web clients over a WebSocket
connection.

When the build finishes, if `--webui` was passed (i.e. if the web server
is running), the build runner does not terminate; it continues running
to serve web requests, allowing interactive control of the build system.

In the web interface is an overall "status" indicating whether a build
is currently running, and also a list of all steps in this build. There
are visual indicators (colors and spinners) for in-progress, succeeded,
and failed steps. There is a "Rebuild" button which will cause the build
system to reset the state of every step (note that this does not affect
caching) and evaluate the step graph again.

If `--time-report` is passed to `zig build`, a new section of the
interface becomes visible, which associates every build step with a
"time report". For most steps, this is just a simple "time taken" value.
However, for `Compile` steps, the compiler communicates with the build
system to provide it with much more interesting information: time taken
for various pipeline phases, with a per-declaration and per-file
breakdown, sorted by slowest declarations/files first. This feature is
still in its early stages: the data can be a little tricky to
understand, and there is no way to, for instance, sort by different
properties, or filter to certain files. However, it has already given us
some interesting statistics, and can be useful for spotting, for
instance, particularly complex and slow compile-time logic.
Additionally, if a compilation uses LLVM, its time report includes the
"LLVM pass timing" information, which was previously accessible with the
(now removed) `-ftime-report` compiler flag.

To make time reports more useful, ZIR and compilation caches are ignored
by the Zig compiler when they are enabled -- in other words, `Compile`
steps *always* run, even if their result should be cached. This means
that the flag can be used to analyze a project's compile time without
having to repeatedly clear cache directory, for instance. However, when
using `-fincremental`, updates other than the first will only show you
the statistics for what changed on that particular update. Notably, this
gives us a fairly nice way to see exactly which declarations were
re-analyzed by an incremental update.

If `--fuzz` is passed to `zig build`, another section of the web
interface becomes visible, this time exposing the fuzzer. This is quite
similar to the fuzzer UI this commit replaces, with only a few cosmetic
tweaks. The interface is closer than before to supporting multiple fuzz
steps at a time (in line with the overall strategy for this build UI,
the goal will be for all of the fuzz steps to be accessible in the same
interface), but still doesn't actually support it. The fuzzer UI looks
quite different under the hood: as a result, various bugs are fixed,
although other bugs remain. For instance, viewing the source code of any
file other than the root of the main module is completely broken (as on
master) due to some bogus file-to-module assignment logic in the fuzzer
UI.

Implementation notes:

* The `lib/build-web/` directory holds the client side of the web UI.

* The general server logic is in `std.Build.WebServer`.

* Fuzzing-specific logic is in `std.Build.Fuzz`.

* `std.Build.abi` is the new home of `std.Build.Fuzz.abi`, since it now
  relates to the build system web UI in general.

* The build runner now has an **actual** general-purpose allocator,
  because thanks to `--watch` and `--webui`, the process can be
  arbitrarily long-lived. The gpa is `std.heap.DebugAllocator`, but the
  arena remains backed by `std.heap.page_allocator` for efficiency. I
  fixed several crashes caused by conflation of `gpa` and `arena` in the
  build runner and `std.Build`, but there may still be some I have
  missed.

* The I/O logic in `std.Build.WebServer` is pretty gnarly; there are a
  *lot* of threads involved. I anticipate this situation improving
  significantly once the `std.Io` interface (with concurrency support)
  is introduced.
2025-08-01 23:48:21 +01:00

823 lines
31 KiB
Zig

gpa: Allocator,
thread_pool: *std.Thread.Pool,
graph: *const Build.Graph,
all_steps: []const *Build.Step,
listen_address: std.net.Address,
ttyconf: std.io.tty.Config,
root_prog_node: std.Progress.Node,
watch: bool,
tcp_server: ?std.net.Server,
serve_thread: ?std.Thread,
base_timestamp: i128,
/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
step_names_trailing: []u8,
/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
/// Accessed atomically.
step_status_bits: []u8,
fuzz: ?Fuzz,
time_report_mutex: std.Thread.Mutex,
time_report_msgs: [][]u8,
time_report_update_times: []i64,
build_status: std.atomic.Value(abi.BuildStatus),
/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
/// to increment this value. Each client thread waits for this increment with `std.Thread.Futex`, so
/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
/// because this value changes quickly so this would result in constantly spamming all clients with
/// an unreasonable number of packets.
update_id: std.atomic.Value(u32),
runner_request_mutex: std.Thread.Mutex,
runner_request_ready_cond: std.Thread.Condition,
runner_request_empty_cond: std.Thread.Condition,
runner_request: ?RunnerRequest,
/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
/// on a fixed interval of this many milliseconds.
const default_update_interval_ms = 500;
/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
pub fn notifyUpdate(ws: *WebServer) void {
_ = ws.update_id.rmw(.Add, 1, .release);
std.Thread.Futex.wake(&ws.update_id, 16);
}
pub const Options = struct {
gpa: Allocator,
thread_pool: *std.Thread.Pool,
graph: *const std.Build.Graph,
all_steps: []const *Build.Step,
ttyconf: std.io.tty.Config,
root_prog_node: std.Progress.Node,
watch: bool,
listen_address: std.net.Address,
};
pub fn init(opts: Options) WebServer {
if (builtin.single_threaded) {
// The upcoming `std.Io` interface should allow us to use `Io.async` and `Io.concurrent`
// instead of threads, so that the web server can function in single-threaded builds.
std.process.fatal("--webui not yet implemented for single-threaded builds", .{});
}
if (builtin.os.tag == .windows) {
// At the time of writing, there are two bugs in the standard library which break this feature on Windows:
// * Reading from a socket on one thread while writing to it on another seems to deadlock.
// * Vectored writes to sockets currently trigger an infinite loop when a buffer has length 0.
//
// Both of these bugs are expected to be solved by changes which are currently in the unmerged
// 'wrangle-writer-buffering' branch. Until that makes it in, this must remain disabled.
std.process.fatal("--webui is currently disabled on Windows due to bugs", .{});
}
const all_steps = opts.all_steps;
const step_names_trailing = opts.gpa.alloc(u8, len: {
var name_bytes: usize = 0;
for (all_steps) |step| name_bytes += step.name.len;
break :len name_bytes + all_steps.len * 4;
}) catch @panic("out of memory");
{
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, *name_len| {
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 = opts.gpa.alloc(
u8,
std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
) catch @panic("out of memory");
@memset(step_status_bits, 0);
const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0;
const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
@memset(time_report_msgs, &.{});
@memset(time_report_update_times, std.math.minInt(i64));
return .{
.gpa = opts.gpa,
.thread_pool = opts.thread_pool,
.graph = opts.graph,
.all_steps = all_steps,
.listen_address = opts.listen_address,
.ttyconf = opts.ttyconf,
.root_prog_node = opts.root_prog_node,
.watch = opts.watch,
.tcp_server = null,
.serve_thread = null,
.base_timestamp = std.time.nanoTimestamp(),
.step_names_trailing = step_names_trailing,
.step_status_bits = step_status_bits,
.fuzz = null,
.time_report_mutex = .{},
.time_report_msgs = time_report_msgs,
.time_report_update_times = time_report_update_times,
.build_status = .init(.idle),
.update_id = .init(0),
.runner_request_mutex = .{},
.runner_request_ready_cond = .{},
.runner_request_empty_cond = .{},
.runner_request = null,
};
}
pub fn deinit(ws: *WebServer) void {
const gpa = ws.gpa;
gpa.free(ws.step_names_trailing);
gpa.free(ws.step_status_bits);
if (ws.fuzz) |*f| f.deinit();
for (ws.time_report_msgs) |msg| gpa.free(msg);
gpa.free(ws.time_report_msgs);
gpa.free(ws.time_report_update_times);
if (ws.serve_thread) |t| {
if (ws.tcp_server) |*s| s.stream.close();
t.join();
}
if (ws.tcp_server) |*s| s.deinit();
gpa.free(ws.step_names_trailing);
}
pub fn start(ws: *WebServer) error{AlreadyReported}!void {
assert(ws.tcp_server == null);
assert(ws.serve_thread == null);
ws.tcp_server = ws.listen_address.listen(.{ .reuse_address = true }) catch |err| {
log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.getPort(), @errorName(err) });
return error.AlreadyReported;
};
ws.serve_thread = std.Thread.spawn(.{}, serve, .{ws}) catch |err| {
log.err("unable to spawn web server thread: {s}", .{@errorName(err)});
ws.tcp_server.?.deinit();
ws.tcp_server = null;
return error.AlreadyReported;
};
log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.listen_address});
if (ws.listen_address.getPort() == 0) {
log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.listen_address});
}
}
fn serve(ws: *WebServer) void {
while (true) {
const connection = ws.tcp_server.?.accept() catch |err| {
log.err("failed to accept connection: {s}", .{@errorName(err)});
return;
};
_ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| {
log.err("unable to spawn connection thread: {s}", .{@errorName(err)});
connection.stream.close();
continue;
};
}
}
pub fn startBuild(ws: *WebServer) void {
if (ws.fuzz) |*fuzz| {
fuzz.deinit();
ws.fuzz = null;
}
for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
ws.build_status.store(.running, .monotonic);
ws.notifyUpdate();
}
pub fn updateStepStatus(ws: *WebServer, step: *Build.Step, new_status: abi.StepUpdate.Status) void {
const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
if (s == step) break @intCast(i);
} else unreachable;
const ptr = &ws.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, @intFromEnum(new_status) ^ old_bits) << bit_offset;
_ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
ws.notifyUpdate();
}
pub fn finishBuild(ws: *WebServer, opts: struct {
fuzz: bool,
}) void {
if (opts.fuzz) {
switch (builtin.os.tag) {
// Current implementation depends on two things that need to be ported to Windows:
// * 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 {s}", .{@tagName(builtin.os.tag)}),
else => {},
}
if (@bitSizeOf(usize) != 64) {
// Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
// being compatible with `std.fs.getEndPos() u64`'s 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(ws) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
ws.fuzz.?.start();
}
ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic);
ws.notifyUpdate();
}
pub fn now(s: *const WebServer) i64 {
return @intCast(std.time.nanoTimestamp() - s.base_timestamp);
}
fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
defer connection.stream.close();
var read_buf: [0x4000]u8 = undefined;
var server: std.http.Server = .init(connection, &read_buf);
while (true) {
var request = server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => return,
else => {
log.err("failed to receive http request: {s}", .{@errorName(err)});
return;
},
};
var ws_send_buf: [0x4000]u8 = undefined;
var ws_recv_buf: [0x4000]u8 align(4) = undefined;
if (std.http.WebSocket.init(&request, &ws_send_buf, &ws_recv_buf) catch |err| {
log.err("failed to initialize websocket connection: {s}", .{@errorName(err)});
return;
}) |ws_init| {
var web_socket = ws_init;
ws.serveWebSocket(&web_socket) catch |err| {
log.err("failed to serve websocket: {s}", .{@errorName(err)});
return;
};
comptime unreachable;
} else {
ws.serveRequest(&request) catch |err| switch (err) {
error.AlreadyReported => return,
else => {
log.err("failed to serve '{s}': {s}", .{ request.head.target, @errorName(err) });
return;
},
};
}
}
}
fn makeIov(s: []const u8) std.posix.iovec_const {
return .{
.base = s.ptr,
.len = s.len,
};
}
fn serveWebSocket(ws: *WebServer, sock: *std.http.WebSocket) !noreturn {
var prev_build_status = ws.build_status.load(.monotonic);
const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len);
defer ws.gpa.free(prev_step_status_bits);
for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
copy.* = @atomicLoad(u8, shared, .monotonic);
}
_ = try std.Thread.spawn(.{}, recvWebSocketMessages, .{ ws, sock });
{
const hello_header: abi.Hello = .{
.status = prev_build_status,
.flags = .{
.time_report = ws.graph.time_report,
},
.timestamp = ws.now(),
.steps_len = @intCast(ws.all_steps.len),
};
try sock.writeMessagev(&.{
makeIov(@ptrCast(&hello_header)),
makeIov(ws.step_names_trailing),
makeIov(prev_step_status_bits),
}, .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);
}
{
ws.time_report_mutex.lock();
defer ws.time_report_mutex.unlock();
for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
if (update_time <= prev_time) continue;
// We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
// that we don't hold up the build system on the client accepting this packet.
const owned_msg = try ws.gpa.dupe(u8, msg);
defer ws.gpa.free(owned_msg);
// Temporarily unlock, then re-lock after the message is sent.
ws.time_report_mutex.unlock();
defer ws.time_report_mutex.lock();
try sock.writeMessage(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, ws.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 = .{
@enumFromInt(@as(u2, @truncate(cur_byte >> 0))),
@enumFromInt(@as(u2, @truncate(cur_byte >> 2))),
@enumFromInt(@as(u2, @truncate(cur_byte >> 4))),
@enumFromInt(@as(u2, @truncate(cur_byte >> 6))),
};
const prev: [4]abi.StepUpdate.Status = .{
@enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))),
@enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))),
@enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))),
@enumFromInt(@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;
std.Thread.Futex.timedWait(&ws.update_id, start_update_id, std.time.ns_per_ms * default_update_interval_ms) catch {};
}
}
fn recvWebSocketMessages(ws: *WebServer, sock: *std.http.WebSocket) void {
while (true) {
const msg = sock.readSmallMessage() catch return;
if (msg.opcode != .binary) continue;
if (msg.data.len == 0) continue;
const tag: abi.ToServerTag = @enumFromInt(msg.data[0]);
switch (tag) {
_ => continue,
.rebuild => while (true) {
ws.runner_request_mutex.lock();
defer ws.runner_request_mutex.unlock();
if (ws.runner_request == null) {
ws.runner_request = .rebuild;
ws.runner_request_ready_cond.signal();
break;
}
ws.runner_request_empty_cond.wait(&ws.runner_request_mutex);
},
}
}
}
fn serveRequest(ws: *WebServer, req: *std.http.Server.Request) !void {
// Strip an optional leading '/debug' component from the request.
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 .ReleaseFast);
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: *std.http.Server.Request,
sub_path: []const u8,
content_type: []const u8,
) !void {
return serveFile(ws, request, .{
.root_dir = ws.graph.zig_lib_directory,
.sub_path = sub_path,
}, content_type);
}
fn serveClientWasm(
ws: *WebServer,
req: *std.http.Server.Request,
optimize_mode: std.builtin.OptimizeMode,
) !void {
var arena_state: std.heap.ArenaAllocator = .init(ws.gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
// We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
const bin_path = try buildClientWasm(ws, arena, optimize_mode);
return serveFile(ws, req, bin_path, "application/wasm");
}
pub fn serveFile(
ws: *WebServer,
request: *std.http.Server.Request,
path: Cache.Path,
content_type: []const u8,
) !void {
const gpa = ws.gpa;
// The desired API is actually sendfile, which will require enhancing std.http.Server.
// 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(gpa, path.sub_path, 10 * 1024 * 1024) catch |err| {
log.err("failed to read '{f}': {s}", .{ path, @errorName(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: *std.http.Server.Request,
paths: []const Cache.Path,
) !void {
const gpa = ws.gpa;
var send_buf: [0x4000]u8 = undefined;
var response = request.respondStreaming(.{
.send_buffer = &send_buf,
.respond_options = .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "application/x-tar" },
cache_control_header,
},
},
});
var cached_cwd_path: ?[]const u8 = null;
defer if (cached_cwd_path) |p| gpa.free(p);
var response_buf: [1024]u8 = undefined;
var adapter = response.writer().adaptToNewApi();
adapter.new_interface.buffer = &response_buf;
var archiver: std.tar.Writer = .{ .underlying_writer = &adapter.new_interface };
for (paths) |path| {
var file = path.root_dir.handle.openFile(path.sub_path, .{}) catch |err| {
log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
continue;
};
defer file.close();
const stat = try file.stat();
var read_buffer: [1024]u8 = undefined;
var file_reader: std.fs.File.Reader = .initSize(file, &read_buffer, stat.size);
// TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can
// be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI:
// it turns out the WASM treats the first path component as the module name, typically
// resulting in modules named "" and "src". The compiler needs to tell the build system
// about the module graph so that the build system can correctly encode this information in
// the tar file.
archiver.prefix = path.root_dir.path orelse cwd: {
if (cached_cwd_path == null) cached_cwd_path = try std.process.getCwdAlloc(gpa);
break :cwd cached_cwd_path.?;
};
try archiver.writeFile(path.sub_path, &file_reader, stat.mtime);
}
// intentionally not calling `archiver.finishPedantically`
try adapter.new_interface.flush();
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 gpa = ws.gpa;
const graph = ws.graph;
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.ArrayListUnmanaged([]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=-",
});
var child: std.process.Child = .init(argv.items, gpa);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Pipe;
try child.spawn();
var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
.stdout = child.stdout.?,
.stderr = child.stderr.?,
});
defer poller.deinit();
try child.stdin.?.writeAll(@ptrCast(@as([]const std.zig.Client.Message.Header, &.{
.{ .tag = .update, .bytes_len = 0 },
.{ .tag = .exit, .bytes_len = 0 },
})));
const Header = std.zig.Server.Message.Header;
var result: ?Cache.Path = null;
var result_error_bundle = std.zig.ErrorBundle.empty;
const stdout = poller.reader(.stdout);
poll: while (true) {
while (stdout.buffered().len < @sizeOf(Header)) if (!(try poller.poll())) break :poll;
const header = stdout.takeStruct(Header, .little) catch unreachable;
while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
const body = stdout.take(header.bytes_len) catch unreachable;
switch (header.tag) {
.zig_version => {
if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
return error.ZigProtocolVersionMismatch;
}
},
.error_bundle => {
const EbHdr = std.zig.Server.Message.ErrorBundle;
const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
const extra_bytes =
body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
const string_bytes =
body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
const unaligned_extra: []align(1) const u32 = @ptrCast(extra_bytes);
const extra_array = try arena.alloc(u32, unaligned_extra.len);
@memcpy(extra_array, unaligned_extra);
result_error_bundle = .{
.string_bytes = try arena.dupe(u8, string_bytes),
.extra = extra_array,
};
},
.emit_digest => {
const EmitDigest = std.zig.Server.Message.EmitDigest;
const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
if (!ebp_hdr.flags.cache_hit) {
log.info("source changes detected; rebuilt wasm component", .{});
}
const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
result = .{
.root_dir = graph.global_cache_root,
.sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
};
},
else => {}, // ignore other messages
}
}
const stderr_contents = try poller.toOwnedSlice(.stderr);
if (stderr_contents.len > 0) {
std.debug.print("{s}", .{stderr_contents});
}
// Send EOF to stdin.
child.stdin.?.close();
child.stdin = null;
switch (try child.wait()) {
.Exited => |code| {
if (code != 0) {
log.err(
"the following command exited with error code {d}:\n{s}",
.{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) },
);
return error.WasmCompilationFailed;
}
},
.Signal, .Stopped, .Unknown => {
log.err(
"the following command terminated unexpectedly:\n{s}",
.{try Build.Step.allocPrintCmd(arena, null, argv.items)},
);
return error.WasmCompilationFailed;
},
}
if (result_error_bundle.errorMessageCount() > 0) {
const color = std.zig.Color.auto;
result_error_bundle.renderToStdErr(color.renderOptions());
log.err("the following command failed with {d} compilation errors:\n{s}", .{
result_error_bundle.errorMessageCount(),
try Build.Step.allocPrintCmd(arena, null, argv.items),
});
return error.WasmCompilationFailed;
}
const base_path = result orelse {
log.err("child process failed to report result\n{s}", .{
try Build.Step.allocPrintCmd(arena, null, argv.items),
});
return error.WasmCompilationFailed;
};
const bin_name = try std.zig.binNameAlloc(arena, .{
.root_name = root_name,
.target = &(std.zig.system.resolveTargetQuery(std.Build.parseTargetQuery(.{
.arch_os_abi = arch_os_abi,
.cpu_features = cpu_features,
}) catch unreachable) catch unreachable),
.output_mode = .Exe,
});
return base_path.join(arena, bin_name);
}
pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
compile: *Build.Step.Compile,
use_llvm: bool,
stats: abi.time_report.CompileResult.Stats,
ns_total: u64,
llvm_pass_timings_len: u32,
files_len: u32,
decls_len: u32,
/// The trailing data of `abi.time_report.CompileResult`, except the step name.
trailing: []const u8,
}) void {
const gpa = ws.gpa;
const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
if (s == &opts.compile.step) break @intCast(i);
} else unreachable;
const old_buf = old: {
ws.time_report_mutex.lock();
defer ws.time_report_mutex.unlock();
const old = ws.time_report_msgs[step_idx];
ws.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);
{
ws.time_report_mutex.lock();
defer ws.time_report_mutex.unlock();
assert(ws.time_report_msgs[step_idx].len == 0);
ws.time_report_msgs[step_idx] = buf;
ws.time_report_update_times[step_idx] = ws.now();
}
ws.notifyUpdate();
}
pub fn updateTimeReportGeneric(ws: *WebServer, step: *Build.Step, ns_total: u64) void {
const gpa = ws.gpa;
const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
if (s == step) break @intCast(i);
} else unreachable;
const old_buf = old: {
ws.time_report_mutex.lock();
defer ws.time_report_mutex.unlock();
const old = ws.time_report_msgs[step_idx];
ws.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 = ns_total,
};
{
ws.time_report_mutex.lock();
defer ws.time_report_mutex.unlock();
assert(ws.time_report_msgs[step_idx].len == 0);
ws.time_report_msgs[step_idx] = buf;
ws.time_report_update_times[step_idx] = ws.now();
}
ws.notifyUpdate();
}
const RunnerRequest = union(enum) {
rebuild,
};
pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
ws.runner_request_mutex.lock();
defer ws.runner_request_mutex.unlock();
if (ws.runner_request) |req| {
ws.runner_request = null;
ws.runner_request_empty_cond.signal();
return req;
}
return null;
}
pub fn wait(ws: *WebServer) RunnerRequest {
ws.runner_request_mutex.lock();
defer ws.runner_request_mutex.unlock();
while (true) {
if (ws.runner_request) |req| {
ws.runner_request = null;
ws.runner_request_empty_cond.signal();
return req;
}
ws.runner_request_ready_cond.wait(&ws.runner_request_mutex);
}
}
const cache_control_header: std.http.Header = .{
.name = "Cache-Control",
.value = "max-age=0, must-revalidate",
};
const builtin = @import("builtin");
const std = @import("std");
const assert = std.debug.assert;
const mem = std.mem;
const log = std.log.scoped(.web_server);
const Allocator = std.mem.Allocator;
const Build = std.Build;
const Cache = Build.Cache;
const Fuzz = Build.Fuzz;
const abi = Build.abi;
const WebServer = @This();