blob: 4340bff65ecc9d9a00e1910f6b0a842191d7b115 [file] [log] [blame] [edit]
import { dequal } from "./dequal.js";
import { E } from "./dom.js";
import { Graph } from "./Graph.js";
import type { BlockPtr, Func, Pass, SampleCounts } from "./iongraph.js";
type KeyPasses = [number | null, number | null, number | null, number | null];
export interface GraphViewerProps {
func: Func,
pass?: number,
sampleCounts?: SampleCounts,
}
export class GraphViewer {
func: Func;
passNumber: number;
keyPasses: KeyPasses;
redundantPasses: number[];
sampleCounts: SampleCounts | undefined;
container: HTMLDivElement;
viewport: HTMLDivElement;
graph: Graph | null;
sidebarLinks: HTMLAnchorElement[];
constructor(root: HTMLElement, {
func,
pass = 0,
sampleCounts
}: GraphViewerProps) {
this.graph = null;
this.func = func;
this.passNumber = pass;
this.sampleCounts = sampleCounts;
this.keyPasses = [null, null, null, null];
{
let lastPass: Pass | null = null;
for (const [i, pass] of func.passes.entries()) {
if (pass.mir.blocks.length > 0) {
if (this.keyPasses[0] === null) {
this.keyPasses[0] = i;
}
if (pass.lir.blocks.length === 0) {
this.keyPasses[1] = i;
}
}
if (pass.lir.blocks.length > 0) {
if (lastPass?.lir.blocks.length === 0) {
this.keyPasses[2] = i;
}
this.keyPasses[3] = i;
}
lastPass = pass;
}
}
this.redundantPasses = [];
{
let lastPass: Pass | null = null;
for (const [i, pass] of func.passes.entries()) {
if (lastPass === null) {
lastPass = pass;
continue;
}
if (dequal(lastPass.mir, pass.mir) && dequal(lastPass.lir, pass.lir)) {
this.redundantPasses.push(i);
}
lastPass = pass;
}
}
this.viewport = E("div", ["ig-flex-grow-1", "ig-overflow-hidden"], div => {
div.style.position = "relative";
})
this.sidebarLinks = func.passes.map((pass, i) => (
E("a", ["ig-link-normal", "ig-pv1", "ig-ph2", "ig-flex", "ig-g2"], a => {
a.href = "#";
a.addEventListener("click", e => {
e.preventDefault();
this.switchPass(i);
});
a.style.minWidth = "100%";
}, [
E("div", ["ig-w1", "ig-tr", "ig-f6", "ig-text-dim"], div => {
div.style.paddingTop = "0.08rem";
div.style.flexShrink = "0";
}, [`${i}`]),
E("div", [this.redundantPasses.includes(i) && "ig-text-dim"], div => {
div.style.whiteSpace = "nowrap";
}, [pass.name]),
])
));
this.container = E("div", ["ig-absolute", "ig-absolute-fill", "ig-flex"], () => { }, [
E("div", ["ig-w5", "ig-br", "ig-flex-shrink-0", "ig-overflow-y-auto", "ig-bg-white"], div => {
div.style.overflowX = "auto";
}, [
...this.sidebarLinks,
]),
this.viewport,
]);
root.appendChild(this.container);
this.keydownHandler = this.keydownHandler.bind(this);
this.tweakHandler = this.tweakHandler.bind(this);
window.addEventListener("keydown", this.keydownHandler);
window.addEventListener("tweak", this.tweakHandler);
this.update();
}
destroy() {
this.container.remove();
window.removeEventListener("keydown", this.keydownHandler);
window.removeEventListener("tweak", this.tweakHandler);
}
update() {
// Update sidebar
for (const [i, link] of this.sidebarLinks.entries()) {
link.classList.toggle("ig-bg-primary", this.passNumber === i);
}
// Update graph
const previousState = this.graph?.exportState();
this.viewport.innerHTML = "";
this.graph = null;
const pass: Pass | undefined = this.func.passes[this.passNumber];
if (pass) {
try {
this.graph = new Graph(this.viewport, pass, { sampleCounts: this.sampleCounts });
if (previousState) {
this.graph.restoreState(previousState, { preserveSelectedBlockPosition: true });
}
} catch (e) {
this.viewport.innerHTML = "An error occurred while laying out the graph. See console.";
console.error(e);
}
}
}
switchPass(pass: number) {
this.passNumber = pass;
this.update();
}
keydownHandler(e: KeyboardEvent) {
switch (e.key) {
case "w":
case "s": {
this.graph?.navigate(e.key === "s" ? "down" : "up");
this.graph?.jumpToBlock(this.graph.lastSelectedBlockPtr);
} break;
case "a":
case "d": {
this.graph?.navigate(e.key === "d" ? "right" : "left");
this.graph?.jumpToBlock(this.graph.lastSelectedBlockPtr);
} break;
case "f": {
for (let i = this.passNumber + 1; i < this.func.passes.length; i++) {
if (!this.redundantPasses.includes(i)) {
this.switchPass(i);
break;
}
}
} break;
case "r": {
for (let i = this.passNumber - 1; i >= 0; i--) {
if (!this.redundantPasses.includes(i)) {
this.switchPass(i);
break;
}
}
} break;
case "1":
case "2":
case "3":
case "4": {
const keyPassIndex = ["1", "2", "3", "4"].indexOf(e.key);
const keyPass = this.keyPasses[keyPassIndex];
if (typeof keyPass === "number") {
this.switchPass(keyPass);
}
} break;
case "c": {
const selected = this.graph?.blocksByPtr.get(this.graph?.lastSelectedBlockPtr ?? -1 as BlockPtr);
if (selected) {
this.graph?.jumpToBlock(selected.ptr, { zoom: 1 });
}
} break;
};
}
tweakHandler() {
this.update();
}
}