Spaces:
Sleeping
Sleeping
File size: 5,754 Bytes
1fbcf0f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | #!/usr/bin/env node
// 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.')
|