Skill Tracer
Skills › gstack › browse › memory-command.ts

memory-command.ts

115 lines · browse/src/memory-command.ts at 3f79d81

1// `$B memory` — diagnostic snapshot of Bun heap + per-tab JS heap +
2// Chromium process tree + bounded buffer sizes. Lives in its own file
3// because the meta-commands dispatcher imports it lazily — projects
4// that never run the diagnostic don't pay the import-graph cost (CDP
5// bridge, memory-snapshot types, buffer accessors).
6
7import type { BrowserManager } from './browser-manager';
8import { formatBytes, type MemorySnapshot, type MemoryStructureStats } from './memory-snapshot';
9import { getModificationHistoryStats } from './cdp-inspector';
10import { getSubscriberCount as getActivitySubscriberCount } from './activity';
11import { getInspectorSubscriberCount } from './server';
12import { consoleBuffer, networkBuffer, dialogBuffer } from './buffers';
13import { getCaptureBuffer } from './network-capture';
15/**
16 * Assemble the MemoryStructureStats from the modules that own each buffer.
17 * Browser-manager doesn't take a hard dep on every buffer-owning module —
18 * the snapshot caller passes them in.
19 */
20function collectStructureStats(): MemoryStructureStats {
21 return {
22 modificationHistory: getModificationHistoryStats(),
23 activitySubscribers: getActivitySubscriberCount(),
24 inspectorSubscribers: getInspectorSubscriberCount(),
25 consoleBufferLen: consoleBuffer.length,
26 networkBufferLen: networkBuffer.length,
27 dialogBufferLen: dialogBuffer.length,
28 captureBufferBytes: getCaptureBuffer().byteSize,
29 };
32/**
33 * Pretty-print the snapshot for terminal output. JSON mode (--json) goes
34 * straight through JSON.stringify so the extension footer and any test
35 * harness can consume it programmatically.
36 */
37function formatSnapshotText(s: MemorySnapshot): string {
38 const lines: string[] = [];
39 lines.push(
40 `Bun server: RSS: ${formatBytes(s.bunServer.rss)} ` +
41 `heap: ${formatBytes(s.bunServer.heapUsed)} / ${formatBytes(s.bunServer.heapTotal)} ` +
42 `external: ${formatBytes(s.bunServer.external)}`,
43 );
45 if (s.processes && s.processes.length > 0) {
46 // Group by type so the user sees "renderer: 12" vs listing 12 separate rows.
47 const byType: Record<string, number> = {};
48 for (const p of s.processes) byType[p.type] = (byType[p.type] ?? 0) + 1;
49 const typeSummary = Object.entries(byType)
50 .map(([t, n]) => `${t}=${n}`)
51 .join(' ');
52 lines.push(`Chromium processes: ${s.processes.length} total (${typeSummary})`);
53 } else if (s.processes === null) {
54 lines.push('Chromium processes: (unavailable — see notes)');
55 } else {
56 lines.push('Chromium processes: 0');
57 }
59 if (s.tabs.length > 0) {
60 // Sort by JS heap descending; show top 10 plus "...N more" tail.
61 const sorted = [...s.tabs].sort((a, b) => b.jsHeapUsed - a.jsHeapUsed);
62 const shown = sorted.slice(0, 10);
63 lines.push(`Renderers: ${s.tabs.length} tabs (top by JS heap):`);
64 for (const t of shown) {
65 const urlShort = t.url.length > 80 ? t.url.slice(0, 77) + '...' : t.url;
66 lines.push(
67 ` [${formatBytes(t.jsHeapUsed).padStart(8)} JS, ` +
68 `${String(t.nodes).padStart(6)} nodes, ` +
69 `${String(t.listeners).padStart(5)} listeners] ` +
70 `tab #${t.id} — ${urlShort}`,
71 );
72 }
73 if (sorted.length > shown.length) {
74 lines.push(` ...and ${sorted.length - shown.length} more`);
75 }
76 } else {
77 lines.push('Renderers: (no tabs tracked)');
78 }
80 lines.push('─────────────────────────────────────────────────');
81 lines.push('In-memory structures (Bun side):');
82 const m = s.structures.modificationHistory;
83 lines.push(
84 ` modificationHistory: ${m.current} / ${m.cap} entries` +
85 (m.evicted > 0 ? ` (${m.evicted} evicted since reset)` : ''),
86 );
87 lines.push(` inspectorSubscribers: ${s.structures.inspectorSubscribers}`);
88 lines.push(` activitySubscribers: ${s.structures.activitySubscribers}`);
89 lines.push(` consoleBuffer: ${s.structures.consoleBufferLen} entries`);
90 lines.push(` networkBuffer: ${s.structures.networkBufferLen} entries`);
91 lines.push(` dialogBuffer: ${s.structures.dialogBufferLen} entries`);
92 lines.push(` captureBuffer: ${formatBytes(s.structures.captureBufferBytes)}`);
94 if (s.notes.length > 0) {
95 lines.push('');
96 lines.push('Notes:');
97 for (const n of s.notes) lines.push(` - ${n}`);
98 }
100 return lines.join('\n');
103export async function handleMemoryCommand(args: string[], bm: BrowserManager): Promise<string> {
104 const jsonMode = args.includes('--json');
105 const structures = collectStructureStats();
106 const snapshot = await bm.getMemorySnapshot(structures);
107 if (jsonMode) return JSON.stringify(snapshot);
108 return formatSnapshotText(snapshot);
111/** Entry point used by the /memory HTTP endpoint — same data, always JSON. */
112export async function buildMemorySnapshotJson(bm: BrowserManager): Promise<MemorySnapshot> {
113 const structures = collectStructureStats();
114 return bm.getMemorySnapshot(structures);