snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,482 @@
#!/usr/bin/env node
// Summarize a Chrome DevTools performance trace into a compact markdown report.
// Surfaces the work that takes too long or happens too often, so an agent can
// reason about a multi-hundred-MB trace without loading the whole thing.
//
// Usage:
// node simplify-trace.mjs <trace.json> [--top N] [--long-task-ms MS] [--out FILE]
//
// For very large traces (>~500MB) increase Node's heap:
// node --max-old-space-size=8192 simplify-trace.mjs <trace.json>
import fs from 'node:fs'
// Sections the report can emit. `default` is the compact overview; the rest are
// opt-in via --only / --all so the tool can answer a narrow question.
const SECTIONS = {
summary: 'header: counts, span, CPU time, idle %',
timeline: 'per-second main-thread busy time (find when activity happens)',
longtasks: 'top-level tasks over the threshold (main-thread jank)',
events: 'hottest event types by self time',
frequent: 'most frequent event types by count',
functions: 'hottest JS functions from the CPU profile (self time)',
categories: 'self time grouped by trace category',
network: 'resource/fetch waterfall: offset, TTFB, duration, size, URL',
websocket: 'WebSocket lifecycle: create/destroy/send/receive/handshake',
}
const DEFAULT_SECTIONS = ['summary', 'longtasks', 'events', 'frequent', 'functions', 'categories']
const ALL_SECTIONS = Object.keys(SECTIONS)
function parseList(s) {
return s.split(',').map((x) => x.trim()).filter(Boolean)
}
function parseArgs(argv) {
const opts = { top: 25, longTaskMs: 50, out: null, file: null, window: null, only: null, except: null, match: null, list: false }
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--top') opts.top = Number(argv[++i])
else if (a === '--long-task-ms') opts.longTaskMs = Number(argv[++i])
else if (a === '--out') opts.out = argv[++i]
else if (a === '--only') opts.only = parseList(argv[++i])
else if (a === '--except') opts.except = parseList(argv[++i])
else if (a === '--all') opts.only = ALL_SECTIONS.slice()
else if (a === '--match') opts.match = argv[++i]
else if (a === '--wall-clock') opts.wallClock = true
else if (a === '--list') opts.list = true
else if (a === '--window') {
// "<startMs>-<endMs>" as offsets from the trace start
const [s, e] = argv[++i].split('-').map(Number)
opts.window = { start: s, end: e }
} else if (!a.startsWith('--')) opts.file = a
}
return opts
}
// Recording artifacts: the profiler starting/stopping shows up as a 50-60ms
// "task" that is not app work. Exclude it from the long-task ranking so it
// doesn't masquerade as the worst jank.
const ARTIFACT_NAMES = new Set(['CpuProfiler::StartProfiling', 'CpuProfiler::StopProfiling'])
const opts = parseArgs(process.argv.slice(2))
if (opts.list) {
console.log('Sections (--only a,b / --except a,b / --all):\n')
for (const k of ALL_SECTIONS) {
const def = DEFAULT_SECTIONS.includes(k) ? ' (default)' : ''
console.log(` ${k.padEnd(11)} ${SECTIONS[k]}${def}`)
}
process.exit(0)
}
if (!opts.file) {
console.error(
'Usage: node simplify-trace.mjs <trace.json> [--top N] [--long-task-ms MS] [--window START-END]\n' +
' [--only a,b | --except a,b | --all] [--match TEXT] [--out FILE] [--list]'
)
process.exit(1)
}
// Resolve which sections to emit, preserving a stable display order.
let wantSections = opts.only ? opts.only : DEFAULT_SECTIONS.slice()
const unknown = wantSections.filter((k) => !SECTIONS[k])
if (unknown.length) {
console.error(`Unknown section(s): ${unknown.join(', ')}. Run --list to see options.`)
process.exit(1)
}
if (opts.except) wantSections = wantSections.filter((k) => !opts.except.includes(k))
const selected = new Set(wantSections)
const matchLc = opts.match ? opts.match.toLowerCase() : null
const matches = (s) => !matchLc || (s || '').toLowerCase().includes(matchLc)
const ms = (us) => Math.round(us / 100) / 10 // microseconds -> ms, 1 decimal
const pct = (n, total) => (total ? Math.round((n / total) * 1000) / 10 : 0)
const raw = JSON.parse(fs.readFileSync(opts.file, 'utf8'))
const events = Array.isArray(raw) ? raw : raw.traceEvents || []
// ---- process / thread names (from metadata 'M' events) -------------------
const threadNames = new Map() // `${pid}:${tid}` -> name
const procNames = new Map() // pid -> name
for (const e of events) {
if (e.ph !== 'M') continue
if (e.name === 'thread_name') threadNames.set(`${e.pid}:${e.tid}`, e.args?.name)
if (e.name === 'process_name') procNames.set(e.pid, e.args?.name)
}
// ---- trace bounds --------------------------------------------------------
// Ignore ts <= 0: metadata ('M') and some bookkeeping events report ts:0 and
// would otherwise pin the trace start to zero and break every offset.
let minTs = Infinity
let maxTs = -Infinity
for (const e of events) {
if (typeof e.ts !== 'number' || e.ts <= 0) continue
if (e.ts < minTs) minTs = e.ts
const end = e.ts + (e.dur || 0)
if (end > maxTs) maxTs = end
}
const traceDur = maxTs - minTs
// ---- optional time window ------------------------------------------------
// --window START-END scopes the whole report to a slice of the recording
// (offsets in ms from trace start) — use it to focus on the moment an action
// happened instead of averaging it against idle time.
const winLo = opts.window ? minTs + opts.window.start * 1000 : -Infinity
const winHi = opts.window ? minTs + opts.window.end * 1000 : Infinity
const inWin = (ts) => ts >= winLo && ts <= winHi
let scopedCount = 0
// ---- aggregate complete (X) events by name, with self time ---------------
// Self time = own duration minus the duration of directly-nested children on
// the same thread. We reconstruct nesting with a per-thread stack.
const byThread = new Map()
for (const e of events) {
if (!inWin(e.ts)) continue
scopedCount++
if (e.ph !== 'X' || typeof e.dur !== 'number') continue
const key = `${e.pid}:${e.tid}`
if (!byThread.has(key)) byThread.set(key, [])
byThread.get(key).push(e)
}
const agg = new Map() // name -> { count, total, self, max }
const topLevelTasks = [] // candidate "long tasks"
const selfByCat = new Map() // category -> self time
function bump(map, k, dur) {
map.set(k, (map.get(k) || 0) + dur)
}
for (const [, list] of byThread) {
// Sort by start asc, then by duration desc so parents precede children.
list.sort((a, b) => a.ts - b.ts || b.dur - a.dur)
const stack = []
for (const e of list) {
while (stack.length && e.ts >= stack[stack.length - 1].ts + stack[stack.length - 1].dur) {
stack.pop()
}
const depth = stack.length
// subtract this event's duration from its parent's self time
if (stack.length) stack[stack.length - 1]._childDur += e.dur
// attribute artifact time (e.g. profiler start) to the enclosing
// top-level task at any depth, so it can be discounted later
if (stack.length && ARTIFACT_NAMES.has(e.name)) stack[0]._artifactDur = (stack[0]._artifactDur || 0) + e.dur
e._childDur = 0
stack.push(e)
if (depth === 0) topLevelTasks.push(e)
}
}
for (const [, list] of byThread) {
for (const e of list) {
const self = e.dur - (e._childDur || 0)
let a = agg.get(e.name)
if (!a) {
a = { count: 0, total: 0, self: 0, max: 0 }
agg.set(e.name, a)
}
a.count++
a.total += e.dur
a.self += self
if (e.dur > a.max) a.max = e.dur
const cat = (e.cat || '').split(',')[0] || '(none)'
bump(selfByCat, cat, self)
}
}
// ---- CPU profile reconstruction (Profile + ProfileChunk) -----------------
// Group chunks by profile id, accumulate node tree + samples, attribute each
// sample's timeDelta to its node. Aggregate self/total time per call frame.
const profiles = new Map() // id -> { nodes: Map, selfByNode: Map }
function getProfile(id) {
if (!profiles.has(id)) profiles.set(id, { nodes: new Map(), selfByNode: new Map() })
return profiles.get(id)
}
for (const e of events) {
if (e.name !== 'ProfileChunk' && e.name !== 'Profile') continue
const id = e.id ?? `${e.pid}:${e.tid}`
const p = getProfile(id)
const data = e.args?.data || {}
const cp = data.cpuProfile || data
if (Array.isArray(cp.nodes)) {
for (const n of cp.nodes) p.nodes.set(n.id, n)
// build parent links: nodes either list children ids or carry a parent id
for (const n of cp.nodes) {
if (n.parent != null) n._parent = n.parent
if (Array.isArray(n.children)) {
for (const c of n.children) {
const child = p.nodes.get(c)
if (child) child._parent = n.id
}
}
}
}
// Always accumulate nodes (they're defined incrementally and an in-window
// sample may reference one declared earlier), but only attribute samples
// from chunks inside the window. Chunk-level granularity is coarse but fine
// for focusing on a moment.
if (!inWin(e.ts)) continue
const samples = cp.samples || data.samples
const deltas = data.timeDeltas || cp.timeDeltas
if (Array.isArray(samples) && Array.isArray(deltas)) {
for (let i = 0; i < samples.length; i++) {
const nid = samples[i]
const dt = deltas[i] || 0
p.selfByNode.set(nid, (p.selfByNode.get(nid) || 0) + dt)
}
}
}
// Aggregate by call frame across all profiles.
const frameSelf = new Map() // key -> { self, fn, loc }
const nodeTotal = new Map() // `${id}:${nodeId}` -> total (self + descendants)
let cpuTotal = 0
function frameKey(cf) {
const fn = cf.functionName || '(anonymous)'
const loc = cf.url ? `${cf.url}:${cf.lineNumber ?? '?'}` : cf.codeType || ''
return { key: `${fn}@@${loc}`, fn, loc }
}
for (const [, p] of profiles) {
// self per frame + roll self up to ancestors for total time
for (const [nid, self] of p.selfByNode) {
cpuTotal += self
const node = p.nodes.get(nid)
if (!node) continue
const { key, fn, loc } = frameKey(node.callFrame || {})
let f = frameSelf.get(key)
if (!f) {
f = { self: 0, total: 0, fn, loc, count: 0 }
frameSelf.set(key, f)
}
f.self += self
// walk ancestors, add to each frame's total (dedup per frame per sample-origin)
const seenFrames = new Set()
let cur = node
let guard = 0
while (cur && guard++ < 1000) {
const fk = frameKey(cur.callFrame || {})
if (!seenFrames.has(fk.key)) {
seenFrames.add(fk.key)
let tf = frameSelf.get(fk.key)
if (!tf) {
tf = { self: 0, total: 0, fn: fk.fn, loc: fk.loc, count: 0 }
frameSelf.set(fk.key, tf)
}
tf.total += self
}
cur = cur._parent != null ? p.nodes.get(cur._parent) : null
}
}
}
// ---- section builders ----------------------------------------------------
const out = []
const N = opts.top
const offset = (ts) => ms(ts - minTs) // ts -> ms offset from trace start
// Wall-clock correlation: metadata.startTime (UTC) anchors offset 0, so trace
// timestamps can be lined up against server logs (zero-cache, sync-worker).
const startEpoch = raw.metadata?.startTime ? Date.parse(raw.metadata.startTime) : NaN
const wallEnabled = opts.wallClock && !Number.isNaN(startEpoch)
const wall = (ts) => new Date(startEpoch + (ts - minTs) / 1000).toISOString().slice(11, 23) + 'Z'
// header cell + row cell for an optional "UTC" column on time-localized tables
const utcHead = wallEnabled ? ' UTC |' : ''
const utcCell = (ts) => (wallEnabled ? ` ${wall(ts)} |` : '')
const builders = {
summary() {
out.push(`# Trace summary: ${opts.file.split('/').pop()}`)
out.push('')
if (opts.window) {
out.push(`- **Window: ${opts.window.start}–${opts.window.end} ms** (of ${ms(traceDur)} ms total)`)
out.push(`- Events in window: **${scopedCount.toLocaleString()}** / ${events.length.toLocaleString()}`)
} else {
out.push(`- Events: **${events.length.toLocaleString()}**`)
out.push(`- Wall-clock span: **${ms(traceDur)} ms**`)
}
if (cpuTotal) out.push(`- Sampled CPU time (JS): **${ms(cpuTotal)} ms**`)
out.push(`- Threads with timed work: **${byThread.size}**`)
if (cpuTotal) {
const idle = frameSelf.get('(idle)@@other')?.self || 0
out.push(`- Idle (sampled): **${ms(idle)} ms (${pct(idle, cpuTotal)}%)**`)
}
out.push('')
},
timeline() {
// Per-second main-thread busy time over the WHOLE trace (ignores --window;
// its purpose is to locate when activity happens so you can then window).
const buckets = new Map()
for (const e of topLevelTasks) {
const sec = Math.floor(offset(e.ts) / 1000)
buckets.set(sec, (buckets.get(sec) || 0) + e.dur)
}
out.push(`## Main-thread busy time (per second)`)
out.push('')
out.push('| sec | busy (ms) |')
out.push('|---|---|')
;[...buckets.entries()]
.sort((a, b) => a[0] - b[0])
.filter(([, d]) => d / 1000 >= 5)
.forEach(([s, d]) => out.push(`| ${s} | ${ms(d)} |`))
out.push('')
},
longtasks() {
const longMs = opts.longTaskMs
const tasks = topLevelTasks
// drop tasks that are themselves an artifact or are >50% artifact time
.filter((e) => e.dur >= longMs * 1000 && !ARTIFACT_NAMES.has(e.name) && (e._artifactDur || 0) < e.dur * 0.5)
.sort((a, b) => b.dur - a.dur)
.slice(0, N)
out.push(`## Long tasks (top-level ≥ ${longMs} ms)`)
out.push('')
if (!tasks.length) {
out.push(`_None ≥ ${longMs} ms (excluding profiler artifacts)._`)
} else {
out.push(`| # | dur (ms) | @ offset (ms) |${utcHead} name | thread |`)
out.push(`|---|---|---|${wallEnabled ? '---|' : ''}---|---|`)
tasks.forEach((e, i) => {
const tn = threadNames.get(`${e.pid}:${e.tid}`) || `tid ${e.tid}`
out.push(`| ${i + 1} | ${ms(e.dur)} | ${offset(e.ts)} |${utcCell(e.ts)} ${e.name} | ${tn} |`)
})
}
out.push('')
},
events() {
const rows = [...agg.entries()]
.map(([name, a]) => ({ name, ...a }))
.filter((a) => matches(a.name))
.sort((a, b) => b.self - a.self)
.slice(0, N)
out.push(`## Hottest event types (by self time)`)
out.push('')
out.push('| name | count | self (ms) | total (ms) | avg (ms) | max (ms) |')
out.push('|---|---|---|---|---|---|')
for (const a of rows) {
out.push(
`| ${a.name} | ${a.count.toLocaleString()} | ${ms(a.self)} | ${ms(a.total)} | ${ms(a.total / a.count)} | ${ms(a.max)} |`
)
}
out.push('')
},
frequent() {
const rows = [...agg.entries()]
.map(([name, a]) => ({ name, ...a }))
.filter((a) => matches(a.name))
.sort((a, b) => b.count - a.count)
.slice(0, N)
out.push(`## Most frequent event types (by count)`)
out.push('')
out.push('| name | count | total (ms) | self (ms) | avg (ms) |')
out.push('|---|---|---|---|---|')
for (const a of rows) {
out.push(`| ${a.name} | ${a.count.toLocaleString()} | ${ms(a.total)} | ${ms(a.self)} | ${ms(a.total / a.count)} |`)
}
out.push('')
},
functions() {
if (!frameSelf.size) return
// Drop synthetic idle/program frames; idle is reported in the header.
const noise = new Set(['(idle)', '(program)', '(root)'])
const rows = [...frameSelf.values()]
.filter((f) => f.self > 0 && !noise.has(f.fn) && (matches(f.fn) || matches(f.loc)))
.sort((a, b) => b.self - a.self)
.slice(0, N)
out.push(`## Hottest JS functions (CPU profile, self time)`)
out.push('')
out.push('| function | self (ms) | self % | total (ms) | location |')
out.push('|---|---|---|---|---|')
for (const f of rows) out.push(`| ${f.fn} | ${ms(f.self)} | ${pct(f.self, cpuTotal)}% | ${ms(f.total)} | ${f.loc} |`)
out.push('')
},
categories() {
const rows = [...selfByCat.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
out.push(`## Self time by category`)
out.push('')
out.push('| category | self (ms) |')
out.push('|---|---|')
for (const [cat, dur] of rows) out.push(`| ${cat} | ${ms(dur)} |`)
out.push('')
},
network() {
// Reconstruct requests from Resource* events keyed by requestId.
const NET = new Set(['ResourceSendRequest', 'ResourceReceiveResponse', 'ResourceFinish'])
const reqs = new Map()
for (const e of events) {
if (!NET.has(e.name)) continue
const d = e.args?.data || {}
const id = d.requestId
if (!id) continue
let r = reqs.get(id)
if (!r) reqs.set(id, (r = {}))
if (e.name === 'ResourceSendRequest') {
r.url = d.url
r.send = e.ts
r.method = d.requestMethod
} else if (e.name === 'ResourceReceiveResponse') {
r.resp = e.ts
r.status = d.statusCode
} else if (e.name === 'ResourceFinish') {
r.finish = e.ts
r.size = d.encodedDataLength
}
}
const list = [...reqs.values()]
.filter((r) => r.send && inWin(r.send) && matches(r.url))
.sort((a, b) => a.send - b.send)
out.push(`## Network requests (${list.length})`)
out.push('')
if (!list.length) {
out.push('_None in scope._')
out.push('')
return
}
out.push(`| @ offset (ms) |${utcHead} ttfb (ms) | dur (ms) | size (KB) | status | url |`)
out.push(`|---|${wallEnabled ? '---|' : ''}---|---|---|---|---|`)
for (const r of list.slice(0, N)) {
const ttfb = r.resp ? ms(r.resp - r.send) : '-'
const dur = r.finish ? ms(r.finish - r.send) : '-'
const kb = r.size ? Math.round(r.size / 102.4) / 10 : '-'
const url = (r.url || '').replace(/^https?:\/\//, '').slice(0, 90)
out.push(`| ${offset(r.send)} |${utcCell(r.send)} ${ttfb} | ${dur} | ${kb} | ${r.status || ''} | ${url} |`)
}
if (list.length > N) out.push(`\n_…${list.length - N} more (raise --top)._`)
out.push('')
},
websocket() {
const ws = events
.filter((e) => /WebSocket/.test(e.name || '') && inWin(e.ts))
.sort((a, b) => a.ts - b.ts)
out.push(`## WebSocket timeline (${ws.length})`)
out.push('')
if (!ws.length) {
out.push('_No WebSocket events in scope._')
out.push('')
return
}
out.push(`| @ offset (ms) |${utcHead} event | url |`)
out.push(`|---|${wallEnabled ? '---|' : ''}---|---|`)
for (const e of ws.slice(0, N)) {
const url = (e.args?.data?.url || '').slice(0, 80)
out.push(`| ${offset(e.ts)} |${utcCell(e.ts)} ${e.name} | ${url} |`)
}
out.push('')
},
}
// Emit selected sections in canonical order.
for (const key of ALL_SECTIONS) if (selected.has(key)) builders[key]()
const report = out.join('\n').replace(/\n{3,}/g, '\n\n')
if (opts.out) {
fs.writeFileSync(opts.out, report)
console.error(`Wrote ${opts.out} (${report.length} bytes) from ${(fs.statSync(opts.file).size / 1e6).toFixed(1)} MB trace`)
} else {
process.stdout.write(report)
}