UNPKG

@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
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, } }