diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2024-08-04 15:27:13 -0700 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2024-08-07 00:48:32 -0700 |
| commit | dec7e45f7c7e61a3778767bbc7f8e1e9a33b01fa (patch) | |
| tree | 8039451139cd95214974c5056538aca823f526d2 /lib/fuzzer | |
| parent | 22925636f7afc0f334f1d44257c007a1d2ccd63f (diff) | |
| download | zig-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.html | 1 | ||||
| -rw-r--r-- | lib/fuzzer/main.js | 233 | ||||
| -rw-r--r-- | lib/fuzzer/wasm/main.zig | 83 |
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 }); +} |
