Replace the custom toDebugGraph() method that generated JSON directly in C++ with a generic GDB Python script (debug_graph.py). The script traverses pointers and structures to build a graph representation for the VS Code Debug Visualizer, enabling external pointer display and more extensible visualization.
Update launch configurations to load the script and to disable array/string truncation (needed for the visualizer JSON). Add a debugAdapterConfigurations entry in settings.json to use the expression template $debug_graph(${expr}) for the cppdbg adapter.
411 lines
11 KiB
JavaScript
411 lines
11 KiB
JavaScript
// 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.
|
|
//
|
|
// Registered via the "debugVisualizer.customVisualizerScriptPaths" setting;
|
|
// the extension hot-reloads this file on save, no debug session restart needed.
|
|
//
|
|
// Data shape produced by cpp/main.cpp's LinkedList<T>::toDebugGraph():
|
|
// 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 above the graph with an arrow to whichever node's id
|
|
// matches its value, since a node's own address equals its first
|
|
// member's address.
|
|
|
|
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 PADDING = 20;
|
|
var ROOT_ROW_HEIGHT = 50;
|
|
var ROOT_BADGE_HEIGHT = 22;
|
|
|
|
function layout(nodes, edges) {
|
|
var ids = nodes.map(function (n) {
|
|
return n.id;
|
|
});
|
|
var adjacency = {};
|
|
var incoming = {};
|
|
ids.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 = [];
|
|
ids.forEach(function (id) {
|
|
if (incoming[id] === 0) {
|
|
level[id] = 0;
|
|
queue.push(id);
|
|
}
|
|
});
|
|
if (queue.length === 0 && ids.length > 0) {
|
|
level[ids[0]] = 0;
|
|
queue.push(ids[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 = {};
|
|
ids.forEach(function (id) {
|
|
var lvl = level[id];
|
|
if (lvl === undefined) {
|
|
maxLevel++;
|
|
lvl = maxLevel;
|
|
}
|
|
var col = colCount[lvl] || 0;
|
|
colCount[lvl] = col + 1;
|
|
positions[id] = { level: lvl, col: col };
|
|
});
|
|
return positions;
|
|
}
|
|
|
|
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 render(data, target, theme) {
|
|
target.innerHTML = "";
|
|
Object.assign(target.style, {
|
|
position: "relative",
|
|
overflow: "auto",
|
|
width: "100%",
|
|
height: "100%",
|
|
boxSizing: "border-box",
|
|
padding: PADDING + "px",
|
|
});
|
|
|
|
var dark = theme === "dark";
|
|
var colors = dark
|
|
? {
|
|
border: "#5a5a5a",
|
|
headerBg: "#333333",
|
|
headerFg: "#e8e8e8",
|
|
bodyBg: "#252526",
|
|
fieldName: "#9cdcfe",
|
|
fieldValue: "#d4d4d4",
|
|
pointerValue: "#ce9178",
|
|
edge: "#8a8a8a",
|
|
}
|
|
: {
|
|
border: "#b8b8b8",
|
|
headerBg: "#eeeeee",
|
|
headerFg: "#1a1a1a",
|
|
bodyBg: "#ffffff",
|
|
fieldName: "#0451a5",
|
|
fieldValue: "#1a1a1a",
|
|
pointerValue: "#a31515",
|
|
edge: "#6b6b6b",
|
|
};
|
|
|
|
var positions = layout(data.nodes, data.edges);
|
|
var roots = data.roots || [];
|
|
var topOffset = roots.length > 0 ? ROOT_ROW_HEIGHT : 0;
|
|
|
|
var byId = {};
|
|
data.nodes.forEach(function (n) {
|
|
byId[n.id] = n;
|
|
});
|
|
|
|
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 pixelPos = {};
|
|
var maxX = 0;
|
|
var maxY = 0;
|
|
Object.keys(positions).forEach(function (id) {
|
|
var p = positions[id];
|
|
var x = p.level * (NODE_WIDTH + H_GAP);
|
|
var y = topOffset + p.col * (Math.max(boxHeights[id], HEADER_HEIGHT + ROW_HEIGHT) + V_GAP);
|
|
pixelPos[id] = { x: x, y: y };
|
|
maxX = Math.max(maxX, x + NODE_WIDTH);
|
|
maxY = Math.max(maxY, y + boxHeights[id]);
|
|
});
|
|
|
|
// Spread root badges above the nodes they point to; unmatched roots
|
|
// (e.g. a pointer into the middle of a struct, or a stale/null value)
|
|
// queue up along the top-left instead.
|
|
var rootPos = {};
|
|
var nextUnmatchedX = 0;
|
|
var badgesPerTarget = {};
|
|
roots.forEach(function (r) {
|
|
var target = pixelPos[r.value];
|
|
var x;
|
|
if (target) {
|
|
var stackIndex = badgesPerTarget[r.value] || 0;
|
|
badgesPerTarget[r.value] = stackIndex + 1;
|
|
x = target.x + stackIndex * 90;
|
|
} else {
|
|
x = nextUnmatchedX;
|
|
nextUnmatchedX += NODE_WIDTH / 2 + 20;
|
|
}
|
|
rootPos[r.name] = { x: x, y: 0, matched: !!target };
|
|
maxX = Math.max(maxX, x + NODE_WIDTH);
|
|
});
|
|
|
|
var canvas = el("div", {
|
|
position: "relative",
|
|
width: maxX + "px",
|
|
height: maxY + "px",
|
|
});
|
|
|
|
var svgNs = "http://www.w3.org/2000/svg";
|
|
var svg = document.createElementNS(svgNs, "svg");
|
|
svg.setAttribute("width", String(maxX));
|
|
svg.setAttribute("height", String(maxY));
|
|
Object.assign(svg.style, {
|
|
position: "absolute",
|
|
left: "0",
|
|
top: "0",
|
|
overflow: "visible",
|
|
pointerEvents: "none",
|
|
});
|
|
|
|
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);
|
|
var defs = document.createElementNS(svgNs, "defs");
|
|
defs.appendChild(marker);
|
|
svg.appendChild(defs);
|
|
|
|
data.edges.forEach(function (e) {
|
|
var from = pixelPos[e.from];
|
|
var to = pixelPos[e.to];
|
|
if (!from || !to) return;
|
|
var x1 = from.x + NODE_WIDTH;
|
|
var y1 = from.y + boxHeights[e.from] / 2;
|
|
var x2 = to.x;
|
|
var y2 = to.y + boxHeights[e.to] / 2;
|
|
|
|
var line = document.createElementNS(svgNs, "path");
|
|
var midX = (x1 + x2) / 2;
|
|
line.setAttribute(
|
|
"d",
|
|
"M" + x1 + "," + y1 + " C " + midX + "," + y1 + " " + midX + "," + y2 + " " + x2 + "," + y2
|
|
);
|
|
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);
|
|
|
|
if (e.label) {
|
|
var text = document.createElementNS(svgNs, "text");
|
|
text.setAttribute("x", String(midX));
|
|
text.setAttribute("y", String((y1 + y2) / 2 - 4));
|
|
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);
|
|
}
|
|
});
|
|
|
|
canvas.appendChild(svg);
|
|
|
|
data.nodes.forEach(function (n) {
|
|
var pos = pixelPos[n.id];
|
|
var box = el("div", {
|
|
position: "absolute",
|
|
left: pos.x + "px",
|
|
top: pos.y + "px",
|
|
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",
|
|
});
|
|
|
|
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);
|
|
canvas.appendChild(box);
|
|
});
|
|
|
|
roots.forEach(function (r) {
|
|
var pos = rootPos[r.name];
|
|
if (pos.matched) {
|
|
var targetPos = pixelPos[r.value];
|
|
var x1 = pos.x + NODE_WIDTH / 2;
|
|
var y1 = ROOT_BADGE_HEIGHT;
|
|
var x2 = targetPos.x + NODE_WIDTH / 2;
|
|
var y2 = targetPos.y;
|
|
var line = document.createElementNS(svgNs, "path");
|
|
line.setAttribute("d", "M" + x1 + "," + y1 + " L " + x2 + "," + y2);
|
|
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);
|
|
}
|
|
|
|
var badge = el("div", {
|
|
position: "absolute",
|
|
left: pos.x + "px",
|
|
top: "0px",
|
|
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: pos.matched ? colors.headerBg : "transparent",
|
|
color: pos.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 + (pos.matched ? "" : " (" + r.value + ")");
|
|
canvas.appendChild(badge);
|
|
});
|
|
|
|
target.appendChild(canvas);
|
|
}
|
|
|
|
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() };
|
|
},
|
|
};
|
|
},
|
|
});
|
|
};
|