mirror of
https://github.com/zadam/trilium.git
synced 2026-01-16 03:24:24 +01:00
chore(scripts): add a script to anlayze performance logs
This commit is contained in:
parent
de6a6cbb07
commit
545464efee
231
scripts/analyze-perf.ts
Normal file
231
scripts/analyze-perf.ts
Normal file
@ -0,0 +1,231 @@
|
||||
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.toFixed(2).padStart(8)}ms [${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.toFixed(0).padStart(8)}ms total (${stats.count.toString().padStart(4)} ops, ${avgTime.toFixed(1)}ms 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';
|
||||
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';
|
||||
} else if (entry.file.includes('.css')) {
|
||||
category = 'CSS';
|
||||
}
|
||||
|
||||
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.toFixed(0).padStart(8)}ms (${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.toFixed(0).padStart(8)}ms (${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);
|
||||
Loading…
x
Reference in New Issue
Block a user