forked from IQ.Lvbs/IQ.Pilot
1216 lines
59 KiB
JavaScript
1216 lines
59 KiB
JavaScript
// ** graph helpers
|
||
|
||
const displaySelection = (sel) => {
|
||
for (const e of document.getElementsByClassName("view")) e.style.display = e.matches(sel) ? "flex" : "none";
|
||
}
|
||
const metadata = document.querySelector(".metadata");
|
||
|
||
const darkenHex = (h, p = 0) =>
|
||
`#${(
|
||
c = parseInt(h.slice(1), 16),
|
||
f = 1 - p / 100,
|
||
((c >> 16 & 255) * f | 0) << 16 |
|
||
((c >> 8 & 255) * f | 0) << 8 |
|
||
((c & 255) * f | 0)
|
||
).toString(16).padStart(6, '0')}`;
|
||
|
||
const ANSI_COLORS = ["#b3b3b3", "#ff6666", "#66b366", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff"];
|
||
const ANSI_COLORS_LIGHT = ["#d9d9d9","#ff9999","#99cc99","#ffff99","#9999ff","#ff99ff","#ccffff","#ffffff"];
|
||
const parseColors = (name, defaultColor="#ffffff") => Array.from(name.matchAll(/(?:\u001b\[(\d+)m([\s\S]*?)\u001b\[0m)|([^\u001b]+)/g),
|
||
([_, code, colored_st, st]) => ({ st: colored_st ?? st, color: code != null ? (code>=90 ? ANSI_COLORS_LIGHT : ANSI_COLORS)[(parseInt(code)-30+60)%60] : defaultColor }));
|
||
|
||
const colored = n => d3.create("span").call(s => s.selectAll("span").data(typeof n === "string" ? parseColors(n) : n).join("span")
|
||
.style("color", d => d.color).text(d => d.st)).node();
|
||
|
||
const rect = (s) => (typeof s === "string" ? document.querySelector(s) : s).getBoundingClientRect();
|
||
|
||
// dims of shapes on the canvas aren't tracked by the browser, we compute it
|
||
const canvasRect = (s, pixelScale) => {
|
||
const { e } = selectShape(s), t = data.tracks.get(s.split("-")[0]);
|
||
const x = pixelScale(e.x), w = pixelScale(e.x+e.width)-x, y = t.offsetY+e.y;
|
||
return {x0:x, x1:x+w, y0:y, y1:y+e.height};
|
||
};
|
||
|
||
let timeout = null;
|
||
const Status = {STARTED:0, COMPLETE:1, ERR:2}
|
||
const updateProgress = (st, msg) => {
|
||
clearTimeout(timeout);
|
||
const msgEl = d3.select("#progress-message").style("display", "none");
|
||
const customEl = d3.select("#custom").style("display", "none");
|
||
if (st === Status.STARTED) {
|
||
msgEl.text(msg);
|
||
timeout = setTimeout(() => msgEl.style("display", "block"), 2000);
|
||
} else if (st === Status.ERR) {
|
||
displaySelection("#custom");
|
||
customEl.html("").append("div").classed("raw-text", true).append(() => codeBlock(msg));
|
||
}
|
||
}
|
||
|
||
function intersectRect(r1, r2) {
|
||
const dx = r2.x-r1.x;
|
||
const dy = r2.y-r1.y;
|
||
if (dx === 0 && dy === 0) throw new Error("Invalid node coordinates, rects must not overlap");
|
||
const scaleX = dx !== 0 ? (r1.width/2)/Math.abs(dx) : Infinity;
|
||
const scaleY = dy !== 0 ? (r1.height/2)/Math.abs(dy) : Infinity;
|
||
const scale = Math.min(scaleX, scaleY);
|
||
return {x:r1.x+dx*scale, y:r1.y+dy*scale};
|
||
}
|
||
|
||
function addTags(root, path) {
|
||
root.selectAll("circle").data(d => d.rect ? [] : [d]).join("circle").attr("r", 5).style("fill", d => d.fill ?? null).style("stroke", d => d.stroke ?? null);
|
||
root.selectAll("rect").data(d => d.rect ? [d] : []).join("rect").attr("x", d => -d.width/2).attr("y", d => -d.height/2)
|
||
.attr("width", d => d.width).attr("height", d => d.height).style("fill", d => d.fill ?? null).style("stroke", d => d.stroke ?? null);
|
||
if (path != null) root.selectAll("path").data(d => [d]).join("path").attr("d", path);
|
||
else root.selectAll("text").data(d => [d]).join("text").text(d => d.text).attr("dy", "0.35em");
|
||
}
|
||
|
||
anchor = null;
|
||
const drawGraph = (data) => {
|
||
const g = dagre.graphlib.json.read(data);
|
||
// draw nodes
|
||
d3.select("#graph-svg").on("click", () => d3.selectAll(".highlight").classed("highlight", false));
|
||
const callCount = g.graph().callCount;
|
||
const nodes = d3.select("#nodes").selectAll("g").data(g.nodes().map(id => g.node(id)), d => d).join("g").attr("class", d => d.className ?? "node")
|
||
.attr("transform", d => `translate(${d.x},${d.y})`).on("click", (e,d) => {
|
||
const parents = g.predecessors(d.id);
|
||
const children = g.successors(d.id);
|
||
if (parents == null && children == null) return;
|
||
const src = [...parents, ...children, d.id];
|
||
nodes.classed("highlight", n => src.includes(n.id)).classed("child", n => children.includes(n.id));
|
||
if (!e.target.classList.contains("token")) labels.selectAll("rect.bg").classed("highlight", false);
|
||
const matchEdge = (v, w) => (v===d.id && children.includes(w)) ? "highlight child " : (parents.includes(v) && w===d.id) ? "highlight " : "";
|
||
d3.select("#edges").selectAll("path.edgePath").attr("class", e => matchEdge(e.v, e.w)+"edgePath");
|
||
d3.select("#edge-labels").selectAll("g.port").attr("class", (_, i, n) => matchEdge(...n[i].id.split("-"))+"port");
|
||
e.stopPropagation();
|
||
});
|
||
nodes.selectAll("rect").data(d => [d]).join("rect").attr("width", d => d.width).attr("height", d => d.height).attr("fill", d => d.color)
|
||
.attr("x", d => -d.width/2).attr("y", d => -d.height/2).classed("node", true);
|
||
const STROKE_WIDTH = 1.4, textSpace = g.graph().textSpace;
|
||
const labels = nodes.selectAll("g.label").data(d => [d]).join("g").attr("class", "label");
|
||
labels.attr("transform", d => `translate(${d.labelX-d.labelWidth/2}, -${d.labelHeight/2+STROKE_WIDTH*2})`);
|
||
const rectGroup = labels.selectAll("g.rect-group").data(d => [d]).join("g").attr("class", "rect-group");
|
||
const tokens = labels.selectAll("g.text-group").data(d => [d]).join("g").attr("class", "text-group").selectAll("text").data(d => {
|
||
if (Array.isArray(d.label)) return [d.label];
|
||
const ret = [[]];
|
||
for (const s of parseColors(d.label, defaultColor="initial")) {
|
||
const color = darkenHex(s.color, 25);
|
||
const lines = s.st.split("\n");
|
||
ret.at(-1).push({ st:lines[0], color });
|
||
for (let i=1; i<lines.length; i++) ret.push([{ st:lines[i], color }]);
|
||
}
|
||
return [ret];
|
||
}).join("text").style("font-family", g.graph().font).selectAll("tspan").data(d => d).join("tspan").attr("x", "0").attr("dy", g.graph().lh)
|
||
.selectAll("tspan").data(d => d).join("tspan").attr("dx", (d, i) => i > 0 && d.st !== "," ? textSpace: 0).text(d => d.st).classed("token", true)
|
||
.attr("xml:space", "preserve").attr("fill", d => d.color);
|
||
const tokensBg = rectGroup.selectAll("rect.bg").data((d, i, nodes) => {
|
||
const ret = [];
|
||
d3.select(nodes[i].parentElement).select("g.text-group").selectAll("tspan.token").each((d, i, nodes) => {
|
||
if (!d.keys?.length) return;
|
||
const b = nodes[i].getBBox(); ret.push({ keys:d.keys, x:b.x, y:b.y, width:b.width, height:b.height });
|
||
});
|
||
return ret;
|
||
}).join("rect").attr("class", "bg").attr("x", d => d.x).attr("y", d => d.y).attr("width", d => d.width).attr("height", d => d.height);
|
||
tokens.on("click", (e, { keys }) => {
|
||
tokensBg.classed("highlight", (d, i, nodes) => !nodes[i].classList.contains("highlight") && d.keys.some(k => keys?.includes(k)));
|
||
});
|
||
addTags(nodes.selectAll("g.tag").data(d => d.tag != null ? [d] : []).join("g").attr("class", "tag")
|
||
.attr("transform", d => `translate(${-d.width/2+8}, ${-d.height/2+8})`).datum(e => ({ text:e.tag })));
|
||
addTags(nodes.selectAll("g.addrspace").data(d => d.addrspace != null ? [d] : []).join("g").attr("class", "tag addrspace")
|
||
.attr("transform", d => `translate(${d.width/2-8}, ${-d.height/2+8})`).datum(e => ({ rect:true, width:10, height:10, fill:e.addrspace, stroke:"none" })));
|
||
const CALL_TAG_WIDTH = 14;
|
||
addTags(nodes.selectAll("g.type").data(d => d.collapsible ? [d] : []).join("g").attr("class", d => `tag clickable ${d.collapsed ? 'collapsed' : 'expanded'}`)
|
||
.attr("transform", d => d.callNode ? `translate(${CALL_TAG_WIDTH/2-d.width/2}, ${0})` : `translate(${-d.width/2}, ${0})`)
|
||
.datum(d => ({ ...d, text:d.collapsed ? "+" : "−", fill:d.callNode ? null : d.color,
|
||
...(d.callNode && { rect:true, width:CALL_TAG_WIDTH }) })).on("click", (e,d) => {
|
||
e.stopPropagation();
|
||
const t = d3.zoomTransform(document.getElementById("graph-svg"));
|
||
const [x, y] = t.apply([d.x, d.y]);
|
||
anchor = {id:d.id, x, y, k:t.k};
|
||
if (d.callNode) {
|
||
if (state.callSrcMask.has(d.id)) state.callSrcMask.delete(d.id); else state.callSrcMask.add(d.id);
|
||
if (state.callSrcMask.size >= callCount) { showCallSrc.toggle.checked = !showCallSrc.toggle.checked; state.callSrcMask.clear(); }
|
||
} else { if (state.expandedNodes.has(d.id)) state.expandedNodes.delete(d.id); else state.expandedNodes.add(d.id); }
|
||
return setState({});
|
||
}));
|
||
addTags(nodes.selectAll("g.ref").data(d => d.ref != null ? [d] : []).join("g").attr("class", "tag ref")
|
||
.attr("transform", d => `translate(${d.width/2-2}, ${-d.height/2+2})`).on("click", (e,d) => { e.stopPropagation(); switchCtx(d.ref); }).datum(d => ({ref:d.ref})),
|
||
"M-1.7 1.7 L1.7 -1.7 M-0.55 -1.7 H1.7 V0.55");
|
||
// draw edges
|
||
const line = d3.line().x(d => d.x).y(d => d.y).curve(d3.curveBasis), edges = g.edges();
|
||
d3.select("#edges").selectAll("path.edgePath").data(edges).join("path").attr("class", "edgePath").attr("d", (e) => {
|
||
const edge = g.edge(e);
|
||
const points = edge.points.slice(1, edge.points.length-1);
|
||
points.unshift(intersectRect(g.node(e.v), points[0]));
|
||
points.push(intersectRect(g.node(e.w), points[points.length-1]));
|
||
return line(points);
|
||
}).attr("marker-end", "url(#arrowhead)").attr("stroke", e => g.edge(e).color || "#4a4b57");
|
||
return g;
|
||
}
|
||
|
||
// ** UOp graph
|
||
|
||
let workerUrl = null, worker = null;
|
||
async function initWorker() {
|
||
const resp = await Promise.all(["/assets/dagrejs.github.io/project/dagre/latest/dagre.min.js","/js/worker.js"].map(u => fetch(u)));
|
||
workerUrl = URL.createObjectURL(new Blob([(await Promise.all(resp.map((r) => r.text()))).join("\n")], { type: "application/javascript" }));
|
||
}
|
||
|
||
function renderDag(layoutSpec, { recenter }) {
|
||
// start calculating the new layout (non-blocking)
|
||
updateProgress(Status.STARTED, "Rendering new graph...");
|
||
if (worker != null) worker.terminate();
|
||
worker = new Worker(workerUrl);
|
||
worker.postMessage(layoutSpec);
|
||
worker.onmessage = (e) => {
|
||
if (e.data.error) {
|
||
updateProgress(Status.ERR, "Error in graph layout:\n"+e.data.error);
|
||
return;
|
||
}
|
||
const data = e.data.result;
|
||
displaySelection("#graph");
|
||
updateProgress(Status.COMPLETE);
|
||
const g = drawGraph(data);
|
||
addTags(d3.select("#edge-labels").selectAll("g").data(data.edges).join("g").attr("transform", (e) => {
|
||
// get a point near the end
|
||
const [p1, p2] = e.value.points.slice(-2);
|
||
const dx = p2.x-p1.x;
|
||
const dy = p2.y-p1.y;
|
||
// normalize to the unit vector
|
||
const len = Math.sqrt(dx*dx + dy*dy);
|
||
const ux = dx / len;
|
||
const uy = dy / len;
|
||
// avoid overlap with the arrowhead
|
||
const offset = 17;
|
||
const x = p2.x - ux * offset;
|
||
const y = p2.y - uy * offset;
|
||
return `translate(${x}, ${y})`
|
||
}).attr("class", e => e.value.label.type).attr("id", e => `${e.v}-${e.w}`).datum(e => ({ text:e.value.label.text })));
|
||
if (anchor != null) {
|
||
const n = g.node(anchor.id);
|
||
d3.select("#graph-svg").call(svgZoom.transform, d3.zoomIdentity.translate(anchor.x-n.x*anchor.k, anchor.y-n.y*anchor.k).scale(anchor.k));
|
||
} else if (recenter) document.getElementById("zoom-to-fit-btn").click();
|
||
anchor = null;
|
||
};
|
||
worker.onerror = (e) => {
|
||
e.preventDefault();
|
||
updateProgress(Status.ERR, "Error in graph layout:\n"+e.message);
|
||
}
|
||
}
|
||
|
||
// ** profiler graph
|
||
|
||
function formatMicroseconds(ts, showUs=true) {
|
||
const s = Math.floor(ts / 1e6), ms = Math.floor((ts % 1e6) / 1e3), us = Math.round(ts % 1e3);
|
||
const parts = [];
|
||
if (s) parts.push(`${s}s`);
|
||
if (ms || (!showUs && !s)) parts.push(`${ms}ms`);
|
||
if (showUs && (us || (!ms && !s))) parts.push(`${us}us`);
|
||
return parts.join(' ');
|
||
}
|
||
|
||
function formatCycles(cycles) {
|
||
const M = Math.floor(cycles / 1e6), K = Math.floor((cycles % 1e6) / 1e3), s = Math.round(cycles % 1e3);
|
||
const parts = [];
|
||
if (M) parts.push(`${M}M`);
|
||
if (K) parts.push(`${K}K`);
|
||
if (s || (!M && !K)) parts.push(`${s}`);
|
||
return parts.join(" ");
|
||
}
|
||
|
||
const formatUnit = (d, unit="") => d3.format(".3~s")(d)+unit;
|
||
const skipFmt = new Set(["tb", "pc", "link"]);
|
||
const formatData = (fmt) => Object.entries(fmt ?? {}).filter(([k]) => !skipFmt.has(k)).map(([k, v]) => typeof(v) === "string" ? `${k} ${v}` : formatUnit(v, k));
|
||
|
||
const waveColor = (op) => {
|
||
let ret = data.waveColors.find(([pattern]) => op.includes(pattern))?.[1] ?? "#ffffff";
|
||
if (op.includes("OTHER_") || op.includes("_ALT")) { ret = darkenHex(ret, 75) }
|
||
if (op.includes("LDS_")) { ret = darkenHex(ret, 25) }
|
||
return ret
|
||
};
|
||
const colorScheme = {TINY:new Map([["Schedule","#1b5745"],["precompile","#1d2e62"],["compile","#63b0cd"],["DEFAULT","#354f52"]]),
|
||
DEFAULT:["#2b2e39", "#2c2f3a", "#31343f", "#323544", "#2d303a", "#2e313c", "#343746", "#353847", "#3c4050", "#404459", "#444862", "#4a4e65"],
|
||
BUFFER:["#342483", "#3E2E94", "#4938A4", "#5442B4", "#5E4CC2", "#674FCA"], SIMD:new Map([["OCC", "#101725"], ["INST", "#0A2042"]]),
|
||
GPC:new Map([["NONE","#1a7a2e"],["MEMORY_DEPENDENCY","#8b1a00"],["EXEC_DEPENDENCY","#006b6b"],["INST_FETCH","#7a7a00"],["SYNC","#6b006b"],
|
||
["PIPE_BUSY","#7a4a00"],["MEMORY_THROTTLE","#5c0000"],["CONSTANT_MEMORY","#1a3d7a"],["NOT_SELECTED","#2e2e3a"],["OTHER","#4a4a55"],
|
||
["SLEEPING","#1a1a2a"],["DEFAULT","#3a3a45"]]), WAVE:waveColor, VMEMEXEC:waveColor, ALUEXEC:waveColor}
|
||
const cycleColors = (lst, i) => lst[i%lst.length];
|
||
|
||
const rescaleTrack = (source, tid, k) => {
|
||
for (const shapes of source.views)
|
||
for (const e of shapes) {
|
||
for (let i=0; i<e.y0.length; i++) {
|
||
e.y0[i] = e.y0[i]*k;
|
||
e.y1[i] = e.y1[i]*k;
|
||
}
|
||
}
|
||
const change = (source.height*k)-source.height;
|
||
const div = document.getElementById(tid);
|
||
div.style.height = rect(div).height+change+"px";
|
||
source.height = source.height*k;
|
||
return change;
|
||
}
|
||
|
||
const drawLine = (ctx, x, y, opts) => {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x[0], y[0]);
|
||
ctx.lineTo(x[1], y[1]);
|
||
ctx.fillStyle = ctx.strokeStyle = opts?.color || "#f0f0f5";
|
||
ctx.stroke();
|
||
}
|
||
|
||
function tabulate(rows) {
|
||
const root = d3.create("div").style("display", "grid").style("grid-template-columns", `${Math.max(...rows.map(x => x[0].length), 0)}ch 1fr`).style("gap", "0.2em").style("white-space", "nowrap");
|
||
for (const [k,v] of rows) { root.append("div").text(k); root.append("div").node().append(v); }
|
||
return root.node();
|
||
}
|
||
|
||
var data, focusedDevice, focusedShape, formatTime, canvasZoom, zoomLevel = d3.zoomIdentity;
|
||
|
||
const canvasDims = () => {
|
||
const sideRect = rect("#device-list");
|
||
return [Math.round(document.querySelector("#profiler").clientWidth-sideRect.width), Math.round(sideRect.height)];
|
||
}
|
||
|
||
function selectShape(key) {
|
||
if (key == null) return {};
|
||
const [t, idx] = key.split("-");
|
||
const track = data.tracks.get(t);
|
||
return { eventType:track?.eventType, e:track?.shapes[idx] };
|
||
}
|
||
|
||
// scaling function for time to pixels
|
||
const timelineScale = () => d3.scaleLinear().domain([data.first, data.dur]).range([0, canvasDims()[0]]);
|
||
|
||
function timeAtCycle(clk) {
|
||
if (clk < data.instSt || clk > data.instEt || data.tracks.get("Shader Clock") == null) return "-";
|
||
let cur = data.instSt, ns = 0, freq = null;
|
||
// walk through all frequency changes and accumulate time in nanoseconds
|
||
for (const [s, v] of data.tracks.get("Shader Clock").valueMap) {
|
||
if (freq != null && freq > 0 && cur < s) {
|
||
const et = Math.min(clk, s);
|
||
ns += (et - cur) * 1e9 / freq;
|
||
cur = et;
|
||
if (cur === clk) break;
|
||
}
|
||
freq = v;
|
||
}
|
||
// ending cycles use the last known frequency
|
||
if (cur < clk) ns += (clk - cur) * 1e9 / freq;
|
||
const remNs = Math.round(ns % 1000);
|
||
return ns/1000>1 ? formatMicroseconds(ns / 1000, true) + (remNs ? ` ${remNs}ns` : "") : Math.round(ns)+"ns";
|
||
}
|
||
|
||
function getZoomIdentity() {
|
||
// for packets, set zoom to the full range of instruction events
|
||
if (data.instSt != null) {
|
||
const k = (data.dur - data.first) / (data.instEt - data.instSt), xscale = timelineScale();
|
||
return d3.zoomIdentity.translate(-xscale(data.instSt) * k, 0).scale(k);
|
||
}
|
||
return d3.zoomIdentity;
|
||
}
|
||
|
||
const Modes = {0:'read', 1:'write', 2:'write+read'};
|
||
|
||
function setFocus(key) {
|
||
if (key !== focusedShape) {
|
||
saveToHistory({ shape:focusedShape });
|
||
// adjust zoom if the entire shape is off screen
|
||
const { eventType, e } = selectShape(key);
|
||
if (e != null) {
|
||
const xscale = timelineScale();
|
||
const [x0, x1] = eventType === EventTypes.EXEC ? [e.x, e.x+e.width] : [e.x[0], e.x.at(-1)];
|
||
const [st, et] = xscale.range().map(zoomLevel.invertX, zoomLevel).map(xscale.invert, xscale);
|
||
if (x1 < st || x0 > et) zoomLevel = d3.zoomIdentity.translate(-xscale((x0+x1)/2-(et-st)/2)*zoomLevel.k, 0).scale(zoomLevel.k);
|
||
}
|
||
const link = e?.arg.link ?? data.links.get(key);
|
||
data.link = link == null ? null : [key, link];
|
||
focusedShape = key; d3.select("#timeline").call(canvasZoom.transform, zoomLevel);
|
||
const tooltip = document.getElementById("tooltip");
|
||
if (tooltip.dataset.key !== key) tooltip.style.display = "none";
|
||
}
|
||
const { eventType, e } = selectShape(key);
|
||
if (metadata.querySelector(".info") == null) d3.select(metadata).html("").append("div").classed("info", true);
|
||
const html = d3.select(".info").html("");
|
||
if (eventType === EventTypes.EXEC) {
|
||
const [n, _, ...rest] = e.arg.tooltipText.split("\n");
|
||
const tableData = [["Name", colored(e.arg.label)], ["Duration", formatTime(e.width)]];
|
||
if (data.instSt != null) {
|
||
const p = d3.create("p");
|
||
p.append("span").text(timeAtCycle(e.x));
|
||
p.append("span").style("margin-left", "8px").style("color", "#f0f0f566").text(formatTime(e.x));
|
||
tableData.push(["Cycle", formatTime(e.x-data.instSt)], ["Time", p.node()]);
|
||
} else tableData.push(["Start Time", formatTime(e.x)]);
|
||
if (data.link != null) tableData.push(["Delay", `${formatTime(Math.abs(selectShape(data.link[0]).e.x - selectShape(data.link[1]).e.x))} Cycles`]);
|
||
html.append(() => tabulate(tableData));
|
||
let group = html.append("div").classed("args", true);
|
||
for (const r of rest) group.append("p").text(r);
|
||
group = html.append("div").classed("args", true);
|
||
for (const b of e.arg.bufs.sort((a, b) => a.num - b.num)) {
|
||
group.append("p").text(`${Modes[b.mode]}@data${b.num} ${formatUnit(b.nbytes, 'B')}`).style("cursor", "pointer").on("click", () => {
|
||
const row = document.getElementById(b.k); if (!isExpanded(row)) { row.click(); }
|
||
setFocus(b.key);
|
||
});
|
||
}
|
||
if (e.arg.ctx != null) {
|
||
const i = e.arg.ctx; s = e.arg.step;
|
||
html.append("a").text(ctxs[i+1].steps[s].name).on("click", () => switchCtx(i, s));
|
||
const prgSrc = ctxs[i+1].steps.findIndex(s => s.name === "View Source");
|
||
if (prgSrc !== -1) html.append("a").text("View Source").on("click", () => switchCtx(i, prgSrc));
|
||
}
|
||
if (e.arg.trace != null) html.append(() => traceBlock(e.arg.trace.slice(1).reverse()));
|
||
}
|
||
if (eventType === EventTypes.BUF) {
|
||
const [dtype, sz, nbytes, dur] = e.arg.tooltipText.split("\n");
|
||
const rows = [["DType", dtype], ["Len", sz], ["Size", nbytes], ["Lifetime", dur]];
|
||
if (e.arg.users != null) rows.push(["Users", e.arg.users.length]);
|
||
html.append(() => tabulate(rows));
|
||
const kernels = html.append("div").classed("args", true);
|
||
for (let u=0; u<e.arg.users?.length; u++) {
|
||
const { repr, num, mode, shape } = e.arg.users[u];
|
||
const p = kernels.append("p").append(() => colored(`[${u}] ${repr} ${Modes[mode]}@data${num}`));
|
||
const shapeInfo = selectShape(shape).e?.arg?.tooltipText?.split("\n");
|
||
if (shapeInfo?.length > 5) p.append("span").text(" "+shapeInfo[5]);
|
||
if (shape != null) p.style("cursor", "pointer").on("click", () => setFocus(shape));
|
||
}
|
||
}
|
||
// instructions list renderer
|
||
let instList = document.getElementById("insts");
|
||
if (data.pcMap == null) return d3.select(instList?.parentElement).html("");
|
||
if (instList == null) {
|
||
let contents = "";
|
||
for (let [pc, label] of Object.entries(data.pcMap)) {
|
||
pc = parseInt(pc);
|
||
const pcHex = pc.toString(16);
|
||
contents += `<div class="line"><span class="left" id="inst-${pc}"><span class="pc">${"0x"+pcHex.padStart(Math.max(4, Math.ceil(pcHex.length/4)*4), 0)}</span><span class="label">${label}</span></span></div>`;
|
||
}
|
||
instList = d3.create("pre").append("code").classed("hljs", true).style("margin-top", "20px").attr("id", "insts").html(contents).node();
|
||
metadata.insertBefore(instList.parentElement, html.node());
|
||
}
|
||
d3.select(instList).selectAll("span").classed("highlight", false);
|
||
let instLine = document.getElementById(`inst-${e?.arg.pc}`);
|
||
if (instLine == null && data.link != null) instLine = document.getElementById(`inst-${selectShape(data.link[1]).e.arg.pc}`);
|
||
if (instLine != null) {
|
||
instLine.classList.add("highlight");
|
||
const r = rect(instLine), c = rect(instList);
|
||
if (Math.max(c.top-r.bottom, r.top-c.bottom)>=-30) instList.scrollTop = instLine.offsetTop-instList.clientHeight/2+instLine.clientHeight/2;
|
||
}
|
||
}
|
||
|
||
const EventTypes = { EXEC:0, BUF:1 };
|
||
|
||
async function renderProfiler(path, opts) {
|
||
displaySelection("#profiler");
|
||
// support non realtime x axis units
|
||
formatTime = opts.unit === "ms" ? formatMicroseconds : formatCycles;
|
||
if (data?.path !== path) { data = {tracks:new Map(), axes:{}, path, first:null, links:new Map()}; focusedDevice = null; focusedShape = null; }
|
||
setFocus(focusedShape);
|
||
// layout once!
|
||
if (data.tracks.size !== 0) return updateProgress(Status.COMPLETE);
|
||
const profiler = d3.select("#profiler").html("");
|
||
const buf = cache[path] ?? await fetchValue(path);
|
||
const view = new DataView(buf);
|
||
let offset = 0;
|
||
const u8 = () => { const ret = view.getUint8(offset); offset += 1; return ret; }
|
||
const u32 = () => { const ret = view.getUint32(offset, true); offset += 4; return ret; }
|
||
const u64 = () => { const ret = new Number(view.getBigUint64(offset, true)); offset += 8; return ret; }
|
||
const f32 = () => { const ret = view.getFloat32(offset, true); offset += 4; return ret; }
|
||
const optional = (i) => i === 0 ? null : i-1;
|
||
const dur = u32(), tracePeak = u64(), indexLen = u32(), layoutsLen = u32(); data.dur = dur;
|
||
const textDecoder = new TextDecoder("utf-8");
|
||
const { strings, dtypeSize, markers, ...extData } = JSON.parse(textDecoder.decode(new Uint8Array(buf, offset, indexLen))); offset += indexLen;
|
||
for (const [k,v] of Object.entries(extData)) data[k] = v;
|
||
// place devices on the y axis and set vertical positions
|
||
const [tickSize, padding, baseOffset] = [5, 8, markers.length ? 14 : 0];
|
||
const secondaryTick = opts.unit == "clk" ? timeAtCycle : null;
|
||
const axisHeight = secondaryTick != null ? tickSize*2+(padding*2) : tickSize;
|
||
const deviceList = profiler.append("div").attr("id", "device-list").style("padding-top", axisHeight+padding+baseOffset+"px");
|
||
const canvas = profiler.append("canvas").attr("id", "timeline").node();
|
||
// NOTE: scrolling via mouse can only zoom the graph
|
||
canvas.addEventListener("wheel", e => (e.stopPropagation(), e.preventDefault()), { passive:false });
|
||
const ctx = canvas.getContext("2d");
|
||
const canvasTop = rect(canvas).top;
|
||
// map event name to shape and label colors
|
||
const colorMap = new Map(), coloredNames = new Map();
|
||
// map shapes by event key
|
||
const shapeMap = new Map();
|
||
const heightScale = d3.scaleLinear().domain([0, tracePeak]).range([4,maxheight=100]);
|
||
for (let i=0; i<layoutsLen; i++) {
|
||
const nameLen = view.getUint8(offset, true); offset += 1;
|
||
const k = textDecoder.decode(new Uint8Array(buf, offset, nameLen)); offset += nameLen;
|
||
const div = deviceList.append("div").attr("id", k).text(k).style("padding", padding+"px").style("width", opts.width);
|
||
const { y:baseY, height:baseHeight } = rect(div.node());
|
||
const [dname, dnum] = k.split(":", 2);
|
||
const colors = colorScheme[dname] ?? colorScheme.DEFAULT;
|
||
const offsetY = baseY-canvasTop+padding/2;
|
||
const shapes = [], visible = [];
|
||
const eventType = u8(), eventsLen = u32();
|
||
const [pcolor, scolor] = path.includes("sqtt") ? ["#00c72f", "#858b9d"] : ["#9ea2ad", null];
|
||
// last row doesn't get a border
|
||
const rowBorderColor = i<layoutsLen-1 ? "#22232a" : null;
|
||
if (rowBorderColor != null) div.style("border-bottom", `1px solid ${rowBorderColor}`);
|
||
if (eventType === EventTypes.EXEC) {
|
||
const levelHeight = (baseHeight-padding)*(opts.heightScale ?? 1);
|
||
const levels = [];
|
||
data.tracks.set(k, { shapes, eventType, visible, offsetY, scolor, pcolor, rowBorderColor });
|
||
let colorKey, ref;
|
||
for (let j=0; j<eventsLen; j++) {
|
||
const e = {name:strings[u32()], ref:optional(u32()), key:optional(u32()), st:u32(), dur:f32(), fmt:JSON.parse(strings[u32()])};
|
||
// find a free level to put the event
|
||
let depth = levels.findIndex(levelEt => e.st >= levelEt);
|
||
const et = e.st+Math.trunc(e.dur);
|
||
if (depth === -1) {
|
||
depth = levels.length;
|
||
levels.push(et);
|
||
} else levels[depth] = et;
|
||
if (depth === 0 || opts.colorByName) colorKey = e.name.split(" ")[0];
|
||
if (!colorMap.has(colorKey)) {
|
||
const color = typeof colors === "function" ? colors(colorKey)
|
||
: colors instanceof Map ? (colors.get(colorKey) || colors.get("DEFAULT")) : cycleColors(colors, colorMap.size);
|
||
colorMap.set(colorKey, d3.rgb(color));
|
||
}
|
||
const fillColor = colorMap.get(colorKey).brighter(0.3*depth).toString();
|
||
let label = coloredNames.get(e.name);
|
||
if (label == null) {
|
||
label = parseColors(e.name).flatMap(({ color, st }) => {
|
||
const parts = [];
|
||
for (let i=0; i<st.length; i+=4) { const part = st.slice(i, i+4); parts.push({ color, st:part, width:ctx.measureText(part).width }); }
|
||
return parts;
|
||
}); coloredNames.set(e.name, label);
|
||
}
|
||
let shapeRef = e.ref;
|
||
if (shapeRef != null) { ref = {ctx:e.ref, step:0}; shapeRef = ref; }
|
||
else if (ref != null) {
|
||
const start = ref.step>0 ? ref.step+1 : 0;
|
||
const steps = ctxs[ref.ctx+1].steps;
|
||
for (let si=start; si<steps.length; si++) {
|
||
if (steps[si].name == e.name) { ref.step = si; shapeRef = ref; break; }
|
||
}
|
||
} else {
|
||
const steps = ctxs[state.currentCtx].steps;
|
||
for (let i=state.currentStep+1; i<steps.length; i++) {
|
||
const loc = steps[i].loc;
|
||
if (loc == null) break;
|
||
if (loc === e.name) { shapeRef = {ctx:state.currentCtx-1, step:i}; break; }
|
||
}
|
||
}
|
||
// tiny device events go straight to the rewrite rule
|
||
const key = k.startsWith("TINY") ? null : `${k}-${j}`;
|
||
const trace = e.fmt.tb, pc = e.fmt.pc, link = e.fmt.link;
|
||
if (link != null) data.links.set(link, key);
|
||
const arg = { tooltipText:[" N:"+shapes.length, formatTime(e.dur), ...formatData(e.fmt)].join("\n"), label, pc, trace, link, bufs:[], key, ctx:shapeRef?.ctx, step:shapeRef?.step };
|
||
if (e.key != null) shapeMap.set(e.key, key);
|
||
// offset y by depth
|
||
shapes.push({x:e.st, y:levelHeight*depth, width:e.dur, height:levelHeight, arg, label:opts.hideLabels ? null : label, fillColor });
|
||
if (j === 0) data.first = data.first == null ? e.st : Math.min(data.first, e.st);
|
||
}
|
||
div.style("height", levelHeight*levels.length+padding+"px").style("pointerEvents", "none");
|
||
} else {
|
||
const linear = u8(), peak = u64();
|
||
const timestamps = [], valueMap = new Map();
|
||
// start by unpacking the raw events
|
||
const memEvents = [];
|
||
let x = 0, y = 0, shapeIdx = 0;
|
||
const allocs = new Map();
|
||
for (let j=0; j<eventsLen; j++) {
|
||
if (linear) { const ts = u32(), value = u64(); timestamps.push(ts); valueMap.set(ts, value); continue; }
|
||
const alloc = u8(), ts = u32(), key = u32();
|
||
if (alloc) {
|
||
const dtype = strings[u32()], sz = u64(), nbytes = dtypeSize[dtype]*sz;
|
||
allocs.set(key, {nbytes, shapeKey:`${k}-${shapeIdx++}`});
|
||
memEvents.push({alloc, key, dtype, sz, nbytes});
|
||
timestamps.push(ts);
|
||
x += 1; y += nbytes; valueMap.set(ts, y);
|
||
} else {
|
||
const users = Array.from({ length: u32() }, () => ({shape:shapeMap.get(u32()), repr:strings[u32()], num:u32(), mode:u8()}));
|
||
const {nbytes, shapeKey} = allocs.get(key); allocs.delete(key);
|
||
users?.forEach((u) => selectShape(u.shape).e?.arg.bufs.push({ key:shapeKey, nbytes, num:u.num, mode:u.mode, k }));
|
||
memEvents.push({alloc, key, users, nbytes});
|
||
timestamps.push(ts); valueMap.set(ts, y);
|
||
x += 1; y -= nbytes;
|
||
}
|
||
}
|
||
timestamps.push(dur);
|
||
const height = linear ? (baseHeight-padding)*(opts.heightScale ?? 1)*2 : heightScale(peak);
|
||
const yscale = d3.scaleLinear().domain([0, peak]).range([height, 0]);
|
||
// generic polygon merger
|
||
const base0 = yscale(0);
|
||
const sum = {x:[], y0:[], y1:[], fillColor:linear ? null : "#2b1b72"};
|
||
for (let i=0; i<timestamps.length-1; i++) {
|
||
const yv = yscale(valueMap.get(timestamps[i]));
|
||
sum.x.push(timestamps[i], timestamps[i+1]); sum.y1.push(yv, yv); sum.y0.push(base0, base0);
|
||
}
|
||
// build individual buffer shapes when user clicks to expand, this detailed layout is n²
|
||
let bufShapes = null;
|
||
const buildBufShapes = () => {
|
||
if (bufShapes != null) return bufShapes;
|
||
bufShapes = [];
|
||
const buf_shapes = new Map(), temp = new Map();
|
||
let x = 0, y = 0;
|
||
for (const e of memEvents) {
|
||
if (e.alloc) {
|
||
const shape = {x:[x], y:[y], dtype:e.dtype, sz:e.sz, nbytes:e.nbytes, key:e.key};
|
||
buf_shapes.set(e.key, shape); temp.set(e.key, shape);
|
||
x += 1; y += e.nbytes;
|
||
} else {
|
||
const free = buf_shapes.get(e.key);
|
||
free.users = e.users;
|
||
x += 1; y -= free.nbytes;
|
||
free.x.push(x); free.y.push(free.y.at(-1));
|
||
temp.delete(e.key);
|
||
for (const [k, v] of temp) {
|
||
if (k <= e.key) continue;
|
||
v.x.push(x, x);
|
||
v.y.push(v.y.at(-1), v.y.at(-1)-free.nbytes);
|
||
}
|
||
}
|
||
}
|
||
for (const [num, {dtype, sz, nbytes, y, x:steps, users}] of buf_shapes) {
|
||
const x = steps.map(s => timestamps[s]);
|
||
const dur = x.at(-1)-x[0];
|
||
const arg = { tooltipText:`${dtype}\n${formatUnit(sz)}\n${formatUnit(nbytes, 'B')}\n${formatTime(dur)}`, users, key:`${k}-${bufShapes.length}` };
|
||
bufShapes.push({ x, y0:y.map(yscale), y1:y.map(y0 => yscale(y0+nbytes)), arg, fillColor:cycleColors(colorScheme.BUFFER, bufShapes.length) });
|
||
}
|
||
return bufShapes;
|
||
};
|
||
if (timestamps.length > 0) data.first = data.first == null ? timestamps[0] : Math.min(data.first, timestamps[0]);
|
||
data.tracks.set(k, { shapes:[sum], eventType, linear, visible, offsetY, pcolor:linear ? "#4fa3cc" : "#c9a8ff", height, peak, scaleFactor:maxheight*4/height,
|
||
get views() { return [[sum], linear ? null : buildBufShapes()]; }, valueMap, rowBorderColor, unit:linear ? "Hz" : "B" });
|
||
div.style("height", height+padding+"px").style("cursor", "pointer").on("click", (e) => {
|
||
if (linear) return;
|
||
const newFocus = e.currentTarget.id === focusedDevice ? null : e.currentTarget.id;
|
||
let offset = 0;
|
||
for (const [tid, track] of data.tracks) {
|
||
track.offsetY += offset;
|
||
if (tid === newFocus) { track.shapes = track.views[1]; offset += rescaleTrack(track, tid, track.scaleFactor); }
|
||
else if (tid === focusedDevice) { track.shapes = track.views[0]; offset += rescaleTrack(track, tid, 1/track.scaleFactor); }
|
||
}
|
||
data.axes.y = newFocus != null ? { domain:[0, (t=data.tracks.get(newFocus)).peak], range:[t.offsetY+t.height, t.offsetY], fmt:t.unit } : null;
|
||
toggleCls(document.getElementById(focusedDevice), document.getElementById(newFocus), "expanded");
|
||
focusedDevice = newFocus;
|
||
return resize();
|
||
});
|
||
}
|
||
}
|
||
for (const m of markers) m.label = m.name.split(/(\s+)/).map(st => ({ st, color:m.color, width:ctx.measureText(st).width }));
|
||
if (data.pcMap != null) setFocus(focusedShape);
|
||
// secondary axis mapping
|
||
let instRange = null;
|
||
for (const [k, { shapes }] of data.tracks) if (!k.includes("Clock") && path.includes("sqtt")) {
|
||
const first = shapes[0].x, last = shapes.at(-1).x+shapes.at(-1).width;
|
||
instRange = instRange == null ? [first, last] : [Math.min(first, instRange[0]), Math.max(last, instRange[1])];
|
||
}
|
||
if (instRange != null) [data.instSt, data.instEt] = instRange;
|
||
updateProgress(Status.COMPLETE);
|
||
// draw events on a timeline
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const ellipsisWidth = ctx.measureText("...").width;
|
||
const drawText = (ctx, label, lx, ly, maxWidth) => {
|
||
let lw = 0;
|
||
for (let li=0; li<label?.length; li++) {
|
||
if (lw+label[li].width+(li===label.length-1 ? 0 : ellipsisWidth)+2 > maxWidth) {
|
||
if (lw>0) ctx.fillText("...", lx+lw, ly);
|
||
break;
|
||
}
|
||
ctx.fillStyle = label[li].color;
|
||
ctx.fillText(label[li].st, lx+lw, ly);
|
||
lw += label[li].width;
|
||
}
|
||
}
|
||
function render(transform) {
|
||
zoomLevel = transform;
|
||
const canvasWidth = canvas.clientWidth;
|
||
ctx.clearRect(0, 0, canvasWidth, canvas.clientHeight);
|
||
// rescale to match current zoom
|
||
const xscale = timelineScale();
|
||
const visibleX = xscale.range().map(zoomLevel.invertX, zoomLevel).map(xscale.invert, xscale);
|
||
const st = visibleX[0], et = visibleX[1];
|
||
xscale.domain([st, et]);
|
||
const profilerEl = profiler.node();
|
||
const visibleYStart = profilerEl.scrollTop-canvasTop + rect(profilerEl).top, visibleYEnd = visibleYStart+profilerEl.clientHeight;
|
||
ctx.textBaseline = "middle";
|
||
// draw shapes
|
||
for (const [k, { shapes, eventType, linear, visible, offsetY, valueMap, pcolor, scolor, unit, rowBorderColor }] of data.tracks) {
|
||
visible.length = 0;
|
||
const trackHeight = rect(document.getElementById(k)).height;
|
||
if (offsetY+trackHeight < visibleYStart || offsetY > visibleYEnd) continue;
|
||
const link0 = data.link?.[0]; const link1 = data.link?.[1], highlightRect = focusedShape != null || data.link != null, splitRects = scolor != null;
|
||
if (eventType === EventTypes.BUF) { // generic polygon
|
||
for (const e of shapes) {
|
||
if (e.x[0]>et || e.x.at(-1)<st) continue;
|
||
ctx.beginPath();
|
||
const x = e.x.map(xscale);
|
||
ctx.moveTo(x[0], offsetY+e.y1[0]);
|
||
for (let i=1; i<x.length; i++) {
|
||
ctx.lineTo(x[i], offsetY+e.y1[i]);
|
||
let arg = e.arg;
|
||
if (arg == null && valueMap != null) arg = {tooltipText: formatUnit(valueMap.get(e.x[i-1]), unit)}
|
||
visible.push({ x0:x[i-1], x1:x[i], y0:offsetY+e.y1[i-1], y1:offsetY+e.y0[i], arg });
|
||
}
|
||
if (linear) { ctx.strokeStyle = pcolor; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
|
||
// walk the path back and fill the complete shape
|
||
else { for (let i=x.length-1; i>=0; i--) ctx.lineTo(x[i], offsetY+e.y0[i]); ctx.closePath(); ctx.fillStyle = e.fillColor; ctx.fill(); }
|
||
if (focusedShape != null && e.arg?.key === focusedShape) { ctx.strokeStyle = pcolor; ctx.stroke(); }
|
||
}
|
||
} else { // contiguous rect
|
||
for (const e of shapes) {
|
||
if (e.x>et || e.x+e.width<st) continue;
|
||
const x = xscale(e.x);
|
||
const y = offsetY+e.y;
|
||
const width = xscale(e.x+e.width)-x;
|
||
visible.push({ y0:y, y1:y+e.height, x0:x, x1:x+width, arg:e.arg });
|
||
ctx.fillStyle = e.fillColor;
|
||
ctx.fillRect(x, y, width, e.height);
|
||
// add label
|
||
drawText(ctx, e.label, x+2, y+e.height/2, width);
|
||
// draw highlights
|
||
if (highlightRect) {
|
||
const key = e.arg.key; if (key === focusedShape || key === link0 || key === link1) { ctx.strokeStyle = pcolor; ctx.strokeRect(x, y, width, e.height); continue; }
|
||
}
|
||
if (splitRects && width > 10) { ctx.strokeStyle = scolor; ctx.strokeRect(x, y, width, e.height); }
|
||
}
|
||
}
|
||
// draw row line
|
||
if (rowBorderColor != null) {
|
||
const y = offsetY+trackHeight-padding/2 - 0.5;
|
||
drawLine(ctx, [0, canvasWidth], [y, y], { color:rowBorderColor });
|
||
}
|
||
}
|
||
// draw the link
|
||
if (data.link != null) {
|
||
const [a, b] = [canvasRect(data.link[0], xscale), canvasRect(data.link[1], xscale)];
|
||
const [left, right] = a.x0 <= b.x0 ? [a, b] : [b, a];
|
||
const startX = left.x1, endX = right.x0;
|
||
const leftY = (left.y0+left.y1)/2, rightY = (right.y0+right.y1)/2;
|
||
const dx = endX-startX, bend = Math.max(12, Math.min(40, dx/2));
|
||
ctx.beginPath(); ctx.moveTo(startX, leftY); ctx.bezierCurveTo(startX+bend, leftY, endX-bend, rightY, endX, rightY); ctx.strokeStyle = "#858b9d"; ctx.stroke();
|
||
}
|
||
// draw axes
|
||
ctx.translate(0, baseOffset);
|
||
const y = secondaryTick != null ? tickSize+padding : 0;
|
||
drawLine(ctx, xscale.range(), [y, y]);
|
||
let lastLabelEnd = -Infinity;
|
||
for (const tick of xscale.ticks()) {
|
||
if (!Number.isInteger(tick)) continue;
|
||
const x = xscale(tick);
|
||
drawLine(ctx, [x, x], [y, y+tickSize]);
|
||
const labelX = x+ctx.lineWidth+2;
|
||
if (labelX <= lastLabelEnd) continue;
|
||
const label = formatTime(tick, et-st <= 1e3);
|
||
ctx.textBaseline = "top";
|
||
ctx.fillText(label, labelX, y+tickSize);
|
||
lastLabelEnd = labelX + ctx.measureText(label).width + 4;
|
||
if (secondaryTick != null) {
|
||
drawLine(ctx, [x, x], [y, y-tickSize]);
|
||
const label = secondaryTick(tick, st, et); ctx.fillText(label, labelX, 0);
|
||
lastLabelEnd = Math.max(lastLabelEnd, labelX + ctx.measureText(label).width + 4);
|
||
}
|
||
}
|
||
if (data.axes.y != null) {
|
||
drawLine(ctx, [0, 0], data.axes.y.range);
|
||
const yscale = d3.scaleLinear().domain(data.axes.y.domain).range(data.axes.y.range);
|
||
for (const tick of yscale.ticks()) {
|
||
const y = yscale(tick);
|
||
drawLine(ctx, [0, tickSize], [y, y]);
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText(formatUnit(tick, data.axes.y.fmt), tickSize+2, y);
|
||
}
|
||
}
|
||
// draw markers
|
||
ctx.translate(0, -baseOffset);
|
||
ctx.textBaseline = "top";
|
||
let prevX = null;
|
||
for (let i=0; i<markers.length; i++) {
|
||
const m = markers[i];
|
||
const x = xscale(m.ts), tx = x+2;
|
||
if (tx-prevX < 2) continue;
|
||
prevX = tx;
|
||
drawLine(ctx, [x, x], [0, canvas.clientHeight], { color:m.color });
|
||
let maxWidth = canvasWidth-(tx);
|
||
const nextMark = markers[i+1]?.ts;
|
||
if (nextMark != null) maxWidth = Math.min(maxWidth, xscale(nextMark)-tx);
|
||
if (maxWidth <= 0) continue;
|
||
drawText(ctx, m.label, tx, 1, maxWidth);
|
||
}
|
||
}
|
||
|
||
function resize() {
|
||
const [width, height] = canvasDims();
|
||
if (canvas.width === width*dpr && canvas.height === height*dpr) return;
|
||
canvas.width = width*dpr;
|
||
canvas.height = height*dpr;
|
||
canvas.style.height = `${height}px`;
|
||
canvas.style.width = `${width}px`;
|
||
ctx.scale(dpr, dpr);
|
||
d3.select(canvas).call(canvasZoom.transform, zoomLevel);
|
||
}
|
||
|
||
zoomLevel = getZoomIdentity();
|
||
canvasZoom = d3.zoom().filter(vizZoomFilter).on("zoom", e => render(e.transform));
|
||
d3.select(canvas).call(canvasZoom);
|
||
document.addEventListener("contextmenu", e => e.ctrlKey && e.preventDefault());
|
||
|
||
new ResizeObserver(([e]) => e.contentRect.width > 0 && resize()).observe(profiler.node());
|
||
profiler.on("scroll", () => render(zoomLevel));
|
||
|
||
function findRectAtPosition(x, y) {
|
||
let track = null;
|
||
for (const k of data.tracks.keys()) {
|
||
const r = rect(document.getElementById(k));
|
||
if (y >= r.y && y <= r.y+r.height) { track = data.tracks.get(k); break; }
|
||
}
|
||
if (track == null) return;
|
||
const R = rect(canvas);
|
||
const X = ((x-R.left) * (canvas.width/R.width))/dpr;
|
||
const Y = ((y-R.top) * (canvas.height/R.height))/dpr;
|
||
for (const r of track.visible) {
|
||
if (Y>=r.y0 && Y<=r.y1 && X>=r.x0 && X<=r.x1) return r.arg;
|
||
}
|
||
}
|
||
|
||
const clickShape = (e) => {
|
||
e.preventDefault();
|
||
const foundRect = findRectAtPosition(e.clientX, e.clientY);
|
||
if (foundRect?.step != null && (foundRect?.key == null || e.type == "dblclick")) { return switchCtx(foundRect.ctx, foundRect.step); }
|
||
if (foundRect?.key != focusedShape) { setFocus(foundRect?.key); }
|
||
}
|
||
canvas.addEventListener("click", clickShape);
|
||
canvas.addEventListener("dblclick", clickShape);
|
||
|
||
canvas.addEventListener("mousemove", e => {
|
||
const foundRect = findRectAtPosition(e.clientX, e.clientY);
|
||
const tooltip = document.getElementById("tooltip");
|
||
if (foundRect?.tooltipText != null) {
|
||
tooltip.replaceChildren(colored(foundRect.label||[]), document.createTextNode(foundRect.tooltipText));
|
||
tooltip.style.display = "block";
|
||
tooltip.style.left = (e.pageX+10)+"px";
|
||
tooltip.style.top = (e.pageY)+"px";
|
||
tooltip.dataset.key = foundRect.key ?? "";
|
||
} else tooltip.style.display = "none";
|
||
});
|
||
canvas.addEventListener("mouseleave", () => document.getElementById("tooltip").style.display = "none");
|
||
}
|
||
|
||
// ** zoom and recentering
|
||
|
||
const vizZoomFilter = e => (!e.ctrlKey || e.type === 'wheel' || e.type === 'mousedown') && !e.button && e.type !== 'dblclick';
|
||
const svgZoom = d3.zoom().filter(vizZoomFilter).on("zoom", (e) => d3.select("#render").attr("transform", e.transform));
|
||
d3.select("#graph-svg").call(svgZoom);
|
||
|
||
// zoom to fit into view
|
||
document.getElementById("zoom-to-fit-btn").addEventListener("click", () => {
|
||
const canvas = d3.select("#timeline");
|
||
if (!canvas.empty() && rect(canvas.node()).width !== 0) {
|
||
return canvas.call(canvasZoom.transform, getZoomIdentity());
|
||
}
|
||
const svg = d3.select("#graph-svg");
|
||
svg.call(svgZoom.transform, d3.zoomIdentity);
|
||
const mainRect = rect(".main-container");
|
||
const x0 = rect(".ctx-list-parent").right;
|
||
const x1 = rect(".metadata-parent").left;
|
||
const pad = 16;
|
||
const R = { x: x0+pad, y: mainRect.top+pad, width: (x1>0 ? x1-x0 : mainRect.width)-2*pad, height: mainRect.height-2*pad };
|
||
const r = rect("#render");
|
||
if (r.width === 0) return;
|
||
const scale = Math.min(R.width/r.width, R.height/r.height);
|
||
const [tx, ty] = [R.x+(R.width-r.width*scale)/2-r.left*scale, R.y+(R.height-r.height*scale)/2];
|
||
svg.call(svgZoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
|
||
});
|
||
|
||
// **** main VIZ interfacae
|
||
|
||
const pathLink = (fp, lineno) => d3.create("a").attr("href", "vscode://file/"+fp+":"+lineno).text(`${fp.split("/").at(-1)}:${lineno}`);
|
||
function codeBlock(st, language, { loc, wrap }={}) {
|
||
const code = document.createElement("code");
|
||
// plaintext renders like a terminal print, otherwise render with syntax highlighting
|
||
if (!language || language === "txt") code.appendChild(colored(st));
|
||
else code.innerHTML = hljs.highlight(st, { language }).value;
|
||
code.className = "hljs";
|
||
const ret = document.createElement("pre");
|
||
if (wrap) ret.className = "wrap";
|
||
if (loc != null) ret.appendChild(pathLink(loc[0], loc[1]).style("margin-bottom", "4px").node());
|
||
ret.appendChild(code);
|
||
return ret;
|
||
}
|
||
function traceBlock(trace) {
|
||
const root = d3.create("pre").append("code").classed("hljs", true);
|
||
for (let i=trace.length-1; i>=0; i--) {
|
||
const [fp, lineno, fn, code] = trace[i];
|
||
root.append("div").style("margin-bottom", "2px").style("display","flex").text(fn+" ").append(() => pathLink(fp, lineno).node());
|
||
root.append("div").html(hljs.highlight(code, { language: "python" }).value).style("margin-bottom", "1ex");
|
||
}
|
||
return root.node().parentNode;
|
||
}
|
||
|
||
function toggleCls(prev, next, cls, value) {
|
||
prev?.classList.remove(cls);
|
||
next?.classList.toggle(cls, value ?? true);
|
||
requestAnimationFrame(() => next?.scrollIntoView({ behavior: "auto", block: "nearest" }));
|
||
}
|
||
|
||
// ** hljs extra definitions for UOps and float4
|
||
hljs.registerLanguage("python", (hljs) => ({
|
||
...hljs.getLanguage("python"),
|
||
case_insensitive: false,
|
||
contains: [
|
||
{ begin: 'dtypes\\.[a-zA-Z_][a-zA-Z0-9_-]*(\\.[a-zA-Z_][a-zA-Z0-9_-]*)*' + '(?=[.\\s\\n[:,(])', className: "type" },
|
||
{ begin: 'dtypes\\.[a-zA-Z_][a-zA-Z0-9_-].vec*' + '(?=[.\\s\\n[:,(])', className: "type" },
|
||
{ begin: '[a-zA-Z_][a-zA-Z0-9_-]*\\.[a-zA-Z_][a-zA-Z0-9_-]*' + '(?=[.\\s\\n[:,()])', className: "operator" },
|
||
{ begin: '[A-Z][a-zA-Z0-9_]*(?=\\()', className: "section", ignoreEnd: true },
|
||
...hljs.getLanguage("python").contains,
|
||
]
|
||
}));
|
||
hljs.registerLanguage("cpp", (hljs) => ({
|
||
...hljs.getLanguage('cpp'),
|
||
contains: [{ begin: '\\b(?:float|half)[0-9]+\\b', className: 'type' }, ...hljs.getLanguage('cpp').contains]
|
||
}));
|
||
|
||
async function fetchValue(path) {
|
||
const res = await fetch(path);
|
||
return (await (res.headers.get("content-type") === "application/json" ? res.json() : res.arrayBuffer()));
|
||
}
|
||
|
||
var ret = [];
|
||
var cache = {};
|
||
var ctxs = null;
|
||
const evtSources = [];
|
||
// VIZ displays graph rewrites in 3 levels, from bottom-up:
|
||
// rewrite: a single UOp transformation
|
||
// step: collection of rewrites
|
||
// context: collection of steps
|
||
const state = {currentCtx:-1, currentStep:0, currentRewrite:0, expandSteps:false, callSrcMask:new Set(), expandedNodes:new Set()};
|
||
function setState(ns) {
|
||
saveToHistory(state);
|
||
const { ctx:prevCtx, step:prevStep } = select(state.currentCtx, state.currentStep);
|
||
const prevRewrite = state.currentRewrite;
|
||
Object.assign(state, ns);
|
||
// update element styles if needed
|
||
const { ctx, step } = select(state.currentCtx, state.currentStep);
|
||
toggleCls(prevCtx, ctx, "expanded", state.expandSteps);
|
||
if (ctx?.id !== prevCtx?.id) {
|
||
toggleCls(prevCtx, ctx, "active");
|
||
}
|
||
if (ctx?.id !== prevCtx?.id || step?.id !== prevStep?.id) {
|
||
toggleCls(prevStep, step, "active");
|
||
// walk the tree back until all parents expanded so that the child is visible
|
||
let e = step;
|
||
while (e?.parentElement?.id.startsWith("step")) {
|
||
e.parentElement.classList.add("expanded");
|
||
e = e.parentElement;
|
||
}
|
||
}
|
||
// re-render
|
||
main();
|
||
}
|
||
|
||
const getSubrewrites = (ul) => ul.querySelectorAll(":scope > ul");
|
||
|
||
function saveToHistory(ns) {
|
||
// NOTE: browser does a structured clone, passing a mutable object is safe.
|
||
history.replaceState(ns, "");
|
||
history.pushState(ns, "");
|
||
}
|
||
|
||
// switch to the start of a new graph and expand all the steps
|
||
const switchCtx = (newCtx, step) => setState({ expandSteps:true, currentCtx:newCtx+1, currentStep:step ?? 0, currentRewrite:0 });
|
||
|
||
window.addEventListener("popstate", (e) => {
|
||
if (e.state?.shape != null) return setFocus(e.state?.shape);
|
||
if (e.state != null) setState(e.state);
|
||
});
|
||
|
||
const createToggle = (id, text) => {
|
||
const label = d3.create("label").text(text).node();
|
||
const toggle = d3.create("input").attr("type", "checkbox").attr("id", id).property("checked", true).node();
|
||
label.prepend(toggle);
|
||
return { toggle, label };
|
||
}
|
||
const showIndexing = createToggle("show-indexing", "Show indexing (r)");
|
||
const showCallSrc = createToggle("show-call-src", "Show all CALL src (c)"); showCallSrc.toggle.checked = false;
|
||
const showSink = createToggle("show-sink", "Show SINK (s)");
|
||
showSink.toggle.checked = false;
|
||
const showGraph = createToggle("show-graph", "Show graph (g)");
|
||
showGraph.toggle.onchange = () => displaySelection(rect("#graph").width > 0 ? "#custom" : "#graph");
|
||
|
||
function appendSteps(root, idx, steps) {
|
||
const stack = [];
|
||
for (const [j,u] of steps.entries()) {
|
||
while (stack.length && stack.at(-1).depth >= u.depth) stack.pop();
|
||
const list = stack.length > 0 ? stack.at(-1).li : root;
|
||
u.li = list.appendChild(document.createElement("ul"));
|
||
u.li.id = `step-${idx}-${j}`
|
||
const p = u.li.appendChild(document.createElement("p"));
|
||
p.appendChild(colored(`${u.name}`+(u.match_count ? ` - ${u.match_count}` : '')));
|
||
p.onclick = (e) => {
|
||
e.stopPropagation();
|
||
const subrewrites = getSubrewrites(e.currentTarget.parentElement);
|
||
if (subrewrites.length) { e.currentTarget.parentElement.classList.toggle("expanded"); }
|
||
setState({ currentStep:j, currentCtx:idx, currentRewrite:0 });
|
||
}
|
||
stack.push(u);
|
||
}
|
||
for (const l of root.querySelectorAll("ul > ul > p")) {
|
||
const subrewrites = getSubrewrites(l.parentElement);
|
||
if (subrewrites.length > 0) { l.appendChild(d3.create("span").text(` (${subrewrites.length})`).node()); l.parentElement.classList.add("has-children"); }
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
// ** left sidebar context list
|
||
if (ctxs == null) {
|
||
ctxs = [{ name:"Profiler", steps:[] }];
|
||
for (const r of await fetchValue("/ctxs")) ctxs.push(r);
|
||
const ctxList = document.querySelector(".ctx-list");
|
||
for (const [i,{name, steps}] of ctxs.entries()) {
|
||
const ul = ctxList.appendChild(document.createElement("ul"));
|
||
ul.id = `ctx-${i}`;
|
||
const p = ul.appendChild(document.createElement("p"));
|
||
p.appendChild(colored(name));
|
||
p.onclick = () => {
|
||
setState(i === state.currentCtx ? { expandSteps:!state.expandSteps } : { expandSteps:true, currentCtx:i, currentStep:0, currentRewrite:0 });
|
||
}
|
||
appendSteps(ul, i, steps);
|
||
}
|
||
return setState({ currentCtx:-1 });
|
||
}
|
||
// ** center graph
|
||
const { currentCtx, currentStep, currentRewrite, expandSteps } = state;
|
||
if (currentCtx == -1) return;
|
||
const ctx = ctxs[currentCtx];
|
||
const step = ctx.steps[currentStep];
|
||
const ckey = step?.query;
|
||
// close any pending event sources
|
||
let activeSrc = null;
|
||
for (const e of evtSources) {
|
||
const url = new URL(e.url);
|
||
if (url.pathname+url.search !== ckey) e.close();
|
||
else if (e.readyState === EventSource.OPEN) activeSrc = e;
|
||
}
|
||
if (ctx.name === "Profiler") return renderProfiler("/get_profile", {unit:"ms", width:"132px"});
|
||
if (workerUrl == null) await initWorker();
|
||
if (ckey in cache) {
|
||
ret = cache[ckey];
|
||
}
|
||
if (!ckey.startsWith("/graph")) {
|
||
if (!(ckey in cache)) cache[ckey] = ret = await fetchValue(ckey);
|
||
if (ret.steps?.length > 0) {
|
||
const el = select(state.currentCtx, state.currentStep);
|
||
if (el.step.querySelectorAll("ul").length === ret.steps.length) return;
|
||
// re render the list with new items
|
||
ctx.steps.push(...ret.steps);
|
||
while (el.ctx.children.length > 1) el.ctx.children[1].remove();
|
||
appendSteps(el.ctx, state.currentCtx, ctx.steps);
|
||
return setState({ currentStep:state.currentStep+1, expandSteps:true });
|
||
}
|
||
// timeline with cycles on the x axis
|
||
if (ret instanceof ArrayBuffer) {
|
||
const pkts = step.query.includes("sqtt");
|
||
return renderProfiler(ckey, {unit:"clk", heightScale:0.5, hideLabels:true, colorByName:pkts});
|
||
}
|
||
metadata.replaceChildren(...((ret.metadata ?? []).map((m) => {
|
||
return tabulate(m.map((e) => [e.label.trim(), typeof e.value === "string" ? e.value : formatUnit(e.value)]));
|
||
})));
|
||
// graph render
|
||
if (ret.data != null) {
|
||
metadata.prepend(showGraph.label);
|
||
renderDag(ret, { recenter:true });
|
||
} else displaySelection("#custom");
|
||
// table / plaintext render
|
||
const root = d3.create("div").classed("raw-text", true);
|
||
function renderTable(root, ret) {
|
||
const table = root.append("table");
|
||
const thead = table.append("thead");
|
||
for (const c of ret.cols) thead.append("th").text(c.title ?? c);
|
||
for (const r of ret.rows) {
|
||
const tr = table.append("tr").classed("main-row", true);
|
||
for (const [i,value] of r.entries()) {
|
||
// nested table
|
||
if (value.cols != null) {
|
||
tr.classed("has-children", true);
|
||
tr.on("click", () => {
|
||
const el = tr.node().nextElementSibling;
|
||
if (el?.classList.contains("nested-row")) { tr.classed("expanded", false); return el.remove(); }
|
||
tr.classed("expanded", true);
|
||
const td = table.insert("tr", () => tr.node().nextSibling).classed("nested-row", true).append("td");
|
||
td.attr("colSpan", ret.cols.length);
|
||
renderTable(td, value);
|
||
});
|
||
continue;
|
||
}
|
||
const td = tr.append("td").classed(ret.cols[i], true);
|
||
// string format scalar values
|
||
td.append(() => typeof value === "string" ? colored(value) : d3.create("p").text(ret.cols[i] === "Duration" ? formatMicroseconds(value) : formatUnit(value)).node());
|
||
}
|
||
}
|
||
return table;
|
||
}
|
||
if (ret.ref != null) {
|
||
const disasmIdx = ctxs[ret.ref+1].steps.findIndex(s => s.name === "View Disassembly")
|
||
metadata.appendChild(d3.create("a").text("View Disassembly").on("click", () => switchCtx(ret.ref, disasmIdx)).node());
|
||
}
|
||
if (ret.cols != null) renderTable(root, ret);
|
||
else if (ret.src != null) root.append(() => codeBlock(ret.src, ret.lang));
|
||
return document.querySelector("#custom").replaceChildren(root.node());
|
||
}
|
||
// ** Graph view
|
||
// if we don't have a complete cache yet we start streaming graphs in this step
|
||
if (!(ckey in cache) || (cache[ckey].length !== step.match_count+1 && activeSrc == null)) {
|
||
ret = [];
|
||
cache[ckey] = ret;
|
||
const eventSource = new EventSource(ckey);
|
||
evtSources.push(eventSource);
|
||
eventSource.onmessage = (e) => {
|
||
if (e.data === "[DONE]") return eventSource.close();
|
||
const chunk = JSON.parse(e.data);
|
||
ret.push(chunk);
|
||
// if it's the first one render this new rgaph
|
||
if (ret.length === 1) return main();
|
||
// otherwise just enable the graph selector
|
||
const ul = document.getElementById(`rewrite-${ret.length-1}`);
|
||
if (ul != null) ul.classList.remove("disabled");
|
||
};
|
||
}
|
||
if (ret.length === 0) return;
|
||
// ** center graph
|
||
const data = ret[currentRewrite];
|
||
const render = (layoutOpts, renderOpts) => renderDag({ data, opts:layoutOpts }, renderOpts);
|
||
const getOpts = () => ({ showIndexing:showIndexing.toggle.checked, showCallSrc:showCallSrc.toggle.checked, showSink:showSink.toggle.checked,
|
||
callSrcMask:state.callSrcMask, expandedNodes:state.expandedNodes });
|
||
render(getOpts(), { recenter:currentRewrite === 0 });
|
||
showIndexing.toggle.onchange = () => render(getOpts(), { recenter:true });
|
||
showCallSrc.toggle.onchange = () => { state.callSrcMask.clear(); render(getOpts(), { recenter:true }); }
|
||
showSink.toggle.onchange = () => render(getOpts(), { recenter:true });
|
||
// ** right sidebar metadata
|
||
metadata.innerHTML = "";
|
||
if (ckey.includes("rewrites")) metadata.append(showIndexing.label, showCallSrc.label, showSink.label);
|
||
if (step.code_line != null) metadata.appendChild(codeBlock(step.code_line, "python", { loc:step.loc, wrap:true }));
|
||
if (step.trace) metadata.appendChild(traceBlock(step.trace));
|
||
if (data.uop != null) metadata.appendChild(codeBlock(data.uop, "python", { wrap:false })).classList.toggle("full-height", step.match_count === 0);
|
||
// ** multi graph in one page
|
||
if (!step.match_count) return;
|
||
const rewriteList = metadata.appendChild(document.createElement("div"));
|
||
rewriteList.className = "rewrite-list";
|
||
for (let s=0; s<=step.match_count; s++) {
|
||
const ul = rewriteList.appendChild(document.createElement("ul"));
|
||
ul.id = `rewrite-${s}`;
|
||
const p = ul.appendChild(document.createElement("p"));
|
||
p.innerText = s;
|
||
ul.onclick = () => setState({ currentRewrite:s });
|
||
ul.className = s > ret.length-1 ? "disabled" : s === currentRewrite ? "active" : "";
|
||
if (s > 0 && s === currentRewrite) {
|
||
const { upat, diff } = ret[s];
|
||
metadata.appendChild(codeBlock(upat[1], "python", { loc:upat[0], wrap:true }));
|
||
const diffCode = metadata.appendChild(document.createElement("pre")).appendChild(document.createElement("code"));
|
||
for (const line of diff) {
|
||
diffCode.appendChild(colored([{st:line, color:line.startsWith("+") ? "#3aa56d" : line.startsWith("-") ? "#d14b4b" : "#f0f0f5"}]));
|
||
diffCode.appendChild(document.createElement("br"));
|
||
}
|
||
diffCode.className = "wrap";
|
||
}
|
||
}
|
||
}
|
||
|
||
// **** collapse/expand
|
||
|
||
let isCollapsed = false;
|
||
document.querySelector(".collapse-btn").addEventListener("click", (e) => {
|
||
isCollapsed = !isCollapsed;
|
||
document.querySelector(".main-container").classList.toggle("collapsed", isCollapsed);
|
||
e.currentTarget.blur();
|
||
e.currentTarget.style.transform = isCollapsed ? "rotate(180deg)" : "rotate(0deg)";
|
||
window.dispatchEvent(new Event("resize"));
|
||
});
|
||
|
||
// **** resizer
|
||
|
||
function appendResizer(element, { minWidth, maxWidth }, left=false) {
|
||
const handle = Object.assign(document.createElement("div"), { className: "resize-handle", style: left ? "right: 0" : "left: 0; margin-top: 0" });
|
||
element.appendChild(handle);
|
||
const resize = (e) => {
|
||
const change = e.clientX - element.dataset.startX;
|
||
let newWidth = ((Number(element.dataset.startWidth)+(left ? change : -change))/Number(element.dataset.containerWidth))*100;
|
||
element.style.width = `${Math.max(minWidth, Math.min(maxWidth, newWidth))}%`;
|
||
};
|
||
handle.addEventListener("mousedown", (e) => {
|
||
e.preventDefault();
|
||
element.dataset.startX = e.clientX;
|
||
element.dataset.containerWidth = rect(".main-container").width;
|
||
element.dataset.startWidth = element.getBoundingClientRect().width;
|
||
document.documentElement.addEventListener("mousemove", resize, false);
|
||
document.documentElement.addEventListener("mouseup", () => {
|
||
document.documentElement.removeEventListener("mousemove", resize, false);
|
||
element.style.userSelect = "initial";
|
||
}, { once: true });
|
||
});
|
||
}
|
||
appendResizer(document.querySelector(".ctx-list-parent"), { minWidth: 15, maxWidth: 50 }, left=true);
|
||
appendResizer(document.querySelector(".metadata-parent"), { minWidth: 20, maxWidth: 50 });
|
||
|
||
// **** keyboard shortcuts
|
||
|
||
const select = (ctx, step) => ({ ctx:document.getElementById(`ctx-${ctx}`), step:document.getElementById(`step-${ctx}-${step}`) });
|
||
const deselect = (element) => {
|
||
const parts = element?.id.split("-").map(Number);
|
||
return element?.id.startsWith("ctx") ? { ctx:parts[1], step:null } : element?.id.startsWith("step") ? {ctx:parts[1], step:parts[2]} : {};
|
||
}
|
||
const isExpanded = (el) => el?.classList.contains("expanded");
|
||
|
||
document.addEventListener("keydown", (event) => {
|
||
const { currentCtx, currentStep, currentRewrite, expandSteps } = state;
|
||
// up and down change the step or context from the list
|
||
const changeStep = expandSteps && ctxs[currentCtx].steps?.length;
|
||
const { step, ctx } = select(currentCtx, currentStep);
|
||
if (event.key == "ArrowUp") {
|
||
event.preventDefault();
|
||
if (changeStep) {
|
||
let prev = deselect(step.previousElementSibling);
|
||
if (prev.step == null && isExpanded(step.parentElement)) prev = deselect(step.parentElement);
|
||
return prev.step != null && !isExpanded(step) && setState({ currentRewrite:0, currentStep:prev.step });
|
||
}
|
||
return setState({ currentStep:0, currentRewrite:0, currentCtx:Math.max(0, currentCtx-1), expandSteps:false });
|
||
}
|
||
if (event.key == "ArrowDown") {
|
||
event.preventDefault();
|
||
if (changeStep) {
|
||
const next = deselect(isExpanded(step) ? step.children[1] : step.nextElementSibling);
|
||
return next.step != null && setState({ currentRewrite:0, currentStep:next.step });
|
||
}
|
||
return setState({ currentStep:0, currentRewrite:0, currentCtx:Math.min(ctxs.length-1, currentCtx+1), expandSteps:false });
|
||
}
|
||
// enter toggles focus on a single rewrite stage
|
||
if (event.key == "Enter") {
|
||
event.preventDefault()
|
||
if (currentCtx === -1) {
|
||
return setState({ currentCtx:0, expandSteps:true });
|
||
}
|
||
if (expandSteps && getSubrewrites(step).length) return step.children[0].click();
|
||
return setState({ expandSteps:!expandSteps });
|
||
}
|
||
// left and right go through rewrites in a single UOp, in profiler go forward/backward in time
|
||
if (event.key == "ArrowLeft" || event.key == "ArrowRight") {
|
||
event.preventDefault()
|
||
if (profiler.style.display !== "none" && focusedShape != null) {
|
||
const [t, idx] = focusedShape.split("-");
|
||
const i = parseInt(idx), last = data.tracks.get(t).shapes.length-1;
|
||
return setFocus(`${t}-${event.key == "ArrowLeft" ? Math.max(0, i-1) : Math.min(last, i+1)}`);
|
||
}
|
||
if (event.key == "ArrowLeft") return setState({ currentRewrite:Math.max(0, currentRewrite-1) });
|
||
const totalRewrites = ret.length-1;
|
||
return setState({ currentRewrite:Math.min(totalRewrites, currentRewrite+1) });
|
||
}
|
||
// space recenters the graph
|
||
if (event.key == " ") {
|
||
event.preventDefault()
|
||
document.getElementById("zoom-to-fit-btn").click();
|
||
}
|
||
// r key toggles indexing
|
||
if (event.key === "r") showIndexing.toggle.click();
|
||
// c key toggles CALL src
|
||
if (event.key === "c" && !event.ctrlKey && !event.metaKey && !event.altKey) showCallSrc.toggle.click();
|
||
// s key toggles SINK
|
||
if (event.key === "s") showSink.toggle.click();
|
||
// g key toggles graph
|
||
if (event.key === "g") showGraph.toggle.click();
|
||
});
|
||
|
||
main()
|