mirror of
https://github.com/zadam/trilium.git
synced 2026-01-15 19:14:30 +01:00
258 lines
9.9 KiB
TypeScript
258 lines
9.9 KiB
TypeScript
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);
|