feat(debug): enhance debug graph functionality with auto-discovery of local and global variables

This commit is contained in:
mynameisdeleted
2026-07-09 13:12:57 -04:00
parent 7630ab9050
commit 43e1eaf749
3 changed files with 522 additions and 58 deletions

View File

@@ -14,9 +14,12 @@
// 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.
// roots: [{ name: "top", value: "0x...", kind? }] -- 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. kind is "local" | "watched" | "global" (missing/unknown
// treated as "watched"); the toolbar offers a show/hide toggle per
// kind that's actually present in the data.
module.exports = function (register, lib) {
var sj = lib.semanticJson;
@@ -41,6 +44,7 @@ module.exports = function (register, lib) {
var sRoot = sj.sOpenObject({
name: sj.sString(),
value: sj.sString(),
kind: sj.sOptionalProp(sj.sString(), {}),
});
var sData = sj.sOpenObject({
@@ -119,7 +123,10 @@ module.exports = function (register, lib) {
}
// ---- simple O(n^2) force-directed layout, vis-network-style ----
function createSimulation(nodeList, edges) {
// isVisible(id) lets collapsed-subtree nodes sit out of the simulation
// entirely (frozen, not just hidden) so they don't invisibly shove
// visible nodes around, and reappear where they last were when expanded.
function createSimulation(nodeList, edges, isVisible) {
var REPULSION = 55000;
var SPRING_LENGTH = NODE_WIDTH + H_GAP;
var SPRING_STIFFNESS = 0.02;
@@ -143,6 +150,7 @@ module.exports = function (register, lib) {
for (var j = i + 1; j < nodeList.length; j++) {
var a = nodeList[i];
var b = nodeList[j];
if (!isVisible(a.id) || !isVisible(b.id)) continue;
var dx = a.x - b.x;
var dy = a.y - b.y;
var distSq = dx * dx + dy * dy || 0.01;
@@ -158,6 +166,7 @@ module.exports = function (register, lib) {
}
edges.forEach(function (e) {
if (!isVisible(e.from) || !isVisible(e.to)) return;
var a = byId[e.from];
var b = byId[e.to];
if (!a || !b) return;
@@ -176,7 +185,7 @@ module.exports = function (register, lib) {
var kinetic = 0;
nodeList.forEach(function (n) {
if (n.fixed) {
if (n.fixed || !isVisible(n.id)) {
n.vx = 0;
n.vy = 0;
return;
@@ -195,6 +204,21 @@ module.exports = function (register, lib) {
return { nodes: nodeList, byId: byId, step: step };
}
// ---- collapse/expand: purely client-side, over the already-fetched graph ----
function subtreeSize(startChildren, childrenOf) {
var seen = {};
var stack = startChildren.slice();
while (stack.length) {
var id = stack.pop();
if (seen[id]) continue;
seen[id] = true;
(childrenOf[id] || []).forEach(function (c) {
if (!seen[c]) stack.push(c);
});
}
return Object.keys(seen).length;
}
function el(tag, style, attrs) {
var e = document.createElement(tag);
if (style) Object.assign(e.style, style);
@@ -222,6 +246,34 @@ module.exports = function (register, lib) {
return b;
}
var ROOT_KINDS = ["local", "watched", "global"];
var ROOT_KIND_LABELS = { local: "Locals", watched: "Watched", global: "Globals" };
function rootKind(r) {
return ROOT_KINDS.indexOf(r.kind) !== -1 ? r.kind : "watched";
}
function kindToggle(labelText, colors) {
var wrap = el("label", {
display: "flex",
alignItems: "center",
gap: "4px",
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",
userSelect: "none",
});
var input = el("input", { cursor: "pointer" }, { type: "checkbox" });
input.checked = true;
wrap.appendChild(input);
wrap.appendChild(document.createTextNode(labelText));
return { wrap: wrap, input: input };
}
function render(data, target, theme) {
target.innerHTML = "";
Object.assign(target.style, {
@@ -278,7 +330,69 @@ module.exports = function (register, lib) {
byId[n.id] = n;
});
var sim = createSimulation(nodeList, data.edges);
var childrenOf = {};
var parentsOf = {};
data.edges.forEach(function (e) {
(childrenOf[e.from] = childrenOf[e.from] || []).push(e.to);
(parentsOf[e.to] = parentsOf[e.to] || []).push(e.from);
});
var collapsed = {}; // nodeId -> bool, user-toggled
var visible = {}; // nodeId -> bool, recomputed by computeVisible()
function computeVisible() {
var newVisible = {};
var hasIncoming = {};
data.edges.forEach(function (e) {
hasIncoming[e.to] = true;
});
var roots = nodeIds.filter(function (id) {
return !hasIncoming[id];
});
if (roots.length === 0 && nodeIds.length > 0) roots = [nodeIds[0]];
var queue = roots.slice();
roots.forEach(function (id) {
newVisible[id] = true;
});
while (queue.length) {
var id = queue.shift();
if (collapsed[id]) continue; // don't expand past a collapsed node
(childrenOf[id] || []).forEach(function (childId) {
if (!newVisible[childId]) {
newVisible[childId] = true;
queue.push(childId);
}
});
}
visible = newVisible;
}
computeVisible();
// A local/global/watched pointer's target can end up hidden inside a
// collapsed subtree; rather than just hiding its arrow, walk up the
// edge graph to whichever visible ancestor is currently standing in
// for that hidden subtree, and point the arrow there instead -- so
// expanding that ancestor is what makes the arrow "arrive" at its
// real target.
function nearestVisibleTarget(id) {
var seen = {};
var queue = [id];
while (queue.length) {
var cur = queue.shift();
if (visible[cur]) return cur;
if (seen[cur]) continue;
seen[cur] = true;
(parentsOf[cur] || []).forEach(function (p) {
if (!seen[p]) queue.push(p);
});
}
return null;
}
var sim = createSimulation(nodeList, data.edges, function (id) {
return !!visible[id];
});
var layoutMode = "auto"; // "auto" (physics) | "layered" (static chain)
var running = true;
@@ -305,18 +419,28 @@ module.exports = function (register, lib) {
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);
function addArrowMarker(id, fill) {
var marker = document.createElementNS(svgNs, "marker");
marker.setAttribute("id", id);
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", fill);
marker.appendChild(arrowPath);
defs.appendChild(marker);
}
// Struct-to-struct pointer fields (e.g. `next`) use the neutral edge
// color; local/watched/global variable pointers use the same accent
// color as pointer values in the field tables, so a glance at an
// arrow's color says whether it's "inside the data structure" or
// "a variable pointing into it".
addArrowMarker("dv-arrow", colors.edge);
addArrowMarker("dv-arrow-root", colors.pointerValue);
svg.appendChild(defs);
world.appendChild(svg);
@@ -341,6 +465,7 @@ module.exports = function (register, lib) {
});
var boxes = {};
var toggles = {};
data.nodes.forEach(function (n) {
var box = el("div", {
position: "absolute",
@@ -361,11 +486,45 @@ module.exports = function (register, lib) {
color: colors.headerFg,
fontWeight: "bold",
borderBottom: "1px solid " + colors.border,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "6px",
});
var headerLabel = el("span", {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
minWidth: "0",
});
header.textContent = n.id;
headerLabel.textContent = n.id;
header.appendChild(headerLabel);
var kids = childrenOf[n.id];
if (kids && kids.length) {
var toggle = el("span", {
flexShrink: "0",
cursor: "pointer",
border: "1px solid " + colors.border,
borderRadius: "3px",
padding: "0 5px",
fontSize: "11px",
lineHeight: "14px",
userSelect: "none",
});
toggle.addEventListener("mousedown", function (e) {
e.stopPropagation(); // don't start a box-drag
});
toggle.addEventListener("click", function (e) {
e.stopPropagation();
collapsed[n.id] = !collapsed[n.id];
computeVisible();
redraw();
if (layoutMode === "auto") stabilize(200);
});
header.appendChild(toggle);
toggles[n.id] = toggle;
}
box.appendChild(header);
var body = el("div", { maxHeight: MAX_VISIBLE_ROWS * ROW_HEIGHT + "px", overflowY: "auto" });
@@ -427,13 +586,12 @@ module.exports = function (register, lib) {
if (matched) {
line = document.createElementNS(svgNs, "path");
line.setAttribute("fill", "none");
line.setAttribute("stroke", colors.edge);
line.setAttribute("stroke", colors.pointerValue);
line.setAttribute("stroke-width", "1.5");
line.setAttribute("stroke-dasharray", "3,3");
line.setAttribute("marker-end", "url(#dv-arrow)");
line.setAttribute("marker-end", "url(#dv-arrow-root)");
svg.appendChild(line);
}
return { root: r, badge: badge, line: line, matched: matched, x: 0 };
return { root: r, kind: rootKind(r), badge: badge, line: line, matched: matched, x: 0 };
});
var nextUnmatchedX = 0;
var badgesPerTarget = {};
@@ -452,14 +610,51 @@ module.exports = function (register, lib) {
});
var autoBtn = button("Auto-arrange", colors);
var layeredBtn = button("Layered", colors);
var expandAllBtn = button("Expand all", colors);
var fitBtn = button("Fit", colors);
var zoomOutBtn = button("−", colors);
var zoomInBtn = button("+", colors);
[autoBtn, layeredBtn, fitBtn, zoomOutBtn, zoomInBtn].forEach(function (b) {
[autoBtn, layeredBtn, expandAllBtn, fitBtn, zoomOutBtn, zoomInBtn].forEach(function (b) {
toolbar.appendChild(b);
});
target.appendChild(toolbar);
// ---- root-kind toggles (Locals / Watched / Globals) ----
var rootKindEnabled = { local: true, watched: true, global: true };
var kindsPresent = {};
roots.forEach(function (r) {
kindsPresent[rootKind(r)] = true;
});
var presentKinds = ROOT_KINDS.filter(function (k) {
return kindsPresent[k];
});
if (presentKinds.length > 0) {
var kindToolbar = el("div", {
position: "absolute",
left: "8px",
top: "8px",
display: "flex",
gap: "6px",
zIndex: "10",
});
presentKinds.forEach(function (kind) {
var cb = kindToggle(ROOT_KIND_LABELS[kind], colors);
cb.input.addEventListener("change", function () {
rootKindEnabled[kind] = cb.input.checked;
redraw();
});
kindToolbar.appendChild(cb.wrap);
});
target.appendChild(kindToolbar);
}
expandAllBtn.addEventListener("click", function () {
collapsed = {};
computeVisible();
redraw();
if (layoutMode === "auto") stabilize(200);
});
function highlightModeButtons() {
autoBtn.style.background = layoutMode === "auto" ? colors.pointerValue : colors.headerBg;
layeredBtn.style.background = layoutMode === "layered" ? colors.pointerValue : colors.headerBg;
@@ -482,9 +677,12 @@ module.exports = function (register, lib) {
}
function fitToView() {
if (nodeList.length === 0) return;
var visibleNodes = nodeList.filter(function (n) {
return visible[n.id];
});
if (visibleNodes.length === 0) return;
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodeList.forEach(function (n) {
visibleNodes.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));
@@ -600,14 +798,31 @@ module.exports = function (register, lib) {
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";
var isVis = !!visible[n.id];
box.style.display = isVis ? "" : "none";
if (isVis) {
box.style.left = n.x - NODE_WIDTH / 2 + "px";
box.style.top = n.y - n.height / 2 + "px";
}
var toggle = toggles[n.id];
if (toggle) {
if (collapsed[n.id]) {
toggle.textContent = "+" + subtreeSize(childrenOf[n.id] || [], childrenOf);
toggle.title = "Expand";
} else {
toggle.textContent = "−";
toggle.title = "Collapse";
}
}
});
edgePaths.forEach(function (ep) {
var a = byId[ep.edge.from];
var b = byId[ep.edge.to];
if (!a || !b) return;
var show = !!(a && b && visible[ep.edge.from] && visible[ep.edge.to]);
ep.line.style.display = show ? "" : "none";
if (ep.text) ep.text.style.display = show ? "" : "none";
if (!show) return;
var x1 = a.x, y1 = a.y, x2 = b.x, y2 = b.y;
var midX = (x1 + x2) / 2;
var midY = (y1 + y2) / 2;
@@ -624,10 +839,22 @@ module.exports = function (register, lib) {
nextUnmatchedX = 0;
badgesPerTarget = {};
rootBadges.forEach(function (rb) {
if (!rootKindEnabled[rb.kind]) {
rb.badge.style.display = "none";
if (rb.line) rb.line.style.display = "none";
return;
}
if (rb.matched) {
var target = byId[rb.root.value];
var stackIndex = badgesPerTarget[rb.root.value] || 0;
badgesPerTarget[rb.root.value] = stackIndex + 1;
var targetId = visible[rb.root.value] ? rb.root.value : nearestVisibleTarget(rb.root.value);
rb.badge.style.display = targetId ? "" : "none";
if (rb.line) rb.line.style.display = targetId ? "" : "none";
if (!targetId) return;
var redirected = targetId !== rb.root.value;
rb.badge.title = rb.root.name + " = " + rb.root.value +
(redirected ? " (inside collapsed " + targetId + ")" : "");
var target = byId[targetId];
var stackIndex = badgesPerTarget[targetId] || 0;
badgesPerTarget[targetId] = 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";
@@ -639,6 +866,7 @@ module.exports = function (register, lib) {
);
}
} else {
rb.badge.style.display = "";
rb.x = nextUnmatchedX;
nextUnmatchedX += NODE_WIDTH / 2 + 20;
rb.badge.style.left = rb.x + "px";
@@ -683,6 +911,27 @@ module.exports = function (register, lib) {
redraw();
fitToView();
requestAnimationFrame(frame);
// The panel/webview may not have its final layout size yet at the
// instant render() runs (fresh panel, window just reloaded, tab just
// became visible) -- fitToView() above would then measure a stale or
// fallback 800x600 target size. Refit once more the first time the
// target's real size shows up differently, then stop watching so we
// don't fight the user's own manual pan/zoom on later resizes.
if (typeof ResizeObserver !== "undefined") {
var lastW = target.clientWidth;
var lastH = target.clientHeight;
var ro = new ResizeObserver(function () {
if (!document.body.contains(target)) {
ro.disconnect(); // this render() call was superseded
return;
}
if (target.clientWidth === lastW && target.clientHeight === lastH) return;
ro.disconnect();
fitToView();
});
ro.observe(target);
}
}
register({