( trigger: 'manual' | 'auto-1.5GB', dumpNumber = 0, )
| 86 | * This helps identify if the leak is in V8 heap (captured) or native memory (not captured). |
| 87 | */ |
| 88 | export async function captureMemoryDiagnostics( |
| 89 | trigger: 'manual' | 'auto-1.5GB', |
| 90 | dumpNumber = 0, |
| 91 | ): Promise<MemoryDiagnostics> { |
| 92 | const usage = process.memoryUsage() |
| 93 | const heapStats = getHeapStatistics() |
| 94 | const resourceUsage = process.resourceUsage() |
| 95 | const uptimeSeconds = process.uptime() |
| 96 | |
| 97 | // getHeapSpaceStatistics() is not available in Bun |
| 98 | let heapSpaceStats: HeapSpaceInfo[] | undefined |
| 99 | try { |
| 100 | heapSpaceStats = getHeapSpaceStatistics() |
| 101 | } catch { |
| 102 | // Not available in Bun runtime |
| 103 | } |
| 104 | |
| 105 | // Get active handles/requests count (these are internal APIs but stable) |
| 106 | const activeHandles = ( |
| 107 | process as unknown as { _getActiveHandles: () => unknown[] } |
| 108 | )._getActiveHandles().length |
| 109 | const activeRequests = ( |
| 110 | process as unknown as { _getActiveRequests: () => unknown[] } |
| 111 | )._getActiveRequests().length |
| 112 | |
| 113 | // Try to count open file descriptors (Linux/macOS) |
| 114 | let openFileDescriptors: number | undefined |
| 115 | try { |
| 116 | openFileDescriptors = (await readdir('/proc/self/fd')).length |
| 117 | } catch { |
| 118 | // Not on Linux - try macOS approach would require lsof, skip for now |
| 119 | } |
| 120 | |
| 121 | // Try to read Linux smaps_rollup for detailed memory breakdown |
| 122 | let smapsRollup: string | undefined |
| 123 | try { |
| 124 | smapsRollup = await readFile('/proc/self/smaps_rollup', 'utf8') |
| 125 | } catch { |
| 126 | // Not on Linux or no access - this is fine |
| 127 | } |
| 128 | |
| 129 | // Calculate native memory (RSS - heap) and growth rate |
| 130 | const nativeMemory = usage.rss - usage.heapUsed |
| 131 | const bytesPerSecond = uptimeSeconds > 0 ? usage.rss / uptimeSeconds : 0 |
| 132 | const mbPerHour = (bytesPerSecond * 3600) / (1024 * 1024) |
| 133 | |
| 134 | // Identify potential leaks |
| 135 | const potentialLeaks: string[] = [] |
| 136 | if (heapStats.number_of_detached_contexts > 0) { |
| 137 | potentialLeaks.push( |
| 138 | `${heapStats.number_of_detached_contexts} detached context(s) - possible iframe/context leak`, |
| 139 | ) |
| 140 | } |
| 141 | if (activeHandles > 100) { |
| 142 | potentialLeaks.push( |
| 143 | `${activeHandles} active handles - possible timer/socket leak`, |
| 144 | ) |
| 145 | } |
no test coverage detected