perf(tooling): add automated streaming profiler
Adds `bun run profile:session`: creates a session, opens it in a real browser, dispatches a prompt through the supported `openchamber session` CLI, and records until the session reports itself idle. No input is synthesised, so everything captured is the app reacting to its own event stream. Streaming is judged by responsiveness rather than totals, so the report leads with the long-task distribution, style recalculation and layout rates, frame production, and the application's own stream counters. Two failure modes are detected rather than reported as clean results. A session belonging to a directory the browser is not viewing renders nothing and produces a perfectly quiet profile, so the run verifies both new message elements in the DOM and message-list render counters. And `RunTask` is only emitted under the disabled-by-default timeline category, so a capture without it reports zero long tasks; the missing-task case is now called out instead of being shown as zero. Metric helpers are shared with the idle profiler.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Metric helpers shared by the idle and streaming profilers.
|
||||
*
|
||||
* Both commands read the same `Performance.getMetrics` counters and need the
|
||||
* same derivations, so the maths lives here and each entry point only decides
|
||||
* which numbers to report.
|
||||
*/
|
||||
|
||||
export const round = (value, digits = 2) => Number(Number(value ?? 0).toFixed(digits))
|
||||
|
||||
export const metricMap = (metrics = []) => Object.fromEntries(metrics.map(({ name, value }) => [name, value]))
|
||||
|
||||
/**
|
||||
* Least-squares slope of a sampled series, in units per second. A slope
|
||||
* separates a genuine upward trend from the sawtooth that garbage collection
|
||||
* produces, which start/end deltas alone cannot distinguish.
|
||||
*/
|
||||
export const growthPerSecond = (samples, key) => {
|
||||
if (samples.length < 2) return 0
|
||||
const meanTime = samples.reduce((total, sample) => total + sample.elapsedSeconds, 0) / samples.length
|
||||
const meanValue = samples.reduce((total, sample) => total + (sample[key] ?? 0), 0) / samples.length
|
||||
let covariance = 0
|
||||
let variance = 0
|
||||
for (const sample of samples) {
|
||||
const timeDelta = sample.elapsedSeconds - meanTime
|
||||
covariance += timeDelta * ((sample[key] ?? 0) - meanValue)
|
||||
variance += timeDelta * timeDelta
|
||||
}
|
||||
return variance === 0 ? 0 : Number((covariance / variance).toFixed(3))
|
||||
}
|
||||
|
||||
/** Percentile of an unsorted numeric series, using nearest-rank. */
|
||||
export const percentile = (values, fraction) => {
|
||||
if (values.length === 0) return 0
|
||||
const sorted = [...values].sort((left, right) => left - right)
|
||||
const rank = Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1))
|
||||
return round(sorted[rank])
|
||||
}
|
||||
|
||||
/**
|
||||
* Long tasks block input and animation, so a streaming capture is judged by
|
||||
* its task-duration distribution rather than by an average frame rate.
|
||||
*/
|
||||
export const summarizeLongTasks = (traceEvents, thresholdMs = 50) => {
|
||||
const durations = traceEvents
|
||||
.filter((event) => event.name === "RunTask" && Number(event.dur) > 0)
|
||||
.map((event) => Number(event.dur) / 1000)
|
||||
const long = durations.filter((duration) => duration >= thresholdMs)
|
||||
return {
|
||||
taskCount: durations.length,
|
||||
longTaskCount: long.length,
|
||||
longTaskTotalMs: round(long.reduce((total, duration) => total + duration, 0)),
|
||||
longestTaskMs: round(Math.max(0, ...durations)),
|
||||
taskP95Ms: percentile(durations, 0.95),
|
||||
taskP99Ms: percentile(durations, 0.99),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user