#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); const args = process.argv.slice(2); function readArg(name) { const index = args.indexOf(name); if (index === -1) { return null; } return args[index + 1] ?? null; } function hasFlag(name) { return args.includes(name); } function resolveCandidateUserDataDirs() { const home = os.homedir(); const appNames = ['Toju', 'metoyou']; if (process.platform === 'win32') { const base = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); return appNames.map((name) => path.join(base, name)); } if (process.platform === 'darwin') { const base = path.join(home, 'Library', 'Application Support'); return appNames.map((name) => path.join(base, name)); } const base = process.env.XDG_CONFIG_HOME || path.join(home, '.config'); return appNames.map((name) => path.join(base, name)); } function resolveDefaultDiagnosticsDir() { const candidates = resolveCandidateUserDataDirs() .map((userDataDir) => path.join(userDataDir, 'diagnostics')) .filter((dir) => fs.existsSync(dir)); if (candidates.length === 0) { return path.join(resolveCandidateUserDataDirs()[0], 'diagnostics'); } return candidates .map((dir) => ({ dir, mtimeMs: fs.statSync(dir).mtimeMs })) .sort((left, right) => right.mtimeMs - left.mtimeMs)[0].dir; } function parseJsonLines(content) { return content .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); } function formatKb(kb) { if (kb == null) { return 'n/a'; } return `${Math.round(kb / 1024)} MB`; } function summarize(entries) { const latestHighMemory = [...entries].reverse().find((entry) => entry.type === 'high-memory'); const latestProcess = [...entries].reverse().find((entry) => entry.type === 'process'); const latestStore = latestHighMemory?.payload?.recentRendererSamples?.store ?? [...entries].reverse().find((entry) => entry.type === 'store')?.payload; const latestComponents = latestHighMemory?.payload?.recentRendererSamples?.components ?? [...entries].reverse().find((entry) => entry.type === 'components')?.payload; const latestHeap = latestHighMemory?.payload?.recentRendererSamples?.heap ?? [...entries].reverse().find((entry) => entry.type === 'heap')?.payload; if (latestHighMemory?.payload?.summary) { const summary = latestHighMemory.payload.summary; console.log(`High memory threshold crossed: ${summary.totalWorkingSetGb} GB (threshold ${summary.thresholdGb} GB)`); if (Array.isArray(summary.topProcesses) && summary.topProcesses.length > 0) { console.log('Top processes:'); for (const process of summary.topProcesses.slice(0, 8)) { console.log(` ${process.type} (pid ${process.pid}): ${formatKb(process.workingSetKb)} (${process.sharePercent}%)`); } } } else if (latestProcess) { console.log(`Process RSS total: ${formatKb(latestProcess.payload.totalWorkingSetKb)}`); } if (latestHeap) { console.log(`Renderer JS heap: ${latestHeap.usedJsHeapMb ?? 'n/a'} MB`); } if (latestHighMemory?.payload?.mainProcessMemoryMb) { const mainMemory = latestHighMemory.payload.mainProcessMemoryMb; console.log(`Main process heap used: ${mainMemory.heapUsedMb ?? 'n/a'} MB`); } if (latestStore?.domains) { console.log('Store domains (estimated bytes):'); for (const [domain, bytes] of Object.entries(latestStore.domains).sort((a, b) => b[1] - a[1])) { console.log(` ${domain}: ${bytes}`); } } if (latestComponents?.domains) { console.log('Live components by domain:'); for (const [domain, count] of Object.entries(latestComponents.domains).sort((a, b) => b[1] - a[1])) { console.log(` ${domain}: ${count}`); } } const leaks = latestComponents?.suspectedLeaks; if (Array.isArray(leaks) && leaks.length > 0) { console.log('Suspected component leaks:'); for (const leak of leaks) { console.log(` ${leak.name}: ${leak.count} (expected ${leak.expected})`); } } } function main() { const dir = readArg('--dir') || resolveDefaultDiagnosticsDir(); const file = readArg('--file'); if (!fs.existsSync(dir) && !file) { console.error(`Diagnostics directory not found: ${dir}`); process.exit(1); } const targetFile = file || findLatestFile(dir); if (!targetFile) { console.error(`No diagnostics files found in ${dir}`); process.exit(1); } console.log(`Reading ${targetFile}`); if (hasFlag('--tail')) { tailFile(targetFile); return; } const entries = parseJsonLines(fs.readFileSync(targetFile, 'utf8')); summarize(entries); } function findLatestFile(dir) { if (!fs.existsSync(dir)) { return null; } return fs.readdirSync(dir) .filter((name) => name.startsWith('perf-') && name.endsWith('.jsonl')) .map((name) => path.join(dir, name)) .sort((left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs)[0] ?? null; } function tailFile(targetFile) { let position = 0; const printNewLines = () => { const stats = fs.statSync(targetFile); const nextSize = stats.size; if (nextSize < position) { position = 0; } if (nextSize === position) { return; } const fd = fs.openSync(targetFile, 'r'); const length = nextSize - position; const buffer = Buffer.alloc(length); fs.readSync(fd, buffer, 0, length, position); fs.closeSync(fd); position = nextSize; for (const line of buffer.toString('utf8').split('\n')) { if (!line.trim()) { continue; } try { const entry = JSON.parse(line); console.log(`[${entry.type}] ${JSON.stringify(entry.payload)}`); } catch { console.log(line); } } }; printNewLines(); setInterval(printNewLines, 1000); } main();