Files
visual_debugger/vis-plugins/node-table-visualizer.js

708 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Custom "Debug Visualizer" (hediet.debug-visualizer) visualization.
// Renders a { kind: { nodeTable: true }, nodes: [...], edges: [...] } payload
// as a row of boxes -- one per struct/node -- where each box is a table of
// that node's member variables (address in the header, fields below,
// pointer values in hex), connected by arrows for pointer fields.
// Nodes are draggable, the canvas is pannable/zoomable, and a toolbar lets
// you switch between a force-directed ("Auto", vis-network-style) layout
// and the original left-to-right layered chain layout, or fit everything
// back into view.
//
// Registered via the "debugVisualizer.customVisualizerScriptPaths" setting;
// the extension hot-reloads this file on save, no debug session restart needed.
//
// Data shape produced by gdb/debug_graph.py's $debug_graph(...):
// nodes: [{ id: "0x...", fields: [{ name, value, isPointer? }, ...] }]
// edges: [{ from: "0x...", to: "0x...", label? }]
// roots: [{ name: "top", value: "0x..." }] -- external pointer variables
// (e.g. a local `top`/`end` in the caller's stack frame); drawn as a
// small pill that tracks above whichever node's id matches its value.
module.exports = function (register, lib) {
var sj = lib.semanticJson;
var sField = sj.sOpenObject({
name: sj.sString(),
value: sj.sString(),
isPointer: sj.sOptionalProp(sj.sBoolean(), {}),
});
var sNode = sj.sOpenObject({
id: sj.sString(),
fields: sj.sArrayOf(sField),
});
var sEdge = sj.sOpenObject({
from: sj.sString(),
to: sj.sString(),
label: sj.sOptionalProp(sj.sString(), {}),
});
var sRoot = sj.sOpenObject({
name: sj.sString(),
value: sj.sString(),
});
var sData = sj.sOpenObject({
kind: sj.sOpenObject({ nodeTable: sj.sLiteral(true) }),
nodes: sj.sArrayOf(sNode),
edges: sj.sArrayOf(sEdge),
roots: sj.sOptionalProp(sj.sArrayOf(sRoot), {}),
});
var HEADER_HEIGHT = 24;
var ROW_HEIGHT = 20;
var MAX_VISIBLE_ROWS = 10;
var NODE_WIDTH = 220;
var H_GAP = 70;
var V_GAP = 24;
var ROOT_BADGE_HEIGHT = 22;
var ROOT_GAP = 30;
var MIN_ZOOM = 0.15;
var MAX_ZOOM = 3;
// ---- seed layout: left-to-right BFS levels, also used as "Layered" mode ----
function layeredPositions(nodeIds, edges, boxHeights) {
var adjacency = {};
var incoming = {};
nodeIds.forEach(function (id) {
adjacency[id] = [];
incoming[id] = 0;
});
edges.forEach(function (e) {
if (adjacency[e.from]) adjacency[e.from].push(e.to);
if (incoming[e.to] !== undefined) incoming[e.to]++;
});
var level = {};
var queue = [];
nodeIds.forEach(function (id) {
if (incoming[id] === 0) {
level[id] = 0;
queue.push(id);
}
});
if (queue.length === 0 && nodeIds.length > 0) {
level[nodeIds[0]] = 0;
queue.push(nodeIds[0]);
}
var head = 0;
var maxLevel = 0;
while (head < queue.length) {
var cur = queue[head++];
var curLevel = level[cur];
maxLevel = Math.max(maxLevel, curLevel);
(adjacency[cur] || []).forEach(function (next) {
if (level[next] === undefined) {
level[next] = curLevel + 1;
queue.push(next);
}
});
}
var colCount = {};
var positions = {};
nodeIds.forEach(function (id) {
var lvl = level[id];
if (lvl === undefined) {
maxLevel++;
lvl = maxLevel;
}
var col = colCount[lvl] || 0;
colCount[lvl] = col + 1;
var x = lvl * (NODE_WIDTH + H_GAP) + NODE_WIDTH / 2;
var y = col * (Math.max(boxHeights[id], HEADER_HEIGHT + ROW_HEIGHT) + V_GAP) + boxHeights[id] / 2;
positions[id] = { x: x, y: y };
});
return positions;
}
// ---- simple O(n^2) force-directed layout, vis-network-style ----
function createSimulation(nodeList, edges) {
var REPULSION = 55000;
var SPRING_LENGTH = NODE_WIDTH + H_GAP;
var SPRING_STIFFNESS = 0.02;
var DAMPING = 0.82;
var GRAVITY = 0.01;
var byId = {};
nodeList.forEach(function (n) {
byId[n.id] = n;
});
function step() {
var fx = {};
var fy = {};
nodeList.forEach(function (n) {
fx[n.id] = 0;
fy[n.id] = 0;
});
for (var i = 0; i < nodeList.length; i++) {
for (var j = i + 1; j < nodeList.length; j++) {
var a = nodeList[i];
var b = nodeList[j];
var dx = a.x - b.x;
var dy = a.y - b.y;
var distSq = dx * dx + dy * dy || 0.01;
var dist = Math.sqrt(distSq);
var force = REPULSION / distSq;
var ux = dx / dist;
var uy = dy / dist;
fx[a.id] += ux * force;
fy[a.id] += uy * force;
fx[b.id] -= ux * force;
fy[b.id] -= uy * force;
}
}
edges.forEach(function (e) {
var a = byId[e.from];
var b = byId[e.to];
if (!a || !b) return;
var dx = b.x - a.x;
var dy = b.y - a.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
var diff = dist - SPRING_LENGTH;
var force = diff * SPRING_STIFFNESS;
var ux = dx / dist;
var uy = dy / dist;
fx[a.id] += ux * force;
fy[a.id] += uy * force;
fx[b.id] -= ux * force;
fy[b.id] -= uy * force;
});
var kinetic = 0;
nodeList.forEach(function (n) {
if (n.fixed) {
n.vx = 0;
n.vy = 0;
return;
}
fx[n.id] -= n.x * GRAVITY;
fy[n.id] -= n.y * GRAVITY;
n.vx = (n.vx + fx[n.id]) * DAMPING;
n.vy = (n.vy + fy[n.id]) * DAMPING;
n.x += n.vx;
n.y += n.vy;
kinetic += n.vx * n.vx + n.vy * n.vy;
});
return kinetic;
}
return { nodes: nodeList, byId: byId, step: step };
}
function el(tag, style, attrs) {
var e = document.createElement(tag);
if (style) Object.assign(e.style, style);
if (attrs) Object.keys(attrs).forEach(function (k) {
e.setAttribute(k, attrs[k]);
});
return e;
}
function button(label, colors) {
var b = el(
"button",
{
font: "12px var(--vscode-editor-font-family, monospace)",
padding: "3px 8px",
borderRadius: "4px",
border: "1px solid " + colors.border,
background: colors.headerBg,
color: colors.headerFg,
cursor: "pointer",
},
{ type: "button" }
);
b.textContent = label;
return b;
}
function render(data, target, theme) {
target.innerHTML = "";
Object.assign(target.style, {
position: "relative",
overflow: "hidden",
width: "100%",
height: "100%",
boxSizing: "border-box",
});
var dark = theme === "dark";
var colors = dark
? {
border: "#5a5a5a",
headerBg: "#333333",
headerFg: "#e8e8e8",
bodyBg: "#252526",
fieldName: "#9cdcfe",
fieldValue: "#d4d4d4",
pointerValue: "#ce9178",
edge: "#8a8a8a",
viewportBg: "#1e1e1e",
}
: {
border: "#b8b8b8",
headerBg: "#eeeeee",
headerFg: "#1a1a1a",
bodyBg: "#ffffff",
fieldName: "#0451a5",
fieldValue: "#1a1a1a",
pointerValue: "#a31515",
edge: "#6b6b6b",
viewportBg: "#fafafa",
};
var roots = data.roots || [];
var boxHeights = {};
data.nodes.forEach(function (n) {
var visibleRows = Math.min(n.fields.length, MAX_VISIBLE_ROWS);
boxHeights[n.id] = HEADER_HEIGHT + Math.max(visibleRows, 1) * ROW_HEIGHT;
});
var nodeIds = data.nodes.map(function (n) {
return n.id;
});
var seed = layeredPositions(nodeIds, data.edges, boxHeights);
var nodeList = data.nodes.map(function (n) {
var p = seed[n.id];
return { id: n.id, x: p.x, y: p.y, vx: 0, vy: 0, fixed: false, height: boxHeights[n.id] };
});
var byId = {};
nodeList.forEach(function (n) {
byId[n.id] = n;
});
var sim = createSimulation(nodeList, data.edges);
var layoutMode = "auto"; // "auto" (physics) | "layered" (static chain)
var running = true;
// ---- DOM: viewport (pan/zoom window) + world (transformed content) ----
var viewport = el("div", {
position: "absolute",
left: "0",
top: "0",
width: "100%",
height: "100%",
overflow: "hidden",
background: colors.viewportBg,
cursor: "grab",
});
var world = el("div", {
position: "absolute",
left: "0",
top: "0",
transformOrigin: "0 0",
});
var svgNs = "http://www.w3.org/2000/svg";
var svg = document.createElementNS(svgNs, "svg");
Object.assign(svg.style, { position: "absolute", left: "0", top: "0", overflow: "visible", pointerEvents: "none" });
var defs = document.createElementNS(svgNs, "defs");
var marker = document.createElementNS(svgNs, "marker");
marker.setAttribute("id", "dv-arrow");
marker.setAttribute("markerWidth", "8");
marker.setAttribute("markerHeight", "8");
marker.setAttribute("refX", "7");
marker.setAttribute("refY", "4");
marker.setAttribute("orient", "auto");
var arrowPath = document.createElementNS(svgNs, "path");
arrowPath.setAttribute("d", "M0,0 L8,4 L0,8 Z");
arrowPath.setAttribute("fill", colors.edge);
marker.appendChild(arrowPath);
defs.appendChild(marker);
svg.appendChild(defs);
world.appendChild(svg);
var edgePaths = data.edges.map(function (e) {
var line = document.createElementNS(svgNs, "path");
line.setAttribute("fill", "none");
line.setAttribute("stroke", colors.edge);
line.setAttribute("stroke-width", "1.5");
line.setAttribute("marker-end", "url(#dv-arrow)");
svg.appendChild(line);
var text = null;
if (e.label) {
text = document.createElementNS(svgNs, "text");
text.setAttribute("fill", colors.edge);
text.setAttribute("font-size", "11");
text.setAttribute("font-family", "monospace");
text.setAttribute("text-anchor", "middle");
text.textContent = e.label;
svg.appendChild(text);
}
return { edge: e, line: line, text: text };
});
var boxes = {};
data.nodes.forEach(function (n) {
var box = el("div", {
position: "absolute",
width: NODE_WIDTH + "px",
border: "1px solid " + colors.border,
borderRadius: "6px",
background: colors.bodyBg,
boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
fontFamily: "var(--vscode-editor-font-family, monospace)",
fontSize: "12px",
overflow: "hidden",
cursor: "grab",
});
var header = el("div", {
padding: "4px 8px",
background: colors.headerBg,
color: colors.headerFg,
fontWeight: "bold",
borderBottom: "1px solid " + colors.border,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
});
header.textContent = n.id;
box.appendChild(header);
var body = el("div", { maxHeight: MAX_VISIBLE_ROWS * ROW_HEIGHT + "px", overflowY: "auto" });
var table = el("table", { borderCollapse: "collapse", width: "100%" });
n.fields.forEach(function (f) {
var row = el("tr");
var nameCell = el("td", {
padding: "2px 8px",
color: colors.fieldName,
whiteSpace: "nowrap",
height: ROW_HEIGHT - 4 + "px",
});
nameCell.textContent = f.name;
var valueCell = el("td", {
padding: "2px 8px",
color: f.isPointer ? colors.pointerValue : colors.fieldValue,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "0",
width: "100%",
});
valueCell.textContent = f.value;
row.appendChild(nameCell);
row.appendChild(valueCell);
table.appendChild(row);
});
body.appendChild(table);
box.appendChild(body);
world.appendChild(box);
boxes[n.id] = box;
});
var rootBadges = roots.map(function (r) {
var matched = !!byId[r.value];
var badge = el("div", {
position: "absolute",
width: NODE_WIDTH / 2 + "px",
height: ROOT_BADGE_HEIGHT + "px",
lineHeight: ROOT_BADGE_HEIGHT + "px",
textAlign: "center",
borderRadius: "11px",
border: "1px solid " + colors.border,
background: matched ? colors.headerBg : "transparent",
color: matched ? colors.headerFg : colors.pointerValue,
fontFamily: "var(--vscode-editor-font-family, monospace)",
fontSize: "12px",
fontWeight: "bold",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
});
badge.title = r.name + " = " + r.value;
badge.textContent = r.name + (matched ? "" : " (" + r.value + ")");
world.appendChild(badge);
var line = null;
if (matched) {
line = document.createElementNS(svgNs, "path");
line.setAttribute("fill", "none");
line.setAttribute("stroke", colors.edge);
line.setAttribute("stroke-width", "1.5");
line.setAttribute("stroke-dasharray", "3,3");
line.setAttribute("marker-end", "url(#dv-arrow)");
svg.appendChild(line);
}
return { root: r, badge: badge, line: line, matched: matched, x: 0 };
});
var nextUnmatchedX = 0;
var badgesPerTarget = {};
viewport.appendChild(world);
target.appendChild(viewport);
// ---- toolbar ----
var toolbar = el("div", {
position: "absolute",
right: "8px",
top: "8px",
display: "flex",
gap: "6px",
zIndex: "10",
});
var autoBtn = button("Auto-arrange", colors);
var layeredBtn = button("Layered", colors);
var fitBtn = button("Fit", colors);
var zoomOutBtn = button("−", colors);
var zoomInBtn = button("+", colors);
[autoBtn, layeredBtn, fitBtn, zoomOutBtn, zoomInBtn].forEach(function (b) {
toolbar.appendChild(b);
});
target.appendChild(toolbar);
function highlightModeButtons() {
autoBtn.style.background = layoutMode === "auto" ? colors.pointerValue : colors.headerBg;
layeredBtn.style.background = layoutMode === "layered" ? colors.pointerValue : colors.headerBg;
}
highlightModeButtons();
// ---- camera (pan/zoom) ----
var camera = { x: target.clientWidth / 2 - NODE_WIDTH / 2, y: 40, zoom: 1 };
function applyCamera() {
world.style.transform = "translate(" + camera.x + "px, " + camera.y + "px) scale(" + camera.zoom + ")";
}
function zoomBy(factor, centerX, centerY) {
var newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, camera.zoom * factor));
camera.x = centerX - ((centerX - camera.x) * newZoom) / camera.zoom;
camera.y = centerY - ((centerY - camera.y) * newZoom) / camera.zoom;
camera.zoom = newZoom;
applyCamera();
}
function fitToView() {
if (nodeList.length === 0) return;
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodeList.forEach(function (n) {
minX = Math.min(minX, n.x - NODE_WIDTH / 2);
maxX = Math.max(maxX, n.x + NODE_WIDTH / 2);
minY = Math.min(minY, n.y - n.height / 2 - (roots.length ? ROOT_BADGE_HEIGHT + ROOT_GAP : 0));
maxY = Math.max(maxY, n.y + n.height / 2);
});
var pad = 30;
var w = Math.max(1, maxX - minX + pad * 2);
var h = Math.max(1, maxY - minY + pad * 2);
var vw = target.clientWidth || 800;
var vh = target.clientHeight || 600;
var zoom = Math.min(MAX_ZOOM, Math.min(vw / w, vh / h));
camera.zoom = zoom;
camera.x = vw / 2 - ((minX + maxX) / 2) * zoom;
camera.y = vh / 2 - ((minY + maxY) / 2) * zoom;
applyCamera();
}
viewport.addEventListener(
"wheel",
function (e) {
e.preventDefault();
var rect = viewport.getBoundingClientRect();
var factor = Math.pow(1.0015, -e.deltaY);
zoomBy(factor, e.clientX - rect.left, e.clientY - rect.top);
},
{ passive: false }
);
zoomInBtn.addEventListener("click", function () {
zoomBy(1.2, target.clientWidth / 2, target.clientHeight / 2);
});
zoomOutBtn.addEventListener("click", function () {
zoomBy(1 / 1.2, target.clientWidth / 2, target.clientHeight / 2);
});
fitBtn.addEventListener("click", fitToView);
autoBtn.addEventListener("click", function () {
layoutMode = "auto";
highlightModeButtons();
nodeList.forEach(function (n) {
if (!n.userPinned) {
n.fixed = false;
n.vx += (Math.random() - 0.5) * 2;
n.vy += (Math.random() - 0.5) * 2;
}
});
stabilize(500);
redraw();
fitToView();
});
layeredBtn.addEventListener("click", function () {
layoutMode = "layered";
highlightModeButtons();
var pos = layeredPositions(nodeIds, data.edges, boxHeights);
nodeList.forEach(function (n) {
n.x = pos[n.id].x;
n.y = pos[n.id].y;
n.vx = 0;
n.vy = 0;
n.fixed = true;
});
redraw();
fitToView();
});
// ---- pan (drag on background) ----
var panDrag = null;
viewport.addEventListener("mousedown", function (e) {
if (e.target !== viewport && e.target !== world && e.target !== svg) return;
panDrag = { startX: e.clientX, startY: e.clientY, camX: camera.x, camY: camera.y };
viewport.style.cursor = "grabbing";
e.preventDefault();
});
// ---- node drag ----
var nodeDrag = null;
data.nodes.forEach(function (n) {
var box = boxes[n.id];
box.addEventListener("mousedown", function (e) {
e.stopPropagation();
var node = byId[n.id];
node.fixed = true;
node.userPinned = true;
nodeDrag = { node: node, startX: e.clientX, startY: e.clientY, nodeX: node.x, nodeY: node.y };
box.style.cursor = "grabbing";
e.preventDefault();
});
});
window.addEventListener("mousemove", function (e) {
if (panDrag) {
camera.x = panDrag.camX + (e.clientX - panDrag.startX);
camera.y = panDrag.camY + (e.clientY - panDrag.startY);
applyCamera();
} else if (nodeDrag) {
nodeDrag.node.x = nodeDrag.nodeX + (e.clientX - nodeDrag.startX) / camera.zoom;
nodeDrag.node.y = nodeDrag.nodeY + (e.clientY - nodeDrag.startY) / camera.zoom;
redraw();
}
});
window.addEventListener("mouseup", function () {
if (panDrag) {
panDrag = null;
viewport.style.cursor = "grab";
}
if (nodeDrag) {
var box = boxes[nodeDrag.node.id];
if (box) box.style.cursor = "grab";
nodeDrag = null;
}
});
// ---- per-frame redraw ----
function redraw() {
nodeList.forEach(function (n) {
var box = boxes[n.id];
box.style.left = n.x - NODE_WIDTH / 2 + "px";
box.style.top = n.y - n.height / 2 + "px";
});
edgePaths.forEach(function (ep) {
var a = byId[ep.edge.from];
var b = byId[ep.edge.to];
if (!a || !b) return;
var x1 = a.x, y1 = a.y, x2 = b.x, y2 = b.y;
var midX = (x1 + x2) / 2;
var midY = (y1 + y2) / 2;
ep.line.setAttribute(
"d",
"M" + x1 + "," + y1 + " C " + midX + "," + y1 + " " + midX + "," + y2 + " " + x2 + "," + y2
);
if (ep.text) {
ep.text.setAttribute("x", String(midX));
ep.text.setAttribute("y", String(midY - 4));
}
});
nextUnmatchedX = 0;
badgesPerTarget = {};
rootBadges.forEach(function (rb) {
if (rb.matched) {
var target = byId[rb.root.value];
var stackIndex = badgesPerTarget[rb.root.value] || 0;
badgesPerTarget[rb.root.value] = stackIndex + 1;
rb.x = target.x + stackIndex * 90;
var by = target.y - target.height / 2 - ROOT_GAP;
rb.badge.style.left = rb.x - NODE_WIDTH / 4 + "px";
rb.badge.style.top = by + "px";
if (rb.line) {
rb.line.setAttribute(
"d",
"M" + rb.x + "," + (by + ROOT_BADGE_HEIGHT) + " L " + target.x + "," + (target.y - target.height / 2)
);
}
} else {
rb.x = nextUnmatchedX;
nextUnmatchedX += NODE_WIDTH / 2 + 20;
rb.badge.style.left = rb.x + "px";
rb.badge.style.top = "0px";
}
});
}
// ---- animation loop ----
var lastKinetic = Infinity;
// Force-directed layouts need many iterations to go from the seed
// positions to something settled; fitting the camera to the seed
// shape (then letting physics keep running) means nodes drift well
// outside whatever we framed. So run the simulation synchronously
// up front -- like vis-network's stabilization pass -- and only
// fit/paint once it's essentially settled.
function stabilize(maxSteps) {
var k = Infinity;
for (var i = 0; i < maxSteps; i++) {
k = sim.step();
if (k < 0.01) break;
}
lastKinetic = k;
running = k >= 0.01;
}
function frame() {
if (!document.body.contains(target)) return; // this render() call was superseded
if (layoutMode === "auto" && running) {
var kinetic = sim.step();
if (kinetic < 0.01 && lastKinetic < 0.01) {
running = false;
}
lastKinetic = kinetic;
}
redraw();
requestAnimationFrame(frame);
}
stabilize(500);
redraw();
fitToView();
requestAnimationFrame(frame);
}
register({
id: "node-table-graph",
name: "Node Table Graph",
serializer: sData,
getVisualization: function (data, self) {
return {
id: "node-table-graph-instance",
name: "Node Table Graph",
priority: 2000,
preload: function () {
return Promise.resolve();
},
render: function (target, options) {
render(data, target, options.theme.kind);
return { renderState: undefined, ready: Promise.resolve() };
},
};
},
});
};