203 lines
4.9 KiB
JavaScript
203 lines
4.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 latestProcess = [...entries].reverse().find((entry) => entry.type === 'process');
|
|
const latestStore = [...entries].reverse().find((entry) => entry.type === 'store');
|
|
const latestComponents = [...entries].reverse().find((entry) => entry.type === 'components');
|
|
const latestHeap = [...entries].reverse().find((entry) => entry.type === 'heap');
|
|
|
|
if (latestProcess) {
|
|
console.log(`Process RSS total: ${formatKb(latestProcess.payload.totalWorkingSetKb)}`);
|
|
}
|
|
|
|
if (latestHeap) {
|
|
console.log(`Renderer JS heap: ${latestHeap.payload.usedJsHeapMb ?? 'n/a'} MB`);
|
|
}
|
|
|
|
if (latestStore?.payload?.domains) {
|
|
console.log('Store domains (estimated bytes):');
|
|
|
|
for (const [domain, bytes] of Object.entries(latestStore.payload.domains).sort((a, b) => b[1] - a[1])) {
|
|
console.log(` ${domain}: ${bytes}`);
|
|
}
|
|
}
|
|
|
|
if (latestComponents?.payload?.domains) {
|
|
console.log('Live components by domain:');
|
|
|
|
for (const [domain, count] of Object.entries(latestComponents.payload.domains).sort((a, b) => b[1] - a[1])) {
|
|
console.log(` ${domain}: ${count}`);
|
|
}
|
|
}
|
|
|
|
const leaks = latestComponents?.payload?.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();
|