* Diagnostic for `$B memory` and the /memory endpoint. * * Collects: * - Bun process memory (cross-platform, accurate, no shelling). * - Per-tab JS heap via CDP Performance.getMetrics — the most portable * per-tab signal CDP exposes. Misses native/GPU/Skia/cache memory *
(structures: MemoryStructureStats)
| 1057 | * browser-manager doesn't take a hard dep on every buffer-owning module. |
| 1058 | */ |
| 1059 | async getMemorySnapshot(structures: MemoryStructureStats): Promise<MemorySnapshot> { |
| 1060 | const bunMem = process.memoryUsage(); |
| 1061 | const notes: string[] = []; |
| 1062 | |
| 1063 | // Per-tab JS heap. Lazy: only the pages we already track. A target |
| 1064 | // that died mid-snapshot is omitted, never throws. |
| 1065 | const tabs: MemoryTabSnapshot[] = []; |
| 1066 | for (const [id, page] of this.pages) { |
| 1067 | try { |
| 1068 | const url = (() => { try { return page.url(); } catch { return ''; } })(); |
| 1069 | const title = await page.title().catch(() => ''); |
| 1070 | const metrics = await withCdpSession(page, async (session) => { |
| 1071 | await session.send('Performance.enable').catch(() => undefined); |
| 1072 | const result = await session.send('Performance.getMetrics'); |
| 1073 | return ((result as { metrics?: Array<{ name: string; value: number }> }).metrics) ?? []; |
| 1074 | }); |
| 1075 | const mm: Record<string, number> = {}; |
| 1076 | for (const m of metrics) mm[m.name] = m.value; |
| 1077 | tabs.push({ |
| 1078 | id, |
| 1079 | url, |
| 1080 | title, |
| 1081 | jsHeapUsed: mm.JSHeapUsedSize ?? 0, |
| 1082 | jsHeapTotal: mm.JSHeapTotalSize ?? 0, |
| 1083 | documents: mm.Documents ?? 0, |
| 1084 | nodes: mm.Nodes ?? 0, |
| 1085 | listeners: mm.JSEventListeners ?? 0, |
| 1086 | }); |
| 1087 | } catch { |
| 1088 | // Target died or CDP unavailable mid-snapshot — skip this tab. |
| 1089 | } |
| 1090 | } |
| 1091 | |
| 1092 | // Chromium process tree. Browser handle may be on the `browser` field |
| 1093 | // (launched mode) or accessible via `context.browser()` (persistent |
| 1094 | // context / headed mode); try both. |
| 1095 | let processes: MemoryProcess[] | null = null; |
| 1096 | const browser: Browser | null = this.browser ?? (this.context ? this.context.browser() : null); |
| 1097 | if (browser) { |
| 1098 | try { |
| 1099 | // `newBrowserCDPSession` is browser-wide. Not exposed on every |
| 1100 | // Playwright TypeScript surface, but present at runtime on the |
| 1101 | // Browser instance — use a typed cast to avoid the @ts-expect-error. |
| 1102 | type BrowserWithCDP = Browser & { |
| 1103 | newBrowserCDPSession?: () => Promise<{ |
| 1104 | send: (method: string, params?: unknown) => Promise<unknown>; |
| 1105 | detach: () => Promise<void>; |
| 1106 | }>; |
| 1107 | }; |
| 1108 | const maybeFactory = (browser as BrowserWithCDP).newBrowserCDPSession; |
| 1109 | if (typeof maybeFactory === 'function') { |
| 1110 | const browserSession = await maybeFactory.call(browser); |
| 1111 | try { |
| 1112 | const info = (await browserSession.send('SystemInfo.getProcessInfo')) as { |
| 1113 | processInfo?: Array<{ id: number; type: string; cpuTime: number }>; |
| 1114 | }; |
| 1115 | processes = (info.processInfo ?? []).map((p) => ({ |
| 1116 | id: p.id, |
no test coverage detected