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.

592 lines
25 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);
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 => {
const ret = [[]];
for (const { st, color } of parseColors(d.label, defaultColor="initial")) {
for (const [i, l] of st.split("\n").entries()) {
if (i > 0) ret.push([]);
ret.at(-1).push({ st:l, color });
}
}
return [ret];
}).join("text").selectAll("tspan").data(d => d).join("tspan").attr("x", "0").attr("dy", 14).selectAll("tspan").data(d => d).join("tspan")
.attr("fill", d => d.color).text(d => d.st).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();
};
}
const ANSI_COLORS = ["#b3b3b3", "#ff6666", "#66b366", "#ffff66", "#6666ff", "#ff66ff", "#66ffff", "#ffffff"];
const parseColors = (name, defaultColor="#ffffff") => [...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] : defaultColor }));
// ** profiler graph
function formatTime(ts, dur=ts) {
if (dur<=1e3) return `${ts.toFixed(2)}us`;
if (dur<=1e6) return `${(ts*1e-3).toFixed(2)}ms`;
return `${(ts*1e-6).toFixed(2)}s`;
}
const formatUnit = (d, unit="") => d3.format(".3~s")(d)+unit;
const colors = ["#1D1F2A", "#2A2D3D", "#373B4F", "#444862", "#12131A", "#2F3244", "#3B3F54", "#4A4E65", "#181A23", "#232532", "#313548", "#404459"];
const bufColors = ["#3A57B7","#5066C1","#6277CD","#7488D8","#8A9BE3","#A3B4F2"];
var profileRet, focusedDevice, canvasZoom, zoomLevel = d3.zoomIdentity;
async function renderProfiler() {
displayGraph("profiler");
d3.select(".metadata").html("");
const profiler = d3.select(".profiler").html("");
const deviceList = profiler.append("div").attr("id", "device-list").node();
const canvas = profiler.append("canvas").attr("id", "timeline").node();
if (profileRet == null) profileRet = await (await fetch("/get_profile")).json()
const { layout, st, et } = profileRet;
const kernelMap = new Map();
for (const [i, c] of ctxs.entries()) kernelMap.set(c.function_name, { name:c.name, i });
// place devices on the y axis and set vertical positions
const [tickSize, padding] = [10, 8];
deviceList.style.paddingTop = `${tickSize+padding}px`;
const ctx = canvas.getContext("2d");
const { top:canvasTop, height:canvasHeight } = rect(canvas);
// color by name
const nameMap = new Map();
const data = {shapes:[], axes:{}};
for (const [k, { timeline, mem }] of Object.entries(layout)) {
if (timeline.shapes.length === 0 && mem.shapes.length == 0) continue;
const div = deviceList.appendChild(document.createElement("div"));
div.innerText = k;
div.style.padding = `${padding}px`;
div.onclick = () => { // TODO: make this feature more visible
focusedDevice = k === focusedDevice ? null : k;
renderProfiler();
}
const { y:baseY, height:baseHeight } = rect(div);
const levelHeight = baseHeight-padding;
const offsetY = baseY-canvasTop+padding/2;
for (const [i,e] of timeline.shapes.entries()) {
const kernel = kernelMap.get(e.name);
if (!nameMap.has(e.name)) {
const label = parseColors(kernel?.name ?? e.name).map(({ color, st }) => ({ color, st, width:ctx.measureText(st).width }));
nameMap.set(e.name, { fillColor:colors[i%colors.length], label });
}
// offset y by depth
data.shapes.push({ x:e.st-st, dur:e.dur, name:e.name, height:levelHeight, y:offsetY+levelHeight*e.depth, kernel, ...nameMap.get(e.name) });
}
// position shapes on the canvas and scale to fit fixed area
const startY = offsetY+(levelHeight*timeline.maxDepth)+padding/2;
let area = 40;
if (k === focusedDevice) {
// expand memory graph for the focused device
area = canvasHeight-baseY;
data.axes.y = { domain:[0, mem.peak], range:[startY+area, startY], fmt:"B" };
}
const yscale = d3.scaleLinear().domain([0, mem.peak]).range([startY+area, startY]);
for (const [i,e] of mem.shapes.entries()) {
const x = e.x.map((i,_) => (mem.timestamps[i] ?? et)-st);
const y1 = e.y.map(yscale);
const y2 = e.y.map(y => yscale(y+e.arg.nbytes));
data.shapes.push({ x, y1, y2, arg:e.arg, color:bufColors[i%bufColors.length] });
}
// lastly, adjust device rect by number of levels
div.style.height = `${Math.max(levelHeight*timeline.maxDepth, baseHeight)+area+padding}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);
// rescale to match current zoom
const xscale = d3.scaleLinear().domain([0, et-st]).range([0, canvas.clientWidth]);
xscale.domain(xscale.range().map(zoomLevel.invertX, zoomLevel).map(xscale.invert, xscale));
let yscale = null;
if (data.axes.y != null) {
yscale = d3.scaleLinear().domain(data.axes.y.domain).range(data.axes.y.range);
}
// draw shapes
for (const e of data.shapes) {
if (Array.isArray(e.x)) {
const x = e.x.map(xscale);
ctx.beginPath();
ctx.moveTo(x[0], e.y1[0]);
for (let i=1; i<x.length; i++) ctx.lineTo(x[i], e.y1[i]);
for (let i=x.length-1; i>=0; i--) ctx.lineTo(x[i], e.y2[i]);
ctx.closePath();
ctx.fillStyle = e.color;
ctx.fill();
const tooltipText = `${e.arg.dtype} len:${formatUnit(e.arg.sz)}\n${formatUnit(e.arg.nbytes, "B")} `;
for (let i = 0; i < x.length - 1; i++) rectLst.push({ x0:x[i], x1:x[i+1], y0:e.y2[i], y1:e.y1[i], tooltipText });
continue;
}
// zoom only changes x and width
const x = xscale(e.x);
const width = xscale(e.x+e.dur)-x;
ctx.fillStyle = e.fillColor;
ctx.fillRect(x, e.y, width, e.height);
rectLst.push({ y0:e.y, y1:e.y+e.height, x0:x, x1:x+width, ref:e.kernel?.i, tooltipText:formatTime(e.dur) });
// add label
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.label.entries()) {
if (labelWidth+l.width+(i===e.label.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;
}
}
// draw axes
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(canvas.clientWidth, 0);
ctx.fillStyle = ctx.strokeStyle = "#f0f0f5";
ctx.lineWidth = 1;
ctx.stroke();
const ticks = xscale.ticks();
for (const [i, tick] of ticks.entries()) {
ctx.beginPath();
// tick line
const x = xscale(tick);
ctx.moveTo(x, 0);
ctx.lineTo(x, tickSize);
ctx.stroke();
// tick label
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);
}
if (yscale != null) {
ctx.beginPath();
ctx.moveTo(0, yscale.range()[1]);
ctx.lineTo(0, yscale.range()[0]);
ctx.stroke();
for (const tick of yscale.ticks()) {
const y = yscale(tick);
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(tickSize, y);
ctx.stroke();
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(formatUnit(tick, data.axes.y.fmt), tickSize+2, y);
}
}
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).call(canvasZoom.transform, zoomLevel);
document.addEventListener("contextmenu", e => e.ctrlKey && e.preventDefault());
function findRectAtPosition(x, y) {
const { top, left, width, height } = rect(canvas);
const X = ((x-left) * (canvas.width/width))/dpr;
const Y = ((y-top) * (canvas.height/height))/dpr;
for (const r of rectLst) {
if (Y>=r.y0 && Y<=r.y1 && X>=r.x0 && X<=r.x1) return r;
}
}
canvas.addEventListener("click", e => {
e.preventDefault();
const foundRect = findRectAtPosition(e.clientX, e.clientY);
if (foundRect?.ref != null) return setCtxWithHistory(foundRect.ref);
});
const tooltip = document.body.appendChild(document.createElement("div"));
tooltip.id = "tooltip";
canvas.addEventListener("mousemove", e => {
const foundRect = findRectAtPosition(e.clientX, e.clientY);
if (foundRect?.tooltipText != null) {
tooltip.style.display = "block";
tooltip.style.left = (e.pageX+10)+"px";
tooltip.style.top = (e.pageY)+"px";
tooltip.innerText = foundRect.tooltipText;
} else tooltip.style.display = "none";
});
canvas.addEventListener("mouseleave", () => tooltip.style.display = "none");
}
// ** 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 (!canvas.empty() && 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;
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()