feat(debug): enhance type hinting in gdb and Python debug visualizers
This commit is contained in:
@@ -158,13 +158,19 @@ def _append_field(fields_out, name, value, is_pointer=False, type_hint=None):
|
||||
|
||||
def _process_member(label, member_val, node_id, fields_out, edges, nodes, visited, type_hint=None):
|
||||
member_type = member_val.type.strip_typedefs()
|
||||
# Every field gets a type hint, not just ones a wrapper (pretty-printer
|
||||
# or Rust-enum-variant) has already labeled -- a plain `int`/`char *`
|
||||
# member is just as worth showing as one that came out of an unwrapped
|
||||
# std::optional. The caller's type_hint (a wrapper name) still wins when
|
||||
# present, since that's more informative than the leaf's own raw type.
|
||||
own_type_hint = type_hint or _type_name(member_type)
|
||||
|
||||
if member_type.code == gdb.TYPE_CODE_PTR:
|
||||
try:
|
||||
ptr_int = int(member_val)
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
_append_field(fields_out, label, _addr_str(ptr_int), is_pointer=True, type_hint=type_hint)
|
||||
_append_field(fields_out, label, _addr_str(ptr_int), is_pointer=True, type_hint=own_type_hint)
|
||||
if ptr_int:
|
||||
edges.append((node_id, _addr_str(ptr_int), label))
|
||||
_visit_pointer(member_val, ptr_int, nodes, edges, visited)
|
||||
@@ -199,7 +205,7 @@ def _process_member(label, member_val, node_id, fields_out, edges, nodes, visite
|
||||
_append_field(fields_out, label, _format_scalar(member_val), type_hint=wrapper_name)
|
||||
return
|
||||
|
||||
_append_field(fields_out, label, _format_scalar(member_val), type_hint=type_hint)
|
||||
_append_field(fields_out, label, _format_scalar(member_val), type_hint=own_type_hint)
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
@@ -217,7 +223,7 @@ def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
else:
|
||||
for leaf_label, leaf_val in leaves:
|
||||
_process_member(leaf_label, leaf_val, node_id, fields_out, edges, nodes, visited, wrapper_name)
|
||||
nodes[node_id] = fields_out
|
||||
nodes[node_id] = {"fields": fields_out, "type": wrapper_name}
|
||||
return
|
||||
|
||||
t = val.type.strip_typedefs()
|
||||
@@ -230,7 +236,7 @@ def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
continue
|
||||
_process_member(f.name, member_val, node_id, fields_out, edges, nodes, visited)
|
||||
|
||||
nodes[node_id] = fields_out
|
||||
nodes[node_id] = {"fields": fields_out, "type": _type_name(t)}
|
||||
|
||||
|
||||
def _visit_pointer(ptr_val, ptr_int, nodes, edges, visited):
|
||||
@@ -269,14 +275,16 @@ def _visit_pointer(ptr_val, ptr_int, nodes, edges, visited):
|
||||
text = text[:500] + "…"
|
||||
except (gdb.error, UnicodeDecodeError):
|
||||
text = _format_scalar(ptr_val.dereference())
|
||||
nodes[node_id] = [{"name": "value", "value": text, "isPointer": False}]
|
||||
leaf_fields = []
|
||||
_append_field(leaf_fields, "value", text, type_hint=_type_name(pointee_type))
|
||||
nodes[node_id] = {"fields": leaf_fields, "type": _type_name(ptr_val.type.strip_typedefs())}
|
||||
else:
|
||||
if node_id in nodes:
|
||||
return # already described (richly or otherwise) via another path
|
||||
pointee = ptr_val.dereference()
|
||||
nodes[node_id] = [
|
||||
{"name": "value", "value": _format_scalar(pointee), "isPointer": False}
|
||||
]
|
||||
leaf_fields = []
|
||||
_append_field(leaf_fields, "value", _format_scalar(pointee), type_hint=_type_name(pointee_type))
|
||||
nodes[node_id] = {"fields": leaf_fields, "type": _type_name(ptr_val.type.strip_typedefs())}
|
||||
|
||||
|
||||
def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
@@ -297,9 +305,9 @@ def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
_describe_struct(root_val, node_id, nodes, edges, visited)
|
||||
return node_id
|
||||
else:
|
||||
nodes["root"] = [
|
||||
{"name": "value", "value": _format_scalar(root_val), "isPointer": False}
|
||||
]
|
||||
leaf_fields = []
|
||||
_append_field(leaf_fields, "value", _format_scalar(root_val), type_hint=_type_name(t))
|
||||
nodes["root"] = {"fields": leaf_fields, "type": _type_name(t)}
|
||||
return None
|
||||
|
||||
|
||||
@@ -424,7 +432,12 @@ def build_graph_json(root_val, watched_roots):
|
||||
continue
|
||||
_add_named_root(sym.name, val, "global", nodes, edges, visited, roots, skip_addrs)
|
||||
|
||||
nodes_json = [{"id": nid, "fields": fields} for nid, fields in nodes.items()]
|
||||
nodes_json = [
|
||||
{"id": nid, "fields": node["fields"], "type": node["type"]}
|
||||
if node.get("type")
|
||||
else {"id": nid, "fields": node["fields"]}
|
||||
for nid, node in nodes.items()
|
||||
]
|
||||
edges_json = [
|
||||
{"from": src, "to": dst, "label": label}
|
||||
for (src, dst, label) in edges
|
||||
|
||||
@@ -28,10 +28,28 @@ This mirrors debuggers/gdb_debug_graph.py's auto-discovery: it's what lets
|
||||
show the whole list, because `lst` (a sibling local) gets auto-walked too.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
|
||||
# On gdb (C++/Rust), typing bare `root` into the Watch box happens to work
|
||||
# as a throwaway "just show me the locals/globals" expression -- gdb
|
||||
# resolves it to a real (if coincidental) always-null `void*` symbol from
|
||||
# libc.so.6, so $debug_graph(root) contributes no primary node of its own
|
||||
# but still triggers full auto-discovery of the frame's locals/globals.
|
||||
# debugpy has no such accident to lean on -- evaluating a name that isn't
|
||||
# actually bound anywhere raises NameError before our code ever runs -- so
|
||||
# this deliberately recreates the same "always-resolvable, always-None"
|
||||
# spelling as a builtin. This runs as an import-time side effect, but that's
|
||||
# safe to rely on even the *first* time someone types `root`: evaluating a
|
||||
# call expression like `__import__('py_debug_graph').debug_graph(root)`
|
||||
# resolves the callable (which imports this module and runs this line)
|
||||
# before it evaluates the `root` argument, so the patch is always in place
|
||||
# by the time `root` itself gets looked up.
|
||||
if not hasattr(builtins, "root"):
|
||||
builtins.root = None
|
||||
|
||||
|
||||
def _addr_str(obj_id):
|
||||
return hex(obj_id)
|
||||
@@ -67,9 +85,15 @@ def _append_field(fields_out, name, value, is_pointer=False, type_hint=None):
|
||||
fields_out.append(field)
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
def _fields_from_items(items, node_id, nodes, edges, visited):
|
||||
"""Builds one field row per (name, value) pair, recursing into
|
||||
describable values as edges exactly like a struct's own members --
|
||||
shared between _describe_struct's vars(obj).items() and the
|
||||
globals()-as-root special case's plain dict.items() below, so a
|
||||
"container of named values" only has one field-building rule
|
||||
regardless of whether the container is an object or a dict."""
|
||||
fields_out = []
|
||||
for name, member_val in vars(val).items():
|
||||
for name, member_val in items:
|
||||
if _is_describable(member_val):
|
||||
member_id = id(member_val)
|
||||
_append_field(fields_out, name, _addr_str(member_id), is_pointer=True, type_hint=_type_name(member_val))
|
||||
@@ -78,8 +102,16 @@ def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
visited.add(member_id)
|
||||
_describe_struct(member_val, _addr_str(member_id), nodes, edges, visited)
|
||||
else:
|
||||
_append_field(fields_out, name, _format_scalar(member_val))
|
||||
nodes[node_id] = fields_out
|
||||
# Every field gets a type hint, not just the describable ones --
|
||||
# a plain int/str/None member is just as worth labeling as a
|
||||
# pointer-like one.
|
||||
_append_field(fields_out, name, _format_scalar(member_val), type_hint=_type_name(member_val))
|
||||
return fields_out
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
fields_out = _fields_from_items(vars(val).items(), node_id, nodes, edges, visited)
|
||||
nodes[node_id] = {"fields": fields_out, "type": _type_name(val)}
|
||||
|
||||
|
||||
def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
@@ -89,7 +121,13 @@ def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
_describe_struct(root_val, node_id, nodes, edges, visited)
|
||||
return node_id
|
||||
if root_val is not None:
|
||||
nodes["root"] = [{"name": "value", "value": _format_scalar(root_val), "isPointer": False}]
|
||||
nodes["root"] = {
|
||||
"fields": [{
|
||||
"name": "value", "value": _format_scalar(root_val), "isPointer": False,
|
||||
"typeHint": _type_name(root_val),
|
||||
}],
|
||||
"type": _type_name(root_val),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@@ -135,7 +173,26 @@ def build_graph_json(root_val, watched_roots, frame):
|
||||
skip_ids = set()
|
||||
used_names = set()
|
||||
|
||||
primary_id = _walk_primary_root(root_val, nodes, edges, visited)
|
||||
if frame is not None and root_val is globals:
|
||||
# Bare `globals` (no parens, e.g. typing "globals" into the Watch
|
||||
# box) evaluates to the *builtin function itself*, not its result --
|
||||
# walked normally that's just an opaque "<function globals at ...>"
|
||||
# scalar. Calling it here would also be wrong: globals() returns
|
||||
# whatever frame *calls* it, which would be this module's own
|
||||
# globals, not the paused frame's. Substituting the frame we
|
||||
# already have (passed in from debug_graph()'s sys._getframe(1))
|
||||
# is what actually shows the debuggee's global variables. Filtered
|
||||
# through the same _skip_global used for auto-discovered globals
|
||||
# below, so this node shows the same clean set, not every dunder,
|
||||
# module and class in the namespace.
|
||||
primary_id = "globals()"
|
||||
fields_out = _fields_from_items(
|
||||
[(k, v) for k, v in frame.f_globals.items() if not _skip_global(k, v)],
|
||||
primary_id, nodes, edges, visited,
|
||||
)
|
||||
nodes[primary_id] = {"fields": fields_out, "type": "dict"}
|
||||
else:
|
||||
primary_id = _walk_primary_root(root_val, nodes, edges, visited)
|
||||
if primary_id is not None:
|
||||
skip_ids.add(primary_id)
|
||||
|
||||
@@ -166,7 +223,12 @@ def build_graph_json(root_val, watched_roots, frame):
|
||||
used_names.add(name)
|
||||
_add_named_root(name, val, "global", nodes, edges, visited, roots, skip_ids)
|
||||
|
||||
nodes_json = [{"id": nid, "fields": fields} for nid, fields in nodes.items()]
|
||||
nodes_json = [
|
||||
{"id": nid, "fields": node["fields"], "type": node["type"]}
|
||||
if node.get("type")
|
||||
else {"id": nid, "fields": node["fields"]}
|
||||
for nid, node in nodes.items()
|
||||
]
|
||||
edges_json = [
|
||||
{"from": src, "to": dst, "label": label}
|
||||
for (src, dst, label) in edges
|
||||
|
||||
@@ -34,6 +34,7 @@ module.exports = function (register, lib) {
|
||||
var sNode = sj.sOpenObject({
|
||||
id: sj.sString(),
|
||||
fields: sj.sArrayOf(sField),
|
||||
type: sj.sOptionalProp(sj.sString(), {}),
|
||||
});
|
||||
|
||||
var sEdge = sj.sOpenObject({
|
||||
@@ -572,7 +573,18 @@ module.exports = function (register, lib) {
|
||||
textOverflow: "ellipsis",
|
||||
minWidth: "0",
|
||||
});
|
||||
headerLabel.textContent = n.id;
|
||||
headerLabel.appendChild(document.createTextNode(n.id));
|
||||
if (n.type) {
|
||||
var nodeTypeSpan = el("span", {
|
||||
color: colors.fieldType,
|
||||
fontSize: "10px",
|
||||
fontWeight: "normal",
|
||||
marginLeft: "6px",
|
||||
});
|
||||
nodeTypeSpan.textContent = n.type;
|
||||
headerLabel.appendChild(nodeTypeSpan);
|
||||
header.title = n.id + " : " + n.type;
|
||||
}
|
||||
header.appendChild(headerLabel);
|
||||
|
||||
var kids = childrenOf[n.id];
|
||||
@@ -752,7 +764,6 @@ module.exports = function (register, lib) {
|
||||
|
||||
return { root: r, kind: rootKind(r), badge: box, line: null, matched: false, height: height, x: 0, y: 0, pinned: false };
|
||||
});
|
||||
var nextUnmatchedX = 0;
|
||||
var badgesPerTarget = {};
|
||||
|
||||
viewport.appendChild(world);
|
||||
@@ -1035,7 +1046,26 @@ module.exports = function (register, lib) {
|
||||
}
|
||||
});
|
||||
|
||||
nextUnmatchedX = 0;
|
||||
// Unmatched-root cards (plain scalar locals/globals/watches) that
|
||||
// aren't pinned by a drag auto-flow into a compact roughly-square
|
||||
// cluster -- sqrt(count) columns -- instead of one ever-widening
|
||||
// row, so a handful of locals reads as a group near the top
|
||||
// instead of a line stretching off to the side. The cluster's
|
||||
// bottom row sits just above the node cloud (same non-overlap
|
||||
// tracking as before); earlier rows stack upward from there.
|
||||
var unmatchedFlow = rootBadges.filter(function (rb) {
|
||||
return !rb.matched && !rb.pinned && rootKindEnabled[rb.kind];
|
||||
});
|
||||
var flowCols = Math.max(1, Math.ceil(Math.sqrt(unmatchedFlow.length)));
|
||||
var flowRows = Math.ceil(unmatchedFlow.length / flowCols);
|
||||
var flowRowHeight = 0;
|
||||
unmatchedFlow.forEach(function (rb) {
|
||||
flowRowHeight = Math.max(flowRowHeight, rb.height);
|
||||
});
|
||||
var flowRowGap = 10;
|
||||
var flowShelfBottom = (visibleTopY === Infinity ? 0 : visibleTopY) - ROOT_GAP;
|
||||
var flowIndex = 0;
|
||||
|
||||
badgesPerTarget = {};
|
||||
rootBadges.forEach(function (rb) {
|
||||
if (!rootKindEnabled[rb.kind]) {
|
||||
@@ -1067,21 +1097,18 @@ module.exports = function (register, lib) {
|
||||
} else {
|
||||
rb.badge.style.display = "";
|
||||
if (!rb.pinned) {
|
||||
rb.x = nextUnmatchedX;
|
||||
// Sit just above whatever's currently the top of the
|
||||
// visible node cloud, rather than a fixed world y --
|
||||
// so this shelf of scalar-local cards tracks the
|
||||
// graph instead of drifting into overlapping it once
|
||||
// "auto" physics (or gravity re-centering) moves
|
||||
// nodes around.
|
||||
var shelfTop = (visibleTopY === Infinity ? 0 : visibleTopY) - ROOT_GAP;
|
||||
rb.y = shelfTop - rb.height;
|
||||
nextUnmatchedX += NODE_WIDTH + 20;
|
||||
var row = Math.floor(flowIndex / flowCols); // 0 = top row
|
||||
var col = flowIndex % flowCols;
|
||||
var rowFromBottom = flowRows - 1 - row; // 0 = row nearest the graph
|
||||
var rowBottom = flowShelfBottom - rowFromBottom * (flowRowHeight + flowRowGap);
|
||||
rb.x = col * (NODE_WIDTH + 20);
|
||||
rb.y = rowBottom - rb.height;
|
||||
flowIndex++;
|
||||
}
|
||||
// A pinned (user-dragged) card keeps whatever rb.x/rb.y
|
||||
// the drag left it at, and drops out of the auto-flow
|
||||
// row entirely -- the remaining unpinned cards close the
|
||||
// gap instead of leaving a hole where it used to sit.
|
||||
// cluster entirely -- the remaining unpinned cards
|
||||
// re-flow to close the gap instead of leaving a hole.
|
||||
rb.badge.style.left = rb.x + "px";
|
||||
rb.badge.style.top = rb.y + "px";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user