33// found in the LICENSE file.
44
55import * as Platform from '../../../core/platform/platform.js' ;
6+ import type * as Protocol from '../../../generated/protocol.js' ;
67import * as Helpers from '../helpers/helpers.js' ;
78import * as Types from '../types/types.js' ;
89
@@ -146,17 +147,23 @@ const ACTIONABLE_FAILURE_REASONS = [
146147
147148// 500ms window.
148149// Use this window to consider events and requests that may have caused a layout shift.
149- const INVALIDATION_WINDOW = Helpers . Timing . secondsToMicroseconds ( Types . Timing . Seconds ( 0.5 ) ) ;
150+ const ROOT_CAUSE_WINDOW = Helpers . Timing . secondsToMicroseconds ( Types . Timing . Seconds ( 0.5 ) ) ;
150151
151152export interface LayoutShiftRootCausesData {
152153 iframeIds : string [ ] ;
153154 fontRequests : Types . Events . SyntheticNetworkRequest [ ] ;
154155 nonCompositedAnimations : NoncompositedAnimationFailure [ ] ;
156+ unsizedImages : Protocol . DOM . BackendNodeId [ ] ;
155157}
156158
157- function isInInvalidationWindow ( event : Types . Events . Event , targetEvent : Types . Events . Event ) : boolean {
159+ /**
160+ * Returns if an event happens within the root cause window, before the target event.
161+ * ROOT_CAUSE_WINDOW v target event
162+ * |------------------------|=======================
163+ */
164+ function isInRootCauseWindow ( event : Types . Events . Event , targetEvent : Types . Events . Event ) : boolean {
158165 const eventEnd = event . dur ? event . ts + event . dur : event . ts ;
159- return eventEnd < targetEvent . ts && eventEnd >= targetEvent . ts - INVALIDATION_WINDOW ;
166+ return eventEnd < targetEvent . ts && eventEnd >= targetEvent . ts - ROOT_CAUSE_WINDOW ;
160167}
161168
162169export function getNonCompositedFailure ( animationEvent : Types . Events . SyntheticAnimationPair ) :
@@ -205,14 +212,14 @@ function getNonCompositedFailureRootCauses(
205212 }
206213 allAnimationFailures . push ( ...failures ) ;
207214
208- const nextPrePaint = getNextPrePaintEvent ( prePaintEvents , animation ) ;
215+ const nextPrePaint = getNextEvent ( prePaintEvents , animation ) as Types . Events . PrePaint | null ;
209216 // If no following prePaint, this is not a root cause.
210217 if ( ! nextPrePaint ) {
211218 continue ;
212219 }
213220
214- // If the animation event is outside the INVALIDATION_WINDOW , it could not be a root cause.
215- if ( ! isInInvalidationWindow ( animation , nextPrePaint ) ) {
221+ // If the animation event is outside the ROOT_CAUSE_WINDOW , it could not be a root cause.
222+ if ( ! isInRootCauseWindow ( animation , nextPrePaint ) ) {
216223 continue ;
217224 }
218225
@@ -269,19 +276,18 @@ function getShiftsByPrePaintEvents(
269276}
270277
271278/**
272- * This gets the first prePaint event that follows the provided event and returns it .
279+ * Given a source event list, this returns the first event of that list that directly follows the target event .
273280 */
274- function getNextPrePaintEvent (
275- prePaintEvents : Types . Events . PrePaint [ ] , targetEvent : Types . Events . Event ) : Types . Events . PrePaint | undefined {
276- // Get the first PrePaint event that happened after the targetEvent.
277- const nextPrePaintIndex = Platform . ArrayUtilities . nearestIndexFromBeginning (
278- prePaintEvents , prePaint => prePaint . ts > targetEvent . ts + ( targetEvent . dur || 0 ) ) ;
281+ function getNextEvent ( sourceEvents : Types . Events . Event [ ] , targetEvent : Types . Events . Event ) : Types . Events . Event |
282+ undefined {
283+ const index = Platform . ArrayUtilities . nearestIndexFromBeginning (
284+ sourceEvents , source => source . ts > targetEvent . ts + ( targetEvent . dur || 0 ) ) ;
279285 // No PrePaint event registered after this event
280- if ( nextPrePaintIndex === null ) {
286+ if ( index === null ) {
281287 return undefined ;
282288 }
283289
284- return prePaintEvents [ nextPrePaintIndex ] ;
290+ return sourceEvents [ index ] ;
285291}
286292
287293/**
@@ -294,7 +300,7 @@ function getIframeRootCauses(
294300 rootCausesByShift : Map < Types . Events . LayoutShift , LayoutShiftRootCausesData > ,
295301 domLoadingEvents : readonly Types . Events . DomLoading [ ] ) : Map < Types . Events . LayoutShift , LayoutShiftRootCausesData > {
296302 for ( const iframeEvent of iframeCreatedEvents ) {
297- const nextPrePaint = getNextPrePaintEvent ( prePaintEvents , iframeEvent ) ;
303+ const nextPrePaint = getNextEvent ( prePaintEvents , iframeEvent ) as Types . Events . PrePaint | null ;
298304 // If no following prePaint, this is not a root cause.
299305 if ( ! nextPrePaint ) {
300306 continue ;
@@ -324,10 +330,43 @@ function getIframeRootCauses(
324330 return rootCausesByShift ;
325331}
326332
333+ /**
334+ * An unsized image is considered a root cause if its PaintImage can be correlated to a
335+ * layout shift. We can correlate PaintImages with unsized images by their matching nodeIds.
336+ * X <- layout shift
337+ * |----------------|
338+ * ^ PrePaint event |-----|
339+ * ^ PaintImage
340+ */
341+ function getUnsizedImageRootCauses (
342+ unsizedImageEvents : readonly Types . Events . LayoutImageUnsized [ ] , paintImageEvents : Types . Events . PaintImage [ ] ,
343+ shiftsByPrePaint : Map < Types . Events . PrePaint , Types . Events . LayoutShift [ ] > ,
344+ rootCausesByShift : Map < Types . Events . LayoutShift , LayoutShiftRootCausesData > ) :
345+ Map < Types . Events . LayoutShift , LayoutShiftRootCausesData > {
346+ shiftsByPrePaint . forEach ( ( shifts , prePaint ) => {
347+ const paintImage = getNextEvent ( paintImageEvents , prePaint ) as Types . Events . PaintImage | null ;
348+ // The unsized image corresponds to this PaintImage.
349+ const matchingNode =
350+ unsizedImageEvents . find ( unsizedImage => unsizedImage . args . data . nodeId === paintImage ?. args . data . nodeId ) ;
351+ if ( ! matchingNode ) {
352+ return ;
353+ }
354+ // The unsized image is a potential root cause of all the shifts of this prePaint.
355+ for ( const shift of shifts ) {
356+ const rootCausesForShift = rootCausesByShift . get ( shift ) ;
357+ if ( ! rootCausesForShift ) {
358+ throw new Error ( 'Unaccounted shift' ) ;
359+ }
360+ rootCausesForShift . unsizedImages . push ( matchingNode . args . data . nodeId ) ;
361+ }
362+ } ) ;
363+ return rootCausesByShift ;
364+ }
365+
327366/**
328367 * A font request is considered a root cause if the request occurs before a prePaint event
329368 * and within this prePaint event a layout shift(s) occurs. Additionally, this font request should
330- * happen within the INVALIDATION_WINDOW of the prePaint event.
369+ * happen within the ROOT_CAUSE_WINDOW of the prePaint event.
331370 */
332371function getFontRootCauses (
333372 networkRequests : Types . Events . SyntheticNetworkRequest [ ] , prePaintEvents : Types . Events . PrePaint [ ] ,
@@ -338,13 +377,13 @@ function getFontRootCauses(
338377 networkRequests . filter ( req => req . args . data . resourceType === 'Font' && req . args . data . mimeType . startsWith ( 'font' ) ) ;
339378
340379 for ( const req of fontRequests ) {
341- const nextPrePaint = getNextPrePaintEvent ( prePaintEvents , req ) ;
380+ const nextPrePaint = getNextEvent ( prePaintEvents , req ) as Types . Events . PrePaint | null ;
342381 if ( ! nextPrePaint ) {
343382 continue ;
344383 }
345384
346- // If the req is outside the INVALIDATION_WINDOW , it could not be a root cause.
347- if ( ! isInInvalidationWindow ( req , nextPrePaint ) ) {
385+ // If the req is outside the ROOT_CAUSE_WINDOW , it could not be a root cause.
386+ if ( ! isInRootCauseWindow ( req , nextPrePaint ) ) {
348387 continue ;
349388 }
350389
@@ -374,25 +413,28 @@ export function generateInsight(parsedTrace: RequiredData<typeof deps>, context:
374413 const iframeEvents = parsedTrace . LayoutShifts . renderFrameImplCreateChildFrameEvents . filter ( isWithinContext ) ;
375414 const networkRequests = parsedTrace . NetworkRequests . byTime . filter ( isWithinContext ) ;
376415 const domLoadingEvents = parsedTrace . LayoutShifts . domLoadingEvents . filter ( isWithinContext ) ;
416+ const unsizedImageEvents = parsedTrace . LayoutShifts . layoutImageUnsizedEvents . filter ( isWithinContext ) ;
377417
378418 const clusterKey = context . navigation ? context . navigationId : Types . Events . NO_NAVIGATION ;
379419 const clusters = parsedTrace . LayoutShifts . clustersByNavigationId . get ( clusterKey ) ?? [ ] ;
380420 const clustersByScore = clusters . toSorted ( ( a , b ) => b . clusterCumulativeScore - a . clusterCumulativeScore ) ;
381421 const worstCluster = clustersByScore . at ( 0 ) ;
382422 const layoutShifts = clusters . flatMap ( cluster => cluster . events ) ;
383423 const prePaintEvents = parsedTrace . LayoutShifts . prePaintEvents . filter ( isWithinContext ) ;
424+ const paintImageEvents = parsedTrace . LayoutShifts . paintImageEvents . filter ( isWithinContext ) ;
384425
385426 // Get root causes.
386427 const rootCausesByShift = new Map < Types . Events . SyntheticLayoutShift , LayoutShiftRootCausesData > ( ) ;
387428 const shiftsByPrePaint = getShiftsByPrePaintEvents ( layoutShifts , prePaintEvents ) ;
388429
389430 for ( const shift of layoutShifts ) {
390- rootCausesByShift . set ( shift , { iframeIds : [ ] , fontRequests : [ ] , nonCompositedAnimations : [ ] } ) ;
431+ rootCausesByShift . set ( shift , { iframeIds : [ ] , fontRequests : [ ] , nonCompositedAnimations : [ ] , unsizedImages : [ ] } ) ;
391432 }
392433
393434 // Populate root causes for rootCausesByShift.
394435 getIframeRootCauses ( iframeEvents , prePaintEvents , shiftsByPrePaint , rootCausesByShift , domLoadingEvents ) ;
395436 getFontRootCauses ( networkRequests , prePaintEvents , shiftsByPrePaint , rootCausesByShift ) ;
437+ getUnsizedImageRootCauses ( unsizedImageEvents , paintImageEvents , shiftsByPrePaint , rootCausesByShift ) ;
396438 const animationFailures =
397439 getNonCompositedFailureRootCauses ( compositeAnimationEvents , prePaintEvents , shiftsByPrePaint , rootCausesByShift ) ;
398440
0 commit comments