@zendesk/react-measure-timing-hooks
Version:
react hooks for measuring time to interactive and time to render of components
183 lines (158 loc) • 6.46 kB
text/typescript
const DEFAULT_QUIET_WINDOW_DURATION = 2_000 // Google used 2 seconds
const DEFAULT_CLUSTER_PADDING = 1_000 // Google used 1 second
const DEFAULT_HEAVY_CLUSTER_THRESHOLD = 250 // Google used 250ms
export interface PerformanceEntryLike {
entryType: string
startTime: number
duration: number
}
export type CPUIdleLongTaskProcessorFn<
T extends number | PerformanceEntryLike,
> = (
entry: T extends PerformanceEntryLike ? T : PerformanceEntryLike,
) => CheckIfQuietWindowPassedResult<T>
export type CheckIfQuietWindowPassedResult<
T extends number | PerformanceEntryLike,
> =
| {
firstCpuIdle: T
}
| {
/** time from timeOrigin when we can check next if we've passed quiet window yet or not */
nextCheck: number
}
export interface CPUIdleLongTaskProcessor<
T extends number | PerformanceEntryLike,
> {
processPerformanceEntry: CPUIdleLongTaskProcessorFn<T>
checkIfQuietWindowPassed: (
time: number,
quietWindowDuration?: number,
) => CheckIfQuietWindowPassedResult<T>
}
export interface CPUIdleProcessorOptions {
getQuietWindowDuration?: (currentEndTime: number, fmp: number) => number
clusterPadding?: number
heavyClusterThreshold?: number
}
const isLongTask = (entry: PerformanceEntryLike) =>
entry.entryType === 'longtask' || entry.entryType === 'long-animation-frame'
export function createCPUIdleProcessor<T extends number | PerformanceEntryLike>(
fmpOrEntry: T,
{
clusterPadding = DEFAULT_CLUSTER_PADDING,
heavyClusterThreshold = DEFAULT_HEAVY_CLUSTER_THRESHOLD,
getQuietWindowDuration,
}: CPUIdleProcessorOptions = {},
): CPUIdleLongTaskProcessor<T> {
const fmp =
typeof fmpOrEntry === 'number'
? fmpOrEntry
: fmpOrEntry.startTime + fmpOrEntry.duration
let possibleFirstCPUIdleTimestamp = fmp
let possibleFirstCPUIdleEntry: PerformanceEntryLike | null =
typeof fmpOrEntry === 'number' ? null : fmpOrEntry
let longTaskClusterDurationTotal = 0 // Total duration of the current long task cluster
// TODO: potentially assume that FMP point is as if inside of a heavy cluster already, this could be done by setting this value to fmp
let endTimeStampOfLastLongTask: number | null = null // End timestamp of the last long task
let lastLongTask: PerformanceEntryLike | null = null
const returnType = typeof fmpOrEntry === 'number' ? 'number' : 'object'
function checkIfQuietWindowPassed(
time: number,
quietWindowDuration = getQuietWindowDuration?.(time, fmp) ??
DEFAULT_QUIET_WINDOW_DURATION,
): CheckIfQuietWindowPassedResult<T> {
if (time - possibleFirstCPUIdleTimestamp > quietWindowDuration) {
// Return the first CPU idle timestamp if in a quiet window
return {
firstCpuIdle: (returnType === 'object'
? possibleFirstCPUIdleEntry
: possibleFirstCPUIdleTimestamp) as T,
}
}
return { nextCheck: time + quietWindowDuration }
}
function processPerformanceEntry(
entry: PerformanceEntryLike,
): CheckIfQuietWindowPassedResult<T> {
const entryEndTime = entry.startTime + entry.duration
const isEntryLongTask = isLongTask(entry)
const quietWindowDuration =
getQuietWindowDuration?.(entryEndTime, fmp) ??
DEFAULT_QUIET_WINDOW_DURATION
const quietWindowCheck = checkIfQuietWindowPassed(
// is not processing a long task, we can assume current clock time is the end time
isEntryLongTask ? entry.startTime : entryEndTime,
quietWindowDuration,
)
if (endTimeStampOfLastLongTask === null) {
// Check if a quiet window has passed without seeing any long tasks
if ('firstCpuIdle' in quietWindowCheck) {
return quietWindowCheck
}
// If this is the first long task
if (isEntryLongTask) {
// Update the end timestamp of the last long task and initialize the cluster
endTimeStampOfLastLongTask = entryEndTime
lastLongTask = entry
// if this longtask overlaps FMP, then push the first CPU idle timestamp to the end of it
if (entry.startTime - fmp < 0) {
longTaskClusterDurationTotal =
entry.duration - Math.abs(entry.startTime - fmp)
if (endTimeStampOfLastLongTask > fmp) {
// Move to the end of the cluster:
possibleFirstCPUIdleTimestamp = endTimeStampOfLastLongTask
possibleFirstCPUIdleEntry = entry
}
} else {
longTaskClusterDurationTotal = entry.duration
}
}
return quietWindowCheck
}
// Calculate time since the last long task
const gapSincePreviousTask = entry.startTime - endTimeStampOfLastLongTask
if (
isEntryLongTask &&
gapSincePreviousTask < clusterPadding &&
gapSincePreviousTask > 0
) {
// Continue to expand the existing cluster
// If less than 1 second since the last long task
// Include the time passed since the last long task in the cluster duration
longTaskClusterDurationTotal += gapSincePreviousTask + entry.duration
endTimeStampOfLastLongTask = entryEndTime // Update the end timestamp of the last long task
lastLongTask = entry
// If the cluster duration exceeds 250ms, update the first CPU idle timestamp
if (
longTaskClusterDurationTotal >= heavyClusterThreshold &&
endTimeStampOfLastLongTask > fmp
) {
// Met criteria for Heavy Cluster
// Move to the end of the cluster
possibleFirstCPUIdleTimestamp = endTimeStampOfLastLongTask
possibleFirstCPUIdleEntry = lastLongTask
}
} else {
// either the quiet window has passed, or we're going to start a new long task cluster
// If no new long tasks have occurred in the last quietWindowDuration
// then we found our First CPU Idle
if ('firstCpuIdle' in quietWindowCheck) {
return quietWindowCheck
}
if (isEntryLongTask) {
// Start a new cluster
longTaskClusterDurationTotal = entry.duration // Reset the cluster duration with the current task
endTimeStampOfLastLongTask = entryEndTime // Update the end timestamp of the last long task
lastLongTask = entry
// possibleFirstCPUIdleTimestamp remains unchanged,
// because we don't know if it's a light or heavy cluster yet
}
}
return quietWindowCheck
}
return {
processPerformanceEntry,
checkIfQuietWindowPassed,
}
}