@zendesk/react-measure-timing-hooks
Version:
react hooks for measuring time to interactive and time to render of components
211 lines (187 loc) • 5.42 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
ComponentRenderSpan,
PerformanceEntrySpan,
Span,
SpanType,
} from '../spanTypes'
export interface ComponentRenderStub
extends Partial<Omit<ComponentRenderSpan<any>, 'startTime' | 'duration'>> {
entryType: 'component-render' | 'component-render-start'
duration: number
startTime?: number
name: string
}
export interface LongTaskStub {
entryType: 'longtask'
duration: number
startTime?: number
name?: string
}
export interface MarkStub
extends Partial<Omit<PerformanceEntrySpan<any>, 'startTime' | 'duration'>> {
entryType: 'mark'
name: string
startTime?: number
}
export interface FmpStub {
entryType: 'fmp'
startTime?: number
}
export interface IdleStub {
entryType: 'idle'
duration: number
}
export type Stub =
| ComponentRenderStub
| LongTaskStub
| MarkStub
| FmpStub
| IdleStub
export const Render = (
name: string,
duration: number,
options: { startTime?: number } & Partial<
Omit<ComponentRenderSpan<any>, 'startTime' | 'duration'>
> = {},
): ComponentRenderStub => ({
entryType: 'component-render',
name,
duration,
startTime: options.startTime,
isIdle:
(options.renderedOutput === 'content' ||
options.renderedOutput === 'error') ??
options.isIdle,
...options,
})
export const LongTask = (
duration: number,
options: { start?: number } = {},
): LongTaskStub => ({
entryType: 'longtask',
duration,
startTime: options.start,
name: 'task',
})
export const Idle = (duration: number): IdleStub => ({
entryType: 'idle',
duration,
})
export const Check: MarkStub = {
entryType: 'mark',
name: 'check',
}
export const FMP: FmpStub = {
entryType: 'fmp',
}
export function makeEntries(events: Stub[]): {
entries: PerformanceEntry[]
fmpTime: number | null
} {
const entries: PerformanceEntry[] = []
let currentTime = 0
let fmpTime = null
for (const event of events) {
const thisEventStartTime =
'startTime' in event && event.startTime !== undefined && event.startTime
const eventStartTime =
thisEventStartTime !== false ? thisEventStartTime : currentTime
const eventDuration = 'duration' in event ? event.duration : 0
switch (event.entryType) {
case 'idle':
break
case 'fmp':
fmpTime = eventStartTime
if (event.startTime === undefined) fmpTime = currentTime
// fallthrough on purpose
// eslint-disable-next-line no-fallthrough
default:
entries.push({
entryType: event.entryType,
name: 'name' in event ? event.name : event.entryType,
startTime: eventStartTime,
duration: eventDuration,
} as PerformanceEntry)
break
}
// Update `currentTime` only if `startTime` is not predefined
if (thisEventStartTime === false) {
currentTime = eventStartTime + eventDuration
}
}
return { entries, fmpTime }
}
export function getSpansFromTimeline<RelationSchemasT>(
_: TemplateStringsArray,
...exprs: (Stub | number)[]
): { spans: Span<RelationSchemasT>[]; fmpTime: number | null } {
const spans: Span<RelationSchemasT>[] = []
let fmpTime: number | null = null
const stubs = exprs.filter((expr) => typeof expr !== 'number')
const allNumbers = exprs.filter((expr) => typeof expr === 'number')
let startTime: number
let time: number[]
if (allNumbers.length === stubs.length + 1) {
startTime = allNumbers[0]!
time = allNumbers.slice(1)
} else if (allNumbers.length === stubs.length) {
startTime = allNumbers[0]!
time = allNumbers
} else {
throw new Error('Invalid timeline, mismatch of events and timestamps')
}
if (startTime === undefined) {
throw new Error('No time provided for the beginning of the timeline')
}
for (let i = 0; i < time.length; i++) {
const currentTime = time[i]
const stub = stubs[i]
if (!stub || typeof currentTime !== 'number') {
throw new Error('Invalid timeline, mismatch of events and timestamps')
}
if (stub.entryType === 'fmp') {
fmpTime = currentTime
}
spans.push({
type: stub.entryType as SpanType,
duration: 0,
name: `${stub.entryType}`,
...stub,
startTime: {
now: 'startTime' in stub ? stub.startTime ?? currentTime : currentTime,
epoch:
'startTime' in stub ? stub.startTime ?? currentTime : currentTime,
},
isIdle:
'isIdle' in stub
? stub.isIdle
: 'name' in stub
? stub.name?.includes('idle')
: undefined,
renderedOutput:
'renderedOutput' in stub
? stub.renderedOutput
: 'name' in stub
? stub.name?.includes('idle')
? 'content'
: 'loading'
: undefined,
performanceEntry: {
duration: 0,
name: `${stub.entryType}`,
...stub,
startTime:
'startTime' in stub ? stub.startTime ?? currentTime : currentTime,
toJSON: () => {},
},
} as Span<RelationSchemasT>)
}
return { spans, fmpTime }
}
// example usage
// const timeline = getEventsFromTimeline`
// Events: ----------${FMP}-----${Task(50)}-------${Task(100)}-------${Task(200)}-------${Check}
// Time: ${0} ${200} ${300} ${350} ${550} ${700}
// `
// console.log(timeline)