1const WebServer = @This();
2
3const builtin = @import("builtin");
4
5const std = @import("std");
6const Allocator = std.mem.Allocator;
7const Cache = std.Build.Cache;
8const Configuration = std.Build.Configuration;
9const Io = std.Io;
10const abi = std.Build.abi;
11const assert = std.debug.assert;
12const http = std.http;
13const log = std.log.scoped(.web_server);
14const mem = std.mem;
15const net = std.Io.net;
16
17const Maker = @import("../Maker.zig");
18const Fuzz = @import("Fuzz.zig");
19const Graph = @import("Graph.zig");
20const Step = @import("Step.zig");
21
22graph: *const Graph,
23listen_address: net.IpAddress,
24root_prog_node: std.Progress.Node,
25
26tcp_server: ?net.Server,
27serve_task: ?Io.Future(Io.Cancelable!void),
28
29/// Uses `Io.Clock.awake`.
30base_timestamp: Io.Timestamp,
31
32fuzz: ?Fuzz,
33
34build_status: std.atomic.Value(abi.BuildStatus),
35/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
36/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so
37/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
38/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
39/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
40/// because this value changes quickly so this would result in constantly spamming all clients with
41/// an unreasonable number of packets.
42update_id: std.atomic.Value(u32),
43
44runner_request_mutex: Io.Mutex,
45runner_request_ready_cond: Io.Condition,
46runner_request_empty_cond: Io.Condition,
47runner_request: ?RunnerRequest,
48
49configured: ?Configured,
50
51const Configured = struct {
52 maker: *Maker,
53 /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
54 step_names_trailing: []u8,
55 /// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
56 /// Accessed atomically.
57 step_status_bits: []u8,
58
59 time_report_mutex: Io.Mutex,
60 time_report_msgs: [][]u8,
61 time_report_update_times: []i64,
62};
63
64/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
65/// on a fixed interval of this many milliseconds.
66const default_update_interval_ms = 500;
67
68pub const base_clock: Io.Clock = .awake;
69
70/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
71pub fn notifyUpdate(ws: *WebServer) void {
72 const io = ws.graph.io;
73 _ = ws.update_id.rmw(.Add, 1, .release);
74 io.futexWake(u32, &ws.update_id.raw, 16);
75}
76
77pub const Options = struct {
78 graph: *const Graph,
79 root_prog_node: std.Progress.Node,
80 listen_address: net.IpAddress,
81 base_timestamp: Io.Clock.Timestamp,
82};
83
84pub fn init(opts: Options) WebServer {
85 // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
86 // instead of threads, so that the web server can function in single-threaded builds.
87 comptime assert(!builtin.single_threaded);
88 assert(opts.base_timestamp.clock == base_clock);
89 return .{
90 .graph = opts.graph,
91 .listen_address = opts.listen_address,
92 .root_prog_node = opts.root_prog_node,
93
94 .tcp_server = null,
95 .serve_task = null,
96
97 .base_timestamp = opts.base_timestamp.raw,
98
99 .fuzz = null,
100
101 .build_status = .init(.idle),
102 .update_id = .init(0),
103
104 .runner_request_mutex = .init,
105 .runner_request_ready_cond = .init,
106 .runner_request_empty_cond = .init,
107 .runner_request = null,
108
109 .configured = null,
110 };
111}
112
113pub fn deinit(ws: *WebServer) void {
114 const graph = ws.graph;
115 const io = graph.io;
116
117 if (ws.fuzz) |*f| f.deinit();
118
119 ws.releaseConfigured();
120
121 if (ws.serve_task) |t| {
122 if (ws.tcp_server) |*s| s.stream.close(io);
123 t.await();
124 }
125 if (ws.tcp_server) |*s| s.deinit();
126}
127
128fn releaseConfigured(ws: *WebServer) void {
129 if (ws.configured) |*configured| {
130 const gpa = configured.maker.gpa;
131 gpa.free(configured.step_names_trailing);
132 gpa.free(configured.step_status_bits);
133 for (configured.time_report_msgs) |msg| gpa.free(msg);
134 gpa.free(configured.time_report_msgs);
135 gpa.free(configured.time_report_update_times);
136 gpa.free(configured.step_names_trailing);
137 ws.configured = null;
138 }
139}
140
141pub fn updateConfiguration(ws: *WebServer, maker: *Maker) !void {
142 const graph = ws.graph;
143 const gpa = maker.gpa;
144 const all_steps = maker.step_stack.keys();
145 const c = &maker.scanned_config.configuration;
146
147 const step_names_trailing = try gpa.alloc(u8, len: {
148 var name_bytes: usize = 0;
149 for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len;
150 break :len name_bytes + all_steps.len * 4;
151 });
152 errdefer gpa.free(step_names_trailing);
153
154 {
155 const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
156 var idx: usize = all_steps.len * 4;
157 for (all_steps, step_name_lens) |step_index, *name_len| {
158 const step_name = step_index.ptr(c).name.slice(c);
159 name_len.* = @intCast(step_name.len);
160 @memcpy(step_names_trailing[idx..][0..step_name.len], step_name);
161 idx += step_name.len;
162 }
163 assert(idx == step_names_trailing.len);
164 }
165
166 const step_status_bits = try gpa.alloc(u8, @divCeil(all_steps.len, 4));
167 errdefer gpa.free(step_status_bits);
168 @memset(step_status_bits, 0);
169
170 const time_reports_len: usize = if (graph.time_report) all_steps.len else 0;
171 const time_report_msgs = try gpa.alloc([]u8, time_reports_len);
172 errdefer gpa.free(time_report_msgs);
173 const time_report_update_times = try gpa.alloc(i64, time_reports_len);
174 errdefer gpa.free(time_report_update_times);
175 @memset(time_report_msgs, &.{});
176 @memset(time_report_update_times, std.math.minInt(i64));
177
178 ws.releaseConfigured();
179
180 ws.configured = .{
181 .maker = maker,
182 .step_names_trailing = step_names_trailing,
183 .step_status_bits = step_status_bits,
184 .time_report_mutex = .init,
185 .time_report_msgs = time_report_msgs,
186 .time_report_update_times = time_report_update_times,
187 };
188}
189
190pub fn start(ws: *WebServer) error{AlreadyReported}!void {
191 assert(ws.tcp_server == null);
192 assert(ws.serve_task == null);
193 const graph = ws.graph;
194 const io = graph.io;
195
196 ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
197 log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
198 return error.AlreadyReported;
199 };
200 ws.serve_task = io.concurrent(serve, .{ws}) catch |err| {
201 log.err("unable to spawn web server thread: {t}", .{err});
202 ws.tcp_server.?.deinit(io);
203 ws.tcp_server = null;
204 return error.AlreadyReported;
205 };
206
207 log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
208 if (ws.listen_address.getPort() == 0) {
209 log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
210 }
211}
212fn serve(ws: *WebServer) Io.Cancelable!void {
213 const graph = ws.graph;
214 const io = graph.io;
215
216 var group: Io.Group = .init;
217 defer group.cancel(io);
218
219 while (true) {
220 var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) {
221 error.Canceled => |e| return e,
222 else => |e| {
223 log.err("failed to accept connection: {t}", .{e});
224 return;
225 },
226 };
227 group.concurrent(io, accept, .{ ws, stream }) catch |err| {
228 log.err("unable to spawn connection thread: {t}", .{err});
229 stream.close(io);
230 continue;
231 };
232 }
233}
234
235pub fn startBuild(ws: *WebServer) void {
236 if (ws.fuzz) |*fuzz| {
237 fuzz.deinit();
238 ws.fuzz = null;
239 }
240 const configured = &ws.configured.?;
241 for (configured.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
242 ws.build_status.store(.running, .monotonic);
243 ws.notifyUpdate();
244}
245
246pub fn updateStepStatus(
247 ws: *WebServer,
248 step_index: Configuration.Step.Index,
249 new_status: abi.StepUpdate.Status,
250) void {
251 const configured = &ws.configured.?;
252 const maker = configured.maker;
253 const all_steps = maker.step_stack.keys();
254 const step_idx: u32 = for (all_steps, 0..) |s, i| {
255 if (s == step_index) break @intCast(i);
256 } else unreachable;
257 const ptr = &configured.step_status_bits[step_idx / 4];
258 const bit_offset: u3 = @intCast((step_idx % 4) * 2);
259 const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
260 const mask = @as(u8, @backingInt(new_status) ^ old_bits) << bit_offset;
261 _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
262 ws.notifyUpdate();
263}
264
265pub fn finishBuild(ws: *WebServer, opts: struct {
266 fuzz: bool,
267}) void {
268 const configured = &ws.configured.?;
269 const maker = configured.maker;
270 const all_steps = maker.step_stack.keys();
271
272 if (opts.fuzz) {
273 switch (builtin.os.tag) {
274 // Current implementation depends on two things that need to be ported to Windows:
275 // * Memory-mapping to share data between the fuzzer and build runner.
276 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
277 // many addresses to source locations).
278 .windows => std.process.fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
279 else => {},
280 }
281 if (@bitSizeOf(usize) != 64) {
282 // Current implementation depends on posix.mmap()'s second
283 // parameter, `length: usize`, being compatible with file system's
284 // u64 return value. This is not the case on 32-bit platforms.
285 // Affects or affected by issues #5185, #22523, and #22464.
286 std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
287 }
288
289 assert(ws.fuzz == null);
290
291 ws.build_status.store(.fuzz_init, .monotonic);
292 ws.notifyUpdate();
293
294 ws.fuzz = Fuzz.init(maker, all_steps, ws.root_prog_node, .{ .forever = .{ .ws = ws } }) catch |err|
295 std.process.fatal("failed to start fuzzer: {t}", .{err});
296 ws.fuzz.?.start();
297 }
298
299 ws.build_status.store(if (maker.watch) .watching else .idle, .monotonic);
300 ws.notifyUpdate();
301}
302
303pub fn now(ws: *const WebServer) i64 {
304 const graph = ws.graph;
305 const io = graph.io;
306 const ts = base_clock.now(io);
307 return @intCast(ws.base_timestamp.durationTo(ts).toNanoseconds());
308}
309
310fn accept(ws: *WebServer, stream: net.Stream) void {
311 const graph = ws.graph;
312 const io = graph.io;
313
314 defer {
315 // `net.Stream.close` wants to helpfully overwrite `stream` with
316 // `undefined`, but it cannot do so since it is an immutable parameter.
317 var copy = stream;
318 copy.close(io);
319 }
320 var send_buffer: [4096]u8 = undefined;
321 var recv_buffer: [4096]u8 = undefined;
322 var connection_reader = stream.reader(io, &recv_buffer);
323 var connection_writer = stream.writer(io, &send_buffer);
324 var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
325
326 while (true) {
327 var request = server.receiveHead() catch |err| switch (err) {
328 error.HttpConnectionClosing => return,
329 else => return log.err("failed to receive http request: {t}", .{err}),
330 };
331 switch (request.upgradeRequested()) {
332 .websocket => |opt_key| {
333 const key = opt_key orelse return log.err("missing websocket key", .{});
334 var web_socket = request.respondWebSocket(.{ .key = key }) catch {
335 return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
336 };
337 ws.serveWebSocket(&web_socket) catch |err| {
338 log.err("failed to serve websocket: {t}", .{err});
339 return;
340 };
341 comptime unreachable;
342 },
343 .other => |name| return log.err("unknown upgrade request: {s}", .{name}),
344 .none => {
345 ws.serveRequest(&request) catch |err| switch (err) {
346 error.AlreadyReported => return,
347 else => {
348 log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
349 return;
350 },
351 };
352 },
353 }
354 }
355}
356
357fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
358 const graph = ws.graph;
359 const gpa = graph.cache.gpa;
360 const io = graph.io;
361 log.err("TODO serve a different message when the configuration changes", .{});
362 const configured = &ws.configured.?;
363 const maker = configured.maker;
364 const all_steps = maker.step_stack.keys();
365
366 var prev_build_status = ws.build_status.load(.monotonic);
367
368 const prev_step_status_bits = try gpa.alloc(u8, configured.step_status_bits.len);
369 defer gpa.free(prev_step_status_bits);
370 for (prev_step_status_bits, configured.step_status_bits) |*copy, *shared| {
371 copy.* = @atomicLoad(u8, shared, .monotonic);
372 }
373
374 var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock });
375 defer recv_thread.cancel(io);
376
377 {
378 const hello_header: abi.Hello = .{
379 .status = prev_build_status,
380 .flags = .{
381 .time_report = graph.time_report,
382 },
383 .timestamp = ws.now(),
384 .steps_len = @intCast(all_steps.len),
385 };
386 var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), configured.step_names_trailing, prev_step_status_bits };
387 try sock.writeMessageVec(&bufs, .binary);
388 }
389
390 var prev_fuzz: Fuzz.Previous = .init;
391 var prev_time: i64 = std.math.minInt(i64);
392 while (true) {
393 const start_time = ws.now();
394 const start_update_id = ws.update_id.load(.acquire);
395
396 if (ws.fuzz) |*fuzz| {
397 try fuzz.sendUpdate(sock, &prev_fuzz);
398 }
399
400 {
401 try configured.time_report_mutex.lock(io);
402 defer configured.time_report_mutex.unlock(io);
403 for (configured.time_report_msgs, configured.time_report_update_times) |msg, update_time| {
404 if (update_time <= prev_time) continue;
405 // We want to send `msg`, but shouldn't block `configured.time_report_mutex` while we do, so
406 // that we don't hold up the build system on the client accepting this packet.
407 const owned_msg = try gpa.dupe(u8, msg);
408 defer gpa.free(owned_msg);
409 // Temporarily unlock, then re-lock after the message is sent.
410 configured.time_report_mutex.unlock(io);
411 defer configured.time_report_mutex.lockUncancelable(io);
412 try sock.writeMessage(owned_msg, .binary);
413 }
414 }
415
416 {
417 const build_status = ws.build_status.load(.monotonic);
418 if (build_status != prev_build_status) {
419 prev_build_status = build_status;
420 const msg: abi.StatusUpdate = .{ .new = build_status };
421 try sock.writeMessage(@ptrCast(&msg), .binary);
422 }
423 }
424
425 for (prev_step_status_bits, configured.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
426 const cur_byte = @atomicLoad(u8, shared, .monotonic);
427 if (prev_byte.* == cur_byte) continue;
428 const cur: [4]abi.StepUpdate.Status = .{
429 @fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 0)))),
430 @fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 2)))),
431 @fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 4)))),
432 @fromBackingInt(@intCast(@as(u2, @truncate(cur_byte >> 6)))),
433 };
434 const prev: [4]abi.StepUpdate.Status = .{
435 @fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 0)))),
436 @fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 2)))),
437 @fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 4)))),
438 @fromBackingInt(@intCast(@as(u2, @truncate(prev_byte.* >> 6)))),
439 };
440 for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
441 const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
442 if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
443 }
444 prev_byte.* = cur_byte;
445 }
446
447 prev_time = start_time;
448
449 const old_cp = io.swapCancelProtection(.blocked);
450 defer _ = io.swapCancelProtection(old_cp);
451 io.futexWaitTimeout(
452 u32,
453 &ws.update_id.raw,
454 start_update_id,
455 .{ .duration = .{
456 .clock = .awake,
457 .raw = .fromMilliseconds(default_update_interval_ms),
458 } },
459 ) catch |err| switch (err) {
460 error.Canceled => unreachable,
461 };
462 }
463}
464fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
465 const graph = ws.graph;
466 const io = graph.io;
467
468 while (true) {
469 const msg = sock.readSmallMessage() catch return;
470 if (msg.opcode != .binary) continue;
471 if (msg.data.len == 0) continue;
472 const tag: abi.ToServerTag = @fromBackingInt(@intCast(msg.data[0]));
473 switch (tag) {
474 _ => continue,
475 .rebuild => while (true) {
476 ws.runner_request_mutex.lock(io) catch |err| switch (err) {
477 error.Canceled => return,
478 };
479 defer ws.runner_request_mutex.unlock(io);
480 if (ws.runner_request == null) {
481 ws.runner_request = .rebuild;
482 ws.runner_request_ready_cond.signal(io);
483 break;
484 }
485 ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
486 },
487 }
488 }
489}
490
491fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
492 // Strip an optional leading '/debug' component from the request.
493 const target: []const u8, const debug: bool = target: {
494 if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
495 if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
496 if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
497 break :target .{ req.head.target, false };
498 };
499
500 if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
501 if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
502 if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
503 if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
504 if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .debug else .fast);
505
506 if (ws.fuzz) |*fuzz| {
507 if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
508 }
509
510 try req.respond("not found", .{
511 .status = .not_found,
512 .extra_headers = &.{
513 .{ .name = "Content-Type", .value = "text/plain" },
514 },
515 });
516}
517
518fn serveLibFile(
519 ws: *WebServer,
520 request: *http.Server.Request,
521 sub_path: []const u8,
522 content_type: []const u8,
523) !void {
524 const graph = ws.graph;
525
526 return serveFile(ws, request, .{
527 .root_dir = graph.zig_lib_directory,
528 .sub_path = sub_path,
529 }, content_type);
530}
531fn serveClientWasm(
532 ws: *WebServer,
533 req: *http.Server.Request,
534 optimize_mode: std.builtin.Optimize,
535) !void {
536 const gpa = ws.graph.cache.gpa;
537
538 var arena_state: std.heap.ArenaAllocator = .init(gpa);
539 defer arena_state.deinit();
540 const arena = arena_state.allocator();
541
542 // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
543 const bin_path = try buildClientWasm(ws, arena, optimize_mode);
544 return serveFile(ws, req, bin_path, "application/wasm");
545}
546
547pub fn serveFile(
548 ws: *WebServer,
549 request: *http.Server.Request,
550 path: Cache.Path,
551 content_type: []const u8,
552) !void {
553 const graph = ws.graph;
554 const gpa = graph.cache.gpa;
555 const io = graph.io;
556
557 // The desired API is actually sendfile, which will require enhancing http.Server.
558 // We load the file with every request so that the user can make changes to the file
559 // and refresh the HTML page without restarting this server.
560 const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
561 log.err("failed to read '{f}': {t}", .{ path, err });
562 return error.AlreadyReported;
563 };
564 defer gpa.free(file_contents);
565 try request.respond(file_contents, .{
566 .extra_headers = &.{
567 .{ .name = "Content-Type", .value = content_type },
568 cache_control_header,
569 },
570 });
571}
572pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
573 const graph = ws.graph;
574 const io = graph.io;
575
576 var send_buffer: [0x4000]u8 = undefined;
577 var response = try request.respondStreaming(&send_buffer, .{
578 .respond_options = .{
579 .extra_headers = &.{
580 .{ .name = "Content-Type", .value = "application/x-tar" },
581 cache_control_header,
582 },
583 },
584 });
585
586 var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
587
588 for (paths) |path| {
589 var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| {
590 log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
591 continue;
592 };
593 defer file.close(io);
594 const stat = try file.stat(io);
595 var read_buffer: [1024]u8 = undefined;
596 var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size);
597
598 archiver.prefix = path.root_dir.path orelse graph.cache.cwd;
599 try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
600 }
601
602 // intentionally not calling `archiver.finishPedantically`
603 try response.end();
604}
605
606fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optimize) !Cache.Path {
607 const root_name = "build-web";
608 const arch_os_abi = "wasm32-freestanding";
609 const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
610
611 const graph = ws.graph;
612 const gpa = graph.cache.gpa;
613 const io = graph.io;
614
615 const main_src_path: Cache.Path = .{
616 .root_dir = graph.zig_lib_directory,
617 .sub_path = "build-web/main.zig",
618 };
619 const walk_src_path: Cache.Path = .{
620 .root_dir = graph.zig_lib_directory,
621 .sub_path = "docs/wasm/Walk.zig",
622 };
623 const html_render_src_path: Cache.Path = .{
624 .root_dir = graph.zig_lib_directory,
625 .sub_path = "docs/wasm/html_render.zig",
626 };
627
628 var argv: std.ArrayList([]const u8) = .empty;
629
630 try argv.appendSlice(arena, &.{
631 graph.zig_exe, "build-exe", //
632 "-fno-entry", //
633 "-O", @tagName(optimize), //
634 "-target", arch_os_abi, //
635 "-mcpu", cpu_features, //
636 "--cache-dir", graph.global_cache_root.path orelse ".", //
637 "--global-cache-dir", graph.global_cache_root.path orelse ".", //
638 "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
639 "--name", root_name, //
640 "-rdynamic", //
641 "-fsingle-threaded", //
642 "--dep", "Walk", //
643 "--dep", "html_render", //
644 try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), //
645 try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), //
646 "--dep", "Walk", //
647 try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), //
648 "--listen=-",
649 });
650
651 const compile_prog_node = ws.root_prog_node.start("Compile WebAssembly Component", 0);
652 defer compile_prog_node.end();
653
654 const result = try std.zig.buildExeSubprocess(gpa, io, .{
655 .argv = argv.items,
656 .cache_root = graph.global_cache_root,
657 .root_name = root_name,
658 .arch_os_abi = arch_os_abi,
659 .cpu_features = cpu_features,
660 .progress_node = compile_prog_node,
661 });
662 if (!result.cache_hit) log.info("source changes detected; rebuilt wasm component", .{});
663 return result.path;
664}
665
666pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
667 compile_step: Configuration.Step.Index,
668
669 use_llvm: bool,
670 stats: abi.time_report.CompileResult.Stats,
671 ns_total: u64,
672
673 llvm_pass_timings_len: u32,
674 files_len: u32,
675 decls_len: u32,
676
677 /// The trailing data of `abi.time_report.CompileResult`, except the step name.
678 trailing: []const u8,
679}) void {
680 const graph = ws.graph;
681 const io = graph.io;
682 const configured = &ws.configured.?;
683 const maker = configured.maker;
684 const gpa = maker.gpa;
685 const all_steps = maker.step_stack.keys();
686
687 const step_idx: u32 = for (all_steps, 0..) |s, i| {
688 if (s == opts.compile_step) break @intCast(i);
689 } else unreachable;
690
691 const old_buf = old: {
692 configured.time_report_mutex.lock(io) catch return;
693 defer configured.time_report_mutex.unlock(io);
694 const old = configured.time_report_msgs[step_idx];
695 configured.time_report_msgs[step_idx] = &.{};
696 break :old old;
697 };
698 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
699
700 const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
701 out_header.* = .{
702 .step_idx = step_idx,
703 .flags = .{
704 .use_llvm = opts.use_llvm,
705 },
706 .stats = opts.stats,
707 .ns_total = opts.ns_total,
708 .llvm_pass_timings_len = opts.llvm_pass_timings_len,
709 .files_len = opts.files_len,
710 .decls_len = opts.decls_len,
711 };
712 @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
713
714 {
715 configured.time_report_mutex.lock(io) catch return;
716 defer configured.time_report_mutex.unlock(io);
717 assert(configured.time_report_msgs[step_idx].len == 0);
718 configured.time_report_msgs[step_idx] = buf;
719 configured.time_report_update_times[step_idx] = ws.now();
720 }
721 ws.notifyUpdate();
722}
723
724pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void {
725 const graph = ws.graph;
726 const io = graph.io;
727 const configured = &ws.configured.?;
728 const maker = configured.maker;
729 const gpa = maker.gpa;
730 const all_steps = maker.step_stack.keys();
731
732 const step_idx: u32 = for (all_steps, 0..) |s, i| {
733 if (s == step_index) break @intCast(i);
734 } else unreachable;
735
736 const old_buf = old: {
737 configured.time_report_mutex.lock(io) catch return;
738 defer configured.time_report_mutex.unlock(io);
739 const old = configured.time_report_msgs[step_idx];
740 configured.time_report_msgs[step_idx] = &.{};
741 break :old old;
742 };
743 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
744 const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
745 out.* = .{
746 .step_idx = step_idx,
747 .ns_total = @intCast(duration.toNanoseconds()),
748 };
749 {
750 configured.time_report_mutex.lock(io) catch return;
751 defer configured.time_report_mutex.unlock(io);
752 assert(configured.time_report_msgs[step_idx].len == 0);
753 configured.time_report_msgs[step_idx] = buf;
754 configured.time_report_update_times[step_idx] = ws.now();
755 }
756 ws.notifyUpdate();
757}
758
759pub fn updateTimeReportRunTest(
760 ws: *WebServer,
761 run_step_index: Configuration.Step.Index,
762 tests: *const Step.Run.CachedTestMetadata,
763 ns_per_test: []const u64,
764) void {
765 const graph = ws.graph;
766 const io = graph.io;
767 const configured = &ws.configured.?;
768 const maker = configured.maker;
769 const gpa = maker.gpa;
770 const all_steps = maker.step_stack.keys();
771
772 const step_idx: u32 = for (all_steps, 0..) |s, i| {
773 if (s == run_step_index) break @intCast(i);
774 } else unreachable;
775
776 assert(tests.names.len == ns_per_test.len);
777 const tests_len: u32 = @intCast(tests.names.len);
778
779 const new_len: usize = len: {
780 var names_len: usize = 0;
781 for (0..tests_len) |i| {
782 names_len += tests.testName(@intCast(i)).len + 1;
783 }
784 break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
785 };
786 const old_buf = old: {
787 configured.time_report_mutex.lock(io) catch return;
788 defer configured.time_report_mutex.unlock(io);
789 const old = configured.time_report_msgs[step_idx];
790 configured.time_report_msgs[step_idx] = &.{};
791 break :old old;
792 };
793 const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
794
795 const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
796 out_header.* = .{
797 .step_idx = step_idx,
798 .tests_len = tests_len,
799 };
800 var offset: usize = @sizeOf(abi.time_report.RunTestResult);
801 const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
802 @memcpy(ns_per_test_out, ns_per_test);
803 offset += tests_len * 8;
804 for (0..tests_len) |i| {
805 const name = tests.testName(@intCast(i));
806 @memcpy(buf[offset..][0..name.len], name);
807 buf[offset..][name.len] = 0;
808 offset += name.len + 1;
809 }
810 assert(offset == buf.len);
811
812 {
813 configured.time_report_mutex.lock(io) catch return;
814 defer configured.time_report_mutex.unlock(io);
815 assert(configured.time_report_msgs[step_idx].len == 0);
816 configured.time_report_msgs[step_idx] = buf;
817 configured.time_report_update_times[step_idx] = ws.now();
818 }
819 ws.notifyUpdate();
820}
821
822const RunnerRequest = union(enum) {
823 rebuild,
824};
825pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
826 const io = ws.graph.io;
827 ws.runner_request_mutex.lock(io) catch return;
828 defer ws.runner_request_mutex.unlock(io);
829 if (ws.runner_request) |req| {
830 ws.runner_request = null;
831 ws.runner_request_empty_cond.signal();
832 return req;
833 }
834 return null;
835}
836pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
837 const io = ws.graph.io;
838 try ws.runner_request_mutex.lock(io);
839 defer ws.runner_request_mutex.unlock(io);
840 while (true) {
841 if (ws.runner_request) |req| {
842 ws.runner_request = null;
843 ws.runner_request_empty_cond.signal(io);
844 return req;
845 }
846 try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
847 }
848}
849
850const cache_control_header: http.Header = .{
851 .name = "Cache-Control",
852 .value = "max-age=0, must-revalidate",
853};