aboutsummaryrefslogtreecommitdiff
path: root/lib/fuzzer
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2024-08-04 15:27:13 -0700
committerAndrew Kelley <andrew@ziglang.org>2024-08-07 00:48:32 -0700
commitdec7e45f7c7e61a3778767bbc7f8e1e9a33b01fa (patch)
tree8039451139cd95214974c5056538aca823f526d2 /lib/fuzzer
parent22925636f7afc0f334f1d44257c007a1d2ccd63f (diff)
downloadzig-dec7e45f7c7e61a3778767bbc7f8e1e9a33b01fa.tar.gz
zig-dec7e45f7c7e61a3778767bbc7f8e1e9a33b01fa.zip
fuzzer web UI: receive coverage information
* libfuzzer: track unique runs instead of deduplicated runs - easier for consumers to notice when to recheck the covered bits. * move common definitions to `std.Build.Fuzz.abi`. build runner sends all the information needed to fuzzer web interface client needed in order to display inline coverage information along with source code.
Diffstat (limited to 'lib/fuzzer')
-rw-r--r--lib/fuzzer/index.html1
-rw-r--r--lib/fuzzer/main.js233
-rw-r--r--lib/fuzzer/wasm/main.zig83
3 files changed, 224 insertions, 93 deletions
diff --git a/lib/fuzzer/index.html b/lib/fuzzer/index.html
index dadc2f91d3..0753bcae67 100644
--- a/lib/fuzzer/index.html
+++ b/lib/fuzzer/index.html
@@ -124,6 +124,7 @@
</style>
</head>
<body>
+ <p id="status">Loading JavaScript...</p>
<div id="sectSource" class="hidden">
<h2>Source Code</h2>
<pre><code id="sourceText"></code></pre>
diff --git a/lib/fuzzer/main.js b/lib/fuzzer/main.js
index 872ac3d4b5..6fed056a5e 100644
--- a/lib/fuzzer/main.js
+++ b/lib/fuzzer/main.js
@@ -1,95 +1,148 @@
(function() {
- const domSectSource = document.getElementById("sectSource");
- const domSourceText = document.getElementById("sourceText");
-
- let wasm_promise = fetch("main.wasm");
- let sources_promise = fetch("sources.tar").then(function(response) {
- if (!response.ok) throw new Error("unable to download sources");
- return response.arrayBuffer();
- });
- var wasm_exports = null;
-
- const text_decoder = new TextDecoder();
- const text_encoder = new TextEncoder();
-
- const eventSource = new EventSource("events");
- eventSource.addEventListener('message', onMessage, false);
-
- WebAssembly.instantiateStreaming(wasm_promise, {
- js: {
- log: function(ptr, len) {
+ const domStatus = document.getElementById("status");
+ const domSectSource = document.getElementById("sectSource");
+ const domSourceText = document.getElementById("sourceText");
+
+ let wasm_promise = fetch("main.wasm");
+ let sources_promise = fetch("sources.tar").then(function(response) {
+ if (!response.ok) throw new Error("unable to download sources");
+ return response.arrayBuffer();
+ });
+ var wasm_exports = null;
+
+ const text_decoder = new TextDecoder();
+ const text_encoder = new TextEncoder();
+
+ domStatus.textContent = "Loading WebAssembly...";
+ WebAssembly.instantiateStreaming(wasm_promise, {
+ js: {
+ log: function(ptr, len) {
+ const msg = decodeString(ptr, len);
+ console.log(msg);
+ },
+ panic: function (ptr, len) {
const msg = decodeString(ptr, len);
- console.log(msg);
- },
- panic: function (ptr, len) {
- const msg = decodeString(ptr, len);
- throw new Error("panic: " + msg);
- },
+ throw new Error("panic: " + msg);
},
- }).then(function(obj) {
- wasm_exports = obj.instance.exports;
- window.wasm = obj; // for debugging
-
- sources_promise.then(function(buffer) {
- const js_array = new Uint8Array(buffer);
- const ptr = wasm_exports.alloc(js_array.length);
- const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
- wasm_array.set(js_array);
- wasm_exports.unpack(ptr, js_array.length);
-
- render();
- });
+ emitSourceIndexChange: onSourceIndexChange,
+ emitCoverageUpdate: onCoverageUpdate,
+ },
+ }).then(function(obj) {
+ wasm_exports = obj.instance.exports;
+ window.wasm = obj; // for debugging
+ domStatus.textContent = "Loading sources tarball...";
+
+ sources_promise.then(function(buffer) {
+ domStatus.textContent = "Parsing sources...";
+ const js_array = new Uint8Array(buffer);
+ const ptr = wasm_exports.alloc(js_array.length);
+ const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
+ wasm_array.set(js_array);
+ wasm_exports.unpack(ptr, js_array.length);
+
+ domStatus.textContent = "Waiting for server to send source location metadata...";
+ connectWebSocket();
});
-
- function onMessage(e) {
- console.log("Message", e.data);
- }
-
- function render() {
- domSectSource.classList.add("hidden");
-
- // TODO this is temporary debugging data
- renderSource("/home/andy/dev/zig/lib/std/zig/tokenizer.zig");
- }
-
- function renderSource(path) {
- const decl_index = findFileRoot(path);
- if (decl_index == null) throw new Error("file not found: " + path);
-
- const h2 = domSectSource.children[0];
- h2.innerText = path;
- domSourceText.innerHTML = declSourceHtml(decl_index);
-
- domSectSource.classList.remove("hidden");
- }
-
- function findFileRoot(path) {
- setInputString(path);
- const result = wasm_exports.find_file_root();
- if (result === -1) return null;
- return result;
- }
-
- function decodeString(ptr, len) {
- if (len === 0) return "";
- return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
- }
-
- function setInputString(s) {
- const jsArray = text_encoder.encode(s);
- const len = jsArray.length;
- const ptr = wasm_exports.set_input_string(len);
- const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
- wasmArray.set(jsArray);
- }
-
- function declSourceHtml(decl_index) {
- return unwrapString(wasm_exports.decl_source_html(decl_index));
- }
-
- function unwrapString(bigint) {
- const ptr = Number(bigint & 0xffffffffn);
- const len = Number(bigint >> 32n);
- return decodeString(ptr, len);
- }
+ });
+
+ function connectWebSocket() {
+ const host = window.document.location.host;
+ const pathname = window.document.location.pathname;
+ const isHttps = window.document.location.protocol === 'https:';
+ const match = host.match(/^(.+):(\d+)$/);
+ const defaultPort = isHttps ? 443 : 80;
+ const port = match ? parseInt(match[2], 10) : defaultPort;
+ const hostName = match ? match[1] : host;
+ const wsProto = isHttps ? "wss:" : "ws:";
+ const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
+ ws = new WebSocket(wsUrl);
+ ws.binaryType = "arraybuffer";
+ ws.addEventListener('message', onWebSocketMessage, false);
+ ws.addEventListener('error', timeoutThenCreateNew, false);
+ ws.addEventListener('close', timeoutThenCreateNew, false);
+ ws.addEventListener('open', onWebSocketOpen, false);
+ }
+
+ function onWebSocketOpen() {
+ console.log("web socket opened");
+ }
+
+ function onWebSocketMessage(ev) {
+ wasmOnMessage(ev.data);
+ }
+
+ function timeoutThenCreateNew() {
+ ws.removeEventListener('message', onWebSocketMessage, false);
+ ws.removeEventListener('error', timeoutThenCreateNew, false);
+ ws.removeEventListener('close', timeoutThenCreateNew, false);
+ ws.removeEventListener('open', onWebSocketOpen, false);
+ ws = null;
+ setTimeout(connectWebSocket, 1000);
+ }
+
+ function wasmOnMessage(data) {
+ const jsArray = new Uint8Array(data);
+ const ptr = wasm_exports.message_begin(jsArray.length);
+ const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
+ wasmArray.set(jsArray);
+ wasm_exports.message_end();
+ }
+
+ function onSourceIndexChange() {
+ console.log("source location index metadata updated");
+ render();
+ }
+
+ function onCoverageUpdate() {
+ console.log("coverage update");
+ }
+
+ function render() {
+ domStatus.classList.add("hidden");
+ domSectSource.classList.add("hidden");
+
+ // TODO this is temporary debugging data
+ renderSource("/home/andy/dev/zig/lib/std/zig/tokenizer.zig");
+ }
+
+ function renderSource(path) {
+ const decl_index = findFileRoot(path);
+ if (decl_index == null) throw new Error("file not found: " + path);
+
+ const h2 = domSectSource.children[0];
+ h2.innerText = path;
+ domSourceText.innerHTML = declSourceHtml(decl_index);
+
+ domSectSource.classList.remove("hidden");
+ }
+
+ function findFileRoot(path) {
+ setInputString(path);
+ const result = wasm_exports.find_file_root();
+ if (result === -1) return null;
+ return result;
+ }
+
+ function decodeString(ptr, len) {
+ if (len === 0) return "";
+ return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
+ }
+
+ function setInputString(s) {
+ const jsArray = text_encoder.encode(s);
+ const len = jsArray.length;
+ const ptr = wasm_exports.set_input_string(len);
+ const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
+ wasmArray.set(jsArray);
+ }
+
+ function declSourceHtml(decl_index) {
+ return unwrapString(wasm_exports.decl_source_html(decl_index));
+ }
+
+ function unwrapString(bigint) {
+ const ptr = Number(bigint & 0xffffffffn);
+ const len = Number(bigint >> 32n);
+ return decodeString(ptr, len);
+ }
})();
diff --git a/lib/fuzzer/wasm/main.zig b/lib/fuzzer/wasm/main.zig
index 5045f784cc..fd785d5723 100644
--- a/lib/fuzzer/wasm/main.zig
+++ b/lib/fuzzer/wasm/main.zig
@@ -1,16 +1,19 @@
const std = @import("std");
const assert = std.debug.assert;
+const abi = std.Build.Fuzz.abi;
+const gpa = std.heap.wasm_allocator;
+const log = std.log;
+const Coverage = std.debug.Coverage;
const Walk = @import("Walk");
const Decl = Walk.Decl;
const html_render = @import("html_render");
-const gpa = std.heap.wasm_allocator;
-const log = std.log;
-
const js = struct {
extern "js" fn log(ptr: [*]const u8, len: usize) void;
extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
+ extern "js" fn emitSourceIndexChange() void;
+ extern "js" fn emitCoverageUpdate() void;
};
pub const std_options: std.Options = .{
@@ -45,6 +48,26 @@ export fn alloc(n: usize) [*]u8 {
return slice.ptr;
}
+var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
+
+/// Resizes the message buffer to be the correct length; returns the pointer to
+/// the query string.
+export fn message_begin(len: usize) [*]u8 {
+ message_buffer.resize(gpa, len) catch @panic("OOM");
+ return message_buffer.items.ptr;
+}
+
+export fn message_end() void {
+ const msg_bytes = message_buffer.items;
+
+ const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
+ switch (tag) {
+ .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
+ .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
+ _ => unreachable,
+ }
+}
+
export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
const tar_bytes = tar_ptr[0..tar_len];
log.debug("received {d} bytes of tar file", .{tar_bytes.len});
@@ -141,3 +164,57 @@ fn fatal(comptime format: []const u8, args: anytype) noreturn {
};
js.panic(line.ptr, line.len);
}
+
+fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
+ const Header = abi.SourceIndexHeader;
+ const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
+
+ const directories_start = @sizeOf(Header);
+ const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
+ const files_start = directories_end;
+ const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
+ const source_locations_start = files_end;
+ const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
+ const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
+
+ const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end]));
+ const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
+ const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
+
+ try updateCoverage(directories, files, source_locations, string_bytes);
+ js.emitSourceIndexChange();
+}
+
+fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
+ recent_coverage_update.clearRetainingCapacity();
+ recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
+ js.emitCoverageUpdate();
+}
+
+var coverage = Coverage.init;
+var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
+/// Contains the most recent coverage update message, unmodified.
+var recent_coverage_update: std.ArrayListUnmanaged(u8) = .{};
+
+fn updateCoverage(
+ directories: []const Coverage.String,
+ files: []const Coverage.File,
+ source_locations: []const Coverage.SourceLocation,
+ string_bytes: []const u8,
+) !void {
+ coverage.directories.clearRetainingCapacity();
+ coverage.files.clearRetainingCapacity();
+ coverage.string_bytes.clearRetainingCapacity();
+ coverage_source_locations.clearRetainingCapacity();
+
+ try coverage_source_locations.appendSlice(gpa, source_locations);
+ try coverage.string_bytes.appendSlice(gpa, string_bytes);
+
+ try coverage.files.entries.resize(gpa, files.len);
+ @memcpy(coverage.files.entries.items(.key), files);
+ try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
+
+ try coverage.directories.entries.resize(gpa, directories.len);
+ @memcpy(coverage.directories.entries.items(.key), directories);
+ try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
+}