diff --git a/docs/usage.md b/docs/usage.md
index 69dc6ba..522d92f 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -78,6 +78,11 @@ the available metadata supports that interpretation. See [limitations](limitatio
### Recursion tree
+Each node shows the call with its argument values, such as
+`towerOfHanoi(2, "B", "C", "A")`. Methods drop their class name and the hidden
+`this` pointer, so a LeetCode `Solution::` method reads like a plain function.
+Nodes widen to fit the arguments (up to 32 characters); hover a node for its full label.
+
Calls branch in invocation order and retain recorded return values. L/R labels
mean first/second child calls, not necessarily `left`/`right` fields. The active
path and step note help you follow the running call.
diff --git a/web/src/CallTree.tsx b/web/src/CallTree.tsx
index 6e573ff..cd33add 100644
--- a/web/src/CallTree.tsx
+++ b/web/src/CallTree.tsx
@@ -10,8 +10,9 @@ import {useZoom, ZoomControl} from './zoom';
type Dims = {width: number; height: number; gap: number; level: number};
// Full nodes carry a status line; compact ones are a single, larger line for wide trees.
const FULL: Dims = {width: 168, height: 44, gap: 22, level: 96};
-// Full labels: 16px bold monospace is about 9.7px per character.
-const FULL_CHARS = 15;
+// Full labels: 16px bold monospace is about 9.7px per character. Nodes grow with their longest
+// label, from 15 characters up to FULL_CHARS, so the arguments are not cut off.
+const FULL_CHARS = 32, FULL_CHAR_WIDTH = 9.7;
const COMPACT: Dims = {width: 116, height: 38, gap: 12, level: 70};
const MAX_NODES = 250;
// Below this, labels stop being readable: the tree scrolls instead, following the running call.
@@ -20,7 +21,7 @@ const MIN_FIT = 0.8;
// Switch to compact nodes when full ones would have to shrink this far to fit.
const COMPACT_BELOW = 0.8;
// Compact labels: 15px monospace is about 8.3px per character.
-const COMPACT_CHARS = 24, CHAR_WIDTH = 8.4;
+const COMPACT_CHARS = 32, CHAR_WIDTH = 8.4;
// Steps closer together than this are a burst, not a step to watch.
const RAPID_STEP_MS = 250;
// Width added to compact nodes so ×N never overprints the label.
@@ -120,14 +121,16 @@ export function CallTree({trace, index, onSelect, selectedCall, baseline = null}
visible.forEach(node => frequencies.set(node.label, (frequencies.get(node.label) ?? 0) + 1));
// Room for a hand-lettered ×N inside the node when any call repeats, so it never meets an edge label.
const anyRepeat = visible.some(node => (frequencies.get(node.label) ?? 0) > 1 && node.frame.locals.some(local => local.is_argument));
- const fullDims = {...FULL, width: FULL.width + (anyRepeat ? REPEAT_ROOM : 0)};
+ const fullChars = Math.min(FULL_CHARS, Math.max(15, ...visible.map(node => node.label.length)));
+ // The width left for text: padding, the tick of a returned call and room for ×N.
+ const fullDims = {...FULL, width: Math.max(FULL.width, Math.round(fullChars * FULL_CHAR_WIDTH + 44)) + (anyRepeat ? REPEAT_ROOM : 0)};
const full = layout(history, order, fullDims);
const fullFit = box.width ? Math.min(1, (box.width - 34) / full.width, (box.height - 34) / full.height) : 1;
const compact = (mode === 'fit' ? fullFit : zoom) < COMPACT_BELOW;
// Compact nodes are as wide as their longest label (up to a cap), so "insert(null, 5) → a3"
// is never cut to "insert(null, …": the arguments are the point of the label.
const longest = Math.min(COMPACT_CHARS, Math.max(8, ...visible.map(node => compactLabel(node).length)));
- const compactDims = {...COMPACT, width: Math.round(longest * CHAR_WIDTH + 24 + (anyRepeat ? REPEAT_ROOM : 0))};
+ const compactDims = {...COMPACT, width: Math.round(longest * CHAR_WIDTH + 32 + (anyRepeat ? REPEAT_ROOM : 0))};
const dims = compact ? compactDims : fullDims;
const {positions, labels, width, height} = compact ? layout(history, order, compactDims) : full;
const seek = (node: CallNode) => onSelect(node.frame, node.last);
@@ -222,7 +225,7 @@ export function CallTree({trace, index, onSelect, selectedCall, baseline = null}
- {compact ? truncate(compactLabel(node), COMPACT_CHARS) : truncate(node.label, FULL_CHARS)}
+ {compact ? truncate(compactLabel(node), COMPACT_CHARS) : truncate(node.label, fullChars)}
{/* A returned call is ticked off inside its box, so "done" never rests on colour alone. */}
{/* Compact labels already say "→ value", so only full nodes carry the tick. */}
{!compact && node.state === 'completed' && }
diff --git a/web/src/callHistory.ts b/web/src/callHistory.ts
index 7ca6a59..6e715c5 100644
--- a/web/src/callHistory.ts
+++ b/web/src/callHistory.ts
@@ -27,6 +27,12 @@ function returned(value: string | null, stop: Snapshot) {
return shortValue(value).text;
}
+/** A method named without its class, `towerOfHanoi` rather than `Solution::towerOfHanoi`:
+ * the tree has room for the name or the values, and the values are the point. */
+export function shortName(name: string) {
+ return name.replace(/^(?:[A-Za-z_]\w*::)+(?=[A-Za-z_~])/, '');
+}
+
/** Replay only the prefix: backward seeking must not reveal future calls. */
export function callHistory(trace: Trace, index: number) {
const nodes = new Map();
@@ -53,9 +59,10 @@ export function callHistory(trace: Trace, index: number) {
// Display observed parameter values, not guessed locals or inferred returns.
// A call's first stop is its entry address, before the prologue stores arguments.
const entering = i === node.first && i > 0;
- const args = frame.locals.filter(local => local.is_argument).map(local =>
+ // `this` is an address the reader never passed, so the label leaves it out.
+ const args = frame.locals.filter(local => local.is_argument && local.name !== 'this').map(local =>
!entering && local.status === 'readable' ? argument(local, stop) : '?');
- node.label = `${frame.function}(${args.join(', ')})`;
+ node.label = `${shortName(frame.function)}(${args.join(', ')})`;
path.push(node);
}
for (const {call_id, value} of stop.returns ?? []) {
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 3f589be..56effff 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -14,7 +14,7 @@ import {Icon, SproutMark} from './Icon';
import {StackTree} from './StackTree';
import {CallTree} from './CallTree';
import {repeatedWork, type WorkSummary} from './RepeatReport';
-import {callHistory} from './callHistory';
+import {callHistory, shortName} from './callHistory';
import {GraphView} from './GraphView';
import {graphsAt, traceHasGraph} from './graph';
import {MemoryGraph} from './MemoryGraph';
@@ -384,11 +384,11 @@ function App() {
{/* Announces where you are when stepping; silent during playback, which would chatter. */}
- {step.frames[0]?.function ?? (step.event === 'exit' ? 'Finished' : 'Stopped')}
+ {step.frames[0] ? shortName(step.frames[0].function) : step.event === 'exit' ? 'Finished' : 'Stopped'}{step.location ? `Line ${step.location.line}` : step.event.replace('_', ' ')}{step.frames.length} active {step.frames.length === 1 ? 'call' : 'calls'}
{changes.heap.includes(index) && Memory changed}{changes.output.includes(index) && New output}
{inspected && }
+ Inspecting {shortName(inspected.function)} · line {inspected.location.line}}
Space play · ← → step · n over · f out