| 1 | import type { PerformanceSnapshot } from "./crash"; |
| 2 | |
| 3 | export function fmtNumber(n: number, digits = 0): string { |
| 4 | return Number.isFinite(n) ? n.toFixed(digits) : "0"; |
| 5 | } |
| 6 | |
| 7 | export function fmtMb(n: number): string { |
| 8 | return `${fmtNumber(n, 1)} MB`; |
| 9 | } |
| 10 | |
| 11 | export function formatPerformanceContext(snapshot: PerformanceSnapshot): string { |
| 12 | const lines = [ |
| 13 | `reason: ${snapshot.reason}`, |
| 14 | `uptime: ${fmtNumber(snapshot.uptimeMs / 1000, 1)}s`, |
| 15 | `visibility: ${snapshot.visibility || "unknown"}`, |
| 16 | `focused: ${snapshot.focused ? "true" : "false"}`, |
| 17 | `online: ${snapshot.online ? "true" : "false"}`, |
| 18 | `hardware concurrency: ${snapshot.hardwareConcurrency || "unknown"}`, |
| 19 | ]; |
| 20 | if (snapshot.deviceMemoryGb) lines.push(`device memory: ${snapshot.deviceMemoryGb} GB`); |
| 21 | if (snapshot.jsHeap) { |
| 22 | const pct = |
| 23 | snapshot.jsHeap.usagePercent !== undefined ? `, ${fmtNumber(snapshot.jsHeap.usagePercent)}% of limit` : ""; |
| 24 | lines.push( |
| 25 | `js heap: ${fmtMb(snapshot.jsHeap.usedMb)} used, ${fmtMb(snapshot.jsHeap.totalMb)} allocated, ${fmtMb(snapshot.jsHeap.limitMb)} limit${pct}`, |
| 26 | ); |
| 27 | } |
| 28 | if (snapshot.eventLoopLag) { |
| 29 | lines.push( |
| 30 | `event loop lag: current ${fmtNumber(snapshot.eventLoopLag.currentMs)}ms, max ${fmtNumber(snapshot.eventLoopLag.maxMs)}ms, avg ${fmtNumber(snapshot.eventLoopLag.avgMs)}ms over ${snapshot.eventLoopLag.samples} samples`, |
| 31 | ); |
| 32 | } |
| 33 | if (snapshot.longTasks) { |
| 34 | const recent = snapshot.longTasks.recent |
| 35 | .map( |
| 36 | (t) => |
| 37 | `${fmtNumber(t.durationMs)}ms @ ${fmtNumber(t.startMs / 1000, 1)}s${t.attribution ? ` (${t.attribution})` : ""}`, |
| 38 | ) |
| 39 | .join("; "); |
| 40 | lines.push( |
| 41 | `long tasks: ${snapshot.longTasks.count} in the last 60s, max ${fmtNumber(snapshot.longTasks.maxMs)}ms, total ${fmtNumber(snapshot.longTasks.totalMs)}ms`, |
| 42 | ); |
| 43 | if (recent) lines.push(`recent long tasks: ${recent}`); |
| 44 | } |
| 45 | if (snapshot.longTaskFrames?.length) { |
| 46 | lines.push("long task top frames (sampled):"); |
| 47 | for (const frame of snapshot.longTaskFrames) lines.push(` ${frame.samples}x ${frame.label}`); |
| 48 | } |
| 49 | if (snapshot.cpuProfile) { |
| 50 | const profile = snapshot.cpuProfile; |
| 51 | lines.push(`CPU profile after trigger: ${profile.status}${profile.durationMs === undefined ? "" : `, ${fmtNumber(profile.durationMs)}ms`}; does not reconstruct the original long task`); |
| 52 | for (const frame of profile.frames ?? []) lines.push(` ${fmtNumber(frame.selfMs, 1)}ms / ${frame.samples} samples: ${frame.label}`); |
| 53 | } |
| 54 | if (snapshot.processes) { |
| 55 | lines.push("process samples: Electron only (Go service excluded); working sets may share pages; CPU is interval average"); |
| 56 | const samples = snapshot.processes.samples; |
| 57 | for (const sample of samples) { |
| 58 | const total = sample.processes.every((p) => p.workingSetMb !== null) |
| 59 | ? fmtMb(sample.processes.reduce((sum, p) => sum + (p.workingSetMb ?? 0), 0)) : "unavailable"; |
| 60 | lines.push(` ${fmtNumber(sample.ageMs)}ms ago: ${sample.processes.length} processes${sample.truncated ? " (truncated)" : ""}, summed working set ${total}`); |
| 61 | } |
| 62 | for (const growth of snapshot.processes.growth ?? []) { |
| 63 | lines.push(`sustained memory growth (not proof of a leak): PID ${growth.pid} ${growth.type}, ${growth.metric} ${fmtMb(growth.baselineMb)} → ${fmtMb(growth.currentMb)} over ${fmtNumber(growth.durationMs / 1000)}s`); |
| 64 | } |
| 65 | const latest = samples[samples.length - 1]; |
| 66 | if (latest) { |
| 67 | lines.push(`latest process CPU interval: ${latest.intervalMs === null ? "baseline unavailable" : `${fmtNumber(latest.intervalMs)}ms`}`); |
| 68 | for (const p of latest.processes) { |
| 69 | lines.push(` PID ${p.pid} ${p.type}: CPU ${p.cpuPercent === null ? "unavailable" : `${fmtNumber(p.cpuPercent, 1)}%`}, working set ${p.workingSetMb === null ? "unavailable" : fmtMb(p.workingSetMb)}, private ${p.privateMb === null ? "unavailable" : fmtMb(p.privateMb)}`); |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | if (snapshot.connection) { |
| 74 | const parts = [ |
| 75 | snapshot.connection.effectiveType, |
| 76 | snapshot.connection.rttMs !== undefined ? `${snapshot.connection.rttMs}ms rtt` : "", |
| 77 | snapshot.connection.downlinkMbps !== undefined ? `${snapshot.connection.downlinkMbps} Mbps` : "", |
| 78 | snapshot.connection.saveData !== undefined ? `saveData ${snapshot.connection.saveData ? "true" : "false"}` : "", |
| 79 | ].filter(Boolean); |
| 80 | if (parts.length) lines.push(`connection: ${parts.join(", ")}`); |
| 81 | } |
| 82 | const pipeline = snapshot.sessionPipeline; |
| 83 | if (pipeline?.activation) { |
| 84 | const a = pipeline.activation; |
| 85 | const parts = [`request ${a.requestId}`]; |
| 86 | if (a.tabId) parts.push(`tab ${a.tabId}`); |
| 87 | if (a.ticketToStartingMs !== undefined) parts.push(`ticket→starting ${fmtNumber(a.ticketToStartingMs)}ms`); |
| 88 | if (a.startingToReadyMs !== undefined) parts.push(`starting→ready ${fmtNumber(a.startingToReadyMs)}ms`); |
| 89 | if (a.totalMs !== undefined) parts.push(`total ${fmtNumber(a.totalMs)}ms`); |
| 90 | if (a.outcome) parts.push(`outcome ${a.outcome}`); |
| 91 | if (a.failureClass) parts.push(`failure ${a.failureClass}`); |
| 92 | lines.push(`activation: ${parts.join(", ")}`); |
| 93 | } |
| 94 | if (pipeline?.history) { |
| 95 | const h = pipeline.history; |
| 96 | lines.push( |
| 97 | `history page: ${h.entries} entries, ${fmtNumber(h.inlineBytes / 1024, 1)} KiB inline, ${fmtNumber(h.durationMs)}ms, source ${h.source || "unknown"}${h.stale ? ", stale" : ""} ` + |
| 98 | `(pages ${h.pages}, stale ${h.staleCount}, index hits ${h.indexHits}, misses ${h.indexMisses})`, |
| 99 | ); |
| 100 | } |
| 101 | if (pipeline?.mountedRows) { |
| 102 | lines.push(`mounted rows: ${pipeline.mountedRows.mounted} of ${pipeline.mountedRows.total}`); |
| 103 | } |
| 104 | if (pipeline?.markdownWorker) { |
| 105 | const w = pipeline.markdownWorker; |
| 106 | lines.push( |
| 107 | `markdown worker: ${w.pending} pending, ${w.completed} parsed, avg ${fmtNumber(w.avgParseMs, 1)}ms, max ${fmtNumber(w.maxParseMs)}ms` + |
| 108 | `${w.fallbackActive ? ", fallback active" : ""}${w.workerFailures > 0 ? `, ${w.workerFailures} worker failures` : ""}`, |
| 109 | ); |
| 110 | } |
| 111 | if (pipeline?.transcriptCache) { |
| 112 | const c = pipeline.transcriptCache; |
| 113 | lines.push( |
| 114 | `transcript cache: ${c.residentSessions}/${c.maxResidentSessions} resident sessions, ` + |
| 115 | `bodies ${fmtMb(c.bodyBytes / 1048576)} of ${fmtMb(c.bodyBudgetBytes / 1048576)}, ` + |
| 116 | `markdown ${fmtMb(c.markdownBytes / 1048576)} of ${fmtMb(c.markdownBudgetBytes / 1048576)}, ` + |
| 117 | `evictions ${c.historyEvictions} history + ${c.markdownEvictions} markdown, ` + |
| 118 | `window ${c.residentWindowEntries} entries over ${c.windowMaxPages} pages, ${c.reclaimedPages} reclaimed`, |
| 119 | ); |
| 120 | } |
| 121 | return lines.join("\n"); |
| 122 | } |
| 123 |