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