@@ -132,60 +132,60 @@ class NetworkAnalyzer {
132132 * single handshake.
133133 * This is the most accurate and preferred method of measurement when the data is available.
134134 *
135- * @param {LH.Artifacts.NetworkRequest[] } records
136- * @return {Map<string, number[]> }
135+ * @param {RequestInfo } info
136+ * @return {number[]|number|undefined }
137137 */
138- static _estimateRTTByOriginViaConnectionTiming ( records ) {
139- return NetworkAnalyzer . _estimateValueByOrigin ( records , ( { timing, connectionReused, record} ) => {
140- if ( connectionReused ) return ;
141-
142- // In LR, network records are missing connection timing, but we've smuggled it in via headers.
143- if ( global . isLightrider && record . lrStatistics ) {
144- if ( record . protocol . startsWith ( 'h3' ) ) {
145- return record . lrStatistics . TCPMs ;
146- } else {
147- return [ record . lrStatistics . TCPMs / 2 , record . lrStatistics . TCPMs / 2 ] ;
148- }
138+ static _estimateRTTViaConnectionTiming ( info ) {
139+ const { timing, connectionReused, record} = info ;
140+ if ( connectionReused ) return ;
141+
142+ // In LR, network records are missing connection timing, but we've smuggled it in via headers.
143+ if ( global . isLightrider && record . lrStatistics ) {
144+ if ( record . protocol . startsWith ( 'h3' ) ) {
145+ return record . lrStatistics . TCPMs ;
146+ } else {
147+ return [ record . lrStatistics . TCPMs / 2 , record . lrStatistics . TCPMs / 2 ] ;
149148 }
149+ }
150150
151- const { connectStart, sslStart, sslEnd, connectEnd} = timing ;
152- if ( connectEnd >= 0 && connectStart >= 0 && record . protocol . startsWith ( 'h3' ) ) {
153- // These values are equal to sslStart and sslEnd for h3.
154- return connectEnd - connectStart ;
155- } else if ( sslStart >= 0 && sslEnd >= 0 && sslStart !== connectStart ) {
156- // SSL can also be more than 1 RT but assume False Start was used.
157- return [ connectEnd - sslStart , sslStart - connectStart ] ;
158- } else if ( connectStart >= 0 && connectEnd >= 0 ) {
159- return connectEnd - connectStart ;
160- }
161- } ) ;
151+ const { connectStart, sslStart, sslEnd, connectEnd} = timing ;
152+ if ( connectEnd >= 0 && connectStart >= 0 && record . protocol . startsWith ( 'h3' ) ) {
153+ // These values are equal to sslStart and sslEnd for h3.
154+ return connectEnd - connectStart ;
155+ } else if ( sslStart >= 0 && sslEnd >= 0 && sslStart !== connectStart ) {
156+ // SSL can also be more than 1 RT but assume False Start was used.
157+ return [ connectEnd - sslStart , sslStart - connectStart ] ;
158+ } else if ( connectStart >= 0 && connectEnd >= 0 ) {
159+ return connectEnd - connectStart ;
160+ }
162161 }
163162
164163 /**
165164 * Estimates the observed RTT to each origin based on how long a download took on a fresh connection.
166165 * NOTE: this will tend to overestimate the actual RTT quite significantly as the download can be
167166 * slow for other reasons as well such as bandwidth constraints.
168167 *
169- * @param {LH.Artifacts.NetworkRequest[] } records
170- * @return {Map<string, number[]> }
168+ * @param {RequestInfo } info
169+ * @return {number|undefined }
171170 */
172- static _estimateRTTByOriginViaDownloadTiming ( records ) {
173- return NetworkAnalyzer . _estimateValueByOrigin ( records , ( { record, timing, connectionReused} ) => {
174- if ( connectionReused ) return ;
175- // Only look at downloads that went past the initial congestion window
176- if ( record . transferSize <= INITIAL_CWD ) return ;
177- if ( ! Number . isFinite ( timing . receiveHeadersEnd ) || timing . receiveHeadersEnd < 0 ) return ;
171+ static _estimateRTTViaDownloadTiming ( info ) {
172+ const { timing, connectionReused, record} = info ;
173+ if ( connectionReused ) return ;
178174
179- // Compute the amount of time downloading everything after the first congestion window took
180- const totalTime = record . networkEndTime - record . networkRequestTime ;
181- const downloadTimeAfterFirstByte = totalTime - timing . receiveHeadersEnd ;
182- const numberOfRoundTrips = Math . log2 ( record . transferSize / INITIAL_CWD ) ;
175+ // Only look at downloads that went past the initial congestion window
176+ if ( record . transferSize <= INITIAL_CWD ) return ;
177+ if ( ! Number . isFinite ( timing . receiveHeadersEnd ) || timing . receiveHeadersEnd < 0 ) return ;
183178
184- // Ignore requests that required a high number of round trips since bandwidth starts to play
185- // a larger role than latency
186- if ( numberOfRoundTrips > 5 ) return ;
187- return downloadTimeAfterFirstByte / numberOfRoundTrips ;
188- } ) ;
179+ // Compute the amount of time downloading everything after the first congestion window took
180+ const totalTime = record . networkEndTime - record . networkRequestTime ;
181+ const downloadTimeAfterFirstByte = totalTime - timing . receiveHeadersEnd ;
182+ const numberOfRoundTrips = Math . log2 ( record . transferSize / INITIAL_CWD ) ;
183+
184+ // Ignore requests that required a high number of round trips since bandwidth starts to play
185+ // a larger role than latency
186+ if ( numberOfRoundTrips > 5 ) return ;
187+
188+ return downloadTimeAfterFirstByte / numberOfRoundTrips ;
189189 }
190190
191191 /**
@@ -194,21 +194,21 @@ class NetworkAnalyzer {
194194 * NOTE: this will tend to overestimate the actual RTT as the request can be delayed for other
195195 * reasons as well such as more SSL handshakes if TLS False Start is not enabled.
196196 *
197- * @param {LH.Artifacts.NetworkRequest[] } records
198- * @return {Map<string, number[]> }
197+ * @param {RequestInfo } info
198+ * @return {number|undefined }
199199 */
200- static _estimateRTTByOriginViaSendStartTiming ( records ) {
201- return NetworkAnalyzer . _estimateValueByOrigin ( records , ( { record , timing, connectionReused} ) => {
202- if ( connectionReused ) return ;
203- if ( ! Number . isFinite ( timing . sendStart ) || timing . sendStart < 0 ) return ;
204-
205- // Assume everything before sendStart was just DNS + (SSL)? + TCP handshake
206- // 1 RT for DNS, 1 RT (maybe) for SSL, 1 RT for TCP
207- let roundTrips = 1 ;
208- if ( ! record . protocol . startsWith ( 'h3' ) ) roundTrips + = 1 ; // TCP
209- if ( record . parsedURL . scheme === 'https' ) roundTrips += 1 ;
210- return timing . sendStart / roundTrips ;
211- } ) ;
200+ static _estimateRTTViaSendStartTiming ( info ) {
201+ const { timing, connectionReused, record } = info ;
202+ if ( connectionReused ) return ;
203+
204+ if ( ! Number . isFinite ( timing . sendStart ) || timing . sendStart < 0 ) return ;
205+
206+ // Assume everything before sendStart was just DNS + ( SSL)? + TCP handshake
207+ // 1 RT for DNS, 1 RT (maybe) for SSL, 1 RT for TCP
208+ let roundTrips = 1 ;
209+ if ( ! record . protocol . startsWith ( 'h3' ) ) roundTrips += 1 ; // TCP
210+ if ( record . parsedURL . scheme === 'https' ) roundTrips += 1 ;
211+ return timing . sendStart / roundTrips ;
212212 }
213213
214214 /**
@@ -217,34 +217,33 @@ class NetworkAnalyzer {
217217 * NOTE: this is the most inaccurate way to estimate the RTT, but in some environments it's all
218218 * we have access to :(
219219 *
220- * @param {LH.Artifacts.NetworkRequest[] } records
221- * @return {Map<string, number[]> }
220+ * @param {RequestInfo } info
221+ * @return {number|undefined }
222222 */
223- static _estimateRTTByOriginViaHeadersEndTiming ( records ) {
224- return NetworkAnalyzer . _estimateValueByOrigin ( records , ( { record , timing, connectionReused} ) => {
225- if ( ! Number . isFinite ( timing . receiveHeadersEnd ) || timing . receiveHeadersEnd < 0 ) return ;
226- if ( ! record . resourceType ) return ;
227-
228- const serverResponseTimePercentage =
229- SERVER_RESPONSE_PERCENTAGE_OF_TTFB [ record . resourceType ] ||
230- DEFAULT_SERVER_RESPONSE_PERCENTAGE ;
231- const estimatedServerResponseTime = timing . receiveHeadersEnd * serverResponseTimePercentage ;
232-
233- // When connection was reused...
234- // TTFB = 1 RT for request + server response time
235- let roundTrips = 1 ;
236-
237- // When connection was fresh...
238- // TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
239- if ( ! connectionReused ) {
240- roundTrips += 1 ; // DNS
241- if ( ! record . protocol . startsWith ( 'h3' ) ) roundTrips += 1 ; // TCP
242- if ( record . parsedURL . scheme === 'https' ) roundTrips += 1 ; // SSL
243- }
223+ static _estimateRTTViaHeadersEndTiming ( info ) {
224+ const { timing, connectionReused, record } = info ;
225+ if ( ! Number . isFinite ( timing . receiveHeadersEnd ) || timing . receiveHeadersEnd < 0 ) return ;
226+ if ( ! record . resourceType ) return ;
227+
228+ const serverResponseTimePercentage =
229+ SERVER_RESPONSE_PERCENTAGE_OF_TTFB [ record . resourceType ] ||
230+ DEFAULT_SERVER_RESPONSE_PERCENTAGE ;
231+ const estimatedServerResponseTime = timing . receiveHeadersEnd * serverResponseTimePercentage ;
232+
233+ // When connection was reused...
234+ // TTFB = 1 RT for request + server response time
235+ let roundTrips = 1 ;
236+
237+ // When connection was fresh...
238+ // TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
239+ if ( ! connectionReused ) {
240+ roundTrips += 1 ; // DNS
241+ if ( ! record . protocol . startsWith ( 'h3' ) ) roundTrips += 1 ; // TCP
242+ if ( record . parsedURL . scheme === 'https' ) roundTrips += 1 ; // SSL
243+ }
244244
245- // subtract out our estimated server response time
246- return Math . max ( ( timing . receiveHeadersEnd - estimatedServerResponseTime ) / roundTrips , 3 ) ;
247- } ) ;
245+ // subtract out our estimated server response time
246+ return Math . max ( ( timing . receiveHeadersEnd - estimatedServerResponseTime ) / roundTrips , 3 ) ;
248247 }
249248
250249 /**
@@ -350,32 +349,61 @@ class NetworkAnalyzer {
350349 useHeadersEndEstimates = true ,
351350 } = options || { } ;
352351
353- let estimatesByOrigin = NetworkAnalyzer . _estimateRTTByOriginViaConnectionTiming ( records ) ;
354- if ( ! estimatesByOrigin . size || forceCoarseEstimates ) {
355- estimatesByOrigin = new Map ( ) ;
356- const estimatesViaDownload = NetworkAnalyzer . _estimateRTTByOriginViaDownloadTiming ( records ) ;
357- const estimatesViaSendStart = NetworkAnalyzer . _estimateRTTByOriginViaSendStartTiming ( records ) ;
358- const estimatesViaTTFB = NetworkAnalyzer . _estimateRTTByOriginViaHeadersEndTiming ( records ) ;
352+ const connectionWasReused = NetworkAnalyzer . estimateIfConnectionWasReused ( records ) ;
353+ const groupedByOrigin = NetworkAnalyzer . groupByOrigin ( records ) ;
359354
360- for ( const [ origin , estimates ] of estimatesViaDownload . entries ( ) ) {
361- if ( ! useDownloadEstimates ) continue ;
362- estimatesByOrigin . set ( origin , estimates ) ;
355+ const estimatesByOrigin = new Map ( ) ;
356+ for ( const [ origin , originRecords ] of groupedByOrigin . entries ( ) ) {
357+ /** @type {number[] } */
358+ const originEstimates = [ ] ;
359+
360+ /**
361+ * @param {(e: RequestInfo) => number[]|number|undefined } estimator
362+ */
363+ // eslint-disable-next-line no-inner-declarations
364+ function collectEstimates ( estimator , multiplier = 1 ) {
365+ for ( const record of originRecords ) {
366+ const timing = record . timing ;
367+ if ( ! timing ) continue ;
368+
369+ const estimates = estimator ( {
370+ record,
371+ timing,
372+ connectionReused : connectionWasReused . get ( record . requestId ) ,
373+ } ) ;
374+ if ( estimates === undefined ) continue ;
375+
376+ if ( ! Array . isArray ( estimates ) ) {
377+ originEstimates . push ( estimates * multiplier ) ;
378+ } else {
379+ originEstimates . push ( ...estimates . map ( e => e * multiplier ) ) ;
380+ }
381+ }
363382 }
364383
365- for ( const [ origin , estimates ] of estimatesViaSendStart . entries ( ) ) {
366- if ( ! useSendStartEstimates ) continue ;
367- const existing = estimatesByOrigin . get ( origin ) || [ ] ;
368- estimatesByOrigin . set ( origin , existing . concat ( estimates ) ) ;
384+ if ( ! forceCoarseEstimates ) {
385+ collectEstimates ( this . _estimateRTTViaConnectionTiming ) ;
369386 }
370387
371- for ( const [ origin , estimates ] of estimatesViaTTFB . entries ( ) ) {
372- if ( ! useHeadersEndEstimates ) continue ;
373- const existing = estimatesByOrigin . get ( origin ) || [ ] ;
374- estimatesByOrigin . set ( origin , existing . concat ( estimates ) ) ;
388+ // Connection timing can be missing for a few reasons:
389+ // - Origin was preconnected, which we don't have instrumentation for.
390+ // - Trace began recording after a connection has already been established (for example, in timespan mode)
391+ // - Perhaps Chrome established a connection already in the background (service worker? Just guessing here)
392+ // - Not provided in LR netstack.
393+ if ( ! originEstimates . length ) {
394+ if ( useDownloadEstimates ) {
395+ collectEstimates ( this . _estimateRTTViaDownloadTiming , coarseEstimateMultiplier ) ;
396+ }
397+ if ( useSendStartEstimates ) {
398+ collectEstimates ( this . _estimateRTTViaSendStartTiming , coarseEstimateMultiplier ) ;
399+ }
400+ if ( useHeadersEndEstimates ) {
401+ collectEstimates ( this . _estimateRTTViaHeadersEndTiming , coarseEstimateMultiplier ) ;
402+ }
375403 }
376404
377- for ( const estimates of estimatesByOrigin . values ( ) ) {
378- estimates . forEach ( ( x , i ) => ( estimates [ i ] = x * coarseEstimateMultiplier ) ) ;
405+ if ( originEstimates . length ) {
406+ estimatesByOrigin . set ( origin , originEstimates ) ;
379407 }
380408 }
381409
0 commit comments