1// Server timestamp.
2var start_fuzzing_timestamp: i64 = undefined;
3var start_fuzzing_n_runs: u64 = undefined;
4
5const 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
15pub 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
45var coverage = Coverage.init;
46/// Index of type `SourceLocationIndex`.
47var coverage_source_locations: std.ArrayList(Coverage.SourceLocation) = .empty;
48/// Contains the most recent coverage update message, unmodified.
49var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, .of(u64)) = .empty;
50
51fn 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
74pub 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
81var entry_points: std.ArrayList(SourceLocationIndex) = .empty;
82
83pub 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`.
93const 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
155fn 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
209export 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
218fn 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
257fn 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
299fn 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
310fn 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
342fn 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
354var 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`.
358export 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
366const std = @import("std");
367const Allocator = std.mem.Allocator;
368const Coverage = std.debug.Coverage;
369const abi = std.Build.abi;
370const assert = std.debug.assert;
371const gpa = std.heap.wasm_allocator;
372
373const Walk = @import("Walk");
374const html_render = @import("html_render");
375
376const nsSince = @import("main.zig").nsSince;
377const Slice = @import("main.zig").Slice;
378const fatal = @import("main.zig").fatal;
379const log = std.log;
380const String = Slice(u8);