license 'use strict' ; fileoverview typedef { Omit < LH . Artifacts . NavigationTraceTimes , 'firstContentfulPaintAllFrames' | 'traceEnd' > } TraceNavigationTimesForFrame typedef { 'lastNavigationStart' | 'firstResourceSendRequest' | 'lighthouseMarker' | 'auto' } TimeOriginDeterminationMethod typedef { Omit < LH . TraceEvent , 'name' | 'args' > & { name : 'FrameCommittedInBrowser' , args : { data : { frame : string , url : string , parent ? : string } } } } FrameCommittedEvent typedef { Omit < LH . TraceEvent , 'name' | 'args' > & { name : 'largestContentfulPaint::Invalidate' | 'largestContentfulPaint::Candidate' , args : { data ? : { size ? : number } , frame : string } } } LCPEvent typedef { Omit < LH . TraceEvent , 'name' | 'args' > & { name : 'largestContentfulPaint::Candidate' , args : { data : { size : number } , frame : string } } } LCPCandidateEvent import { log } from '../core/logger' ; const ACCEPTABLE_NAVIGATION_URL_REGEX = /^(chrome|https?):/ ; const BASE_RESPONSE_LATENCY = 16 ; const SCHEDULABLE_TASK_TITLE_LH = 'RunTask' ; const SCHEDULABLE_TASK_TITLE_ALT1 = 'ThreadControllerImpl::RunTask' ; const SCHEDULABLE_TASK_TITLE_ALT2 = 'ThreadControllerImpl::DoWork' ; const SCHEDULABLE_TASK_TITLE_ALT3 = 'TaskQueueManager::ProcessTaskFromWorkQueue' ; class TraceProcessor { static get TIMESPAN_MARKER_ID ( ) { return '__lighthouseTimespanStart__' ; } return { Error } static createNoNavstartError ( ) { return new Error ( 'No navigationStart event found' ) ; } return { Error } static createNoResourceSendRequestError ( ) { return new Error ( 'No ResourceSendRequest event found' ) ; } return { Error } static createNoTracingStartedError ( ) { return new Error ( 'No tracingStartedInBrowser event found' ) ; } return { Error } static createNoFirstContentfulPaintError ( ) { return new Error ( 'No FirstContentfulPaint event found' ) ; } return { Error } static createNoLighthouseMarkerError ( ) { return new Error ( 'No Lighthouse timespan marker event found' ) ; } param { LH . TraceEvent } event static _isNavigationStartOfInterest ( event ) { return ( event . name === 'navigationStart' && ( ! event . args . data || ! event . args . data . documentLoaderURL || ACCEPTABLE_NAVIGATION_URL_REGEX . test ( event . args . data . documentLoaderURL ) ) ) ; } param { number [ ] } tsGroupIndices param { number [ ] } timestampSortedIndices param { number } indexOfTsGroupIndicesStart param { LH . TraceEvent [ ] } traceEvents return { number [ ] } static _sortTimestampEventGroup ( tsGroupIndices , timestampSortedIndices , indexOfTsGroupIndicesStart , traceEvents ) { param { number } i const lookupArrayIndexByTsIndex = i => timestampSortedIndices [ i ] ; param { number } i const lookupEventByTsIndex = i => traceEvents [ lookupArrayIndexByTsIndex ( i ) ] ; type { Array < number > } const eEventIndices = [ ] ; type { Array < number > } const bxEventIndices = [ ] ; type { Array < number > } const otherEventIndices = [ ] ; for ( const tsIndex of tsGroupIndices ) { const arrayIndex = lookupArrayIndexByTsIndex ( tsIndex ) ; const event = lookupEventByTsIndex ( tsIndex ) ; if ( event . ph === 'E' ) eEventIndices . push ( arrayIndex ) ; else if ( event . ph === 'X' || event . ph === 'B' ) bxEventIndices . push ( arrayIndex ) ; else otherEventIndices . push ( arrayIndex ) ; } type { Map < number , number > } const effectiveDuration = new Map ( ) ; for ( const index of bxEventIndices ) { const event = traceEvents [ index ] ; if ( event . ph === 'X' ) { effectiveDuration . set ( index , event . dur ) ; } else { let duration = Number . MAX_SAFE_INTEGER ; let additionalNestedEventsWithSameName = 0 ; const startIndex = indexOfTsGroupIndicesStart + tsGroupIndices . length ; for ( let j = startIndex ; j < timestampSortedIndices . length ; j ++ ) { const potentialMatchingEvent = lookupEventByTsIndex ( j ) ; const eventMatches = potentialMatchingEvent . name === event . name && potentialMatchingEvent . pid === event . pid && potentialMatchingEvent . tid === event . tid ; if ( ! eventMatches ) continue ; if ( potentialMatchingEvent . ph === 'E' && additionalNestedEventsWithSameName === 0 ) { duration = potentialMatchingEvent . ts - event . ts ; break ; } else if ( potentialMatchingEvent . ph === 'E' ) { additionalNestedEventsWithSameName -- ; } else if ( potentialMatchingEvent . ph === 'B' ) { additionalNestedEventsWithSameName ++ ; } } effectiveDuration . set ( index , duration ) ; } } bxEventIndices . sort ( ( indexA , indexB ) => ( effectiveDuration . get ( indexB ) || 0 ) - ( effectiveDuration . get ( indexA ) || 0 ) || indexA - indexB ) ; otherEventIndices . sort ( ( indexA , indexB ) => indexA - indexB ) ; return [ ... eEventIndices , ... bxEventIndices , ... otherEventIndices ] ; } param { LH . TraceEvent [ ] } traceEvents param { ( e : LH . TraceEvent ) => boolean } filter static filteredTraceSort ( traceEvents , filter ) { const indices = [ ] ; for ( let srcIndex = 0 ; srcIndex < traceEvents . length ; srcIndex ++ ) { if ( filter ( traceEvents [ srcIndex ] ) ) { indices . push ( srcIndex ) ; } } indices . sort ( ( indexA , indexB ) => traceEvents [ indexA ] . ts - traceEvents [ indexB ] . ts ) ; for ( let i = 0 ; i < indices . length - 1 ; i ++ ) { const ts = traceEvents [ indices [ i ] ] . ts ; const tsGroupIndices = [ i ] ; for ( let j = i + 1 ; j < indices . length ; j ++ ) { if ( traceEvents [ indices [ j ] ] . ts !== ts ) break ; tsGroupIndices . push ( j ) ; } if ( tsGroupIndices . length === 1 ) continue ; const finalIndexOrder = TraceProcessor . _sortTimestampEventGroup ( tsGroupIndices , indices , i , traceEvents ) ; indices . splice ( i , finalIndexOrder . length , ... finalIndexOrder ) ; i += tsGroupIndices . length - 1 ; } const sorted = [ ] ; for ( let i = 0 ; i < indices . length ; i ++ ) { sorted . push ( traceEvents [ indices [ i ] ] ) ; } return sorted ; } param { LH . TraceEvent [ ] } events static assertHasToplevelEvents ( events ) { const hasToplevelTask = events . some ( this . isScheduleableTask ) ; if ( ! hasToplevelTask ) { throw new Error ( 'Could not find any top level events' ) ; } } see https param { ! Array < number > } durations param { number } totalTime param { ! Array < number > } percentiles param { number = } clippedLength return { ! Array < { percentile : number , time : number } > } private static _riskPercentiles ( durations , totalTime , percentiles , clippedLength = 0 ) { let busyTime = 0 ; for ( let i = 0 ; i < durations . length ; i ++ ) { busyTime += durations [ i ] ; } busyTime -= clippedLength ; let completedTime = totalTime - busyTime ; let duration = 0 ; let cdfTime = completedTime ; const results = [ ] ; let durationIndex = - 1 ; let remainingCount = durations . length + 1 ; if ( clippedLength > 0 ) { remainingCount -- ; } for ( const percentile of percentiles ) { const percentileTime = percentile * totalTime ; while ( cdfTime < percentileTime && durationIndex < durations . length - 1 ) { completedTime += duration ; remainingCount -= duration < 0 ? - 1 : 1 ; if ( clippedLength > 0 && clippedLength < durations [ durationIndex + 1 ] ) { duration = - clippedLength ; clippedLength = 0 ; } else { durationIndex ++ ; duration = durations [ durationIndex ] ; } cdfTime = completedTime + Math . abs ( duration ) * remainingCount ; } results . push ( { percentile , time : Math . max ( 0 , ( percentileTime - completedTime ) / remainingCount ) + BASE_RESPONSE_LATENCY , } ) ; } return results ; } see https param { Array < ToplevelEvent > } events param { number } startTime param { number } endTime param { ! Array < number > = } percentiles return { ! Array < { percentile : number , time : number } > } static getRiskToResponsiveness ( events , startTime , endTime , percentiles = [ 0.5 , 0.75 , 0.9 , 0.99 , 1 ] ) { const totalTime = endTime - startTime ; percentiles . sort ( ( a , b ) => a - b ) ; const ret = this . getMainThreadTopLevelEventDurations ( events , startTime , endTime ) ; return this . _riskPercentiles ( ret . durations , totalTime , percentiles , ret . clippedLength ) ; } param { Array < ToplevelEvent > } topLevelEvents param { number } startTime param { number } endTime return { { durations : Array < number > , clippedLength : number } } static getMainThreadTopLevelEventDurations ( topLevelEvents , startTime = 0 , endTime = Infinity ) { type { Array < number > } const durations = [ ] ; let clippedLength = 0 ; for ( const event of topLevelEvents ) { if ( event . end < startTime || event . start > endTime ) { continue ; } let duration = event . duration ; let eventStart = event . start ; if ( eventStart < startTime ) { eventStart = startTime ; duration = event . end - startTime ; } if ( event . end > endTime ) { clippedLength = duration - ( endTime - eventStart ) ; } durations . push ( duration ) ; } durations . sort ( ( a , b ) => a - b ) ; return { durations , clippedLength , } ; } param { LH . Artifacts . ProcessedTrace } trace param { number = } startTime param { number = } endTime return { Array < ToplevelEvent > } static getMainThreadTopLevelEvents ( trace , startTime = 0 , endTime = Infinity ) { const topLevelEvents = [ ] ; for ( const event of trace . mainThreadEvents ) { if ( ! this . isScheduleableTask ( event ) || ! event . dur ) continue ; const start = ( event . ts - trace . timeOriginEvt . ts ) / 1000 ; const end = ( event . ts + event . dur - trace . timeOriginEvt . ts ) / 1000 ; if ( start > endTime || end < startTime ) continue ; topLevelEvents . push ( { start , end , duration : event . dur / 1000 , } ) ; } return topLevelEvents ; } param { LH . TraceEvent [ ] } events return { { pid : number , tid : number , frameId : string } } static findMainFrameIds ( events ) { const startedInBrowserEvt = events . find ( e => e . name === 'TracingStartedInBrowser' ) ; if ( startedInBrowserEvt && startedInBrowserEvt . args . data && startedInBrowserEvt . args . data . frames ) { const mainFrame = startedInBrowserEvt . args . data . frames . find ( frame => ! frame . parent ) ; const frameId = mainFrame && mainFrame . frame ; const pid = mainFrame && mainFrame . processId ; const threadNameEvt = events . find ( e => e . pid === pid && e . ph === 'M' && e . cat === '__metadata' && e . name === 'thread_name' && e . args . name === 'CrRendererMain' ) ; const tid = threadNameEvt && threadNameEvt . tid ; if ( pid && tid && frameId ) { return { pid , tid , frameId , } ; } } const startedInPageEvt = events . find ( e => e . name === 'TracingStartedInPage' ) ; if ( startedInPageEvt && startedInPageEvt . args && startedInPageEvt . args . data ) { const frameId = startedInPageEvt . args . data . page ; if ( frameId ) { return { pid : startedInPageEvt . pid , tid : startedInPageEvt . tid , frameId , } ; } } const navStartEvt = events . find ( e => Boolean ( e . name === 'navigationStart' && e . args && e . args . data && e . args . data . isLoadingMainFrame && e . args . data . documentLoaderURL ) ) ; const firstResourceSendEvt = events . find ( e => e . name === 'ResourceSendRequest' ) ; if ( navStartEvt && navStartEvt . args && navStartEvt . args . data && firstResourceSendEvt && firstResourceSendEvt . pid === navStartEvt . pid && firstResourceSendEvt . tid === navStartEvt . tid ) { const frameId = navStartEvt . args . frame ; if ( frameId ) { return { pid : navStartEvt . pid , tid : navStartEvt . tid , frameId , } ; } } throw this . createNoTracingStartedError ( ) ; } param { LH . TraceEvent } evt return { boolean } static isScheduleableTask ( evt ) { return ( evt . name === SCHEDULABLE_TASK_TITLE_LH || evt . name === SCHEDULABLE_TASK_TITLE_ALT1 || evt . name === SCHEDULABLE_TASK_TITLE_ALT2 || evt . name === SCHEDULABLE_TASK_TITLE_ALT3 ) ; } param { LH . TraceEvent } evt return { evt is LCPEvent } static isLCPEvent ( evt ) { if ( evt . name !== 'largestContentfulPaint::Invalidate' && evt . name !== 'largestContentfulPaint::Candidate' ) return false ; return Boolean ( evt . args && evt . args . frame ) ; } param { LH . TraceEvent } evt return { evt is LCPCandidateEvent } static isLCPCandidateEvent ( evt ) { return Boolean ( evt . name === 'largestContentfulPaint::Candidate' && evt . args && evt . args . frame && evt . args . data && evt . args . data . size !== undefined ) ; } param { LH . TraceEvent [ ] } events param { LH . TraceEvent } timeOriginEvent return { { lcp : LCPEvent | undefined , invalidated : boolean } } static computeValidLCPAllFrames ( events , timeOriginEvent ) { const lcpEvents = events . filter ( this . isLCPEvent ) . reverse ( ) ; type { Map < string , LCPEvent > } const finalLcpEventsByFrame = new Map ( ) ; for ( const e of lcpEvents ) { if ( e . ts <= timeOriginEvent . ts ) break ; const frame = e . args . frame ; if ( finalLcpEventsByFrame . has ( frame ) ) continue ; finalLcpEventsByFrame . set ( frame , e ) ; } type { LCPCandidateEvent | undefined } let maxLcpAcrossFrames ; for ( const lcp of finalLcpEventsByFrame . values ( ) ) { if ( ! this . isLCPCandidateEvent ( lcp ) ) continue ; if ( ! maxLcpAcrossFrames || lcp . args . data . size > maxLcpAcrossFrames . args . data . size ) { maxLcpAcrossFrames = lcp ; } } return { lcp : maxLcpAcrossFrames , invalidated : Boolean ( ! maxLcpAcrossFrames && finalLcpEventsByFrame . size ) , } ; } param { Array < { id : string , url : string , parent ? : string } > } frames return { Map < string , string > } static resolveRootFrames ( frames ) { type { Map < string , string > } const parentFrames = new Map ( ) ; for ( const frame of frames ) { if ( ! frame . parent ) continue ; parentFrames . set ( frame . id , frame . parent ) ; } type { Map < string , string > } const frameIdToRootFrameId = new Map ( ) ; for ( const frame of frames ) { let cur = frame . id ; while ( parentFrames . has ( cur ) ) { cur = parentFrames . get ( cur ) ; } frameIdToRootFrameId . set ( frame . id , cur ) ; } return frameIdToRootFrameId ; } param { LH . Trace } trace param { { timeOriginDeterminationMethod ? : TimeOriginDeterminationMethod } } options return { LH . Artifacts . ProcessedTrace } static processTrace ( trace , options ) { const { timeOriginDeterminationMethod = 'auto' } = options || { } ; const keyEvents = this . filteredTraceSort ( trace . traceEvents , e => { return ( e . cat . includes ( 'blink.user_timing' ) || e . cat . includes ( 'loading' ) || e . cat . includes ( 'devtools.timeline' ) || e . cat === '__metadata' ) ; } ) ; const mainFrameIds = this . findMainFrameIds ( keyEvents ) ; const frames = keyEvents . filter ( return { evt is FrameCommittedEvent } evt => { return Boolean ( evt . name === 'FrameCommittedInBrowser' && evt . args . data && evt . args . data . frame && evt . args . data . url ) ; } ) . map ( evt => { return { id : evt . args . data . frame , url : evt . args . data . url , parent : evt . args . data . parent , } ; } ) ; const frameIdToRootFrameId = this . resolveRootFrames ( frames ) ; const frameEvents = keyEvents . filter ( e => e . args . frame === mainFrameIds . frameId ) ; let frameTreeEvents = [ ] ; if ( frameIdToRootFrameId . has ( mainFrameIds . frameId ) ) { frameTreeEvents = keyEvents . filter ( e => { return ( e . args . frame && frameIdToRootFrameId . get ( e . args . frame ) === mainFrameIds . frameId ) ; } ) ; } else { log ( 'frameTreeEvents may be incomplete, make sure the trace has FrameCommittedInBrowser events' ) ; frameIdToRootFrameId . set ( mainFrameIds . frameId , mainFrameIds . frameId ) ; frameTreeEvents = frameEvents ; } const timeOriginEvt = this . computeTimeOrigin ( { keyEvents , frameEvents , mainFrameIds } , timeOriginDeterminationMethod ) ; const processEvents = TraceProcessor . filteredTraceSort ( trace . traceEvents , e => e . pid === mainFrameIds . pid ) ; const mainThreadEvents = processEvents . filter ( e => e . tid === mainFrameIds . tid ) ; const traceEnd = this . computeTraceEnd ( trace . traceEvents , timeOriginEvt ) ; return { frames , mainThreadEvents , frameEvents , frameTreeEvents , processEvents , mainFrameIds , timeOriginEvt , timings : { timeOrigin : 0 , traceEnd : traceEnd . timing , } , timestamps : { timeOrigin : timeOriginEvt . ts , traceEnd : traceEnd . timestamp , } , } ; } param { LH . Artifacts . ProcessedTrace } processedTrace return { LH . Artifacts . ProcessedNavigation } static processNavigation ( processedTrace ) { const { frameEvents , frameTreeEvents , timeOriginEvt , timings , timestamps } = processedTrace ; const frameTimings = this . computeNavigationTimingsForFrame ( frameEvents , { timeOriginEvt , } ) ; const fcpAllFramesEvt = frameTreeEvents . find ( e => e . name === 'firstContentfulPaint' && e . ts > timeOriginEvt . ts ) ; if ( ! fcpAllFramesEvt ) { throw this . createNoFirstContentfulPaintError ( ) ; } const lcpAllFramesEvt = this . computeValidLCPAllFrames ( frameTreeEvents , timeOriginEvt ) . lcp ; param { number } ts const getTiming = ts => ( ts - timeOriginEvt . ts ) / 1000 ; param { number = } ts const maybeGetTiming = ts => ( ts === undefined ? undefined : getTiming ( ts ) ) ; return { timings : { timeOrigin : timings . timeOrigin , firstPaint : frameTimings . timings . firstPaint , firstContentfulPaint : frameTimings . timings . firstContentfulPaint , firstContentfulPaintAllFrames : getTiming ( fcpAllFramesEvt . ts ) , firstMeaningfulPaint : frameTimings . timings . firstMeaningfulPaint , largestContentfulPaint : frameTimings . timings . largestContentfulPaint , largestContentfulPaintAllFrames : maybeGetTiming ( lcpAllFramesEvt && lcpAllFramesEvt . ts ) , load : frameTimings . timings . load , domContentLoaded : frameTimings . timings . domContentLoaded , traceEnd : timings . traceEnd , } , timestamps : { timeOrigin : timestamps . timeOrigin , firstPaint : frameTimings . timestamps . firstPaint , firstContentfulPaint : frameTimings . timestamps . firstContentfulPaint , firstContentfulPaintAllFrames : fcpAllFramesEvt . ts , firstMeaningfulPaint : frameTimings . timestamps . firstMeaningfulPaint , largestContentfulPaint : frameTimings . timestamps . largestContentfulPaint , largestContentfulPaintAllFrames : lcpAllFramesEvt && lcpAllFramesEvt . ts , load : frameTimings . timestamps . load , domContentLoaded : frameTimings . timestamps . domContentLoaded , traceEnd : timestamps . traceEnd , } , firstPaintEvt : frameTimings . firstPaintEvt , firstContentfulPaintEvt : frameTimings . firstContentfulPaintEvt , firstContentfulPaintAllFramesEvt : fcpAllFramesEvt , firstMeaningfulPaintEvt : frameTimings . firstMeaningfulPaintEvt , largestContentfulPaintEvt : frameTimings . largestContentfulPaintEvt , largestContentfulPaintAllFramesEvt : lcpAllFramesEvt , loadEvt : frameTimings . loadEvt , domContentLoadedEvt : frameTimings . domContentLoadedEvt , fmpFellBack : frameTimings . fmpFellBack , lcpInvalidated : frameTimings . lcpInvalidated , } ; } param { Array < LH . TraceEvent > } events param { LH . TraceEvent } timeOriginEvt return { { timing : number , timestamp : number } } static computeTraceEnd ( events , timeOriginEvt ) { let maxTs = - Infinity ; for ( const event of events ) { maxTs = Math . max ( event . ts + ( event . dur || 0 ) , maxTs ) ; } return { timestamp : maxTs , timing : ( maxTs - timeOriginEvt . ts ) / 1000 } ; } param { { keyEvents : Array < LH . TraceEvent > , frameEvents : Array < LH . TraceEvent > , mainFrameIds : { frameId : string } } } traceEventSubsets param { TimeOriginDeterminationMethod } method return { LH . TraceEvent } static computeTimeOrigin ( traceEventSubsets , method ) { const lastNavigationStart = ( ) => { const frameEvents = traceEventSubsets . frameEvents ; return frameEvents . filter ( this . _isNavigationStartOfInterest ) . pop ( ) ; } ; const lighthouseMarker = ( ) => { const frameEvents = traceEventSubsets . keyEvents ; return frameEvents . find ( evt => evt . name === 'clock_sync' && evt . args . sync_id === TraceProcessor . TIMESPAN_MARKER_ID ) ; } ; switch ( method ) { case 'firstResourceSendRequest' : { const fetchStart = traceEventSubsets . keyEvents . find ( event => { if ( event . name !== 'ResourceSendRequest' ) return false ; const data = event . args . data || { } ; return data . frame === traceEventSubsets . mainFrameIds . frameId ; } ) ; if ( ! fetchStart ) throw this . createNoResourceSendRequestError ( ) ; return fetchStart ; } case 'lastNavigationStart' : { const navigationStart = lastNavigationStart ( ) ; if ( ! navigationStart ) throw this . createNoNavstartError ( ) ; return navigationStart ; } case 'lighthouseMarker' : { const marker = lighthouseMarker ( ) ; if ( ! marker ) throw this . createNoLighthouseMarkerError ( ) ; return marker ; } case 'auto' : { const marker = lighthouseMarker ( ) || lastNavigationStart ( ) ; if ( ! marker ) throw this . createNoNavstartError ( ) ; return marker ; } } } param { Array < LH . TraceEvent > } frameEvents param { { timeOriginEvt : LH . TraceEvent } } options static computeNavigationTimingsForFrame ( frameEvents , options ) { const { timeOriginEvt } = options ; const firstPaint = frameEvents . find ( e => e . name === 'firstPaint' && e . ts > timeOriginEvt . ts ) ; const firstContentfulPaint = frameEvents . find ( e => e . name === 'firstContentfulPaint' && e . ts > timeOriginEvt . ts ) ; if ( ! firstContentfulPaint ) { throw this . createNoFirstContentfulPaintError ( ) ; } let firstMeaningfulPaint = frameEvents . find ( e => e . name === 'firstMeaningfulPaint' && e . ts > timeOriginEvt . ts ) ; let fmpFellBack = false ; if ( ! firstMeaningfulPaint ) { const fmpCand = 'firstMeaningfulPaintCandidate' ; fmpFellBack = true ; log ( `No firstMeaningfulPaint found, falling back to last ${ fmpCand }` ) ; const lastCandidate = frameEvents . filter ( e => e . name === fmpCand ) . pop ( ) ; if ( ! lastCandidate ) { log ( 'No `firstMeaningfulPaintCandidate` events found in trace' ) ; } firstMeaningfulPaint = lastCandidate ; } const lcpResult = this . computeValidLCPAllFrames ( frameEvents , timeOriginEvt ) ; const load = frameEvents . find ( e => e . name === 'loadEventEnd' && e . ts > timeOriginEvt . ts ) ; const domContentLoaded = frameEvents . find ( e => e . name === 'domContentLoadedEventEnd' && e . ts > timeOriginEvt . ts ) ; param { { ts : number } = } event const getTimestamp = event => event && event . ts ; type { TraceNavigationTimesForFrame } const timestamps = { timeOrigin : timeOriginEvt . ts , firstPaint : getTimestamp ( firstPaint ) , firstContentfulPaint : firstContentfulPaint . ts , firstMeaningfulPaint : getTimestamp ( firstMeaningfulPaint ) , largestContentfulPaint : getTimestamp ( lcpResult . lcp ) , load : getTimestamp ( load ) , domContentLoaded : getTimestamp ( domContentLoaded ) , } ; param { number } ts const getTiming = ts => ( ts - timeOriginEvt . ts ) / 1000 ; param { number = } ts const maybeGetTiming = ts => ( ts === undefined ? undefined : getTiming ( ts ) ) ; type { TraceNavigationTimesForFrame } const timings = { timeOrigin : 0 , firstPaint : maybeGetTiming ( timestamps . firstPaint ) , firstContentfulPaint : getTiming ( timestamps . firstContentfulPaint ) , firstMeaningfulPaint : maybeGetTiming ( timestamps . firstMeaningfulPaint ) , largestContentfulPaint : maybeGetTiming ( timestamps . largestContentfulPaint ) , load : maybeGetTiming ( timestamps . load ) , domContentLoaded : maybeGetTiming ( timestamps . domContentLoaded ) , } ; return { timings , timestamps , timeOriginEvt : timeOriginEvt , firstPaintEvt : firstPaint , firstContentfulPaintEvt : firstContentfulPaint , firstMeaningfulPaintEvt : firstMeaningfulPaint , largestContentfulPaintEvt : lcpResult . lcp , loadEvt : load , domContentLoadedEvt : domContentLoaded , fmpFellBack , lcpInvalidated : lcpResult . invalidated , } ; } } export default TraceProcessor ;