Skip to content

Commit 176c847

Browse files
feat: add a RAM view for the memory canvas
A third board tab renders a dense memory map: each object's address, byte size, reference count, and value. Addresses are monotonic and 16-byte aligned (models malloc); a numpy buffer draws its raw byte cells and every ndarray window over it paints its offset/shape range as a colored strip aligned to the same byte columns — a slice is visibly a narrower window over the same contiguous run. Wired as a 'ram' BoardTab with a renderRamBoard that stays in sync with the Memory tab on every state change.
1 parent be5e7f6 commit 176c847

3 files changed

Lines changed: 188 additions & 8 deletions

File tree

‎src/ui/app.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { checkGoal } from '../engine/checks';
1010
import type { GoalSpec, Level, MemoryState, Snapshot } from '../engine/types';
1111
import { splitPythonStatements } from '../engine/steps';
1212
import { allLevels, getLevel, levelIndex, sequences } from '../levels/index';
13-
import { renderMemory } from './memory';
13+
import { renderMemory, renderRam } from './memory';
1414
import { BoardView } from './board';
1515
import { TerminalView } from './terminal';
1616
import { ScriptEditorView } from './editor';
@@ -179,9 +179,10 @@ export class App {
179179
this.board = new BoardView(
180180
this.root.querySelector('#board-wrap')!,
181181
(tab) => {
182-
// After a tab switch the #canvas is rebuilt; redraw memory if we're back
183-
// on the Memory tab.
182+
// After a tab switch its canvas is rebuilt; redraw whichever tab we
183+
// just landed on.
184184
if (tab === 'memory') this.renderBoard();
185+
if (tab === 'ram') this.renderRamBoard();
185186
},
186187
);
187188
this.editor = new ScriptEditorView(
@@ -303,7 +304,7 @@ export class App {
303304
this.terminal.setHint(goalLines[0] ?? null);
304305

305306
this.board.setPlots(this.session.state.plots);
306-
this.renderBoard();
307+
this.renderBoards();
307308
this.refreshDock();
308309
this.updateTitle();
309310

@@ -333,7 +334,7 @@ export class App {
333334

334335
this.session.reset();
335336
this.board.setPlots([]);
336-
this.renderBoard();
337+
this.renderBoards();
337338
this.renderDock();
338339
this.updateTitle();
339340
}
@@ -356,6 +357,17 @@ export class App {
356357
renderMemory(this.board.canvasEl, state, edges, live);
357358
}
358359

360+
private renderRamBoard(): void {
361+
const state = this.session.state;
362+
renderRam(this.board.ramCanvasEl, state, liveObjectIds(state));
363+
}
364+
365+
/** Redraw both memory boards so the Memory and RAM tabs stay in sync. */
366+
private renderBoards(): void {
367+
this.renderBoard();
368+
this.renderRamBoard();
369+
}
370+
359371
/* —— Execution & verification —— */
360372

361373
private executeCode(raw: string): void {
@@ -384,7 +396,7 @@ export class App {
384396
}
385397

386398
this.board.setPlots(this.session.state.plots);
387-
this.renderBoard();
399+
this.renderBoards();
388400
if (this.level) this.checkVerification(source);
389401
}
390402

@@ -649,7 +661,7 @@ export class App {
649661
this.statementsUsed = prev.statementsUsed;
650662
this.levelCompleted = false;
651663
this.terminal.push('meta', 'Undid last statement.');
652-
this.renderBoard();
664+
this.renderBoards();
653665
this.board.setPlots(state.plots);
654666
this.refreshDock();
655667
}

‎src/ui/board.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@
1010
import type { PlotSpec } from '../engine/types';
1111
import { escapeHtml } from './dialog';
1212

13-
export type BoardTab = 'memory' | 'plot';
13+
export type BoardTab = 'memory' | 'plot' | 'ram';
1414

1515
export class BoardView {
1616
private root: HTMLElement;
1717
private canvas!: SVGSVGElement;
18+
private ramCanvas!: SVGSVGElement;
1819
private plotBody!: HTMLElement;
1920
private memoryTabBtn!: HTMLButtonElement;
2021
private plotTabBtn!: HTMLButtonElement;
22+
private ramTabBtn!: HTMLButtonElement;
2123
private currentTab: BoardTab = 'memory';
2224
private plots: PlotSpec[] = [];
2325
private onTabChange?: (tab: BoardTab) => void;
@@ -33,6 +35,11 @@ export class BoardView {
3335
return this.canvas;
3436
}
3537

38+
/** The live RAM canvas (App drives it via renderRam). */
39+
get ramCanvasEl(): SVGSVGElement {
40+
return this.ramCanvas;
41+
}
42+
3643
setTab(tab: BoardTab): void {
3744
if (tab === this.currentTab) return;
3845
this.currentTab = tab;
@@ -72,6 +79,10 @@ export class BoardView {
7279
<span>Plot</span>
7380
${hasPlot ? '<span class="tab-badge ok">●</span>' : ''}
7481
</button>
82+
<button type="button" class="board-tab${this.currentTab === 'ram' ? ' is-active' : ''}" data-tab="ram" role="tab" aria-selected="${this.currentTab === 'ram'}">
83+
<span>RAM</span>
84+
<span class="tab-badge">addr · ref</span>
85+
</button>
7586
</div>
7687
</div>
7788
<div class="board-body">
@@ -81,16 +92,22 @@ export class BoardView {
8192
<div class="board-pane${this.currentTab === 'plot' ? ' is-active' : ''}" data-pane="plot">
8293
<div class="plot-stage" id="plot-stage"></div>
8394
</div>
95+
<div class="board-pane${this.currentTab === 'ram' ? ' is-active' : ''}" data-pane="ram">
96+
<div class="canvas-wrap"><svg id="ram-canvas" xmlns="http://www.w3.org/2000/svg"></svg></div>
97+
</div>
8498
</div>
8599
`;
86100

87101
this.canvas = this.root.querySelector('#canvas')!;
102+
this.ramCanvas = this.root.querySelector('#ram-canvas')!;
88103
this.plotBody = this.root.querySelector('#plot-stage')!;
89104
this.memoryTabBtn = this.root.querySelector('[data-tab="memory"]') as HTMLButtonElement;
90105
this.plotTabBtn = this.root.querySelector('[data-tab="plot"]') as HTMLButtonElement;
106+
this.ramTabBtn = this.root.querySelector('[data-tab="ram"]') as HTMLButtonElement;
91107

92108
this.memoryTabBtn.addEventListener('click', () => this.setTab('memory'));
93109
this.plotTabBtn.addEventListener('click', () => this.setTab('plot'));
110+
this.ramTabBtn.addEventListener('click', () => this.setTab('ram'));
94111

95112
this.renderPlot();
96113
}

‎src/ui/memory.ts‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,157 @@ function drawEdge(parent: SVGElement, from: Pos, to: Pos, opacity = 1): void {
298298
}
299299
}
300300

301+
/**
302+
* The RAM view: a dense memory map showing where each object *lives* — its
303+
* address, size, and reference count — rather than the abstract graph. Columns:
304+
* ADDR (hex, monotonic like malloc) · OBJ (id) · TYPE · BYTES · REFC · VALUE.
305+
*
306+
* For a numpy buffer the VALUE cell draws the raw byte cells; every ndarray
307+
* window over that buffer then paints its own offset/shape window as a tinted
308+
* strip beneath the cells — the flagship "how it works in RAM" picture, where
309+
* a slice is just a narrower window over the same contiguous run.
310+
*/
311+
export function renderRam(svg: SVGSVGElement, state: MemoryState, liveIds: string[]): void {
312+
while (svg.firstChild) svg.removeChild(svg.firstChild);
313+
314+
const ids = liveIds.filter((id) => state.heap[id]);
315+
const HEADER_X = 14;
316+
const COL_OBJ = HEADER_X + 56;
317+
const COL_TYPE = COL_OBJ + 92;
318+
const COL_BYTES = COL_TYPE + 96;
319+
const COL_REF = COL_BYTES + 64;
320+
const COL_VALUE = COL_REF + 52;
321+
const VALUE_W = 360;
322+
const width = COL_VALUE + VALUE_W + 20;
323+
const ROW_H = 24;
324+
const ROW_GAP = 8;
325+
const mono = 'IBM Plex Mono, monospace';
326+
const CELL_W = 30;
327+
const CELL_GAP = 2;
328+
const CELL_H = 16;
329+
const VIEW_H = 14;
330+
331+
// One color per ndarray view over a given buffer, so multiple overlapping
332+
// windows stay distinguishable in the column strip.
333+
const VIEW_PALETTE = [COLORS.amber, COLORS.coral, COLORS.teal];
334+
335+
const bufferInfo = new Map<string, { data: number[]; y: number }>();
336+
const viewLists = new Map<string, string[]>(); // bufferId -> ndarray ids in order
337+
for (const id of ids) {
338+
const obj = state.heap[id]!;
339+
if (obj.type === 'ndarray' && isNdarrayValue(obj.value)) {
340+
const v = obj.value as { buffer: string };
341+
const list = viewLists.get(v.buffer) ?? [];
342+
list.push(id);
343+
viewLists.set(v.buffer, list);
344+
}
345+
}
346+
const rowH = (obj: { id: string; type: string }): number => {
347+
if (obj.type !== 'buffer') return ROW_H;
348+
const n = viewLists.get(obj.id)?.length ?? 0;
349+
return 48 + n * VIEW_H;
350+
};
351+
const totalH = Math.max(ids.reduce((acc, id) => acc + rowH(state.heap[id]!) + ROW_GAP, 26), 220);
352+
353+
svg.setAttribute('viewBox', `0 0 ${width} ${totalH}`);
354+
svg.setAttribute('width', '100%');
355+
svg.setAttribute('height', String(Math.max(totalH, 200)));
356+
357+
// Column header band
358+
addText(svg, HEADER_X, 18, 'ADDR', COLORS.muted, 10, 'start', 'middle', mono);
359+
addText(svg, COL_OBJ, 18, 'OBJ', COLORS.muted, 10, 'start', 'middle', mono);
360+
addText(svg, COL_TYPE, 18, 'TYPE', COLORS.muted, 10, 'start', 'middle', mono);
361+
addText(svg, COL_BYTES, 18, 'BYTES', COLORS.muted, 10, 'start', 'middle', mono);
362+
addText(svg, COL_REF, 18, 'REFC', COLORS.muted, 10, 'start', 'middle', mono);
363+
addText(svg, COL_VALUE, 18, 'VALUE', COLORS.muted, 10, 'start', 'middle', mono);
364+
365+
let y = 34;
366+
for (const id of ids) {
367+
const obj = state.heap[id]!;
368+
const h = rowH(obj);
369+
const midY = y + h / 2;
370+
const isBuffer = obj.type === 'buffer';
371+
372+
// row divider
373+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
374+
line.setAttribute('x1', String(HEADER_X));
375+
line.setAttribute('x2', String(width - 14));
376+
line.setAttribute('y1', String(y));
377+
line.setAttribute('y2', String(y));
378+
line.setAttribute('stroke', COLORS.line);
379+
line.setAttribute('stroke-width', '1');
380+
svg.appendChild(line);
381+
382+
addText(svg, HEADER_X, midY, hexAddr(obj.addr), COLORS.amber, 11, 'start', 'middle', mono);
383+
addText(svg, COL_OBJ, midY, obj.id, COLORS.ink, 11, 'start', 'middle', mono);
384+
addText(svg, COL_TYPE, midY, obj.type, COLORS.teal, 11, 'start', 'middle', mono);
385+
addText(svg, COL_BYTES, midY, String(obj.byteSize), COLORS.ink, 11, 'start', 'middle', mono);
386+
addText(svg, COL_REF, midY, String(obj.refCount), obj.refCount === 0 ? COLORS.coral : COLORS.ink, 11, 'start', 'middle', mono);
387+
388+
if (isBuffer) {
389+
const data = (obj.value as { data?: number[] }).data ?? [];
390+
const barY = y + ROW_H + 4;
391+
for (let i = 0; i < data.length; i++) {
392+
const cx = COL_VALUE + i * (CELL_W + CELL_GAP);
393+
const cell = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
394+
cell.setAttribute('x', String(cx));
395+
cell.setAttribute('y', String(barY));
396+
cell.setAttribute('width', String(CELL_W));
397+
cell.setAttribute('height', String(CELL_H));
398+
cell.setAttribute('rx', '3');
399+
cell.setAttribute('fill', '#223038');
400+
cell.setAttribute('stroke', COLORS.line);
401+
svg.appendChild(cell);
402+
addText(svg, cx + CELL_W / 2, barY + CELL_H / 2, String(data[i]), COLORS.ink, 10, 'middle', 'middle', mono);
403+
}
404+
addText(svg, COL_VALUE, midY, `${hexAddr(obj.addr)} · ${data.length}×8B`, COLORS.muted, 10, 'start', 'middle', mono);
405+
bufferInfo.set(id, { data, y: barY });
406+
} else {
407+
const label = obj.type === 'ndarray'
408+
? `${obj.repr}`
409+
: obj.repr.length > 40 ? obj.repr.slice(0, 40) + '…' : obj.repr;
410+
addText(svg, COL_VALUE, midY, label, COLORS.ink, 11, 'start', 'middle', mono);
411+
}
412+
413+
// An ndarray view: paint its window in its buffer's column strip, aligned
414+
// to the same byte columns. Multiple views over one buffer stack as separate
415+
// rows, each showing the exact [offset..offset+shape) range it reads.
416+
if (obj.type === 'ndarray' && isNdarrayValue(obj.value)) {
417+
const v = obj.value as { buffer: string; offset: number; shape: number };
418+
const buf = bufferInfo.get(v.buffer);
419+
const list = viewLists.get(v.buffer) ?? [];
420+
const idx = list.indexOf(id);
421+
const bufLen = buf ? buf.data.length : 0;
422+
if (buf) {
423+
const color = VIEW_PALETTE[idx % VIEW_PALETTE.length];
424+
const barY = buf.y + CELL_H + 4 + idx * VIEW_H;
425+
const x = COL_VALUE + v.offset * (CELL_W + CELL_GAP);
426+
const w = Math.max(v.shape, 1) * (CELL_W + CELL_GAP) - CELL_GAP;
427+
const win = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
428+
win.setAttribute('x', String(x));
429+
win.setAttribute('y', String(barY));
430+
win.setAttribute('width', String(w));
431+
win.setAttribute('height', String(VIEW_H - 4));
432+
win.setAttribute('rx', '2');
433+
win.setAttribute('fill', color);
434+
win.setAttribute('fill-opacity', '0.28');
435+
win.setAttribute('stroke', color);
436+
win.setAttribute('stroke-width', '1');
437+
svg.appendChild(win);
438+
// Label in a gutter just past the buffer cells so it never collides.
439+
const labelX = COL_VALUE + Math.max(bufLen, v.offset + v.shape) * (CELL_W + CELL_GAP) + 8;
440+
addText(svg, labelX, barY + (VIEW_H - 4) / 2, `${obj.id} [${v.offset}..${v.offset + v.shape}]`, color, 9, 'start', 'middle', mono);
441+
}
442+
}
443+
444+
y += h + ROW_GAP;
445+
}
446+
}
447+
448+
function hexAddr(addr: number): string {
449+
return `0x${addr.toString(16).padStart(4, '0')}`;
450+
}
451+
301452
function addText(parent: SVGElement, x: number, y: number, text: string, fill: string, size: number, anchor: string, baseline: string, family = 'IBM Plex Sans, sans-serif'): void {
302453
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
303454
t.setAttribute('x', String(x));

0 commit comments

Comments
 (0)