Spaces:
Sleeping
Sleeping
| // Chrome Performance Trace Analyzer β outputs a compact summary | |
| // Usage: node analyze-trace.js <trace.json> | |
| const fs = require('fs') | |
| const path = require('path') | |
| const file = process.argv[2] | |
| if (!file) { console.error('Usage: node analyze-trace.js <trace.json>'); process.exit(1) } | |
| console.log(`Reading ${path.basename(file)} (${(fs.statSync(file).size / 1e6).toFixed(1)} MB)...`) | |
| const raw = fs.readFileSync(file, 'utf8') | |
| console.log('Parsing...') | |
| const trace = JSON.parse(raw) | |
| const events = Array.isArray(trace) ? trace : (trace.traceEvents || []) | |
| console.log(`Total events: ${events.length.toLocaleString()}\n`) | |
| // ββ 1. Long Tasks (> 50ms on main thread) ββββββββββββββββββββββββββββββββββββ | |
| const LONG_TASK_MS = 50 | |
| const tasks = events | |
| .filter(e => e.ph === 'X' && e.dur && e.dur > LONG_TASK_MS * 1000) | |
| .sort((a, b) => b.dur - a.dur) | |
| .slice(0, 30) | |
| console.log(`βββ TOP LONG TASKS (>${LONG_TASK_MS}ms) βββ`) | |
| for (const t of tasks) { | |
| const ms = (t.dur / 1000).toFixed(1) | |
| const name = t.name || '(unknown)' | |
| const cat = t.cat || '' | |
| console.log(` ${ms.padStart(8)}ms ${name} [${cat}]`) | |
| } | |
| // ββ 2. Summarise all complete events by name ββββββββββββββββββββββββββββββββββ | |
| const byName = new Map() | |
| for (const e of events) { | |
| if (e.ph !== 'X' || !e.dur) continue | |
| const key = e.name | |
| const existing = byName.get(key) | |
| if (existing) { | |
| existing.totalMs += e.dur / 1000 | |
| existing.count++ | |
| if (e.dur > existing.maxMs * 1000) existing.maxMs = e.dur / 1000 | |
| } else { | |
| byName.set(key, { totalMs: e.dur / 1000, count: 1, maxMs: e.dur / 1000 }) | |
| } | |
| } | |
| const topByTotal = [...byName.entries()] | |
| .sort((a, b) => b[1].totalMs - a[1].totalMs) | |
| .slice(0, 40) | |
| console.log('\nβββ TOP EVENTS BY TOTAL TIME βββ') | |
| console.log(' Total(ms) Max(ms) Count Name') | |
| for (const [name, s] of topByTotal) { | |
| console.log( | |
| ` ${s.totalMs.toFixed(1).padStart(9)} ${s.maxMs.toFixed(1).padStart(7)} ${String(s.count).padStart(5)} ${name}` | |
| ) | |
| } | |
| // ββ 3. React-specific events ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const reactKeywords = ['react', 'React', 'setState', 'useState', 'useMemo', 'useEffect', | |
| 'reconcil', 'Reconcil', 'render', 'Render', 'commit', 'Commit', 'fiber', 'Fiber', | |
| 'Marker', 'MapView', 'photoUrl', 'createPlace', 'markers'] | |
| const reactEvents = [...byName.entries()] | |
| .filter(([name]) => reactKeywords.some(k => name.includes(k))) | |
| .sort((a, b) => b[1].totalMs - a[1].totalMs) | |
| .slice(0, 30) | |
| if (reactEvents.length > 0) { | |
| console.log('\nβββ REACT / MAP EVENTS βββ') | |
| for (const [name, s] of reactEvents) { | |
| console.log(` ${s.totalMs.toFixed(1).padStart(9)}ms total ${s.maxMs.toFixed(1).padStart(7)}ms max ${s.count}x ${name}`) | |
| } | |
| } | |
| // ββ 4. V8 / JS heavy hitters βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const jsEvents = [...byName.entries()] | |
| .filter(([, s]) => s.totalMs > 20) | |
| .filter(([name]) => { | |
| const cat = (events.find(e => e.name === name)?.cat || '') | |
| return cat.includes('v8') || cat.includes('devtools.timeline') || name.includes('JS') || name.includes('Compile') || name.includes('GC') | |
| }) | |
| .sort((a, b) => b[1].totalMs - a[1].totalMs) | |
| .slice(0, 20) | |
| if (jsEvents.length > 0) { | |
| console.log('\nβββ V8 / JS EVENTS (>20ms total) βββ') | |
| for (const [name, s] of jsEvents) { | |
| console.log(` ${s.totalMs.toFixed(1).padStart(9)}ms ${s.count}x ${name}`) | |
| } | |
| } | |
| // ββ 5. CPU profile β top self-time functions βββββββββββββββββββββββββββββββββ | |
| const profileChunks = events.filter(e => e.name === 'ProfileChunk') | |
| if (profileChunks.length > 0) { | |
| const selfTime = new Map() | |
| for (const chunk of profileChunks) { | |
| const nodes = chunk.args?.data?.cpuProfile?.nodes || [] | |
| const samples = chunk.args?.data?.cpuProfile?.samples || [] | |
| const timeDeltas = chunk.args?.data?.timeDeltas || [] | |
| // Build node map | |
| const nodeMap = new Map(nodes.map(n => [n.id, n])) | |
| // Accumulate self time per node | |
| for (let i = 0; i < samples.length; i++) { | |
| const nodeId = samples[i] | |
| const dt = (timeDeltas[i] || 0) / 1000 // Β΅s β ms | |
| const node = nodeMap.get(nodeId) | |
| if (!node) continue | |
| const fn = node.callFrame?.functionName || '(anonymous)' | |
| const url = node.callFrame?.url || '' | |
| const line = node.callFrame?.lineNumber || 0 | |
| const key = `${fn} @ ${url.split('/').slice(-2).join('/')}:${line}` | |
| selfTime.set(key, (selfTime.get(key) || 0) + dt) | |
| } | |
| } | |
| const topSelf = [...selfTime.entries()] | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 40) | |
| console.log('\nβββ CPU PROFILE β TOP SELF-TIME FUNCTIONS βββ') | |
| for (const [name, ms] of topSelf) { | |
| console.log(` ${ms.toFixed(1).padStart(8)}ms ${name}`) | |
| } | |
| } | |
| // ββ 6. Paint / Layout costs βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const renderCats = ['Layout', 'UpdateLayoutTree', 'Paint', 'CompositeLayers', 'RasterTask'] | |
| console.log('\nβββ RENDERING COSTS βββ') | |
| for (const cat of renderCats) { | |
| const s = byName.get(cat) | |
| if (s) console.log(` ${s.totalMs.toFixed(1).padStart(9)}ms total ${s.maxMs.toFixed(1).padStart(7)}ms max ${s.count}x ${cat}`) | |
| } | |
| console.log('\nDone.') | |