( trigger: 'manual' | 'auto-1.5GB' = 'manual', dumpNumber = 0, )
| 219 | * diagnostics first, we still get useful memory info even if the snapshot fails. |
| 220 | */ |
| 221 | export async function performHeapDump( |
| 222 | trigger: 'manual' | 'auto-1.5GB' = 'manual', |
| 223 | dumpNumber = 0, |
| 224 | ): Promise<HeapDumpResult> { |
| 225 | try { |
| 226 | const sessionId = getSessionId() |
| 227 | |
| 228 | // Capture diagnostics before any other async I/O — |
| 229 | // the heap dump itself allocates memory and would skew the numbers. |
| 230 | const diagnostics = await captureMemoryDiagnostics(trigger, dumpNumber) |
| 231 | |
| 232 | const toGB = (bytes: number): string => |
| 233 | (bytes / 1024 / 1024 / 1024).toFixed(3) |
| 234 | logForDebugging(`[HeapDump] Memory state: |
| 235 | heapUsed: ${toGB(diagnostics.memoryUsage.heapUsed)} GB (in snapshot) |
| 236 | external: ${toGB(diagnostics.memoryUsage.external)} GB (NOT in snapshot) |
| 237 | rss: ${toGB(diagnostics.memoryUsage.rss)} GB (total process) |
| 238 | ${diagnostics.analysis.recommendation}`) |
| 239 | |
| 240 | const dumpDir = getDesktopPath() |
| 241 | await getFsImplementation().mkdir(dumpDir) |
| 242 | |
| 243 | const suffix = dumpNumber > 0 ? `-dump${dumpNumber}` : '' |
| 244 | const heapFilename = `${sessionId}${suffix}.heapsnapshot` |
| 245 | const diagFilename = `${sessionId}${suffix}-diagnostics.json` |
| 246 | const heapPath = join(dumpDir, heapFilename) |
| 247 | const diagPath = join(dumpDir, diagFilename) |
| 248 | |
| 249 | // Write diagnostics first (cheap, unlikely to fail) |
| 250 | await writeFile(diagPath, jsonStringify(diagnostics, null, 2), { |
| 251 | mode: 0o600, |
| 252 | }) |
| 253 | logForDebugging(`[HeapDump] Diagnostics written to ${diagPath}`) |
| 254 | |
| 255 | // Write heap snapshot (this can crash for very large heaps) |
| 256 | await writeHeapSnapshot(heapPath) |
| 257 | logForDebugging(`[HeapDump] Heap dump written to ${heapPath}`) |
| 258 | |
| 259 | logEvent('tengu_heap_dump', { |
| 260 | triggerManual: trigger === 'manual', |
| 261 | triggerAuto15GB: trigger === 'auto-1.5GB', |
| 262 | dumpNumber, |
| 263 | success: true, |
| 264 | }) |
| 265 | |
| 266 | return { success: true, heapPath, diagPath } |
| 267 | } catch (err) { |
| 268 | const error = toError(err) |
| 269 | logError(error) |
| 270 | logEvent('tengu_heap_dump', { |
| 271 | triggerManual: trigger === 'manual', |
| 272 | triggerAuto15GB: trigger === 'auto-1.5GB', |
| 273 | dumpNumber, |
| 274 | success: false, |
| 275 | }) |
| 276 | return { success: false, error: error.message } |
| 277 | } |
| 278 | } |
no test coverage detected