// @ts-check
//
const traceResult = getTransformedTraceResult();
function onLoad() {
const appData = {
traceIndex: traceResult.traces.length - 1,
selectedNodeId: traceResult.traces[traceResult.traces.length - 1].printNodeId,
};
const slider = createSlider(value => {
appData.traceIndex = value;
appData.selectedNodeId = traceResult.traces[value].printNodeId;
refreshApp();
});
const codeView = createCodeView();
const infoArea = createInfoArea();
const graph = createGraph(printNodeId => {
const traceIndex = getNextTraceIndex();
// might not have had a trace that visited this print node
if (traceIndex >= 0) {
appData.traceIndex = traceIndex;
}
appData.selectedNodeId = printNodeId;
refreshApp();
function getNextTraceIndex() {
if (appData.selectedNodeId === printNodeId) {
const traceIndex = traceResult.traces.findIndex((t, index) => index > appData.traceIndex && t.printNodeId === printNodeId);
if (traceIndex >= 0) {
return traceIndex;
}
}
return traceResult.traces.findIndex(t => t.printNodeId === printNodeId);
}
});
const mainElement = document.createElement("div");
mainElement.id = "main";
const splitViewElement = document.createElement("div");
splitViewElement.id = "split-view";
splitViewElement.appendChild(codeView.element);
const graphContainer = document.createElement("div");
graphContainer.id = "graph-container";
graphContainer.appendChild(graph.element);
const nodeInfoArea = createNodeInfoArea();
graphContainer.appendChild(nodeInfoArea.element);
splitViewElement.appendChild(graphContainer);
mainElement.appendChild(splitViewElement);
mainElement.appendChild(infoArea.element);
mainElement.appendChild(slider.element);
document.body.appendChild(mainElement);
refreshApp();
function refreshApp() {
slider.setMax(traceResult.traces.length - 1);
slider.setValue(appData.traceIndex);
codeView.setTraceIndex(appData.traceIndex);
infoArea.setTraceIndex(appData.traceIndex);
graph.setSelectedNodeId(appData.selectedNodeId);
nodeInfoArea.setSelectedNodeId(appData.selectedNodeId);
}
}
/** @param {import("./types").PrintNode} node */
function getLastNodes(node) {
while (node.nextPrintNodeId != null) {
node = traceResult.getPrintNode(node.nextPrintNodeId);
}
/** @type {import("./types").PrintNode[]} */
const lastNodes = [];
if (node.printItem.kind === "condition") {
const condition = node.printItem.content;
if (condition.truePath == null || condition.falsePath == null) {
lastNodes.push(node);
}
if (condition.truePath != null) {
lastNodes.push(...getLastNodes(traceResult.getPrintNode(condition.truePath)));
}
if (condition.falsePath != null) {
lastNodes.push(...getLastNodes(traceResult.getPrintNode(condition.falsePath)));
}
} else if (node.printItem.kind === "rcPath") {
lastNodes.push(...getLastNodes(traceResult.getPrintNode(node.printItem.content)));
} else {
lastNodes.push(node);
}
return lastNodes;
}
/** @param {import("./types").PrintNode} node */
function getNodeHoverText(node) {
const printItem = node.printItem;
switch (printItem.kind) {
case "condition":
return `Condition: ${printItem.content.name} (${node.printNodeId})`;
case "info":
return `Info: ${printItem.content.content.name} - ${printItem.content.kind} (${node.printNodeId})`;
case "rcPath":
return `RcPath (${node.printNodeId})`;
case "signal":
return `Signal: ${printItem.content} (${node.printNodeId})`;
case "string":
return `String: ${printItem.content} (${node.printNodeId})`;
case "anchor":
return `Anchor: ${printItem.content.name} (${node.printNodeId})`;
case "conditionReevaluation":
return `Condition reevaluation: ${printItem.content.name} (${printItem.content.conditionId}) (${node.printNodeId})`;
}
}
function getNodesAndLinks() {
/** @type {import("./types").GraphPrintNode[]} */
const nodes = traceResult.printNodes.map(node => ({ id: node.printNodeId, printNode: node, sources: [], targets: [], depthY: 0 }));
const nodesMap = new Map(nodes.map(n => [n.printNode.printNodeId, n]));
/** @type {{ source: number; target: number; color: string | undefined; originatingNodeId: number | undefined }[]} */
const links = [];
for (const node of nodes) {
const printItem = node.printNode.printItem;
const printNode = node.printNode;
if (printItem.kind === "rcPath") {
const target = getNodeById(printItem.content);
addLink(node, target);
addLinksToLastNodes(node, target);
} else if (printItem.kind === "condition") {
const condition = printItem.content;
if (condition.truePath != null) {
const target = getNodeById(condition.truePath);
addLink(node, target, "green");
addLinksToLastNodes(node, target);
}
if (condition.falsePath != null) {
const target = getNodeById(condition.falsePath);
addLink(node, target, "red");
addLinksToLastNodes(node, target);
}
if ((condition.truePath == null || condition.falsePath == null) && printNode.nextPrintNodeId != null) {
addLink(node, getNodeById(printNode.nextPrintNodeId));
}
} else if (printNode.nextPrintNodeId != null) {
addLink(node, getNodeById(printNode.nextPrintNodeId));
}
}
setDepthY(nodes[0]);
return { nodes, links };
/** @param {import("./types").GraphPrintNode} firstNode */
function setDepthY(firstNode) {
if (firstNode.sources.length > 0) {
throw new Error("Must provide the root node.");
}
/** @type {Set} */
const analyzedNodes = new Set();
const nodesToAnalyze = [firstNode];
while (nodesToAnalyze.length > 0) {
const node = nodesToAnalyze.pop();
if (node == null) {
continue;
}
node.depthY = node.sources.length === 0 ? 0 : Math.max(...node.sources.map(s => s.depthY)) + 1;
if (!analyzedNodes.has(node.printNode.printNodeId)) {
analyzedNodes.add(node.printNode.printNodeId);
nodesToAnalyze.push(...node.targets);
}
}
}
/**
* @param {import("./types").GraphPrintNode} source
* @param {import("./types").GraphPrintNode} target
*/
function addLinksToLastNodes(source, target) {
if (source.printNode.nextPrintNodeId != null) {
const nextPrintNodeTarget = getNodeById(source.printNode.nextPrintNodeId);
for (const lastNode of getLastNodes(target.printNode)) {
addLink(getNodeById(lastNode.printNodeId), nextPrintNodeTarget, undefined, source.printNode.printNodeId);
}
}
}
/**
* @param {import("./types").GraphPrintNode} source
* @param {import("./types").GraphPrintNode} target
* @param {string} [color]
* @param {number} [originatingNodeId]
*/
function addLink(source, target, color, originatingNodeId) {
if (color == null && source.printNode.printItem.kind === "condition") {
const condition = source.printNode.printItem.content;
color = condition.truePath == null && condition.falsePath == null
? undefined
: condition.falsePath == null
? "red"
: "green";
}
source.targets.push(target);
target.sources.push(source);
links.push({
source: source.id,
target: target.id,
color,
originatingNodeId,
});
}
/** @param {number} id */
function getNodeById(id) {
const node = nodesMap.get(id);
if (node == null) {
throw new Error(`Could not find node: ${id}`);
}
return node;
}
}
/** @param {(printNodeId: number) => void} onPrintNodeSelect */
function createGraph(onPrintNodeSelect) {
const { links, nodes } = getNodesAndLinks();
let wasMouseActivity = false;
const width = 400;
const height = 400;
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(/** @param {any} d */ d => d.id).distance(10))
.force("charge", d3.forceManyBody().strength(-3000))
.force("y", d3.forceY().y(/** @param {any} d */ d => d.depthY * 125));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("font", "40px sans-serif")
.on("wheel", () => wasMouseActivity = true)
.on("click", () => wasMouseActivity = true);
const arrow = svg.append("svg:defs").selectAll("marker")
.data(["end"])
.enter().append("svg:marker")
.attr("id", String)
.attr("orient", "auto");
const arrowInnerPath = arrow.append("svg:path").attr("fill", "#000");
const drag = d3
.drag()
.on("drag", /** @param {any} event @param {any} d */ function(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this).raise().attr("transform", `translate(${d.x}, ${d.y})`);
refreshLinks();
});
const nodeRadius = 15;
const linkThickness = 5;
const linkG = svg.append("g");
const link = linkG
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-opacity", 0.6)
.attr("stroke", /** @param {any} d */ d => getLineColor(d))
.attr("data-originating-node-id", /** @param {any} d */ d => d.originatingNodeId)
.style("stroke-width", linkThickness)
.attr("marker-end", "url(#end)")
.on("click", /** @param {any} _ @param {any} d */ (_, d) => {
/** @type {number | undefined} */
const originatingNodeId = d.originatingNodeId;
if (originatingNodeId != null) {
onPrintNodeSelect(originatingNodeId);
}
});
link.append("title")
.text(/** @param {any} d */ d => {
if (d.originatingNodeId != null) {
return getNodeHoverText(traceResult.getPrintNode(d.originatingNodeId));
}
return undefined;
});
const nodeG = svg.append("g");
const nodeGInner = nodeG.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.call(drag);
const nodeCircle = nodeGInner
.append("circle")
.attr("r", nodeRadius)
.attr("fill", /** @param {any} d */ d => getNodeColor(d.printNode))
.attr("stroke", "#000")
.attr("id", /** @param {any} d */ d => `node${d.id}`)
.on("click", /** @param {any} _ @param {any} d */ (_, d) => {
onPrintNodeSelect(d.id);
});
nodeGInner
.append("text")
.attr("x", 50)
.attr("y", "0.31em")
.text(/** @param {any} d */ d => getNodeHoverText(d.printNode))
.clone(true).lower()
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 3);
/** @type {any} */
let transform;
/** @type {number} */
let sqrtK;
const zoom = d3.zoom().on("zoom", /** @param {any} e */ e => {
transform = e.transform;
nodeG.attr("transform", transform);
sqrtK = Math.sqrt(transform.k);
nodeCircle.attr("r", nodeRadius / sqrtK)
.attr("stroke-width", 1 / sqrtK);
linkG.attr("transform", transform);
link.style("stroke-width", linkThickness / sqrtK);
arrow.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("viewBox", `0 0 ${5 / sqrtK} ${5 / sqrtK}`)
.attr("refX", 8 / sqrtK)
.attr("refY", 2.5 / sqrtK);
arrowInnerPath.attr("d", `M 0 0 L ${5 / sqrtK} ${2.5 / sqrtK} L 0 ${5 / sqrtK} z`);
refreshSelectedNode();
});
simulation.on("tick", () => {
refreshLinks();
let minX = Number.MAX_SAFE_INTEGER;
let maxX = Number.MIN_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER;
let maxY = Number.MIN_SAFE_INTEGER;
nodeGInner
.attr("transform", /** @param {any} d */ d => {
minX = Math.min(minX, d.x);
maxX = Math.max(maxX, d.x);
minY = Math.min(minY, d.y);
maxY = Math.max(maxY, d.y);
return `translate(${d.x}, ${d.y})`;
});
if (!wasMouseActivity) {
svg.call(
zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(0.95 / Math.max((maxX - minX) / width, (maxY - minY) / height))
.translate(-(minX + maxX) / 2, -(minY + maxY) / 2),
);
}
});
function refreshLinks() {
link
.attr("x1", /** @param {any} d */ d => d.source.x)
.attr("y1", /** @param {any} d */ d => d.source.y)
.attr("x2", /** @param {any} d */ d => d.target.x)
.attr("y2", /** @param {any} d */ d => d.target.y);
}
let lastId = 0;
return {
element: svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity)
.node(),
/** @param {number} selectedNodeId */
setSelectedNodeId(selectedNodeId) {
d3.select(`#node${lastId}`)
.attr("stroke", "#000")
.attr("stroke-width", 1 / sqrtK)
.attr("r", nodeRadius / sqrtK);
d3.selectAll(`[data-originating-node-id="${lastId}"]`)
.style("stroke-width", linkThickness / sqrtK)
.attr("marker-end", "url(#end)");
lastId = selectedNodeId;
refreshSelectedNode();
},
};
function refreshSelectedNode() {
d3.select(`#node${lastId}`)
.attr("stroke", "red")
.attr("stroke-width", 4 / sqrtK)
.attr("r", (nodeRadius + 10) / sqrtK);
d3.selectAll(`[data-originating-node-id="${lastId}"]`)
.style("stroke-width", (linkThickness + 15) / sqrtK)
// not worth the hassle to resize this
.attr("marker-end", "");
}
/** @param {any} d */
function getLineColor(d) {
return d.color || (d.originatingNodeId != null ? "blue" : "#000");
}
}
function createNodeInfoArea() {
const containerElement = document.createElement("div");
containerElement.id = "node-info-area";
const colorRectangle = document.createElement("span");
colorRectangle.id = "color-rectangle";
containerElement.appendChild(colorRectangle);
const nameElement = document.createElement("span");
containerElement.appendChild(nameElement);
return {
element: containerElement,
/** @param {number} selectedNodeId */
setSelectedNodeId(selectedNodeId) {
const printNode = traceResult.getPrintNode(selectedNodeId);
colorRectangle.style.backgroundColor = getNodeColor(printNode);
nameElement.textContent = getNodeHoverText(printNode);
},
};
}
function createInfoArea() {
const mainElement = document.createElement("div");
mainElement.id = "info-area";
const currentTimeLabel = document.createElement("label");
currentTimeLabel.textContent = "Time:";
mainElement.appendChild(currentTimeLabel);
const timeSpan = document.createElement("span");
mainElement.appendChild(timeSpan);
return {
element: mainElement,
/** @param {number} index */
setTraceIndex(index) {
const trace = traceResult.traces[index];
timeSpan.textContent = formatNanos(trace.nanos);
},
};
}
function createCodeView() {
const tabChars = "→ ";
const spaceChar = "·";
const mainElement = document.createElement("div");
mainElement.id = "code-view";
let lastTraceIndex = -1;
return {
element: mainElement,
/** @param {number} index */
setTraceIndex(index) {
if (lastTraceIndex === index) {
return;
}
const trace = traceResult.traces[index];
clearElementChildren(mainElement);
if (trace.writerNodeId != null) {
const startWriterNode = traceResult.getWriterNode(trace.writerNodeId);
/** @type {HTMLElement[]} */
const elements = [];
fillWriterNodeElements(startWriterNode, elements);
elements.reverse(); // reverse to get them in forward order
for (const childElement of elements) {
mainElement.appendChild(childElement);
}
}
// scroll to the bottom
mainElement.scrollTop = mainElement.scrollHeight;
lastTraceIndex = index;
},
};
/**
* @param {import("./types").WriterNode} node
* @param {HTMLElement[]} elements
*/
function fillWriterNodeElements(node, elements) {
if (node.text === "\n" || node.text === "\r\n") {
elements.push(document.createElement("br"));
} else {
const texts = node.text.split(/\r?\n/);
for (const [i, text] of texts.entries()) {
if (i > 0) {
elements.push(document.createElement("br"));
}
for (const segment of extractTextSegments(text).reverse()) {
if (segment.kind === "text") {
const element = document.createElement("span");
element.className = "writer-node";
element.innerText = segment.text;
elements.push(element);
} else if (segment.kind === "space") {
const element = document.createElement("span");
element.className = "writer-node writer-node-space";
element.innerText = spaceChar.repeat(segment.count);
elements.push(element);
} else if (segment.kind === "tab") {
const element = document.createElement("span");
element.className = "writer-node writer-node-tab";
element.innerHTML = tabChars.repeat(segment.count);
elements.push(element);
}
}
}
}
if (node.previousNodeId != null) {
fillWriterNodeElements(
traceResult.getWriterNode(node.previousNodeId),
elements,
);
}
}
}
/** @param {HTMLElement} element */
function clearElementChildren(element) {
let last;
while (last = element.lastChild) {
element.removeChild(last);
}
}
/** @param {(value: number) => void} onChange */
function createSlider(onChange) {
const element = document.createElement("div");
element.id = "slider";
const input = document.createElement("input");
input.type = "range";
input.addEventListener("input", () => {
// todo: debounce
onChange(input.valueAsNumber);
});
input.min = "0";
element.appendChild(input);
return {
element,
/** @param {number} max */
setMax(max) {
if (input.max !== max.toString()) {
input.max = max.toString();
}
},
/** @param {number} value */
setValue(value) {
if (input.value !== value.toString()) {
input.value = value.toString();
}
},
};
}
function getTransformedTraceResult() {
const writerNodes = getWriterNodesMap();
const printNodes = getPrintNodesMap();
return {
traces: rawTraceResult.traces,
printNodes: rawTraceResult.printNodes,
/** @param {number} id */
getWriterNode(id) {
const node = writerNodes.get(id);
if (node == null) {
throw new Error(`Could not find writer node ${id}.`);
}
return node;
},
/** @param {number} id */
getPrintNode(id) {
const node = printNodes.get(id);
if (node == null) {
throw new Error(`Could not find print node ${id}.`);
}
return node;
},
};
function getWriterNodesMap() {
/** @type {Map} */
const map = new Map();
for (const node of rawTraceResult.writerNodes) {
map.set(node.writerNodeId, node);
}
return map;
}
function getPrintNodesMap() {
/** @type {Map} */
const map = new Map();
for (const node of rawTraceResult.printNodes) {
map.set(node.printNodeId, node);
}
return map;
}
}
/** @param {import("./types").PrintNode} node */
function getNodeColor(node) {
switch (node.printItem.kind) {
case "info":
return "blue";
case "condition":
return "orange";
case "signal":
return "yellow";
case "rcPath":
return "green";
case "string":
return "#ccc";
case "anchor":
return "pink";
case "conditionReevaluation":
return "purple";
}
}
/** @param {number} nanos */
function formatNanos(nanos) {
const characters = nanos.toString();
let finalText = "";
for (let i = 0; i < characters.length; i++) {
if (i > 0 && i % 3 === 0) {
finalText = "," + finalText;
}
finalText = characters[characters.length - 1 - i] + finalText;
}
return finalText + "ns";
}
/** @param {string} text */
function extractTextSegments(text) {
let spaceCount = 0;
let tabCount = 0;
let lastIndex = 0;
/** @type {import("./types").CodeViewTextSegment[]} */
const segments = [];
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === " ") {
addTabIfNecessary();
addLastTextIfNecessary(i);
spaceCount++;
lastIndex = i + 1;
} else if (char === "\t") {
addSpaceIfNecessary();
addLastTextIfNecessary(i);
tabCount++;
lastIndex = i + 1;
} else {
addSpaceIfNecessary();
addTabIfNecessary();
}
}
addLastTextIfNecessary(text.length);
addSpaceIfNecessary();
addTabIfNecessary();
return segments;
function addTabIfNecessary() {
if (tabCount > 0) {
segments.push({
kind: "tab",
count: tabCount,
});
tabCount = 0;
}
}
function addSpaceIfNecessary() {
if (spaceCount > 0) {
segments.push({
kind: "space",
count: spaceCount,
});
spaceCount = 0;
}
}
/** @param {number} currentIndex */
function addLastTextIfNecessary(currentIndex) {
if (lastIndex !== currentIndex) {
segments.push({
kind: "text",
text: text.substring(lastIndex, currentIndex),
});
}
}
}