trilium/scripts/analyze-perf.ts
2026-01-10 21:13:34 +02:00

258 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as fs from 'fs';
import * as path from 'path';
interface PerfEntry {
operation: string;
time: number;
file: string;
fullLine: string;
}
interface AggregatedStats {
totalTime: number;
count: number;
files: string[];
}
function stripAnsi(text: string): string {
// Remove ANSI escape codes - both the ESC[ format and the literal bracket format
return text.replace(/\x1b\[[0-9;]*m/g, '').replace(/\[(\d+)m/g, '');
}
function parsePerf(filePath: string): PerfEntry[] {
// Read as UTF-16 LE which is what PowerShell's Tee-Object writes
const content = fs.readFileSync(filePath, 'utf16le');
const lines = content.split('\n');
const entries: PerfEntry[] = [];
// Match patterns like: "vite:load 7776.91ms [fs] /path/to/file"
// or "vite:transform 1234.56ms /path/to/file"
const timePattern = /(\d+\.\d+)ms/;
const operationPattern = /vite:(load|transform|resolve|time|import-analysis)/;
for (let line of lines) {
// Strip ANSI color codes
line = stripAnsi(line);
const timeMatch = line.match(timePattern);
const operationMatch = line.match(operationPattern);
if (timeMatch && operationMatch) {
const time = parseFloat(timeMatch[1]);
const operation = operationMatch[1];
// Extract file path - it's usually after the timing and optional [fs]/[plugin] marker
let file = 'unknown';
// Match file paths after [fs] or [plugin] or directly after timing
const pathMatch = line.match(/(?:\[(?:fs|plugin)\]\s+)?([/\\]?[\w/.@-]+\.[a-z]+(?:\?[^\s]*)?)/i);
if (pathMatch) {
file = pathMatch[1];
// Normalize path separators
file = file.replace(/\\/g, '/');
// Remove query params
file = file.replace(/\?.*$/, '');
// Get just the filename for cleaner output
const fileName = path.basename(file);
if (file.includes('node_modules')) {
const nodeModulesMatch = file.match(/node_modules\/([^\/]+)/);
if (nodeModulesMatch) {
file = `npm:${nodeModulesMatch[1]}/${fileName}`;
}
} else if (file.includes('packages/')) {
const packagesMatch = file.match(/packages\/([^\/]+)/);
if (packagesMatch) {
file = `pkg:${packagesMatch[1]}/${fileName}`;
}
} else if (file.includes('.cache/vite/deps/')) {
const depsMatch = file.match(/deps\/([^?]+)/);
if (depsMatch) {
file = `deps:${depsMatch[1]}`;
}
} else if (file.startsWith('/')) {
// Remove leading /src/ or similar
file = file.replace(/^\/src\//, '');
if (!file.includes('/')) {
file = fileName;
}
} else {
file = fileName;
}
}
entries.push({
operation,
time,
file,
fullLine: line.trim()
});
}
}
return entries;
}
function analyzePerf(entries: PerfEntry[]) {
console.log('\n📊 VITE PERFORMANCE ANALYSIS\n');
console.log('='.repeat(80));
// Top 20 slowest individual operations
console.log('\n🐌 TOP 20 SLOWEST OPERATIONS:\n');
const sorted = [...entries].sort((a, b) => b.time - a.time).slice(0, 20);
sorted.forEach((entry, i) => {
console.log(`${String(i + 1).padStart(2)}. ${(entry.time / 1000).toFixed(2).padStart(6)}s [${entry.operation.padEnd(15)}] ${entry.file}`);
});
// Aggregate by operation type
console.log('\n⚙ BY OPERATION TYPE:\n');
const byOperation = new Map<string, AggregatedStats>();
for (const entry of entries) {
if (!byOperation.has(entry.operation)) {
byOperation.set(entry.operation, { totalTime: 0, count: 0, files: [] });
}
const stats = byOperation.get(entry.operation)!;
stats.totalTime += entry.time;
stats.count++;
}
const operationsSorted = Array.from(byOperation.entries())
.sort((a, b) => b[1].totalTime - a[1].totalTime);
operationsSorted.forEach(([op, stats]) => {
const avgTime = stats.totalTime / stats.count;
console.log(`${op.padEnd(20)} ${(stats.totalTime / 1000).toFixed(1).padStart(6)}s total (${stats.count.toString().padStart(4)} ops, ${(avgTime / 1000).toFixed(3)}s avg)`);
});
// Aggregate by package/category
console.log('\n📦 BY PACKAGE/CATEGORY:\n');
const byPackage = new Map<string, AggregatedStats>();
for (const entry of entries) {
let category = 'Other';
// Check package prefixes first
if (entry.file.startsWith('pkg:ckeditor5')) {
category = 'CKEditor Core';
} else if (entry.file.startsWith('pkg:')) {
category = entry.file.split('/')[0];
} else if (entry.file.startsWith('deps:ckeditor5-premium')) {
category = 'CKEditor Premium';
} else if (entry.file.startsWith('deps:ckeditor5')) {
category = 'CKEditor Core (deps)';
} else if (entry.file.startsWith('deps:@codemirror')) {
category = 'CodeMirror';
} else if (entry.file.startsWith('deps:')) {
category = 'Dependencies';
}
// Break down app source files
else if (entry.file.includes('widgets/')) {
if (entry.file.includes('type_widgets/')) {
category = 'App: Type Widgets';
} else if (entry.file.includes('collections/')) {
category = 'App: Collections';
} else if (entry.file.includes('ribbon/')) {
category = 'App: Ribbon';
} else if (entry.file.includes('dialogs/')) {
category = 'App: Dialogs';
} else if (entry.file.includes('launch_bar/')) {
category = 'App: Launch Bar';
} else {
category = 'App: Widgets';
}
} else if (entry.file.includes('services/')) {
category = 'App: Services';
} else if (entry.file.includes('components/')) {
category = 'App: Components';
} else if (entry.file.includes('menus/')) {
category = 'App: Menus';
} else if (entry.file.includes('.css')) {
category = 'CSS';
} else if (entry.file.match(/\.(png|jpg|jpeg|svg|gif|woff|woff2|ttf|eot)$/i)) {
category = 'Assets';
}
if (!byPackage.has(category)) {
byPackage.set(category, { totalTime: 0, count: 0, files: [] });
}
const stats = byPackage.get(category)!;
stats.totalTime += entry.time;
stats.count++;
if (!stats.files.includes(entry.file)) {
stats.files.push(entry.file);
}
}
const packagesSorted = Array.from(byPackage.entries())
.sort((a, b) => b[1].totalTime - a[1].totalTime);
packagesSorted.forEach(([pkg, stats]) => {
console.log(`${pkg.padEnd(30)} ${(stats.totalTime / 1000).toFixed(1).padStart(6)}s (${stats.count.toString().padStart(4)} files)`);
});
// CKEditor breakdown
console.log('\n✏ CKEDITOR PLUGIN BREAKDOWN:\n');
const ckeditorEntries = entries.filter(e =>
e.file.includes('ckeditor5') &&
(e.file.includes('admonition') ||
e.file.includes('footnotes') ||
e.file.includes('math') ||
e.file.includes('mermaid') ||
e.file.includes('keyboard-marker'))
);
const byCKPlugin = new Map<string, AggregatedStats>();
for (const entry of ckeditorEntries) {
let plugin = 'unknown';
if (entry.file.includes('admonition')) plugin = 'admonition';
else if (entry.file.includes('footnotes')) plugin = 'footnotes';
else if (entry.file.includes('math')) plugin = 'math';
else if (entry.file.includes('mermaid')) plugin = 'mermaid';
else if (entry.file.includes('keyboard-marker')) plugin = 'keyboard-marker';
if (!byCKPlugin.has(plugin)) {
byCKPlugin.set(plugin, { totalTime: 0, count: 0, files: [] });
}
const stats = byCKPlugin.get(plugin)!;
stats.totalTime += entry.time;
stats.count++;
}
const pluginsSorted = Array.from(byCKPlugin.entries())
.sort((a, b) => b[1].totalTime - a[1].totalTime);
pluginsSorted.forEach(([plugin, stats]) => {
console.log(`${plugin.padEnd(20)} ${(stats.totalTime / 1000).toFixed(1).padStart(6)}s (${stats.count} files)`);
});
// Summary stats
const totalTime = entries.reduce((sum, e) => sum + e.time, 0);
const totalOps = entries.length;
console.log('\n📈 SUMMARY:\n');
console.log(`Total operations: ${totalOps}`);
console.log(`Total time: ${(totalTime / 1000).toFixed(1)}s`);
console.log(`Average per op: ${(totalTime / totalOps).toFixed(1)}ms`);
console.log(`Operations > 500ms: ${entries.filter(e => e.time > 500).length}`);
console.log(`Operations > 1000ms: ${entries.filter(e => e.time > 1000).length}`);
console.log(`Operations > 3000ms: ${entries.filter(e => e.time > 3000).length}`);
console.log('\n' + '='.repeat(80) + '\n');
}
// Main
const perfFile = path.join(process.cwd(), 'perf.txt');
if (!fs.existsSync(perfFile)) {
console.error('❌ perf.txt not found. Run with DEBUG=vite:* first.');
process.exit(1);
}
console.log(`📂 Reading ${perfFile}...`);
const entries = parsePerf(perfFile);
console.log(`📊 Found ${entries.length} performance entries`);
if (entries.length === 0) {
console.error('❌ No performance data found in perf.txt');
console.error(' Expected lines like: "vite:load 123.45ms [fs] /path/to/file"');
process.exit(1);
}
analyzePerf(entries);