zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

blob fb78e96a (27108B) - Raw


      1 const builtin = @import("builtin");
      2 
      3 const std = @import("../../std.zig");
      4 const Allocator = std.mem.Allocator;
      5 const Build = std.Build;
      6 const Step = std.Build.Step;
      7 const Coverage = std.debug.Coverage;
      8 const abi = std.Build.Fuzz.abi;
      9 const log = std.log;
     10 const assert = std.debug.assert;
     11 const Cache = std.Build.Cache;
     12 const Path = Cache.Path;
     13 
     14 const WebServer = @This();
     15 
     16 gpa: Allocator,
     17 global_cache_directory: Build.Cache.Directory,
     18 zig_lib_directory: Build.Cache.Directory,
     19 zig_exe_path: []const u8,
     20 listen_address: std.net.Address,
     21 fuzz_run_steps: []const *Step.Run,
     22 
     23 /// Messages from fuzz workers. Protected by mutex.
     24 msg_queue: std.ArrayListUnmanaged(Msg),
     25 /// Protects `msg_queue` only.
     26 mutex: std.Thread.Mutex,
     27 /// Signaled when there is a message in `msg_queue`.
     28 condition: std.Thread.Condition,
     29 
     30 coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
     31 /// Protects `coverage_files` only.
     32 coverage_mutex: std.Thread.Mutex,
     33 /// Signaled when `coverage_files` changes.
     34 coverage_condition: std.Thread.Condition,
     35 
     36 /// Time at initialization of WebServer.
     37 base_timestamp: i128,
     38 
     39 const fuzzer_bin_name = "fuzzer";
     40 const fuzzer_arch_os_abi = "wasm32-freestanding";
     41 const fuzzer_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
     42 
     43 const CoverageMap = struct {
     44     mapped_memory: []align(std.mem.page_size) const u8,
     45     coverage: Coverage,
     46     source_locations: []Coverage.SourceLocation,
     47     /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
     48     entry_points: std.ArrayListUnmanaged(u32),
     49     start_timestamp: i64,
     50 
     51     fn deinit(cm: *CoverageMap, gpa: Allocator) void {
     52         std.posix.munmap(cm.mapped_memory);
     53         cm.coverage.deinit(gpa);
     54         cm.* = undefined;
     55     }
     56 };
     57 
     58 const Msg = union(enum) {
     59     coverage: struct {
     60         id: u64,
     61         run: *Step.Run,
     62     },
     63     entry_point: struct {
     64         coverage_id: u64,
     65         addr: u64,
     66     },
     67 };
     68 
     69 pub fn run(ws: *WebServer) void {
     70     var http_server = ws.listen_address.listen(.{
     71         .reuse_address = true,
     72     }) catch |err| {
     73         log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.in.getPort(), @errorName(err) });
     74         return;
     75     };
     76     const port = http_server.listen_address.in.getPort();
     77     log.info("web interface listening at http://127.0.0.1:{d}/", .{port});
     78     if (ws.listen_address.in.getPort() == 0)
     79         log.info("hint: pass --port {d} to use this same port next time", .{port});
     80 
     81     while (true) {
     82         const connection = http_server.accept() catch |err| {
     83             log.err("failed to accept connection: {s}", .{@errorName(err)});
     84             return;
     85         };
     86         _ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| {
     87             log.err("unable to spawn connection thread: {s}", .{@errorName(err)});
     88             connection.stream.close();
     89             continue;
     90         };
     91     }
     92 }
     93 
     94 fn now(s: *const WebServer) i64 {
     95     return @intCast(std.time.nanoTimestamp() - s.base_timestamp);
     96 }
     97 
     98 fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
     99     defer connection.stream.close();
    100 
    101     var read_buffer: [0x4000]u8 = undefined;
    102     var server = std.http.Server.init(connection, &read_buffer);
    103     var web_socket: std.http.WebSocket = undefined;
    104     var send_buffer: [0x4000]u8 = undefined;
    105     var ws_recv_buffer: [0x4000]u8 align(4) = undefined;
    106     while (server.state == .ready) {
    107         var request = server.receiveHead() catch |err| switch (err) {
    108             error.HttpConnectionClosing => return,
    109             else => {
    110                 log.err("closing http connection: {s}", .{@errorName(err)});
    111                 return;
    112             },
    113         };
    114         if (web_socket.init(&request, &send_buffer, &ws_recv_buffer) catch |err| {
    115             log.err("initializing web socket: {s}", .{@errorName(err)});
    116             return;
    117         }) {
    118             serveWebSocket(ws, &web_socket) catch |err| {
    119                 log.err("unable to serve web socket connection: {s}", .{@errorName(err)});
    120                 return;
    121             };
    122         } else {
    123             serveRequest(ws, &request) catch |err| switch (err) {
    124                 error.AlreadyReported => return,
    125                 else => |e| {
    126                     log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) });
    127                     return;
    128                 },
    129             };
    130         }
    131     }
    132 }
    133 
    134 fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
    135     if (std.mem.eql(u8, request.head.target, "/") or
    136         std.mem.eql(u8, request.head.target, "/debug") or
    137         std.mem.eql(u8, request.head.target, "/debug/"))
    138     {
    139         try serveFile(ws, request, "fuzzer/web/index.html", "text/html");
    140     } else if (std.mem.eql(u8, request.head.target, "/main.js") or
    141         std.mem.eql(u8, request.head.target, "/debug/main.js"))
    142     {
    143         try serveFile(ws, request, "fuzzer/web/main.js", "application/javascript");
    144     } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
    145         try serveWasm(ws, request, .ReleaseFast);
    146     } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
    147         try serveWasm(ws, request, .Debug);
    148     } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or
    149         std.mem.eql(u8, request.head.target, "/debug/sources.tar"))
    150     {
    151         try serveSourcesTar(ws, request);
    152     } else {
    153         try request.respond("not found", .{
    154             .status = .not_found,
    155             .extra_headers = &.{
    156                 .{ .name = "content-type", .value = "text/plain" },
    157             },
    158         });
    159     }
    160 }
    161 
    162 fn serveFile(
    163     ws: *WebServer,
    164     request: *std.http.Server.Request,
    165     name: []const u8,
    166     content_type: []const u8,
    167 ) !void {
    168     const gpa = ws.gpa;
    169     // The desired API is actually sendfile, which will require enhancing std.http.Server.
    170     // We load the file with every request so that the user can make changes to the file
    171     // and refresh the HTML page without restarting this server.
    172     const file_contents = ws.zig_lib_directory.handle.readFileAlloc(gpa, name, 10 * 1024 * 1024) catch |err| {
    173         log.err("failed to read '{}{s}': {s}", .{ ws.zig_lib_directory, name, @errorName(err) });
    174         return error.AlreadyReported;
    175     };
    176     defer gpa.free(file_contents);
    177     try request.respond(file_contents, .{
    178         .extra_headers = &.{
    179             .{ .name = "content-type", .value = content_type },
    180             cache_control_header,
    181         },
    182     });
    183 }
    184 
    185 fn serveWasm(
    186     ws: *WebServer,
    187     request: *std.http.Server.Request,
    188     optimize_mode: std.builtin.OptimizeMode,
    189 ) !void {
    190     const gpa = ws.gpa;
    191 
    192     var arena_instance = std.heap.ArenaAllocator.init(gpa);
    193     defer arena_instance.deinit();
    194     const arena = arena_instance.allocator();
    195 
    196     // Do the compilation every request, so that the user can edit the files
    197     // and see the changes without restarting the server.
    198     const wasm_base_path = try buildWasmBinary(ws, arena, optimize_mode);
    199     const bin_name = try std.zig.binNameAlloc(arena, .{
    200         .root_name = fuzzer_bin_name,
    201         .target = std.zig.system.resolveTargetQuery(std.Build.parseTargetQuery(.{
    202             .arch_os_abi = fuzzer_arch_os_abi,
    203             .cpu_features = fuzzer_cpu_features,
    204         }) catch unreachable) catch unreachable,
    205         .output_mode = .Exe,
    206     });
    207     // std.http.Server does not have a sendfile API yet.
    208     const bin_path = try wasm_base_path.join(arena, bin_name);
    209     const file_contents = try bin_path.root_dir.handle.readFileAlloc(gpa, bin_path.sub_path, 10 * 1024 * 1024);
    210     defer gpa.free(file_contents);
    211     try request.respond(file_contents, .{
    212         .extra_headers = &.{
    213             .{ .name = "content-type", .value = "application/wasm" },
    214             cache_control_header,
    215         },
    216     });
    217 }
    218 
    219 fn buildWasmBinary(
    220     ws: *WebServer,
    221     arena: Allocator,
    222     optimize_mode: std.builtin.OptimizeMode,
    223 ) !Path {
    224     const gpa = ws.gpa;
    225 
    226     const main_src_path: Build.Cache.Path = .{
    227         .root_dir = ws.zig_lib_directory,
    228         .sub_path = "fuzzer/web/main.zig",
    229     };
    230     const walk_src_path: Build.Cache.Path = .{
    231         .root_dir = ws.zig_lib_directory,
    232         .sub_path = "docs/wasm/Walk.zig",
    233     };
    234     const html_render_src_path: Build.Cache.Path = .{
    235         .root_dir = ws.zig_lib_directory,
    236         .sub_path = "docs/wasm/html_render.zig",
    237     };
    238 
    239     var argv: std.ArrayListUnmanaged([]const u8) = .{};
    240 
    241     try argv.appendSlice(arena, &.{
    242         ws.zig_exe_path, "build-exe", //
    243         "-fno-entry", //
    244         "-O", @tagName(optimize_mode), //
    245         "-target", fuzzer_arch_os_abi, //
    246         "-mcpu", fuzzer_cpu_features, //
    247         "--cache-dir", ws.global_cache_directory.path orelse ".", //
    248         "--global-cache-dir", ws.global_cache_directory.path orelse ".", //
    249         "--name", fuzzer_bin_name, //
    250         "-rdynamic", //
    251         "-fsingle-threaded", //
    252         "--dep", "Walk", //
    253         "--dep", "html_render", //
    254         try std.fmt.allocPrint(arena, "-Mroot={}", .{main_src_path}), //
    255         try std.fmt.allocPrint(arena, "-MWalk={}", .{walk_src_path}), //
    256         "--dep", "Walk", //
    257         try std.fmt.allocPrint(arena, "-Mhtml_render={}", .{html_render_src_path}), //
    258         "--listen=-",
    259     });
    260 
    261     var child = std.process.Child.init(argv.items, gpa);
    262     child.stdin_behavior = .Pipe;
    263     child.stdout_behavior = .Pipe;
    264     child.stderr_behavior = .Pipe;
    265     try child.spawn();
    266 
    267     var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
    268         .stdout = child.stdout.?,
    269         .stderr = child.stderr.?,
    270     });
    271     defer poller.deinit();
    272 
    273     try sendMessage(child.stdin.?, .update);
    274     try sendMessage(child.stdin.?, .exit);
    275 
    276     const Header = std.zig.Server.Message.Header;
    277     var result: ?Path = null;
    278     var result_error_bundle = std.zig.ErrorBundle.empty;
    279 
    280     const stdout = poller.fifo(.stdout);
    281 
    282     poll: while (true) {
    283         while (stdout.readableLength() < @sizeOf(Header)) {
    284             if (!(try poller.poll())) break :poll;
    285         }
    286         const header = stdout.reader().readStruct(Header) catch unreachable;
    287         while (stdout.readableLength() < header.bytes_len) {
    288             if (!(try poller.poll())) break :poll;
    289         }
    290         const body = stdout.readableSliceOfLen(header.bytes_len);
    291 
    292         switch (header.tag) {
    293             .zig_version => {
    294                 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
    295                     return error.ZigProtocolVersionMismatch;
    296                 }
    297             },
    298             .error_bundle => {
    299                 const EbHdr = std.zig.Server.Message.ErrorBundle;
    300                 const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
    301                 const extra_bytes =
    302                     body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
    303                 const string_bytes =
    304                     body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
    305                 // TODO: use @ptrCast when the compiler supports it
    306                 const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes);
    307                 const extra_array = try arena.alloc(u32, unaligned_extra.len);
    308                 @memcpy(extra_array, unaligned_extra);
    309                 result_error_bundle = .{
    310                     .string_bytes = try arena.dupe(u8, string_bytes),
    311                     .extra = extra_array,
    312                 };
    313             },
    314             .emit_digest => {
    315                 const EmitDigest = std.zig.Server.Message.EmitDigest;
    316                 const ebp_hdr = @as(*align(1) const EmitDigest, @ptrCast(body));
    317                 if (!ebp_hdr.flags.cache_hit) {
    318                     log.info("source changes detected; rebuilt wasm component", .{});
    319                 }
    320                 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
    321                 result = .{
    322                     .root_dir = ws.global_cache_directory,
    323                     .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
    324                 };
    325             },
    326             else => {}, // ignore other messages
    327         }
    328 
    329         stdout.discard(body.len);
    330     }
    331 
    332     const stderr = poller.fifo(.stderr);
    333     if (stderr.readableLength() > 0) {
    334         const owned_stderr = try stderr.toOwnedSlice();
    335         defer gpa.free(owned_stderr);
    336         std.debug.print("{s}", .{owned_stderr});
    337     }
    338 
    339     // Send EOF to stdin.
    340     child.stdin.?.close();
    341     child.stdin = null;
    342 
    343     switch (try child.wait()) {
    344         .Exited => |code| {
    345             if (code != 0) {
    346                 log.err(
    347                     "the following command exited with error code {d}:\n{s}",
    348                     .{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) },
    349                 );
    350                 return error.WasmCompilationFailed;
    351             }
    352         },
    353         .Signal, .Stopped, .Unknown => {
    354             log.err(
    355                 "the following command terminated unexpectedly:\n{s}",
    356                 .{try Build.Step.allocPrintCmd(arena, null, argv.items)},
    357             );
    358             return error.WasmCompilationFailed;
    359         },
    360     }
    361 
    362     if (result_error_bundle.errorMessageCount() > 0) {
    363         const color = std.zig.Color.auto;
    364         result_error_bundle.renderToStdErr(color.renderOptions());
    365         log.err("the following command failed with {d} compilation errors:\n{s}", .{
    366             result_error_bundle.errorMessageCount(),
    367             try Build.Step.allocPrintCmd(arena, null, argv.items),
    368         });
    369         return error.WasmCompilationFailed;
    370     }
    371 
    372     return result orelse {
    373         log.err("child process failed to report result\n{s}", .{
    374             try Build.Step.allocPrintCmd(arena, null, argv.items),
    375         });
    376         return error.WasmCompilationFailed;
    377     };
    378 }
    379 
    380 fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
    381     const header: std.zig.Client.Message.Header = .{
    382         .tag = tag,
    383         .bytes_len = 0,
    384     };
    385     try file.writeAll(std.mem.asBytes(&header));
    386 }
    387 
    388 fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
    389     ws.coverage_mutex.lock();
    390     defer ws.coverage_mutex.unlock();
    391 
    392     // On first connection, the client needs to know what time the server
    393     // thinks it is to rebase timestamps.
    394     {
    395         const timestamp_message: abi.CurrentTime = .{ .base = ws.now() };
    396         try web_socket.writeMessage(std.mem.asBytes(&timestamp_message), .binary);
    397     }
    398 
    399     // On first connection, the client needs all the coverage information
    400     // so that subsequent updates can contain only the updated bits.
    401     var prev_unique_runs: usize = 0;
    402     var prev_entry_points: usize = 0;
    403     try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
    404     while (true) {
    405         ws.coverage_condition.timedWait(&ws.coverage_mutex, std.time.ns_per_ms * 500) catch {};
    406         try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
    407     }
    408 }
    409 
    410 fn sendCoverageContext(
    411     ws: *WebServer,
    412     web_socket: *std.http.WebSocket,
    413     prev_unique_runs: *usize,
    414     prev_entry_points: *usize,
    415 ) !void {
    416     const coverage_maps = ws.coverage_files.values();
    417     if (coverage_maps.len == 0) return;
    418     // TODO: make each events URL correspond to one coverage map
    419     const coverage_map = &coverage_maps[0];
    420     const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
    421     const seen_pcs = cov_header.seenBits();
    422     const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
    423     const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
    424     if (prev_unique_runs.* != unique_runs) {
    425         // There has been an update.
    426         if (prev_unique_runs.* == 0) {
    427             // We need to send initial context.
    428             const header: abi.SourceIndexHeader = .{
    429                 .flags = .{},
    430                 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
    431                 .files_len = @intCast(coverage_map.coverage.files.entries.len),
    432                 .source_locations_len = @intCast(coverage_map.source_locations.len),
    433                 .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
    434                 .start_timestamp = coverage_map.start_timestamp,
    435             };
    436             const iovecs: [5]std.posix.iovec_const = .{
    437                 makeIov(std.mem.asBytes(&header)),
    438                 makeIov(std.mem.sliceAsBytes(coverage_map.coverage.directories.keys())),
    439                 makeIov(std.mem.sliceAsBytes(coverage_map.coverage.files.keys())),
    440                 makeIov(std.mem.sliceAsBytes(coverage_map.source_locations)),
    441                 makeIov(coverage_map.coverage.string_bytes.items),
    442             };
    443             try web_socket.writeMessagev(&iovecs, .binary);
    444         }
    445 
    446         const header: abi.CoverageUpdateHeader = .{
    447             .n_runs = n_runs,
    448             .unique_runs = unique_runs,
    449         };
    450         const iovecs: [2]std.posix.iovec_const = .{
    451             makeIov(std.mem.asBytes(&header)),
    452             makeIov(std.mem.sliceAsBytes(seen_pcs)),
    453         };
    454         try web_socket.writeMessagev(&iovecs, .binary);
    455 
    456         prev_unique_runs.* = unique_runs;
    457     }
    458 
    459     if (prev_entry_points.* != coverage_map.entry_points.items.len) {
    460         const header: abi.EntryPointHeader = .{
    461             .flags = .{
    462                 .locs_len = @intCast(coverage_map.entry_points.items.len),
    463             },
    464         };
    465         const iovecs: [2]std.posix.iovec_const = .{
    466             makeIov(std.mem.asBytes(&header)),
    467             makeIov(std.mem.sliceAsBytes(coverage_map.entry_points.items)),
    468         };
    469         try web_socket.writeMessagev(&iovecs, .binary);
    470 
    471         prev_entry_points.* = coverage_map.entry_points.items.len;
    472     }
    473 }
    474 
    475 fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
    476     const gpa = ws.gpa;
    477 
    478     var arena_instance = std.heap.ArenaAllocator.init(gpa);
    479     defer arena_instance.deinit();
    480     const arena = arena_instance.allocator();
    481 
    482     var send_buffer: [0x4000]u8 = undefined;
    483     var response = request.respondStreaming(.{
    484         .send_buffer = &send_buffer,
    485         .respond_options = .{
    486             .extra_headers = &.{
    487                 .{ .name = "content-type", .value = "application/x-tar" },
    488                 cache_control_header,
    489             },
    490         },
    491     });
    492 
    493     const DedupeTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
    494     var dedupe_table: DedupeTable = .{};
    495     defer dedupe_table.deinit(gpa);
    496 
    497     for (ws.fuzz_run_steps) |run_step| {
    498         const compile_step_inputs = run_step.producer.?.step.inputs.table;
    499         for (compile_step_inputs.keys(), compile_step_inputs.values()) |dir_path, *file_list| {
    500             try dedupe_table.ensureUnusedCapacity(gpa, file_list.items.len);
    501             for (file_list.items) |sub_path| {
    502                 // Special file "." means the entire directory.
    503                 if (std.mem.eql(u8, sub_path, ".")) continue;
    504                 const joined_path = try dir_path.join(arena, sub_path);
    505                 _ = dedupe_table.getOrPutAssumeCapacity(joined_path);
    506             }
    507         }
    508     }
    509 
    510     const deduped_paths = dedupe_table.keys();
    511     const SortContext = struct {
    512         pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
    513             _ = this;
    514             return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
    515                 .lt => true,
    516                 .gt => false,
    517                 .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
    518             };
    519         }
    520     };
    521     std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
    522 
    523     var cwd_cache: ?[]const u8 = null;
    524 
    525     var archiver = std.tar.writer(response.writer());
    526 
    527     for (deduped_paths) |joined_path| {
    528         var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| {
    529             log.err("failed to open {}: {s}", .{ joined_path, @errorName(err) });
    530             continue;
    531         };
    532         defer file.close();
    533 
    534         archiver.prefix = joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache);
    535         try archiver.writeFile(joined_path.sub_path, file);
    536     }
    537 
    538     // intentionally omitting the pointless trailer
    539     //try archiver.finish();
    540     try response.end();
    541 }
    542 
    543 fn memoizedCwd(arena: Allocator, opt_ptr: *?[]const u8) ![]const u8 {
    544     if (opt_ptr.*) |cached| return cached;
    545     const result = try std.process.getCwdAlloc(arena);
    546     opt_ptr.* = result;
    547     return result;
    548 }
    549 
    550 const cache_control_header: std.http.Header = .{
    551     .name = "cache-control",
    552     .value = "max-age=0, must-revalidate",
    553 };
    554 
    555 pub fn coverageRun(ws: *WebServer) void {
    556     ws.mutex.lock();
    557     defer ws.mutex.unlock();
    558 
    559     while (true) {
    560         ws.condition.wait(&ws.mutex);
    561         for (ws.msg_queue.items) |msg| switch (msg) {
    562             .coverage => |coverage| prepareTables(ws, coverage.run, coverage.id) catch |err| switch (err) {
    563                 error.AlreadyReported => continue,
    564                 else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
    565             },
    566             .entry_point => |entry_point| addEntryPoint(ws, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
    567                 error.AlreadyReported => continue,
    568                 else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
    569             },
    570         };
    571         ws.msg_queue.clearRetainingCapacity();
    572     }
    573 }
    574 
    575 fn prepareTables(
    576     ws: *WebServer,
    577     run_step: *Step.Run,
    578     coverage_id: u64,
    579 ) error{ OutOfMemory, AlreadyReported }!void {
    580     const gpa = ws.gpa;
    581 
    582     ws.coverage_mutex.lock();
    583     defer ws.coverage_mutex.unlock();
    584 
    585     const gop = try ws.coverage_files.getOrPut(gpa, coverage_id);
    586     if (gop.found_existing) {
    587         // We are fuzzing the same executable with multiple threads.
    588         // Perhaps the same unit test; perhaps a different one. In any
    589         // case, since the coverage file is the same, we only have to
    590         // notice changes to that one file in order to learn coverage for
    591         // this particular executable.
    592         return;
    593     }
    594     errdefer _ = ws.coverage_files.pop();
    595 
    596     gop.value_ptr.* = .{
    597         .coverage = std.debug.Coverage.init,
    598         .mapped_memory = undefined, // populated below
    599         .source_locations = undefined, // populated below
    600         .entry_points = .{},
    601         .start_timestamp = ws.now(),
    602     };
    603     errdefer gop.value_ptr.coverage.deinit(gpa);
    604 
    605     const rebuilt_exe_path = run_step.rebuilt_executable.?;
    606     var debug_info = std.debug.Info.load(gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
    607         log.err("step '{s}': failed to load debug information for '{}': {s}", .{
    608             run_step.step.name, rebuilt_exe_path, @errorName(err),
    609         });
    610         return error.AlreadyReported;
    611     };
    612     defer debug_info.deinit(gpa);
    613 
    614     const coverage_file_path: Build.Cache.Path = .{
    615         .root_dir = run_step.step.owner.cache_root,
    616         .sub_path = "v/" ++ std.fmt.hex(coverage_id),
    617     };
    618     var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
    619         log.err("step '{s}': failed to load coverage file '{}': {s}", .{
    620             run_step.step.name, coverage_file_path, @errorName(err),
    621         });
    622         return error.AlreadyReported;
    623     };
    624     defer coverage_file.close();
    625 
    626     const file_size = coverage_file.getEndPos() catch |err| {
    627         log.err("unable to check len of coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
    628         return error.AlreadyReported;
    629     };
    630 
    631     const mapped_memory = std.posix.mmap(
    632         null,
    633         file_size,
    634         std.posix.PROT.READ,
    635         .{ .TYPE = .SHARED },
    636         coverage_file.handle,
    637         0,
    638     ) catch |err| {
    639         log.err("failed to map coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
    640         return error.AlreadyReported;
    641     };
    642     gop.value_ptr.mapped_memory = mapped_memory;
    643 
    644     const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
    645     const pcs = header.pcAddrs();
    646     const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
    647     errdefer gpa.free(source_locations);
    648 
    649     // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
    650     // counters feature is not sorted.
    651     var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .{};
    652     defer sorted_pcs.deinit(gpa);
    653     try sorted_pcs.resize(gpa, pcs.len);
    654     @memcpy(sorted_pcs.items(.pc), pcs);
    655     for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
    656     sorted_pcs.sortUnstable(struct {
    657         addrs: []const u64,
    658 
    659         pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
    660             return ctx.addrs[a_index] < ctx.addrs[b_index];
    661         }
    662     }{ .addrs = sorted_pcs.items(.pc) });
    663 
    664     debug_info.resolveAddresses(gpa, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
    665         log.err("failed to resolve addresses to source locations: {s}", .{@errorName(err)});
    666         return error.AlreadyReported;
    667     };
    668 
    669     for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl;
    670     gop.value_ptr.source_locations = source_locations;
    671 
    672     ws.coverage_condition.broadcast();
    673 }
    674 
    675 fn addEntryPoint(ws: *WebServer, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void {
    676     ws.coverage_mutex.lock();
    677     defer ws.coverage_mutex.unlock();
    678 
    679     const coverage_map = ws.coverage_files.getPtr(coverage_id).?;
    680     const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
    681     const pcs = header.pcAddrs();
    682     // Since this pcs list is unsorted, we must linear scan for the best index.
    683     const index = i: {
    684         var best: usize = 0;
    685         for (pcs[1..], 1..) |elem_addr, i| {
    686             if (elem_addr == addr) break :i i;
    687             if (elem_addr > addr) continue;
    688             if (elem_addr > pcs[best]) best = i;
    689         }
    690         break :i best;
    691     };
    692     if (index >= pcs.len) {
    693         log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
    694             addr, pcs[0], pcs[pcs.len - 1],
    695         });
    696         return error.AlreadyReported;
    697     }
    698     if (false) {
    699         const sl = coverage_map.source_locations[index];
    700         const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
    701         log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
    702             addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
    703         });
    704     }
    705     const gpa = ws.gpa;
    706     try coverage_map.entry_points.append(gpa, @intCast(index));
    707 }
    708 
    709 fn makeIov(s: []const u8) std.posix.iovec_const {
    710     return .{
    711         .base = s.ptr,
    712         .len = s.len,
    713     };
    714 }