mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 05:11:46 +00:00
feat(import): selective GPX/KML element import and performance improvements
Add type-selector UI in the file import modal letting users choose which GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points, paths) to import. KML LineString placemarks are now imported as path places with route_geometry. Performance improvements: - Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut unnecessary re-renders in PlacesSidebar - Add weatherQueue to cap concurrent weather fetches at 3 - Replace sequential per-place deletes with a single bulkDelete API call (new DELETE /places/bulk endpoint + deletePlacesMany service) - Memoize atlas/photo/weather service calls to avoid redundant requests - Add multi-select mode to PlacesSidebar for bulk operations Add large GPX/KML/KMZ fixtures for integration/perf testing and two profiler analysis scripts under scripts/.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#!/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.')
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const file = process.argv[2]
|
||||
if (!file) { console.error('Usage: node analyze-react-profiler.cjs <profile.json>'); process.exit(1) }
|
||||
|
||||
const raw = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'))
|
||||
const root = raw.dataForRoots[0]
|
||||
const commits = root.commitData
|
||||
|
||||
// snapshots: array of [fiberId, {displayName, ...}]
|
||||
const nameMap = new Map()
|
||||
for (const snap of root.snapshots) {
|
||||
const id = snap[0]
|
||||
const data = snap[1]
|
||||
if (data?.displayName) nameMap.set(id, data.displayName)
|
||||
}
|
||||
|
||||
console.log(`Commits: ${commits.length} Tracked components: ${nameMap.size}`)
|
||||
|
||||
// Probe the unit of fiberActualDurations against the known commit duration
|
||||
// fiberActualDurations contains durations for the subtree — the root fiber's
|
||||
// actual duration should be >= commit.duration. Find a plausible scale factor.
|
||||
const c0 = commits[0]
|
||||
const knownDur = c0.duration // already in ms per React DevTools spec
|
||||
const rootId = root.rootID ?? 1
|
||||
// Check a few values to pick scale
|
||||
const sampleDurs = c0.fiberActualDurations.slice(0, 10).map(e => e[1])
|
||||
console.log(`\nDebug — commit[0].duration=${knownDur}ms, first 5 raw fiberActualDurations values:`, sampleDurs.slice(0,5))
|
||||
// If max sample > 10*knownDur, values are in units of 1/100 ms; otherwise already ms
|
||||
const maxSample = Math.max(...c0.fiberActualDurations.map(e => e[1]))
|
||||
const scale = maxSample > knownDur * 10 ? 0.01 : 1
|
||||
|
||||
console.log(`Unit scale: ${scale === 0.01 ? '1/100 ms (dividing by 100)' : 'ms (no conversion)'}\n`)
|
||||
|
||||
// --- 1. Commit summary ---
|
||||
const fmt = (v) => v == null ? ' -' : (v * 1).toFixed(1).padStart(7)
|
||||
console.log('=== Commit summary ===')
|
||||
console.log(' # t(s) dur(ms) passive(ms) effects(ms) priority')
|
||||
const sorted = [...commits].map((c, i) => ({ i, ...c })).sort((a, b) => b.duration - a.duration)
|
||||
for (const c of sorted.slice(0, 15)) {
|
||||
const ts = (c.timestamp / 1000).toFixed(3)
|
||||
console.log(` ${String(c.i).padStart(2)} ${ts} ${fmt(c.duration)} ${fmt(c.passiveEffectDuration)} ${fmt(c.effectDuration)} ${c.priorityLevel ?? ''}`)
|
||||
}
|
||||
|
||||
// --- 2. Aggregate self + actual duration per component ---
|
||||
const selfTotals = new Map() // name → { total, count, max }
|
||||
const actualTotals = new Map()
|
||||
|
||||
for (const commit of commits) {
|
||||
for (const [id, raw] of commit.fiberActualDurations) {
|
||||
const dur = raw * scale
|
||||
const name = nameMap.get(id) ?? `(fiber#${id})`
|
||||
const e = actualTotals.get(name) ?? { total: 0, count: 0, max: 0 }
|
||||
e.total += dur; e.count += 1; e.max = Math.max(e.max, dur)
|
||||
actualTotals.set(name, e)
|
||||
}
|
||||
for (const [id, raw] of commit.fiberSelfDurations) {
|
||||
const dur = raw * scale
|
||||
const name = nameMap.get(id) ?? `(fiber#${id})`
|
||||
const e = selfTotals.get(name) ?? { total: 0, count: 0, max: 0 }
|
||||
e.total += dur; e.count += 1; e.max = Math.max(e.max, dur)
|
||||
selfTotals.set(name, e)
|
||||
}
|
||||
}
|
||||
|
||||
const ranked = [...selfTotals.entries()]
|
||||
.sort((a, b) => b[1].total - a[1].total)
|
||||
.filter(([, s]) => s.total > 0.5)
|
||||
|
||||
console.log('\n=== Top 40 components by SELF render time (excludes children) ===')
|
||||
console.log(' Component Self total Renders Self max Actual total')
|
||||
for (const [name, s] of ranked.slice(0, 40)) {
|
||||
const actual = actualTotals.get(name) ?? { total: 0 }
|
||||
console.log(
|
||||
` ${name.padEnd(48)} ${s.total.toFixed(1).padStart(8)} ms` +
|
||||
` ${String(s.count).padStart(6)}x` +
|
||||
` ${s.max.toFixed(1).padStart(7)} ms` +
|
||||
` ${actual.total.toFixed(1).padStart(10)} ms`
|
||||
)
|
||||
}
|
||||
|
||||
console.log('\n=== Most frequently re-rendering components (top 20) ===')
|
||||
const byCount = [...selfTotals.entries()].sort((a, b) => b[1].count - a[1].count)
|
||||
console.log(' Component Renders Self total')
|
||||
for (const [name, s] of byCount.slice(0, 20)) {
|
||||
console.log(` ${name.padEnd(48)} ${String(s.count).padStart(6)}x ${s.total.toFixed(1).padStart(8)} ms`)
|
||||
}
|
||||
|
||||
const totalPassive = commits.reduce((a, c) => a + (c.passiveEffectDuration ?? 0), 0)
|
||||
const totalCommit = commits.reduce((a, c) => a + c.duration, 0)
|
||||
console.log(`\n=== Totals ===`)
|
||||
console.log(` Total commit render time: ${totalCommit.toFixed(1)} ms (${commits.length} commits)`)
|
||||
console.log(` Total passive effect time: ${totalPassive.toFixed(1)} ms (useEffect)`)
|
||||
console.log(` Avg commit duration: ${(totalCommit / commits.length).toFixed(1)} ms`)
|
||||
Reference in New Issue
Block a user