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