Skip to content

Commit

Permalink
Address review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
lpereira committed May 30, 2024
1 parent 1de200e commit 2aaa7b4
Showing 1 changed file with 148 additions and 78 deletions.
226 changes: 148 additions & 78 deletions crates/explorer/src/index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
/* global window, document */
/*** LRU Cache *****************************************************************/

/*** State *********************************************************************/
class Cache {
constructor(size, getFunc) {
// Maps preserve the insertion order, so we can use it to implement a naïve LRU
// cache.
this.cache = new Map();
this.cacheSize = size;
this.getFunc = getFunc;
}

class State {
constructor(wat, asm) {
this.wat = wat;
this.asm = asm;
get(key) {
let v = this.cache.get(key);
if (v !== undefined) {
// Remove the found element from the cache so it can be inserted it again
// at the end before returning.
this.cache.delete(key);
} else {
v = this.getFunc(key);
if (this.cache.size > this.cache.cacheSize) {
// Evict the oldest item from the cache.
this.cache.delete(this.cache.keys().next().value);
}
}
this.cache.set(key, v);
return v;
}
}

const state = (window.STATE = new State(window.WAT, window.ASM));

/*** Colors for Offsets **********************************************************/

const rgbToLuma = (rgb) => {
const rgbToLuma = rgb => {
// Use the NTSC color space (https://en.wikipedia.org/wiki/YIQ) to determine
// the luminance of this color. (This is an approximation using powers of two,
// to avoid multiplications and divisions. It's good enough for our purposes.)
// the luminance (Y) of this color. (This is an approximation using powers of two,
// to avoid multiplications and divisions. It's not accurate, but it's good enough
// for our purposes.)
let [r, g, b] = rgbToTriple(rgb);
return (((r << 8) + (g << 9) + (b << 7)) >> 10) + (g & 31);
};
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;

// Convert a color as a 24-bit number into a list with 3 elements: R, G, and B,
// each ranging [0, 255].
const rgbToTriple = rgb => [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff];

// Use CRC24 as a way to calculate a color for a given Wasm offset. This
// particular algorithm has been chosen because it produces bright, vibrant
// colors, that don't repeat often, and is easily implementable.
const calculateRgbForOffset = offset => {
const crc24 = (crc, byte) => {
// CRC computation adapted from Wikipedia[1] (shift-register based division versions.)
// [1] https://en.m.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks
Expand All @@ -39,23 +55,46 @@ const rgbForOffset = (offset) => {
}
return crc;
};
let orig_offset = offset;

// Feed the offset into the CRC24 algorithm, one byte at a time.
for (color = offset; offset; offset >>= 8)
color = crc24(color, offset & 0xff);
// Avoid colors that are too close to white.
color = rgbToLuma(color) > 200 ? color ^ 0xa5a5a5 : color;
offsetToRgb[orig_offset] = color;
return color;

// Avoid colors that are too close to white. Flip some bits around
// so that the color components are more pronounced.
return rgbToLuma(color) > 200 ? color ^ 0xa5a5a5 : color;
};
const rgbToCss = (rgb) => `rgba(${rgbToTriple(rgb).join(",")})`;
const rgbDarken = (rgb) => {

// Memoize all colors for a given Wasm offset. Cache isn't used here since,
// when rendering the Wat side, we use the fact that if a color has not been
// assigned during the rendering of the Native Asm side, that block of Wasm
// instructions isn't colored.
let offsetToRgb = new Map();
const rgbForOffset = offset => {
let rgb = offsetToRgb.get(offset);
if (rgb === undefined) {
rgb = calculateRgbForOffset(offset);
offsetToRgb.set(offset, rgb);
}
return rgb;
};

// Convert a color in a 24-bit number to a string suitable for CSS styling.
const rgbToCss = rgb => `rgba(${rgbToTriple(rgb).join(",")})`;

// Darkens a color in a 24-bit number slightly by subtracting at most 0x20
// from each color component; e.g. RGB(175, 161, 10) becomes RGB(143, 129, 0).
// This loses some color information, but it's good enough for our use case here.
const rgbDarken = rgb => {
let [r, g, b] = rgbToTriple(rgb);
return (
((r - Math.min(r, 0x20)) << 16) |
((g - Math.min(g, 0x20)) << 8) |
(b - Math.min(b, 0x20))
);
};

// Adjust the color styles of a DOM element for a given Wasm offset.
const adjustColorForOffset = (element, offset) => {
let backgroundColor = rgbForOffset(offset);
element.style.backgroundColor = rgbToCss(backgroundColor);
Expand All @@ -70,12 +109,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) {
Expand All @@ -95,20 +134,29 @@ const renderInst = (mnemonic, operands) => {
}
};

const linkElements = (element) => {
const selector = (offset) =>
document.querySelectorAll(`[data-wasm-offset="${offset}"]`);
// Connects callbacks to mouse hovering events so elements are properly highlighted when
// hovered, and the bridging element is drawn between the instruction lists.
const linkedElementCache = new Cache(256, offset =>
document.querySelectorAll(`[data-wasm-offset="${offset}"]`),
);
const linkElements = element => {
const eachElementWithSameWasmOff = (event, closure) => {
let offset = event.target.dataset.wasmOffset;
if (offset !== null) {
for (const elem of selector(offset)) closure(elem);
// Run the loop inside an animation frame. Since we're modifying the DOM,
// do so when the browser has some breathing room.
window.requestAnimationFrame(() => {
linkedElementCache.get(offset).forEach(closure);
});
}
};

element.addEventListener(
"click",
(event) => {
event => {
document.getElementById("bridge").style.display = "none";
eachElementWithSameWasmOff(event, (elem) => {
eachElementWithSameWasmOff(event, elem => {
if (elem === event.target) return; // Only scroll into view the other elements.
elem.scrollIntoView({
behavior: "smooth",
block: "center",
Expand All @@ -118,61 +166,83 @@ const linkElements = (element) => {
},
{ passive: true },
);
element.addEventListener("mouseenter", (event) => {

element.addEventListener("mouseenter", event => {
let offset = event.target.dataset.wasmOffset;
if (offset === null) return;

// Gather all elements related to the desired offset. Put the one in the WAT
// view first, and then all the others subsequently; this is done so we can
// calculate the polygon to bridge the WAT and the ASM views.
let elems = selector(offset);
let wat_elem, asm_elems;
let elems = linkedElementCache.get(offset);
if (elems.length < 2) return;

let watElem, asmElems;
if (elems.length == 2) {
// The most common case: only two elements matching a given Wasm offset, so
// no need to convert the NodeListOf returned by selector() to an array like
// in the general case below so we can sort by the X position.
let rect0 = elems[0].getBoundingClientRect();
let rect1 = elems[1].getBoundingClientRect();
if (rect0.x < rect1.x) {
wat_elem = elems[0];
asm_elems = [elems[1]];
watElem = elems[0];
asmElems = [elems[1]];
} else {
wat_elem = elems[1];
asm_elems = [elems[0]];
watElem = elems[1];
asmElems = [elems[0]];
}
} else if (elems.length < 2) {
return;
} else {
elems = Array.from(selector(offset).entries()).map((elem) => elem[1]);
elems.sort(
elems = Array.from(elems).sort(
(elem0, elem1) =>
elem0.getBoundingClientRect().x - elem1.getBoundingClientRect().x,
);
wat_elem = elems[0];
asm_elems = elems.slice(1);
watElem = elems[0];
asmElems = elems.slice(1);
}

// Calculate all the points that form the polygon that's drawn between
// the Wasm code and the Native Asm code. Start with a width of 16px,
// but recalculate it based on the position of the list elements as we
// iterate over them. One 4-point polygon will be constructed for each
// block of Native Asm code that correlates to one block of Wasm code.
let bridgeWidth = 16;
let wat_rect = wat_elem.getBoundingClientRect();
let points = asm_elems
.map((elem) => {
let watRect = watElem.getBoundingClientRect();
let points = asmElems
.map(elem => {
let rect = elem.getBoundingClientRect();
bridgeWidth = rect.left - wat_rect.width;
return `0 ${wat_rect.y - 2}px, 100% ${rect.y - 2}px, 100% ${rect.bottom + 2}px, 0 ${wat_rect.bottom + 2}px`;
bridgeWidth = rect.left - watRect.width;
return `0 ${watRect.y - 2}px, 100% ${rect.y - 2}px, 100% ${rect.bottom + 2}px, 0 ${watRect.bottom + 2}px`;
})
.join(",");
let bridge = document.getElementById("bridge");
bridge.style.display = "block";
bridge.style.left = `${wat_rect.width}px`;
bridge.style.width = `${bridgeWidth}px`;
bridge.style.clipPath = `polygon(${points})`;
bridge.style.backgroundColor = wat_elem.style.backgroundColor;
let outline = `2px solid ${rgbToCss(rgbDarken(rgbForOffset(offset)))}`;
for (const elem of elems) {
// TODO: if any of these elems is out of view, show in the pop-up there it is (up or down)
elem.setAttribute("title", `WASM offset @ ${offset}`);
elem.classList.add("hovered");
elem.style.outline = outline;
}

// Perform the DOM modification inside an animation frame to give the browser a bit of
// a breathing room.
window.requestAnimationFrame(() => {
// Change the bridging element styling: change the color to be consistent with
// the Wasm offset, and use the points calculated above to give it a shape that
// makes it look like it's bridging the left and right lists.
let bridge = document.getElementById("bridge");
bridge.style.display = "block";
bridge.style.left = `${watRect.width}px`;
bridge.style.width = `${bridgeWidth}px`;
bridge.style.clipPath = `polygon(${points})`;
bridge.style.backgroundColor = watElem.style.backgroundColor;

// Draw a 2px dark outline in each block of instructions so it stands out a bit better
// when hovered.
let outline = `2px solid ${rgbToCss(rgbDarken(rgbForOffset(offset)))}`;
for (const elem of elems) {
// TODO: if any of these elems is out of view, show in the pop-up there it is (up or down)
elem.setAttribute("title", `WASM offset @ ${offset}`);
elem.classList.add("hovered");
elem.style.outline = outline;
}
});
});
element.addEventListener("mouseleave", (event) => {

element.addEventListener("mouseleave", event => {
document.getElementById("bridge").style.display = "none";
eachElementWithSameWasmOff(event, (elem) => {
eachElementWithSameWasmOff(event, elem => {
elem.removeAttribute("title");
elem.classList.remove("hovered");
elem.style.outline = "";
Expand All @@ -187,23 +257,23 @@ const createDivForCode = () => {
};

// Render the ASM.
for (const func of state.asm.functions) {
for (const func of window.ASM.functions) {
const funcElem = document.createElement("div");

const funcHeader = document.createElement("h3");
let func_name =
let functionName =
func.name === null ? `function[${func.func_index}]` : func.name;
let demangled_name =
func.demangled_name !== null ? func.demangled_name : func_name;
funcHeader.textContent = `Disassembly of function <${demangled_name}>:`;
funcHeader.title = `Function ${func.func_index}: ${func_name}`;
let demangledName =
func.demangled_name !== null ? func.demangled_name : functionName;
funcHeader.textContent = `Disassembly of function <${demangledName}>:`;
funcHeader.title = `Function ${func.func_index}: ${functionName}`;
funcElem.appendChild(funcHeader);

let currentBlock = createDivForCode();
let disasmBuffer = [];
let lastOffset = null;

const addCurrentBlock = (offset) => {
const addCurrentBlock = offset => {
currentBlock.setAttribute("data-wasm-offset", offset);

if (offset !== null) {
Expand Down Expand Up @@ -232,13 +302,13 @@ for (const func of state.asm.functions) {
}

// Render the WAT.
for (const chunk of state.wat.chunks) {
for (const chunk of window.WAT.chunks) {
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) {
if (offsetToRgb.get(chunk.wasm_offset) !== undefined) {
adjustColorForOffset(block, chunk.wasm_offset);
linkElements(block);
}
Expand Down

0 comments on commit 2aaa7b4

Please sign in to comment.