From a7a5bfe19abc50dddbf1a09fce04fb2f6d35f19c Mon Sep 17 00:00:00 2001 From: "L. Pereira" Date: Thu, 16 May 2024 16:09:47 -0700 Subject: [PATCH 01/22] Auto-format explorer index.js --- crates/explorer/src/index.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/explorer/src/index.js b/crates/explorer/src/index.js index 629d73ff415f..ccf4e6a23fab 100644 --- a/crates/explorer/src/index.js +++ b/crates/explorer/src/index.js @@ -32,7 +32,7 @@ const offsetToHue = new Map(); // Get the hue for the given offset, or assign it a new one if it doesn't have // one already. -const hueForOffset = offset => { +const hueForOffset = (offset) => { if (offsetToHue.has(offset)) { return offsetToHue.get(offset); } else { @@ -44,7 +44,7 @@ const hueForOffset = offset => { // Get the hue for the given offset, only if the offset has already been // assigned a hue. -const existingHueForOffset = offset => { +const existingHueForOffset = (offset) => { return offsetToHue.get(offset); }; @@ -86,7 +86,7 @@ const addAsmElem = (offset, elem) => { const watElem = document.getElementById("wat"); watElem.addEventListener( "click", - event => { + (event) => { if (event.target.dataset.wasmOffset == null) { return; } @@ -109,7 +109,7 @@ watElem.addEventListener( const asmElem = document.getElementById("asm"); asmElem.addEventListener( "click", - event => { + (event) => { if (event.target.dataset.wasmOffset == null) { return; } @@ -129,7 +129,7 @@ asmElem.addEventListener( { passive: true }, ); -const onMouseEnter = event => { +const onMouseEnter = (event) => { if (event.target.dataset.wasmOffset == null) { return; } @@ -141,7 +141,7 @@ const onMouseEnter = event => { } }; -const onMouseLeave = event => { +const onMouseLeave = (event) => { if (event.target.dataset.wasmOffset == null) { return; } @@ -159,12 +159,12 @@ const repeat = (s, n) => { return s.repeat(n >= 0 ? n : 0); }; -const renderAddress = addr => { +const renderAddress = (addr) => { let hex = addr.toString(16); return repeat("0", 8 - hex.length) + hex; }; -const renderBytes = bytes => { +const renderBytes = (bytes) => { let s = ""; for (let i = 0; i < bytes.length; i++) { if (i != 0) { From 7bdb73f769663542fff965156c1607e2c3096d69 Mon Sep 17 00:00:00 2001 From: "L. Pereira" Date: Thu, 16 May 2024 17:27:11 -0700 Subject: [PATCH 02/22] Use CRC24 to pick colors for offsets in explore --- crates/explorer/src/index.js | 74 +++++++++++++++--------------------- 1 file changed, 30 insertions(+), 44 deletions(-) diff --git a/crates/explorer/src/index.js b/crates/explorer/src/index.js index ccf4e6a23fab..b5df132f01cd 100644 --- a/crates/explorer/src/index.js +++ b/crates/explorer/src/index.js @@ -11,42 +11,32 @@ class State { const state = (window.STATE = new State(window.WAT, window.ASM)); -/*** Hues for Offsets **********************************************************/ +/*** Colors for Offsets **********************************************************/ -const hues = [ - 80, 160, 240, 320, 40, 120, 200, 280, 20, 100, 180, 260, 340, 60, 140, 220, - 300, -]; +const offsetToRgb = new Map(); -const nextHue = (function () { - let i = 0; - return () => { - return hues[++i % hues.length]; - }; -})(); - -// NB: don't just assign hues based on something simple like `hues[offset % -// hues.length]` since that can suffer from bias due to certain alignments -// happening more or less frequently. -const offsetToHue = new Map(); - -// Get the hue for the given offset, or assign it a new one if it doesn't have -// one already. -const hueForOffset = (offset) => { - if (offsetToHue.has(offset)) { - return offsetToHue.get(offset); - } else { - let hue = nextHue(); - offsetToHue.set(offset, hue); - return hue; +// Get the RGB color for the given offset. (Memoize to avoid recalculating.) +const rgbForOffset = (offset) => { + if (offsetToRgb.has(offset)) { + return offsetToRgb.get(offset); } + const crc24 = (crc, byte) => { + crc ^= byte << 16; + for (let bit = 0; bit < 8; bit++) { + crc = (crc & 0x800000 ? (crc << 1) ^ 0xfa5711 : crc << 1) & 0xffffff; + } + return crc; + }; + let color; + for (color = offset; offset; offset >>= 8) + color = crc24(color, offset & 0xff); + color = `${(color >> 16) & 0xff}, ${(color >> 8) & 0xff}, ${color & 0xff}`; + offsetToRgb.set(offset, color); + return color; }; -// Get the hue for the given offset, only if the offset has already been -// assigned a hue. -const existingHueForOffset = (offset) => { - return offsetToHue.get(offset); -}; +const dimColorForOffset = (offset) => `rgba(${rgbForOffset(offset)}, 0.3)`; +const brightColorForOffset = (offset) => `rgba(${rgbForOffset(offset)}, 0.7)`; // Get WAT chunk elements by Wasm offset. const watByOffset = new Map(); @@ -135,9 +125,9 @@ const onMouseEnter = (event) => { } const offset = parseInt(event.target.dataset.wasmOffset); - const hue = hueForOffset(offset); + const color = brightColorForOffset(offset); for (const elem of anyByOffset.get(offset)) { - elem.style.backgroundColor = `hsl(${hue} 75% 80%)`; + elem.style.backgroundColor = color; } }; @@ -147,9 +137,9 @@ const onMouseLeave = (event) => { } const offset = parseInt(event.target.dataset.wasmOffset); - const hue = hueForOffset(offset); + const color = dimColorForOffset(offset); for (const elem of anyByOffset.get(offset)) { - elem.style.backgroundColor = `hsl(${hue} 50% 95%)`; + elem.style.backgroundColor = color; } }; @@ -204,8 +194,7 @@ for (const func of state.asm.functions) { instElem.textContent = `${renderAddress(inst.address)} ${renderBytes(inst.bytes)} ${renderInst(inst.mnemonic, inst.operands)}\n`; if (inst.wasm_offset != null) { instElem.setAttribute("data-wasm-offset", inst.wasm_offset); - const hue = hueForOffset(inst.wasm_offset); - instElem.style.backgroundColor = `hsl(${hue} 50% 90%)`; + instElem.style.backgroundColor = dimColorForOffset(inst.wasm_offset); instElem.addEventListener("mouseenter", onMouseEnter); instElem.addEventListener("mouseleave", onMouseLeave); addAsmElem(inst.wasm_offset, instElem); @@ -223,13 +212,10 @@ for (const chunk of state.wat.chunks) { const chunkElem = document.createElement("span"); if (chunk.wasm_offset != null) { chunkElem.dataset.wasmOffset = chunk.wasm_offset; - const hue = existingHueForOffset(chunk.wasm_offset); - if (hue) { - chunkElem.style.backgroundColor = `hsl(${hue} 50% 95%)`; - chunkElem.addEventListener("mouseenter", onMouseEnter); - chunkElem.addEventListener("mouseleave", onMouseLeave); - addWatElem(chunk.wasm_offset, chunkElem); - } + chunkElem.style.backgroundColor = dimColorForOffset(chunk.wasm_offset); + chunkElem.addEventListener("mouseenter", onMouseEnter); + chunkElem.addEventListener("mouseleave", onMouseLeave); + addWatElem(chunk.wasm_offset, chunkElem); } chunkElem.textContent = chunk.wat; watElem.appendChild(chunkElem); From 503f0549d4a0630c4af9e286fbcab64c62314b09 Mon Sep 17 00:00:00 2001 From: "L. Pereira" Date: Thu, 16 May 2024 21:29:57 -0700 Subject: [PATCH 03/22] Make better use of the DOM In the ASM view, instead of creating one element per instruction, create one per WASM chunk. This significantly reduces the amount of elements we have to take care of, including event listeners. For both views, get rid of all the maps to look up DOM elements by WASM offset and use CSS selectors to find them. This made things quite a bit smoother. To highlight items, we now add a class to elements with the same WASM offset, and remove it when we don't want to highlight them anymore. In addition to this, use a bit more tricks to get brighter colors from the CRC24 algorithm we're now using to pick the colors. (Since the colors can now be very dark, we get the luminance by using the NTSC color space, or YIQ, and use contrasting colors.) --- crates/explorer/src/index.css | 9 ++ crates/explorer/src/index.js | 226 ++++++++++++++-------------------- 2 files changed, 100 insertions(+), 135 deletions(-) diff --git a/crates/explorer/src/index.css b/crates/explorer/src/index.css index bf52ac021851..8ee24094b8ba 100644 --- a/crates/explorer/src/index.css +++ b/crates/explorer/src/index.css @@ -14,6 +14,15 @@ body { height: 100%; } +.highlight { + white-space: pre; + font-family: monospace; + opacity: 0.75; +} +.hovered { + opacity: 1; +} + #wat { width: 50%; height: 100%; diff --git a/crates/explorer/src/index.js b/crates/explorer/src/index.js index b5df132f01cd..1508f3ac1b92 100644 --- a/crates/explorer/src/index.js +++ b/crates/explorer/src/index.js @@ -16,10 +16,24 @@ const state = (window.STATE = new State(window.WAT, window.ASM)); const offsetToRgb = new Map(); // Get the RGB color for the given offset. (Memoize to avoid recalculating.) + +const rgbToTriple = (rgb) => [ + (rgb >> 16) & 0xff, + (rgb >> 8) & 0xff, + rgb & 0xff, +]; +const rgbToLuminance = (rgb) => { + // Use the NTSC color space (https://en.wikipedia.org/wiki/YIQ) to determine + // the luminance of this color. + let [r, g, b] = rgbToTriple(rgb); + return (r * 299.0 + g * 587.0 + b * 114.0) / 1000.0; +}; +const rgbToCss = (rgb) => `rgb(${rgbToTriple(rgb).join(",")})`; + const rgbForOffset = (offset) => { - if (offsetToRgb.has(offset)) { - return offsetToRgb.get(offset); - } + let color = offsetToRgb[offset]; + if (color !== undefined) return color; + const crc24 = (crc, byte) => { crc ^= byte << 16; for (let bit = 0; bit < 8; bit++) { @@ -27,120 +41,19 @@ const rgbForOffset = (offset) => { } return crc; }; - let color; + let orig_offset = offset; for (color = offset; offset; offset >>= 8) color = crc24(color, offset & 0xff); - color = `${(color >> 16) & 0xff}, ${(color >> 8) & 0xff}, ${color & 0xff}`; - offsetToRgb.set(offset, color); + color = rgbToLuminance(color) > 127 ? color ^ 0xa5a5a5 : color; + offsetToRgb[orig_offset] = color; return color; }; -const dimColorForOffset = (offset) => `rgba(${rgbForOffset(offset)}, 0.3)`; -const brightColorForOffset = (offset) => `rgba(${rgbForOffset(offset)}, 0.7)`; - -// Get WAT chunk elements by Wasm offset. -const watByOffset = new Map(); - -// Get asm instruction elements by Wasm offset. -const asmByOffset = new Map(); - -// Get all (WAT chunk or asm instruction) elements by offset. -const anyByOffset = new Map(); - -const addWatElem = (offset, elem) => { - if (!watByOffset.has(offset)) { - watByOffset.set(offset, []); - } - watByOffset.get(offset).push(elem); - - if (!anyByOffset.has(offset)) { - anyByOffset.set(offset, []); - } - anyByOffset.get(offset).push(elem); -}; - -const addAsmElem = (offset, elem) => { - if (!asmByOffset.has(offset)) { - asmByOffset.set(offset, []); - } - asmByOffset.get(offset).push(elem); - - if (!anyByOffset.has(offset)) { - anyByOffset.set(offset, []); - } - anyByOffset.get(offset).push(elem); -}; - -/*** Event Handlers ************************************************************/ - -const watElem = document.getElementById("wat"); -watElem.addEventListener( - "click", - (event) => { - if (event.target.dataset.wasmOffset == null) { - return; - } - - const offset = parseInt(event.target.dataset.wasmOffset); - if (!asmByOffset.get(offset)) { - return; - } - - const firstAsmElem = asmByOffset.get(offset)[0]; - firstAsmElem.scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "nearest", - }); - }, - { passive: true }, -); - -const asmElem = document.getElementById("asm"); -asmElem.addEventListener( - "click", - (event) => { - if (event.target.dataset.wasmOffset == null) { - return; - } - - const offset = parseInt(event.target.dataset.wasmOffset); - if (!watByOffset.get(offset)) { - return; - } - - const firstWatElem = watByOffset.get(offset)[0]; - firstWatElem.scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "nearest", - }); - }, - { passive: true }, -); - -const onMouseEnter = (event) => { - if (event.target.dataset.wasmOffset == null) { - return; - } - - const offset = parseInt(event.target.dataset.wasmOffset); - const color = brightColorForOffset(offset); - for (const elem of anyByOffset.get(offset)) { - elem.style.backgroundColor = color; - } -}; - -const onMouseLeave = (event) => { - if (event.target.dataset.wasmOffset == null) { - return; - } - - const offset = parseInt(event.target.dataset.wasmOffset); - const color = dimColorForOffset(offset); - for (const elem of anyByOffset.get(offset)) { - elem.style.backgroundColor = color; - } +const adjustColorForOffset = (element, offset) => { + let backgroundColor = rgbForOffset(offset); + element.style.backgroundColor = rgbToCss(backgroundColor); + element.style.color = + rgbToLuminance(backgroundColor) > 128 ? "#101010" : "#dddddd"; }; /*** Rendering *****************************************************************/ @@ -174,8 +87,41 @@ const renderInst = (mnemonic, operands) => { } }; -// Render the ASM. +const linkElements = (element) => { + const eachElementWithSameWasmOff = (event, closure) => { + let offset = event.target.dataset.wasmOffset; + if (offset !== null) { + let elems = document.querySelectorAll(`[data-wasm-offset="${offset}"]`); + for (const elem of elems) closure(elem); + } + }; + element.addEventListener("click", (event) => { + eachElementWithSameWasmOff(event, (elem) => + elem.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest", + }), + ); + }); + element.addEventListener("mouseenter", (event) => + eachElementWithSameWasmOff(event, (elem) => elem.classList.add("hovered")), + ); + element.addEventListener("mouseleave", (event) => + eachElementWithSameWasmOff(event, (elem) => + elem.classList.remove("hovered"), + ), + ); +}; +const createDivForCode = () => { + let div = document.createElement("div"); + div.classList.add("highlight"); + return div; +}; + +// Render the ASM. +let lastOffset = null; for (const func of state.asm.functions) { const funcElem = document.createElement("div"); @@ -188,35 +134,45 @@ for (const func of state.asm.functions) { funcHeader.title = `Function ${func.func_index}: ${func_name}`; funcElem.appendChild(funcHeader); - const bodyElem = document.createElement("pre"); + let currentBlock = createDivForCode(); + let disasmBuffer = []; + + const addCurrentBlock = (offset) => { + currentBlock.setAttribute("data-wasm-offset", offset); + if (offset !== null) adjustColorForOffset(currentBlock, offset); + currentBlock.innerText = disasmBuffer.join("\n"); + linkElements(currentBlock); + funcElem.appendChild(currentBlock); + disasmBuffer = []; + }; + for (const inst of func.instructions) { - const instElem = document.createElement("span"); - instElem.textContent = `${renderAddress(inst.address)} ${renderBytes(inst.bytes)} ${renderInst(inst.mnemonic, inst.operands)}\n`; - if (inst.wasm_offset != null) { - instElem.setAttribute("data-wasm-offset", inst.wasm_offset); - instElem.style.backgroundColor = dimColorForOffset(inst.wasm_offset); - instElem.addEventListener("mouseenter", onMouseEnter); - instElem.addEventListener("mouseleave", onMouseLeave); - addAsmElem(inst.wasm_offset, instElem); + if (lastOffset !== inst.wasm_offset) { + addCurrentBlock(inst.wasm_offset); + currentBlock = createDivForCode(); + lastOffset = inst.wasm_offset; } - bodyElem.appendChild(instElem); + + disasmBuffer.push( + `${renderAddress(inst.address)} ${renderBytes(inst.bytes)} ${renderInst(inst.mnemonic, inst.operands)}`, + ); } - funcElem.appendChild(bodyElem); + addCurrentBlock(lastOffset); - asmElem.appendChild(funcElem); + document.getElementById("asm").appendChild(funcElem); } // Render the WAT. - for (const chunk of state.wat.chunks) { - const chunkElem = document.createElement("span"); - if (chunk.wasm_offset != null) { - chunkElem.dataset.wasmOffset = chunk.wasm_offset; - chunkElem.style.backgroundColor = dimColorForOffset(chunk.wasm_offset); - chunkElem.addEventListener("mouseenter", onMouseEnter); - chunkElem.addEventListener("mouseleave", onMouseLeave); - addWatElem(chunk.wasm_offset, chunkElem); + if (chunk.wasm_offset === null) continue; + const block = createDivForCode(); + block.dataset.wasmOffset = chunk.wasm_offset; + block.innerText = chunk.wat; + + if (offsetToRgb[chunk.wasm_offset] !== undefined) { + adjustColorForOffset(block, chunk.wasm_offset); + linkElements(block); } - chunkElem.textContent = chunk.wat; - watElem.appendChild(chunkElem); + + document.getElementById("wat").appendChild(block); } From 83029d2016efdbbf7db4e24363af33e1e9c2017e Mon Sep 17 00:00:00 2001 From: "L. Pereira" Date: Fri, 17 May 2024 00:11:25 -0700 Subject: [PATCH 04/22] Draw polygon bridging WAT and ASM views --- crates/explorer/src/index.css | 20 ++++++++- crates/explorer/src/index.js | 80 +++++++++++++++++++++-------------- crates/explorer/src/lib.rs | 1 + 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/crates/explorer/src/index.css b/crates/explorer/src/index.css index 8ee24094b8ba..1e6d0d7c8eac 100644 --- a/crates/explorer/src/index.css +++ b/crates/explorer/src/index.css @@ -17,10 +17,26 @@ body { .highlight { white-space: pre; font-family: monospace; - opacity: 0.75; } .hovered { - opacity: 1; +} +.light-text { + color: #dddddd; +} +.dark-text { + color: #101010; +} + +#bridge { + position: absolute; + z-index: 100; + top: 0px; + bottom: 0px; + height: 100%; + width: 16px; + background-color: red; + clip-path: polygon(0px 0px, 0px 10px, 20px 100px, 100px 100px); + display: none; } #wat { diff --git a/crates/explorer/src/index.js b/crates/explorer/src/index.js index 1508f3ac1b92..6eb2b469fba9 100644 --- a/crates/explorer/src/index.js +++ b/crates/explorer/src/index.js @@ -13,27 +13,22 @@ const state = (window.STATE = new State(window.WAT, window.ASM)); /*** Colors for Offsets **********************************************************/ -const offsetToRgb = new Map(); - -// Get the RGB color for the given offset. (Memoize to avoid recalculating.) - -const rgbToTriple = (rgb) => [ - (rgb >> 16) & 0xff, - (rgb >> 8) & 0xff, - rgb & 0xff, -]; -const rgbToLuminance = (rgb) => { +const rgbToLuma = (rgb) => { // Use the NTSC color space (https://en.wikipedia.org/wiki/YIQ) to determine // the luminance of this color. let [r, g, b] = rgbToTriple(rgb); return (r * 299.0 + g * 587.0 + b * 114.0) / 1000.0; }; -const rgbToCss = (rgb) => `rgb(${rgbToTriple(rgb).join(",")})`; - +const rgbToTriple = (rgb) => [ + (rgb >> 16) & 0xff, + (rgb >> 8) & 0xff, + rgb & 0xff, +]; +// Get the RGB color for the given offset. (Memoize to avoid recalculating.) +const offsetToRgb = new Map(); const rgbForOffset = (offset) => { let color = offsetToRgb[offset]; if (color !== undefined) return color; - const crc24 = (crc, byte) => { crc ^= byte << 16; for (let bit = 0; bit < 8; bit++) { @@ -44,16 +39,17 @@ const rgbForOffset = (offset) => { let orig_offset = offset; for (color = offset; offset; offset >>= 8) color = crc24(color, offset & 0xff); - color = rgbToLuminance(color) > 127 ? color ^ 0xa5a5a5 : color; + color = rgbToLuma(color) > 127 ? color ^ 0xa5a5a5 : color; offsetToRgb[orig_offset] = color; return color; }; - +const rgbToCss = (rgb) => `rgba(${rgbToTriple(rgb).join(",")})`; const adjustColorForOffset = (element, offset) => { let backgroundColor = rgbForOffset(offset); element.style.backgroundColor = rgbToCss(backgroundColor); - element.style.color = - rgbToLuminance(backgroundColor) > 128 ? "#101010" : "#dddddd"; + element.classList.add( + rgbToLuma(backgroundColor) > 128 ? "dark-text" : "light-text", + ); }; /*** Rendering *****************************************************************/ @@ -88,11 +84,12 @@ const renderInst = (mnemonic, operands) => { }; const linkElements = (element) => { + const selector = (offset) => + document.querySelectorAll(`[data-wasm-offset="${offset}"]`); const eachElementWithSameWasmOff = (event, closure) => { let offset = event.target.dataset.wasmOffset; if (offset !== null) { - let elems = document.querySelectorAll(`[data-wasm-offset="${offset}"]`); - for (const elem of elems) closure(elem); + for (const elem of selector(offset)) closure(elem); } }; element.addEventListener("click", (event) => { @@ -104,14 +101,33 @@ const linkElements = (element) => { }), ); }); - element.addEventListener("mouseenter", (event) => - eachElementWithSameWasmOff(event, (elem) => elem.classList.add("hovered")), - ); - element.addEventListener("mouseleave", (event) => + element.addEventListener("mouseenter", (event) => { + let offset = event.target.dataset.wasmOffset; + if (offset === null) return; + let elems = selector(offset); + let rect0 = elems[0].getBoundingClientRect(); + let rect1 = elems[1].getBoundingClientRect(); + if (rect0.x > rect1.x) { + [rect0, rect1] = [rect1, rect0]; + } + let bridge = document.getElementById("bridge"); + if (rect0.y < 0 || rect0.bottom < 0) { + bridge.style.display = "none"; + } else { + bridge.style.display = "block"; + bridge.style.left = `${rect0.width}px`; + bridge.style.clipPath = `polygon(0 ${rect0.y}px, 100% ${rect1.y}px, 100% ${rect1.bottom}px, 0 ${rect0.bottom}px)`; + bridge.style.backgroundColor = elems[0].style.backgroundColor; + } + elems[0].classList.add("hovered"); + elems[1].classList.add("hovered"); + }); + element.addEventListener("mouseleave", (event) => { + document.getElementById("bridge").style.display = "none"; eachElementWithSameWasmOff(event, (elem) => elem.classList.remove("hovered"), - ), - ); + ); + }); }; const createDivForCode = () => { @@ -139,23 +155,25 @@ for (const func of state.asm.functions) { const addCurrentBlock = (offset) => { currentBlock.setAttribute("data-wasm-offset", offset); - if (offset !== null) adjustColorForOffset(currentBlock, offset); + if (offset !== null) { + adjustColorForOffset(currentBlock, offset); + linkElements(currentBlock); + } + currentBlock.innerText = disasmBuffer.join("\n"); - linkElements(currentBlock); funcElem.appendChild(currentBlock); disasmBuffer = []; }; for (const inst of func.instructions) { + disasmBuffer.push( + `${renderAddress(inst.address)} ${renderBytes(inst.bytes)} ${renderInst(inst.mnemonic, inst.operands)}`, + ); if (lastOffset !== inst.wasm_offset) { addCurrentBlock(inst.wasm_offset); currentBlock = createDivForCode(); lastOffset = inst.wasm_offset; } - - disasmBuffer.push( - `${renderAddress(inst.address)} ${renderBytes(inst.bytes)} ${renderInst(inst.mnemonic, inst.operands)}`, - ); } addCurrentBlock(lastOffset); diff --git a/crates/explorer/src/lib.rs b/crates/explorer/src/lib.rs index cd9c4e3fc4ad..131700c65ff0 100644 --- a/crates/explorer/src/lib.rs +++ b/crates/explorer/src/lib.rs @@ -37,6 +37,7 @@ pub fn generate(

     
+