Commit 3d48602c99

Andrew Kelley <andrew@ziglang.org>
2024-08-06 00:32:19
fuzzer web UI: annotated PCs in source view
1 parent ef4c219
Changed files (4)
lib/docs/wasm/html_render.zig
@@ -16,6 +16,16 @@ pub const RenderSourceOptions = struct {
     skip_comments: bool = false,
     collapse_whitespace: bool = false,
     fn_link: Decl.Index = .none,
+    /// Assumed to be sorted ascending.
+    source_location_annotations: []const Annotation = &.{},
+    /// Concatenated with dom_id.
+    annotation_prefix: []const u8 = "l",
+};
+
+pub const Annotation = struct {
+    file_byte_offset: u32,
+    /// Concatenated with annotation_prefix.
+    dom_id: u32,
 };
 
 pub fn fileSourceHtml(
@@ -51,6 +61,8 @@ pub fn fileSourceHtml(
         }
     }
 
+    var next_annotate_index: usize = 0;
+
     for (
         token_tags[start_token..end_token],
         token_starts[start_token..end_token],
@@ -74,6 +86,18 @@ pub fn fileSourceHtml(
         if (tag == .eof) break;
         const slice = ast.tokenSlice(token_index);
         cursor = start + slice.len;
+
+        // Insert annotations.
+        while (true) {
+            if (next_annotate_index >= options.source_location_annotations.len) break;
+            const next_annotation = options.source_location_annotations[next_annotate_index];
+            if (cursor < next_annotation.file_byte_offset) break;
+            try out.writer(gpa).print("<span id=\"{s}{d}\"></span>", .{
+                options.annotation_prefix, next_annotation.dom_id,
+            });
+            next_annotate_index += 1;
+        }
+
         switch (tag) {
             .eof => unreachable,
 
lib/fuzzer/wasm/main.zig
@@ -280,12 +280,71 @@ const SourceLocationIndex = enum(u32) {
     ) error{ OutOfMemory, SourceUnavailable }!void {
         const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
         const root_node = walk_file_index.findRootDecl().get().ast_node;
-        html_render.fileSourceHtml(walk_file_index, out, root_node, .{}) catch |err| {
+        var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
+        defer annotations.deinit(gpa);
+        try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
+        html_render.fileSourceHtml(walk_file_index, out, root_node, .{
+            .source_location_annotations = annotations.items,
+        }) catch |err| {
             fatal("unable to render source: {s}", .{@errorName(err)});
         };
     }
 };
 
+fn computeSourceAnnotations(
+    cov_file_index: Coverage.File.Index,
+    walk_file_index: Walk.File.Index,
+    annotations: *std.ArrayListUnmanaged(html_render.Annotation),
+    source_locations: []const Coverage.SourceLocation,
+) !void {
+    // Collect all the source locations from only this file into this array
+    // first, then sort by line, col, so that we can collect annotations with
+    // O(N) time complexity.
+    var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
+    defer locs.deinit(gpa);
+
+    for (source_locations, 0..) |sl, sli_usize| {
+        if (sl.file != cov_file_index) continue;
+        const sli: SourceLocationIndex = @enumFromInt(sli_usize);
+        try locs.append(gpa, sli);
+    }
+
+    std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
+        pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
+            _ = context;
+            const lhs_ptr = lhs.ptr();
+            const rhs_ptr = rhs.ptr();
+            if (lhs_ptr.line < rhs_ptr.line) return true;
+            if (lhs_ptr.line > rhs_ptr.line) return false;
+            return lhs_ptr.column < rhs_ptr.column;
+        }
+    }.lessThan);
+
+    const source = walk_file_index.get_ast().source;
+    var line: usize = 1;
+    var column: usize = 1;
+    var next_loc_index: usize = 0;
+    for (source, 0..) |byte, offset| {
+        if (byte == '\n') {
+            line += 1;
+            column = 1;
+        } else {
+            column += 1;
+        }
+        while (true) {
+            if (next_loc_index >= locs.items.len) return;
+            const next_sli = locs.items[next_loc_index];
+            const next_sl = next_sli.ptr();
+            if (next_sl.line > line or (next_sl.line == line and next_sl.column > column)) break;
+            try annotations.append(gpa, .{
+                .file_byte_offset = offset,
+                .dom_id = @intFromEnum(next_sli),
+            });
+            next_loc_index += 1;
+        }
+    }
+}
+
 var coverage = Coverage.init;
 /// Index of type `SourceLocationIndex`.
 var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
lib/fuzzer/index.html
@@ -52,6 +52,14 @@
         cursor: default;
       }
 
+      .l {
+          display: inline-block;
+          background: white;
+          width: 1em;
+          height: 1em;
+          border-radius: 1em;
+      }
+
       .tok-kw {
           color: #333;
           font-weight: bold;
lib/fuzzer/main.js
@@ -33,7 +33,7 @@
           throw new Error("panic: " + msg);
       },
       emitSourceIndexChange: onSourceIndexChange,
-      emitCoverageUpdate: renderStats,
+      emitCoverageUpdate: onCoverageUpdate,
       emitEntryPointsUpdate: renderStats,
     },
   }).then(function(obj) {
@@ -112,7 +112,7 @@
   }
 
   function onWebSocketOpen() {
-    console.log("web socket opened");
+    //console.log("web socket opened");
   }
 
   function onWebSocketMessage(ev) {
@@ -141,6 +141,11 @@
     if (curNavLocation != null) renderSource(curNavLocation);
   }
 
+  function onCoverageUpdate() {
+    renderStats();
+    renderCoverage();
+  }
+
   function render() {
     domStatus.classList.add("hidden");
   }
@@ -166,6 +171,15 @@
     domSectStats.classList.remove("hidden");
   }
 
+  function renderCoverage() {
+    for (let i = 0; i < domSourceText.children.length; i += 1) {
+      const childDom = domSourceText.children[i];
+      if (childDom.id != null && childDom.id[0] == "l") {
+        childDom.classList.add("l");
+      }
+    }
+  }
+
   function resizeDomList(listDom, desiredLen, templateHtml) {
     for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
         listDom.insertAdjacentHTML('beforeend', templateHtml);
@@ -190,12 +204,10 @@
     domSectSource.classList.remove("hidden");
 
     const slDom = document.getElementById("l" + sourceLocationIndex);
-    if (slDom != null) {
-      slDom.scrollIntoView({
-        behavior: "smooth",
-        block: "center",
-      });
-    }
+    slDom.scrollIntoView({
+      behavior: "smooth",
+      block: "center",
+    });
   }
 
   function decodeString(ptr, len) {