| 1 | // Server timestamp. |
| 2 | var start_fuzzing_timestamp: i64 = undefined; |
| 3 | var start_fuzzing_n_runs: u64 = undefined; |
| 4 | |
| 5 | const js = struct { |
| 6 | extern "fuzz" fn requestSources() void; |
| 7 | extern "fuzz" fn ready() void; |
| 8 | |
| 9 | extern "fuzz" fn updateStats(html_ptr: [*]const u8, html_len: usize) void; |
| 10 | extern "fuzz" fn updateEntryPoints(html_ptr: [*]const u8, html_len: usize) void; |
| 11 | extern "fuzz" fn updateSource(html_ptr: [*]const u8, html_len: usize) void; |
| 12 | extern "fuzz" fn updateCoverage(covered_ptr: [*]const SourceLocationIndex, covered_len: u32) void; |
| 13 | }; |
| 14 | |
| 15 | pub fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void { |
| 16 | Walk.files.clearRetainingCapacity(); |
| 17 | Walk.decls.clearRetainingCapacity(); |
| 18 | Walk.modules.clearRetainingCapacity(); |
| 19 | recent_coverage_update.clearRetainingCapacity(); |
| 20 | selected_source_location = null; |
| 21 | |
| 22 | js.requestSources(); |
| 23 | |
| 24 | const Header = abi.fuzz.SourceIndexHeader; |
| 25 | const header: *align(1) const Header = @ptrCast(msg_bytes[0..@sizeOf(Header)]); |
| 26 | |
| 27 | const directories_start = @sizeOf(Header); |
| 28 | const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String); |
| 29 | const files_start = directories_end; |
| 30 | const files_end = files_start + header.files_len * @sizeOf(Coverage.File); |
| 31 | const source_locations_start = files_end; |
| 32 | const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation); |
| 33 | const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len]; |
| 34 | |
| 35 | const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end])); |
| 36 | const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end])); |
| 37 | const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end])); |
| 38 | |
| 39 | start_fuzzing_timestamp = header.start_timestamp; |
| 40 | start_fuzzing_n_runs = header.start_n_runs; |
| 41 | try updateCoverageSources(directories, files, source_locations, string_bytes); |
| 42 | js.ready(); |
| 43 | } |
| 44 | |
| 45 | var coverage = Coverage.init; |
| 46 | /// Index of type `SourceLocationIndex`. |
| 47 | var coverage_source_locations: std.ArrayList(Coverage.SourceLocation) = .empty; |
| 48 | /// Contains the most recent coverage update message, unmodified. |
| 49 | var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, .of(u64)) = .empty; |
| 50 | |
| 51 | fn updateCoverageSources( |
| 52 | directories: []const Coverage.String, |
| 53 | files: []const Coverage.File, |
| 54 | source_locations: []const Coverage.SourceLocation, |
| 55 | string_bytes: []const u8, |
| 56 | ) !void { |
| 57 | coverage.directories.clearRetainingCapacity(); |
| 58 | coverage.files.clearRetainingCapacity(); |
| 59 | coverage.string_bytes.clearRetainingCapacity(); |
| 60 | coverage_source_locations.clearRetainingCapacity(); |
| 61 | |
| 62 | try coverage_source_locations.appendSlice(gpa, source_locations); |
| 63 | try coverage.string_bytes.appendSlice(gpa, string_bytes); |
| 64 | |
| 65 | try coverage.files.entries.resize(gpa, files.len); |
| 66 | @memcpy(coverage.files.entries.items(.key), files); |
| 67 | try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); |
| 68 | |
| 69 | try coverage.directories.entries.resize(gpa, directories.len); |
| 70 | @memcpy(coverage.directories.entries.items(.key), directories); |
| 71 | try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items }); |
| 72 | } |
| 73 | |
| 74 | pub fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void { |
| 75 | recent_coverage_update.clearRetainingCapacity(); |
| 76 | recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM"); |
| 77 | try updateStats(); |
| 78 | try updateCoverage(); |
| 79 | } |
| 80 | |
| 81 | var entry_points: std.ArrayList(SourceLocationIndex) = .empty; |
| 82 | |
| 83 | pub fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void { |
| 84 | const header: *align(1) const abi.fuzz.EntryPointHeader = @ptrCast(msg_bytes[0..@sizeOf(abi.fuzz.EntryPointHeader)]); |
| 85 | const slis: []align(1) const SourceLocationIndex = @ptrCast(msg_bytes[@sizeOf(abi.fuzz.EntryPointHeader)..]); |
| 86 | assert(slis.len == header.locsLen()); |
| 87 | try entry_points.resize(gpa, slis.len); |
| 88 | @memcpy(entry_points.items, slis); |
| 89 | try updateEntryPoints(); |
| 90 | } |
| 91 | |
| 92 | /// Index into `coverage_source_locations`. |
| 93 | const SourceLocationIndex = enum(u32) { |
| 94 | _, |
| 95 | |
| 96 | fn haveCoverage(sli: SourceLocationIndex) bool { |
| 97 | return @backingInt(sli) < coverage_source_locations.items.len; |
| 98 | } |
| 99 | |
| 100 | fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation { |
| 101 | return &coverage_source_locations.items[@backingInt(sli)]; |
| 102 | } |
| 103 | |
| 104 | fn sourceLocationLinkHtml( |
| 105 | sli: SourceLocationIndex, |
| 106 | out: *std.ArrayList(u8), |
| 107 | focused: bool, |
| 108 | ) error{OutOfMemory}!void { |
| 109 | const sl = sli.ptr(); |
| 110 | try out.print(gpa, "<code{s}>", .{ |
| 111 | @as([]const u8, if (focused) " class=\"status-running\"" else ""), |
| 112 | }); |
| 113 | try sli.appendPath(out); |
| 114 | try out.print(gpa, ":{d}:{d} </code><button class=\"linkish\" onclick=\"wasm_exports.fuzzSelectSli({d});\">View</button>", .{ |
| 115 | sl.line, |
| 116 | sl.column, |
| 117 | @backingInt(sli), |
| 118 | }); |
| 119 | } |
| 120 | |
| 121 | fn appendPath(sli: SourceLocationIndex, out: *std.ArrayList(u8)) error{OutOfMemory}!void { |
| 122 | const sl = sli.ptr(); |
| 123 | const file = coverage.fileAt(sl.file); |
| 124 | const file_name = coverage.stringAt(file.basename); |
| 125 | const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]); |
| 126 | try html_render.appendEscaped(out, dir_name); |
| 127 | try out.appendSlice(gpa, "/"); |
| 128 | try html_render.appendEscaped(out, file_name); |
| 129 | } |
| 130 | |
| 131 | fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index { |
| 132 | var buf: std.ArrayList(u8) = .empty; |
| 133 | defer buf.deinit(gpa); |
| 134 | sli.appendPath(&buf) catch @panic("OOM"); |
| 135 | return @fromBackingInt(@intCast(Walk.files.getIndex(buf.items) orelse return null)); |
| 136 | } |
| 137 | |
| 138 | fn fileHtml( |
| 139 | sli: SourceLocationIndex, |
| 140 | out: *std.ArrayList(u8), |
| 141 | ) error{ OutOfMemory, SourceUnavailable }!void { |
| 142 | const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable; |
| 143 | const root_node = walk_file_index.findRootDecl().get().ast_node; |
| 144 | var annotations: std.ArrayList(html_render.Annotation) = .empty; |
| 145 | defer annotations.deinit(gpa); |
| 146 | try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items); |
| 147 | html_render.fileSourceHtml(walk_file_index, out, root_node, .{ |
| 148 | .source_location_annotations = annotations.items, |
| 149 | }) catch |err| { |
| 150 | fatal("unable to render source: {s}", .{@errorName(err)}); |
| 151 | }; |
| 152 | } |
| 153 | }; |
| 154 | |
| 155 | fn computeSourceAnnotations( |
| 156 | cov_file_index: Coverage.File.Index, |
| 157 | walk_file_index: Walk.File.Index, |
| 158 | annotations: *std.ArrayList(html_render.Annotation), |
| 159 | source_locations: []const Coverage.SourceLocation, |
| 160 | ) !void { |
| 161 | // Collect all the source locations from only this file into this array |
| 162 | // first, then sort by line, col, so that we can collect annotations with |
| 163 | // O(N) time complexity. |
| 164 | var locs: std.ArrayList(SourceLocationIndex) = .empty; |
| 165 | defer locs.deinit(gpa); |
| 166 | |
| 167 | for (source_locations, 0..) |sl, sli_usize| { |
| 168 | if (sl.file != cov_file_index) continue; |
| 169 | const sli: SourceLocationIndex = @fromBackingInt(@intCast(sli_usize)); |
| 170 | try locs.append(gpa, sli); |
| 171 | } |
| 172 | |
| 173 | std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct { |
| 174 | pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool { |
| 175 | _ = context; |
| 176 | const lhs_ptr = lhs.ptr(); |
| 177 | const rhs_ptr = rhs.ptr(); |
| 178 | if (lhs_ptr.line < rhs_ptr.line) return true; |
| 179 | if (lhs_ptr.line > rhs_ptr.line) return false; |
| 180 | return lhs_ptr.column < rhs_ptr.column; |
| 181 | } |
| 182 | }.lessThan); |
| 183 | |
| 184 | const source = walk_file_index.get_ast().source; |
| 185 | var line: usize = 1; |
| 186 | var column: usize = 1; |
| 187 | var next_loc_index: usize = 0; |
| 188 | for (source, 0..) |byte, offset| { |
| 189 | if (byte == '\n') { |
| 190 | line += 1; |
| 191 | column = 1; |
| 192 | } else { |
| 193 | column += 1; |
| 194 | } |
| 195 | while (true) { |
| 196 | if (next_loc_index >= locs.items.len) return; |
| 197 | const next_sli = locs.items[next_loc_index]; |
| 198 | const next_sl = next_sli.ptr(); |
| 199 | if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break; |
| 200 | try annotations.append(gpa, .{ |
| 201 | .file_byte_offset = offset, |
| 202 | .dom_id = @backingInt(next_sli), |
| 203 | }); |
| 204 | next_loc_index += 1; |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | export fn fuzzUnpackSources(tar_ptr: [*]u8, tar_len: usize) void { |
| 210 | const tar_bytes = tar_ptr[0..tar_len]; |
| 211 | log.debug("received {d} bytes of sources.tar", .{tar_bytes.len}); |
| 212 | |
| 213 | unpackSourcesInner(tar_bytes) catch |err| { |
| 214 | fatal("unable to unpack sources.tar: {s}", .{@errorName(err)}); |
| 215 | }; |
| 216 | } |
| 217 | |
| 218 | fn unpackSourcesInner(tar_bytes: []u8) !void { |
| 219 | var tar_reader: std.Io.Reader = .fixed(tar_bytes); |
| 220 | var file_name_buffer: [1024]u8 = undefined; |
| 221 | var link_name_buffer: [1024]u8 = undefined; |
| 222 | var it: std.tar.Iterator = .init(&tar_reader, .{ |
| 223 | .file_name_buffer = &file_name_buffer, |
| 224 | .link_name_buffer = &link_name_buffer, |
| 225 | }); |
| 226 | while (try it.next()) |tar_file| { |
| 227 | switch (tar_file.kind) { |
| 228 | .file => { |
| 229 | if (tar_file.size == 0 and tar_file.name.len == 0) break; |
| 230 | if (std.mem.endsWith(u8, tar_file.name, ".zig")) { |
| 231 | log.debug("found file: '{s}'", .{tar_file.name}); |
| 232 | const file_name = try gpa.dupe(u8, tar_file.name); |
| 233 | // This is a hack to guess modules from the tar file contents. To handle modules |
| 234 | // properly, the build system will need to change the structure here to have one |
| 235 | // directory per module. This in turn requires compiler enhancements to allow |
| 236 | // the build system to actually discover the required information. |
| 237 | const mod_name, const is_module_root = p: { |
| 238 | if (std.mem.find(u8, file_name, "std/")) |i| break :p .{ "std", std.mem.eql(u8, file_name[i + 4 ..], "std.zig") }; |
| 239 | if (std.mem.endsWith(u8, file_name, "/builtin.zig")) break :p .{ "builtin", true }; |
| 240 | break :p .{ "root", std.mem.endsWith(u8, file_name, "/root.zig") }; |
| 241 | }; |
| 242 | const gop = try Walk.modules.getOrPut(gpa, mod_name); |
| 243 | const file: Walk.File.Index = @fromBackingInt(@intCast(Walk.files.entries.len)); |
| 244 | if (!gop.found_existing or is_module_root) gop.value_ptr.* = file; |
| 245 | const file_bytes = tar_reader.take(@intCast(tar_file.size)) catch unreachable; |
| 246 | it.unread_file_bytes = 0; // we have read the whole thing |
| 247 | assert(file == try Walk.add_file(file_name, file_bytes)); |
| 248 | } else { |
| 249 | log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name}); |
| 250 | } |
| 251 | }, |
| 252 | else => continue, |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | fn updateStats() error{OutOfMemory}!void { |
| 258 | // No @setFloatMode(.optimized) since some stats may be at zero and lead to divisions by zero |
| 259 | |
| 260 | if (recent_coverage_update.items.len == 0) return; |
| 261 | |
| 262 | const hdr: *abi.fuzz.CoverageUpdateHeader = @ptrCast(@alignCast( |
| 263 | recent_coverage_update.items[0..@sizeOf(abi.fuzz.CoverageUpdateHeader)], |
| 264 | )); |
| 265 | |
| 266 | const covered_src_locs: usize = n: { |
| 267 | var n: usize = 0; |
| 268 | const covered_bits = recent_coverage_update.items[@sizeOf(abi.fuzz.CoverageUpdateHeader)..]; |
| 269 | for (covered_bits) |byte| n += @popCount(byte); |
| 270 | break :n n; |
| 271 | }; |
| 272 | const total_src_locs = coverage_source_locations.items.len; |
| 273 | |
| 274 | const avg_speed: f64 = speed: { |
| 275 | const ns_elapsed: f64 = @floatFromInt(nsSince(start_fuzzing_timestamp)); |
| 276 | const n_runs: f64 = @floatFromInt(hdr.n_runs -% start_fuzzing_n_runs); |
| 277 | break :speed n_runs / (ns_elapsed / std.time.ns_per_s); |
| 278 | }; |
| 279 | |
| 280 | const html = try std.fmt.allocPrint(gpa, |
| 281 | \\<span slot="stat-total-runs">{d}</span> |
| 282 | \\<span slot="stat-unique-runs">{d} ({d:.1}%)</span> |
| 283 | \\<span slot="stat-coverage">{d} / {d} ({d:.1}%)</span> |
| 284 | \\<span slot="stat-speed">{d:.0}</span> |
| 285 | , .{ |
| 286 | hdr.n_runs, |
| 287 | hdr.unique_runs, |
| 288 | @as(f64, @floatFromInt(hdr.unique_runs)) / @as(f64, @floatFromInt(hdr.n_runs)) * 100, |
| 289 | covered_src_locs, |
| 290 | total_src_locs, |
| 291 | @as(f64, @floatFromInt(covered_src_locs)) / @as(f64, @floatFromInt(total_src_locs)) * 100, |
| 292 | avg_speed, |
| 293 | }); |
| 294 | defer gpa.free(html); |
| 295 | |
| 296 | js.updateStats(html.ptr, html.len); |
| 297 | } |
| 298 | |
| 299 | fn updateEntryPoints() error{OutOfMemory}!void { |
| 300 | var html: std.ArrayList(u8) = .empty; |
| 301 | defer html.deinit(gpa); |
| 302 | for (entry_points.items) |sli| { |
| 303 | try html.appendSlice(gpa, "<li>"); |
| 304 | try sli.sourceLocationLinkHtml(&html, selected_source_location == sli); |
| 305 | try html.appendSlice(gpa, "</li>\n"); |
| 306 | } |
| 307 | js.updateEntryPoints(html.items.ptr, html.items.len); |
| 308 | } |
| 309 | |
| 310 | fn updateCoverage() error{OutOfMemory}!void { |
| 311 | if (recent_coverage_update.items.len == 0) return; |
| 312 | const want_file = (selected_source_location orelse return).ptr().file; |
| 313 | |
| 314 | var covered: std.ArrayList(SourceLocationIndex) = .empty; |
| 315 | defer covered.deinit(gpa); |
| 316 | |
| 317 | // This code assumes 64-bit elements, which is incorrect if the executable |
| 318 | // being fuzzed is not a 64-bit CPU. It also assumes little-endian which |
| 319 | // can also be incorrect. |
| 320 | comptime assert(abi.fuzz.CoverageUpdateHeader.trailing[0] == .pc_bits_usize); |
| 321 | const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64); |
| 322 | const covered_bits = std.mem.bytesAsSlice( |
| 323 | u64, |
| 324 | recent_coverage_update.items[@sizeOf(abi.fuzz.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)], |
| 325 | ); |
| 326 | var sli: SourceLocationIndex = @fromBackingInt(@intCast(0)); |
| 327 | for (covered_bits) |elem| { |
| 328 | try covered.ensureUnusedCapacity(gpa, 64); |
| 329 | for (0..@bitSizeOf(u64)) |i| { |
| 330 | if ((elem & (@as(u64, 1) << @intCast(i))) != 0) { |
| 331 | if (sli.ptr().file == want_file) { |
| 332 | covered.appendAssumeCapacity(sli); |
| 333 | } |
| 334 | } |
| 335 | sli = @fromBackingInt(@intCast(@backingInt(sli) + 1)); |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | js.updateCoverage(covered.items.ptr, covered.items.len); |
| 340 | } |
| 341 | |
| 342 | fn updateSource() error{OutOfMemory}!void { |
| 343 | if (recent_coverage_update.items.len == 0) return; |
| 344 | const file_sli = selected_source_location.?; |
| 345 | var html: std.ArrayList(u8) = .empty; |
| 346 | defer html.deinit(gpa); |
| 347 | file_sli.fileHtml(&html) catch |err| switch (err) { |
| 348 | error.OutOfMemory => |e| return e, |
| 349 | error.SourceUnavailable => {}, |
| 350 | }; |
| 351 | js.updateSource(html.items.ptr, html.items.len); |
| 352 | } |
| 353 | |
| 354 | var selected_source_location: ?SourceLocationIndex = null; |
| 355 | |
| 356 | /// This function is not used directly by `main.js`, but a reference to it is |
| 357 | /// emitted by `SourceLocationIndex.sourceLocationLinkHtml`. |
| 358 | export fn fuzzSelectSli(sli: SourceLocationIndex) void { |
| 359 | if (!sli.haveCoverage()) return; |
| 360 | selected_source_location = sli; |
| 361 | updateEntryPoints() catch @panic("out of memory"); // highlights the selected one green |
| 362 | updateSource() catch @panic("out of memory"); |
| 363 | updateCoverage() catch @panic("out of memory"); |
| 364 | } |
| 365 | |
| 366 | const std = @import("std"); |
| 367 | const Allocator = std.mem.Allocator; |
| 368 | const Coverage = std.debug.Coverage; |
| 369 | const abi = std.Build.abi; |
| 370 | const assert = std.debug.assert; |
| 371 | const gpa = std.heap.wasm_allocator; |
| 372 | |
| 373 | const Walk = @import("Walk"); |
| 374 | const html_render = @import("html_render"); |
| 375 | |
| 376 | const nsSince = @import("main.zig").nsSince; |
| 377 | const Slice = @import("main.zig").Slice; |
| 378 | const fatal = @import("main.zig").fatal; |
| 379 | const log = std.log; |
| 380 | const String = Slice(u8); |