225 lines
5.9 KiB
JavaScript
225 lines
5.9 KiB
JavaScript
#!/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();
|