openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

671 lines
28 KiB

// **** graph renderers
const displayGraph = (cls) => {
for (const e of document.getElementsByClassName("view")) e.style.display = e.classList.contains(cls) ? "flex" : "none";
}
// ** UOp graph
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};
}
const rect = (s) => (typeof s === "string" ? document.querySelector(s) : s).getBoundingClientRect();
let [workerUrl, worker, timeout] = [null, null, null];
async function renderDag(graph, additions, recenter=false) {
// start calculating the new layout (non-blocking)
if (worker == null) {
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" }));
worker = new Worker(workerUrl);
} else {
worker.terminate();
worker = new Worker(workerUrl);
}
if (timeout != null) clearTimeout(timeout);
const progressMessage = document.querySelector(".progress-message");
timeout = setTimeout(() => {progressMessage.style.display = "block"}, 2000);
worker.postMessage({graph, additions, ctxs});
worker.onmessage = (e) => {
displayGraph("graph");
progressMessage.style.display = "none";
clearTimeout(timeout);
d3.select("#bars").html("");
const g = dagre.graphlib.json.read(e.data);
// draw nodes
const STROKE_WIDTH = 1.4;
const nodes = d3.select("#nodes").selectAll("g").data(g.nodes().map(id => g.node(id)), d => d).join("g")
.attr("transform", d => `translate(${d.x},${d.y})`).classed("clickable", d => d.ref != null)
.on("click", (_,d) => setCtxWithHistory(d.ref));
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).attr("style", d => d.style ?? `stroke:#4a4b57; stroke-width:${STROKE_WIDTH}px;`);
nodes.selectAll("g.label").data(d => [d]).join("g").attr("class", "label").attr("transform", d => {
const x = (d.width-d.padding*2)/2;
const y = (d.height-d.padding*2)/2+STROKE_WIDTH;
return `translate(-${x}, -${y})`;
}).selectAll("text").data(d => [d.label.split("\n")]).join("text").selectAll("tspan").data(d => d).join("tspan").text(d => d).attr("x", "0")
.attr("dy", 14).attr("xml:space", "preserve");
const tags = 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})`);
tags.selectAll("circle").data(d => [d]).join("circle");
tags.selectAll("text").data(d => [d.tag]).join("text").text(d => d).attr("dy", "0.35em");
// draw edges
const line = d3.line().x(d => d.x).y(d => d.y).curve(d3.curveBasis);
d3.select("#edges").selectAll("path.edgePath").data(g.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)");
const edgeLabels = d3.select("#edge-labels").selectAll("g").data(g.edges().filter(e => g.edge(e).label != null)).join("g").attr("transform", (e) => {
// get a point near the end
const [p1, p2] = g.edge(e).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", "tag");
edgeLabels.selectAll("circle").data(e => [g.edge(e).label]).join("circle");
edgeLabels.selectAll("text").data(e => [g.edge(e).label]).join("text").text(d => d).attr("dy", "0.35em");
if (recenter) document.getElementById("zoom-to-fit-btn").click();
};
}
// ** Memory graph (WIP)
DTYPE_SIZE = {"bool": 1, "char": 1, "uchar": 1, "short": 2, "ushort": 2, "int": 4, "uint": 4,
"long": 8, "ulong": 8, "half": 2, "bfloat": 2, "float": 4, "double": 8}
function getBuffer(e) {
const [_, size, dtype, num, device] = e.label.split("\n");
return {nbytes:size*DTYPE_SIZE[dtype.split("dtypes.")[1]], dtype, device:device.split(" ")[1], num:parseInt(num.split(" ")[1])};
}
function pluralize(num, name, alt=null) {
return num === 1 ? `${num} ${name}` : `${num} ${alt ?? name+'s'}`
}
function renderMemoryGraph(graph) {
displayGraph("graph");
// ** construct alloc/free traces
// we can map reads/writes from the kernel graph
const actions = [];
const children = new Map(); // {buffer: [...assign]}
for (const [k,v] of Object.entries(graph)) {
if (!v.label.startsWith("ASSIGN")) continue;
actions.push({ op: "write", buffer: v.src[0] });
for (const ks of graph[v.src[1]].src) {
const node = graph[ks];
const s = node.label.startsWith("ASSIGN") ? node.src[0] : ks;
if (!children.has(s)) children.set(s, []);
children.get(s).push(v);
if (s !== v.src[0]) actions.push({ op: "read", buffer: s });
}
}
const prealloc = new Set();
const traces = [];
for (const a of actions) {
// a buffer is allocated immediately before the first write
// TODO: we don't know the buffer is preallocated if there's only an assign in the graph
if (a.op === "write") {
traces.push({ type: "alloc", buffer: a.buffer });
}
else {
if (traces.find(t => t.buffer === a.buffer && t.type === "alloc") == null) {
prealloc.add(a.buffer);
}
else if (a === actions.findLast(({ buffer }) => buffer === a.buffer)) {
traces.push({type: "free", buffer: a.buffer });
}
}
}
// ** get coordinates and layout for each buffer
const ret = {};
let timestep = 0; // x
let memUsed = 0; // y
for (const id of prealloc) {
const buf = getBuffer(graph[id]);
ret[id] = { x: [timestep], y: [memUsed], buf, id };
memUsed += buf.nbytes;
}
let peak = memUsed;
const liveBufs = [...prealloc];
for (const t of traces) {
const buf = getBuffer(graph[t.buffer]);
const idx = liveBufs.findLastIndex(b => t.buffer === b);
// alloc
if (idx === -1) {
liveBufs.push(t.buffer);
ret[t.buffer] = { x: [timestep], y: [memUsed], buf, id: t.buffer };
memUsed += buf.nbytes;
peak = Math.max(memUsed, peak);
timestep += 1;
} // free
else {
memUsed -= buf.nbytes;
timestep += 1;
const removed = ret[liveBufs.splice(idx, 1)[0]];
removed.x.push(timestep);
removed.y.push(removed.y.at(-1));
if (idx < liveBufs.length) {
for (let j=idx; j<liveBufs.length; j++) {
const b = ret[liveBufs[j]];
b.x.push(timestep, timestep);
b.y.push(b.y.at(-1), b.y.at(-1)-buf.nbytes);
}
}
}
}
for (const id of liveBufs) {
const b = ret[id];
b.x.push(timestep);
b.y.push(b.y.at(-1));
}
// ** render traces
// clear existing groups
document.querySelector(".progress-message").style.display = "none";
for (c of document.getElementById("render").children) c.innerHTML = "";
const render = d3.select("#bars");
const yscale = d3.scaleLinear().domain([0, peak]).range([576, 0]);
const xscale = d3.scaleLinear().domain([0, timestep]).range([0, 1024]);
const axesGroup = render.append("g").attr("id", "axes");
const nbytes_format = (d) => d3.format(".3~s")(d)+"B";
axesGroup.append("g").call(d3.axisLeft(yscale).tickFormat(nbytes_format));
axesGroup.append("g").attr("transform", `translate(0, ${yscale.range()[0]})`).call(d3.axisBottom(xscale).tickFormat(() => ""));
const polygonGroup = render.append("g").attr("id", "polygons");
const colors = ["7aa2f7", "ff9e64", "f7768e", "2ac3de", "7dcfff", "1abc9c", "9ece6a", "e0af68", "bb9af7", "9d7cd8", "ff007c"];
const polygons = polygonGroup.selectAll("polygon").data(Object.values(ret)).join("polygon").attr("points", (d) => {
const xs = d.x.map(t => xscale(t));
const y1 = d.y.map(t => yscale(t));
const y2 = d.y.map(t => yscale(t+d.buf.nbytes));
const p0 = xs.map((x, i) => `${x},${y1[i]}`);
const p1 = xs.map((x, i) => `${x},${y2[i]}`).reverse();
return `${p0.join(' ')} ${p1.join(' ')}`;
}).attr("fill", d => `#${colors[d.buf.num % colors.length]}`).on("mouseover", (e, { id, buf, x }) => {
d3.select(e.currentTarget).attr("stroke", "rgba(26, 27, 38, 0.8)").attr("stroke-width", 0.8);
const metadata = document.querySelector(".metadata");
document.getElementById("current-buf")?.remove();
const { num, dtype, nbytes, ...rest } = buf;
let label = `<BUFFER n${num} ${dtype} ${nbytes_format(nbytes)}>\nalive for ${pluralize(x[x.length-1]-x[0], 'timestep')}`;
label += '\n'+Object.entries(rest).map(([k, v]) => `${k}=${v}`).join('\n');
const buf_children = children.get(id);
if (buf_children) {
label += `\n${pluralize(buf_children.length, 'child', 'children')}\n`;
label += buf_children.map((c,i) => `[${i+1}] `+graph[c.src[1]].label.split("\n")[1]).join("\n");
}
metadata.appendChild(Object.assign(document.createElement("pre"), { innerText: label, id: "current-buf", className: "wrap" }));
}).on("mouseout", (e, _) => {
d3.select(e.currentTarget).attr("stroke", null).attr("stroke-width", null);
document.getElementById("current-buf")?.remove()
});
// TODO: add the kernel line here
document.getElementById("zoom-to-fit-btn").click();
}
const ANSI_COLORS = ["#b3b3b3", "#ff6666", "#66b366", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff"];
const parseColors = (name) => [...name.matchAll(/(?:\u001b\[(\d+)m([\s\S]*?)\u001b\[0m)|([^\u001b]+)/g)].map(([_, code, colored_st, st]) =>
({ st: colored_st ?? st, color: code != null ? ANSI_COLORS[(parseInt(code)-30+60)%60] : "#ffffff" }));
// ** profiler graph
function formatTime(ts, dur) {
if (dur<=1e3) return `${ts}us`;
if (dur<=1e6) return `${(ts*1e-3).toFixed(2)}ms`;
return `${(ts*1e-6).toFixed(2)}s`;
}
const colors = ["#1D1F2A", "#2A2D3D", "#373B4F", "#444862", "#12131A", "#2F3244", "#3B3F54", "#4A4E65", "#181A23", "#232532", "#313548", "#404459"];
var data, canvasZoom, zoomLevel = d3.zoomIdentity;
async function renderProfiler() {
displayGraph("profiler");
d3.select(".metadata").html("");
if (data != null) return;
// fetch and process data
const { traceEvents } = await (await fetch("/get_profile")).json();
let st, et;
const events = new Map();
for (const e of traceEvents) {
if (e.name === "process_name") events.set(e.pid, { name:e.args.name, events:[] });
if (e.ph === "X") {
if (st == null) [st, et] = [e.ts, e.ts+e.dur];
else {
st = Math.min(st, e.ts);
et = Math.max(et, e.ts+e.dur);
}
events.get(e.pid).events.push(e);
}
}
const kernelMap = new Map();
for (const [i, c] of ctxs.entries()) kernelMap.set(c.name.replace(/\x1b\[\d+m(.*?)\x1b\[0m/g, "$1"), { name:c.name, i });
// place devices on the y axis and set vertical positions
const [tickSize, padding] = [10, 8];
const deviceList = document.getElementById("device-list");
deviceList.style.paddingTop = `${tickSize+padding}px`;
const canvas = document.getElementById("timeline");
const ctx = canvas.getContext("2d");
const canvasTop = rect(canvas).top;
// color by name
const nameMap = new Map();
data = [];
for (const [k, v] of events) {
if (v.events.length === 0) continue;
const div = deviceList.appendChild(document.createElement("div"));
div.id = `pid-${k}`;
div.innerText = v.name;
div.style.padding = `${padding}px`;
const { y:baseY, height:baseHeight } = rect(`#pid-${k}`);
// position events on the y axis, stack ones that overlap
const levels = [];
v.events.sort((a,b) => (a.ts-st) - (b.ts-st));
for (const [i,e] of v.events.entries()) {
// assign to the first free depth
const start = e.ts-st;
const end = start+e.dur;
let depth = levels.findIndex(l => start >= l);
if (depth === -1) {
depth = levels.length;
levels.push(end);
} else {
levels[depth] = end;
}
// offset y by depth
const height = baseHeight-padding;
const y = (baseY-canvasTop+padding/2)+height*depth;
if (!nameMap.has(e.name)) {
const labelParts = parseColors(kernelMap.get(e.name)?.name ?? e.name).map(({ color, st }) => ({ color, st, width:ctx.measureText(st).width }));
nameMap.set(e.name, { bgColor:colors[i%colors.length], labelParts });
}
data.push({ x:start, dur:e.dur, name:e.name, height, y, ...nameMap.get(e.name) });
}
// lastly, adjust device rect by number of levels
div.style.height = `${baseHeight*levels.length}px`;
}
// draw events on a timeline
const dpr = window.devicePixelRatio || 1;
const ellipsisWidth = ctx.measureText("...").width;
const rectLst = [];
function render(transform=null) {
if (transform != null) zoomLevel = transform;
rectLst.length = 0;
ctx.save();
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
// time axis
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(canvas.clientWidth, 0);
ctx.fillStyle = ctx.strokeStyle = "#f0f0f5";
ctx.lineWidth = 1;
ctx.stroke();
// xticks
const scale = d3.scaleLinear().domain([0, et-st]).range([0, canvas.clientWidth]);
scale.domain(scale.range().map(zoomLevel.invertX, zoomLevel).map(scale.invert, scale));
const ticks = scale.ticks();
for (const [i, tick] of ticks.entries()) {
ctx.beginPath();
const x = (i/(ticks.length-1))*canvas.clientWidth;
ctx.moveTo(x, ctx.lineWidth);
ctx.lineTo(x, tickSize+ctx.lineWidth);
ctx.stroke();
ctx.fontSize = "10px";
ctx.textBaseline = "top";
ctx.textAlign = i === ticks.length-1 ? "right" : "left";
const padding = i === ticks.length-1 ? -1 : 1;
ctx.fillText(formatTime(tick, et-st), x+(ctx.lineWidth+2)*padding, tickSize);
}
// programs
for (const e of data) {
// zoom only changes x and width
const x = scale(e.x);
const width = scale(e.x+e.dur)-x;
ctx.fillStyle = e.bgColor;
ctx.fillRect(x, e.y, width, e.height);
rectLst.push({ y0:e.y, y1:e.y+e.height, x0:x, x1:x+width, name:e.name })
// add labels
ctx.textAlign = "left";
ctx.textBaseline = "middle";
let [labelX, labelWidth] = [x+2, 0];
const labelY = e.y+e.height/2;
for (const [i,l] of e.labelParts.entries()) {
if (labelWidth+l.width+(i===e.labelParts.length-1 ? 0 : ellipsisWidth)+2 > width) {
if (labelWidth !== 0) ctx.fillText("...", labelX, labelY);
break;
}
ctx.fillStyle = l.color;
ctx.fillText(l.st, labelX, labelY);
labelWidth += l.width;
labelX += l.width;
}
}
ctx.restore();
}
function resize() {
let { width, height } = rect(".profiler");
width -= rect("#device-list").width+padding;
canvas.width = width*dpr;
canvas.height = height*dpr;
canvas.style.height = `${height}px`;
canvas.style.width = `${width}px`;
ctx.scale(dpr, dpr);
render();
}
resize();
window.addEventListener("resize", resize);
canvasZoom = d3.zoom().filter(e => (!e.ctrlKey || e.type === 'wheel' || e.type === 'mousedown') && !e.button)
.scaleExtent([1, Infinity]).translateExtent([[0,0], [Infinity,0]]).on("zoom", e => render(e.transform));
d3.select(canvas).call(canvasZoom);
document.addEventListener("contextmenu", e => e.ctrlKey && e.preventDefault());
canvas.addEventListener("click", e => {
e.preventDefault();
const { top, left, width, height } = rect(canvas);
const clickX = ((e.clientX-left) * (canvas.width/width))/dpr;
const clickY = ((e.clientY-top) * (canvas.height/height))/dpr;
for (const r of rectLst) {
if (clickY>=r.y0 && clickY<=r.y1 && clickX>=r.x0 && clickX<=r.x1) {
return setCtxWithHistory(kernelMap.get(r.name)?.i);
}
}
});
}
// ** zoom and recentering
const svgZoom = d3.zoom().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 (rect(canvas.node()).width !== 0) {
return canvas.call(canvasZoom.transform, d3.zoomIdentity);
}
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
function codeBlock(st, language, { loc, wrap }) {
const code = document.createElement("code");
code.innerHTML = hljs.highlight(st, { language }).value;
code.className = "hljs";
const ret = document.createElement("pre");
if (wrap) ret.className = "wrap";
if (loc != null) {
const link = ret.appendChild(document.createElement("a"));
link.href = "vscode://file"+loc.join(":");
link.textContent = `${loc[0].split("/").at(-1)}:${loc[1]}`+"\n\n";
}
ret.appendChild(code);
return ret;
}
function setActive(e) {
if (e == null) return;
e.classList.add("active");
requestAnimationFrame(() => e.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]
}));
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};
function setState(ns) {
const { currentCtx:prevCtx, currentStep:prevStep } = state;
Object.assign(state, ns);
// update element styles if needed
document.getElementById(`ctx-${state.currentCtx}`)?.classList.toggle("expanded", state.expandSteps);
if (state.currentCtx !== prevCtx) {
document.getElementById(`ctx-${prevCtx}`)?.classList.remove("active", "expanded");
setActive(document.getElementById(`ctx-${state.currentCtx}`));
}
if (state.currentCtx !== prevCtx || state.currentStep !== prevStep) {
document.getElementById(`step-${prevCtx}-${prevStep}`)?.classList.remove("active");
setActive(document.getElementById(`step-${state.currentCtx}-${state.currentStep}`));
}
// re-render
main();
}
// set a new context and keep the old one in browser history
function setCtxWithHistory(newCtx) {
if (newCtx == null) return;
// NOTE: browser does a structured clone, passing a mutable object is safe.
history.replaceState(state, "");
history.pushState(state, "");
setState({ expandSteps:true, currentCtx:newCtx, currentStep:0, currentRewrite:0 });
}
window.addEventListener("popstate", (e) => {
if (e.state != null) setState(e.state);
});
async function main() {
// ** left sidebar context list
if (ctxs == null) {
ctxs = [{ name:"Profiler", steps:[] }];
for (const r of (await (await fetch("/ctxs")).json())) 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.innerHTML = parseColors(name).map(c => `<span style="color: ${c.color}">${c.st}</span>`).join("");
p.onclick = () => {
setState(i === state.currentCtx ? { expandSteps:!state.expandSteps } : { expandSteps:true, currentCtx:i, currentStep:0, currentRewrite:0 });
}
for (const [j,u] of steps.entries()) {
const inner = ul.appendChild(document.createElement("ul"));
inner.id = `step-${i}-${j}`;
inner.innerText = `${u.name ?? u.loc[0].replaceAll("\\", "/").split("/").pop()+':'+u.loc[1]} - ${u.match_count}`;
inner.style.marginLeft = `${8*u.depth}px`;
inner.onclick = (e) => {
e.stopPropagation();
setState({ currentStep:j, currentCtx:i, currentRewrite:0 });
}
}
}
return setState({ currentCtx:-1 });
}
// ** center graph
const { currentCtx, currentStep, currentRewrite, expandSteps } = state;
if (currentCtx == -1) return;
const ctx = ctxs[currentCtx];
if (ctx.name === "Profiler") return renderProfiler();
const step = ctx.steps[currentStep];
const ckey = `ctx=${currentCtx-1}&idx=${currentStep}`;
// close any pending event sources
let activeSrc = null;
for (const e of evtSources) {
if (e.url.split("?")[1] !== ckey) e.close();
else if (e.readyState === EventSource.OPEN) activeSrc = e;
}
if (ckey in cache) {
ret = cache[ckey];
}
// if we don't have a complete cache yet we start streaming rewrites in this step
if (!(ckey in cache) || (cache[ckey].length !== step.match_count+1 && activeSrc == null)) {
ret = [];
cache[ckey] = ret;
const eventSource = new EventSource(`/ctxs?${ckey}`);
evtSources.push(eventSource);
eventSource.onmessage = (e) => {
if (e.data === "END") 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;
if (step.name == "View Memory Graph") {
renderMemoryGraph(ret[currentRewrite].graph);
} else {
renderDag(ret[currentRewrite].graph, ret[currentRewrite].changed_nodes || [], recenter=currentRewrite === 0);
}
// ** right sidebar code blocks
const metadata = document.querySelector(".metadata");
const [code, lang] = ctx.kernel_code != null ? [ctx.kernel_code, "cpp"] : [ret[currentRewrite].uop, "python"];
metadata.replaceChildren(codeBlock(step.code_line, "python", { loc:step.loc, wrap:true }), codeBlock(code, lang, { wrap:false }));
// ** rewrite steps
if (step.match_count >= 1) {
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.innerText = s;
ul.id = `rewrite-${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) {
const span = diffCode.appendChild(document.createElement("span"));
span.style.color = line.startsWith("+") ? "#3aa56d" : line.startsWith("-") ? "#d14b4b" : "#f0f0f5";
span.innerText = line;
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
document.addEventListener("keydown", async function(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;
if (event.key == "ArrowUp") {
event.preventDefault();
if (changeStep) {
return setState({ currentRewrite:0, currentStep:Math.max(0, currentStep-1) });
}
return setState({ currentStep:0, currentRewrite:0, currentCtx:Math.max(0, currentCtx-1), expandSteps:false });
}
if (event.key == "ArrowDown") {
event.preventDefault();
if (changeStep) {
const totalUOps = ctxs[currentCtx].steps.length-1;
return setState({ currentRewrite:0, currentStep:Math.min(totalUOps, currentStep+1) });
}
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 });
}
return setState({ expandSteps:!expandSteps });
}
// left and right go through rewrites in a single UOp
if (event.key == "ArrowLeft") {
event.preventDefault()
return setState({ currentRewrite:Math.max(0, currentRewrite-1) });
}
if (event.key == "ArrowRight") {
event.preventDefault()
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();
}
});
main()