feat(debug): enhance debug graph functionality with auto-discovery of local and global variables
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user