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(×tamp_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 }