A CPU sampling profile attributes native work to `(program)`, which during streaming accounted for three quarters of all busy time and said nothing about where it went. The timeline trace names that work, so the streaming report now lists total and maximum time per trace event, skipping container events whose duration already includes the work below them.
88 lines
3.6 KiB
JavaScript
88 lines
3.6 KiB
JavaScript
/**
|
|
* 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])
|
|
}
|
|
|
|
// `RunTask` and `RunMicrotasks` are containers: their duration already
|
|
// includes the work below them, so counting them would double-count.
|
|
const CONTAINER_TRACE_EVENTS = new Set(["RunTask", "RunMicrotasks", "ProfileChunk", "Profile"])
|
|
|
|
/**
|
|
* Breaks recorded time down by trace event.
|
|
*
|
|
* A CPU sampling profile attributes native work to `(program)`, which hides
|
|
* whether time went to HTML parsing, style recalculation, layout, or paint.
|
|
* The timeline trace names that work explicitly, so this is what turns "76% of
|
|
* busy time is native" into an actionable list.
|
|
*/
|
|
export const summarizeTraceEvents = (traceEvents, topCount = 15) => {
|
|
const totals = new Map()
|
|
for (const event of traceEvents) {
|
|
if (event.ph !== "X" || !(Number(event.dur) > 0)) continue
|
|
if (CONTAINER_TRACE_EVENTS.has(event.name)) continue
|
|
const entry = totals.get(event.name) ?? { name: event.name, count: 0, totalMs: 0, maxMs: 0 }
|
|
const durationMs = Number(event.dur) / 1000
|
|
entry.count += 1
|
|
entry.totalMs += durationMs
|
|
if (durationMs > entry.maxMs) entry.maxMs = durationMs
|
|
totals.set(event.name, entry)
|
|
}
|
|
return [...totals.values()]
|
|
.sort((left, right) => right.totalMs - left.totalMs)
|
|
.slice(0, topCount)
|
|
.map((entry) => ({ ...entry, totalMs: round(entry.totalMs), maxMs: round(entry.maxMs) }))
|
|
}
|
|
|
|
/**
|
|
* 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),
|
|
}
|
|
}
|