@paulirish/trace_engine 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/analyze-trace.mjs +1 -1
  2. package/core/platform/DevToolsPath.d.ts +4 -13
  3. package/core/platform/DevToolsPath.js +7 -4
  4. package/core/platform/DevToolsPath.js.map +1 -7
  5. package/core/platform/MimeType.d.ts +27 -0
  6. package/core/platform/MimeType.js +119 -86
  7. package/core/platform/MimeType.js.map +1 -7
  8. package/core/platform/Timing.d.ts +7 -0
  9. package/core/platform/Timing.js +7 -4
  10. package/core/platform/Timing.js.map +1 -7
  11. package/core/platform/UIString.d.ts +2 -5
  12. package/core/platform/UIString.js +5 -2
  13. package/core/platform/UIString.js.map +1 -7
  14. package/core/platform/UserVisibleError.js +19 -10
  15. package/core/platform/UserVisibleError.js.map +1 -7
  16. package/core/platform/array-utilities.d.ts +48 -10
  17. package/core/platform/array-utilities.js +160 -124
  18. package/core/platform/array-utilities.js.map +1 -7
  19. package/core/platform/brand.d.ts +14 -0
  20. package/core/platform/brand.js +5 -1
  21. package/core/platform/brand.js.map +1 -7
  22. package/core/platform/date-utilities.js +10 -6
  23. package/core/platform/date-utilities.js.map +1 -7
  24. package/core/platform/dom-utilities.d.ts +3 -1
  25. package/core/platform/dom-utilities.js +94 -83
  26. package/core/platform/dom-utilities.js.map +1 -7
  27. package/core/platform/keyboard-utilities.d.ts +2 -0
  28. package/core/platform/keyboard-utilities.js +15 -24
  29. package/core/platform/keyboard-utilities.js.map +1 -7
  30. package/core/platform/map-utilities.d.ts +4 -0
  31. package/core/platform/map-utilities.js +66 -60
  32. package/core/platform/map-utilities.js.map +1 -7
  33. package/core/platform/number-utilities.js +66 -55
  34. package/core/platform/number-utilities.js.map +1 -7
  35. package/core/platform/platform.d.ts +5 -1
  36. package/core/platform/platform.js +54 -37
  37. package/core/platform/platform.js.map +1 -7
  38. package/core/platform/promise-utilities.d.ts +10 -0
  39. package/core/platform/promise-utilities.js +16 -8
  40. package/core/platform/promise-utilities.js.map +1 -7
  41. package/core/platform/set-utilities.js +20 -17
  42. package/core/platform/set-utilities.js.map +1 -7
  43. package/core/platform/string-utilities.d.ts +32 -1
  44. package/core/platform/string-utilities.js +453 -379
  45. package/core/platform/string-utilities.js.map +1 -7
  46. package/core/platform/typescript-utilities.d.ts +5 -5
  47. package/core/platform/typescript-utilities.js +19 -7
  48. package/core/platform/typescript-utilities.js.map +1 -7
  49. package/generated/protocol.d.ts +2081 -347
  50. package/generated/protocol.js +5 -2230
  51. package/models/cpu_profile/CPUProfileDataModel.d.ts +77 -0
  52. package/models/cpu_profile/CPUProfileDataModel.js +492 -359
  53. package/models/cpu_profile/CPUProfileDataModel.js.map +1 -7
  54. package/models/cpu_profile/ProfileTreeModel.d.ts +29 -0
  55. package/models/cpu_profile/ProfileTreeModel.js +87 -82
  56. package/models/cpu_profile/ProfileTreeModel.js.map +1 -7
  57. package/models/cpu_profile/cpu_profile.d.ts +3 -0
  58. package/models/cpu_profile/cpu_profile.js +7 -7
  59. package/models/cpu_profile/cpu_profile.js.map +1 -7
  60. package/models/trace/EntriesFilter.d.ts +55 -0
  61. package/models/trace/EntriesFilter.js +227 -166
  62. package/models/trace/EntriesFilter.js.map +1 -7
  63. package/models/trace/LegacyTracingModel.js.map +1 -7
  64. package/models/trace/ModelImpl.d.ts +110 -0
  65. package/models/trace/ModelImpl.js +161 -102
  66. package/models/trace/ModelImpl.js.map +1 -7
  67. package/models/trace/Processor.d.ts +36 -0
  68. package/models/trace/Processor.js +197 -163
  69. package/models/trace/Processor.js.map +1 -7
  70. package/models/trace/TracingManager.js.map +1 -7
  71. package/models/trace/extras/FetchNodes.d.ts +46 -0
  72. package/models/trace/extras/FetchNodes.js +132 -91
  73. package/models/trace/extras/FetchNodes.js.map +1 -7
  74. package/models/trace/extras/FilmStrip.d.ts +19 -0
  75. package/models/trace/extras/FilmStrip.js +38 -31
  76. package/models/trace/extras/FilmStrip.js.map +1 -7
  77. package/models/trace/extras/MainThreadActivity.d.ts +2 -0
  78. package/models/trace/extras/MainThreadActivity.js +72 -56
  79. package/models/trace/extras/MainThreadActivity.js.map +1 -7
  80. package/models/trace/extras/Metadata.d.ts +2 -0
  81. package/models/trace/extras/Metadata.js +42 -26
  82. package/models/trace/extras/Metadata.js.map +1 -7
  83. package/models/trace/extras/extras.js.map +1 -7
  84. package/models/trace/handlers/AnimationHandler.d.ts +8 -0
  85. package/models/trace/handlers/AnimationHandler.js +22 -20
  86. package/models/trace/handlers/AnimationHandler.js.map +1 -7
  87. package/models/trace/handlers/AuctionWorkletsHandler.d.ts +8 -0
  88. package/models/trace/handlers/AuctionWorkletsHandler.js +143 -89
  89. package/models/trace/handlers/AuctionWorkletsHandler.js.map +1 -7
  90. package/models/trace/handlers/FramesHandler.d.ts +76 -0
  91. package/models/trace/handlers/FramesHandler.js +424 -355
  92. package/models/trace/handlers/FramesHandler.js.map +1 -7
  93. package/models/trace/handlers/GPUHandler.d.ts +11 -0
  94. package/models/trace/handlers/GPUHandler.js +41 -37
  95. package/models/trace/handlers/GPUHandler.js.map +1 -7
  96. package/models/trace/handlers/InitiatorsHandler.d.ts +10 -0
  97. package/models/trace/handlers/InitiatorsHandler.js +164 -113
  98. package/models/trace/handlers/InitiatorsHandler.js.map +1 -7
  99. package/models/trace/handlers/InvalidationsHandler.d.ts +10 -0
  100. package/models/trace/handlers/InvalidationsHandler.js +101 -79
  101. package/models/trace/handlers/InvalidationsHandler.js.map +1 -7
  102. package/models/trace/handlers/LargestImagePaintHandler.d.ts +5 -0
  103. package/models/trace/handlers/LargestImagePaintHandler.js +32 -12
  104. package/models/trace/handlers/LargestImagePaintHandler.js.map +1 -7
  105. package/models/trace/handlers/LargestTextPaintHandler.d.ts +5 -0
  106. package/models/trace/handlers/LargestTextPaintHandler.js +20 -12
  107. package/models/trace/handlers/LargestTextPaintHandler.js.map +1 -7
  108. package/models/trace/handlers/LayerTreeHandler.d.ts +13 -0
  109. package/models/trace/handlers/LayerTreeHandler.js +96 -70
  110. package/models/trace/handlers/LayerTreeHandler.js.map +1 -7
  111. package/models/trace/handlers/LayoutShiftsHandler.d.ts +44 -0
  112. package/models/trace/handlers/LayoutShiftsHandler.js +304 -227
  113. package/models/trace/handlers/LayoutShiftsHandler.js.map +1 -7
  114. package/models/trace/handlers/MemoryHandler.d.ts +7 -0
  115. package/models/trace/handlers/MemoryHandler.js +14 -11
  116. package/models/trace/handlers/MemoryHandler.js.map +1 -7
  117. package/models/trace/handlers/MetaHandler.d.ts +37 -0
  118. package/models/trace/handlers/MetaHandler.js +314 -226
  119. package/models/trace/handlers/MetaHandler.js.map +1 -7
  120. package/models/trace/handlers/ModelHandlers.d.ts +21 -0
  121. package/models/trace/handlers/ModelHandlers.js +25 -22
  122. package/models/trace/handlers/ModelHandlers.js.map +1 -7
  123. package/models/trace/handlers/NetworkRequestsHandler.d.ts +17 -0
  124. package/models/trace/handlers/NetworkRequestsHandler.js +342 -218
  125. package/models/trace/handlers/NetworkRequestsHandler.js.map +1 -7
  126. package/models/trace/handlers/PageLoadMetricsHandler.d.ts +67 -0
  127. package/models/trace/handlers/PageLoadMetricsHandler.js +357 -284
  128. package/models/trace/handlers/PageLoadMetricsHandler.js.map +1 -7
  129. package/models/trace/handlers/RendererHandler.d.ts +101 -0
  130. package/models/trace/handlers/RendererHandler.js +295 -191
  131. package/models/trace/handlers/RendererHandler.js.map +1 -7
  132. package/models/trace/handlers/SamplesHandler.d.ts +46 -0
  133. package/models/trace/handlers/SamplesHandler.js +195 -158
  134. package/models/trace/handlers/SamplesHandler.js.map +1 -7
  135. package/models/trace/handlers/ScreenshotsHandler.d.ts +7 -0
  136. package/models/trace/handlers/ScreenshotsHandler.js +63 -41
  137. package/models/trace/handlers/ScreenshotsHandler.js.map +1 -7
  138. package/models/trace/handlers/Threads.d.ts +33 -0
  139. package/models/trace/handlers/Threads.js +85 -67
  140. package/models/trace/handlers/Threads.js.map +1 -7
  141. package/models/trace/handlers/UserInteractionsHandler.d.ts +57 -0
  142. package/models/trace/handlers/UserInteractionsHandler.js +240 -141
  143. package/models/trace/handlers/UserInteractionsHandler.js.map +1 -7
  144. package/models/trace/handlers/UserTimingsHandler.d.ts +28 -0
  145. package/models/trace/handlers/UserTimingsHandler.js +91 -80
  146. package/models/trace/handlers/UserTimingsHandler.js.map +1 -7
  147. package/models/trace/handlers/WarningsHandler.d.ts +14 -0
  148. package/models/trace/handlers/WarningsHandler.js +100 -62
  149. package/models/trace/handlers/WarningsHandler.js.map +1 -7
  150. package/models/trace/handlers/WorkersHandler.d.ts +11 -0
  151. package/models/trace/handlers/WorkersHandler.js +40 -38
  152. package/models/trace/handlers/WorkersHandler.js.map +1 -7
  153. package/models/trace/handlers/handlers.d.ts +3 -0
  154. package/models/trace/handlers/handlers.js +7 -4
  155. package/models/trace/handlers/handlers.js.map +1 -7
  156. package/models/trace/handlers/types.d.ts +45 -0
  157. package/models/trace/handlers/types.js +15 -15
  158. package/models/trace/handlers/types.js.map +1 -7
  159. package/models/trace/helpers/SamplesIntegrator.d.ts +49 -0
  160. package/models/trace/helpers/SamplesIntegrator.js +381 -204
  161. package/models/trace/helpers/SamplesIntegrator.js.map +1 -7
  162. package/models/trace/helpers/Timing.d.ts +26 -0
  163. package/models/trace/helpers/Timing.js +131 -110
  164. package/models/trace/helpers/Timing.js.map +1 -7
  165. package/models/trace/helpers/Trace.d.ts +37 -0
  166. package/models/trace/helpers/Trace.js +200 -166
  167. package/models/trace/helpers/Trace.js.map +1 -7
  168. package/models/trace/helpers/TreeHelpers.d.ts +90 -0
  169. package/models/trace/helpers/TreeHelpers.js +203 -100
  170. package/models/trace/helpers/TreeHelpers.js.map +1 -7
  171. package/models/trace/helpers/helpers.d.ts +4 -0
  172. package/models/trace/helpers/helpers.js +8 -5
  173. package/models/trace/helpers/helpers.js.map +1 -7
  174. package/models/trace/root-causes/LayoutShift.d.ts +119 -0
  175. package/models/trace/root-causes/LayoutShift.js +470 -323
  176. package/models/trace/root-causes/LayoutShift.js.map +1 -7
  177. package/models/trace/root-causes/RootCauses.d.ts +14 -0
  178. package/models/trace/root-causes/RootCauses.js +9 -6
  179. package/models/trace/root-causes/RootCauses.js.map +1 -7
  180. package/models/trace/root-causes/root-causes.d.ts +1 -0
  181. package/models/trace/root-causes/root-causes.js +5 -2
  182. package/models/trace/root-causes/root-causes.js.map +1 -7
  183. package/models/trace/trace.d.ts +11 -0
  184. package/models/trace/trace.js +17 -23
  185. package/models/trace/trace.js.map +1 -7
  186. package/models/trace/types/Configuration.d.ts +33 -0
  187. package/models/trace/types/Configuration.js +25 -14
  188. package/models/trace/types/Configuration.js.map +1 -7
  189. package/models/trace/types/File.d.ts +23 -0
  190. package/models/trace/types/File.js +5 -6
  191. package/models/trace/types/File.js.map +1 -7
  192. package/models/trace/types/Timing.d.ts +25 -0
  193. package/models/trace/types/Timing.js +10 -11
  194. package/models/trace/types/Timing.js.map +1 -7
  195. package/models/trace/types/TraceEvents.d.ts +1571 -0
  196. package/models/trace/types/TraceEvents.js +174 -381
  197. package/models/trace/types/TraceEvents.js.map +1 -7
  198. package/models/trace/types/types.d.ts +4 -0
  199. package/models/trace/types/types.js +8 -5
  200. package/models/trace/types/types.js.map +1 -7
  201. package/package.json +1 -1
  202. package/TracingManager.js +0 -0
  203. package/extras/extras.js +0 -0
  204. package/trace.mjs +0 -6980
  205. package/trace.mjs.map +0 -8
@@ -1,370 +1,517 @@
1
- import * as Platform from "../../../core/platform/platform.js";
2
- import * as Helpers from "../helpers/helpers.js";
3
- import * as Types from "../types/types.js";
4
- const fontRequestsByPrePaint = /* @__PURE__ */ new Map();
5
- const renderBlocksByPrePaint = /* @__PURE__ */ new Map();
1
+ // Copyright 2023 The Chromium Authors. All rights reserved.
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ import * as Platform from '../../../core/platform/platform.js';
5
+ import * as Helpers from '../helpers/helpers.js';
6
+ import * as Types from '../types/types.js';
7
+ const fontRequestsByPrePaint = new Map();
8
+ const renderBlocksByPrePaint = new Map();
6
9
  function setDefaultValue(map, shift) {
7
- Platform.MapUtilities.getWithDefault(map, shift, () => {
8
- return {
9
- unsizedMedia: [],
10
- iframes: [],
11
- fontChanges: [],
12
- renderBlockingRequests: [],
13
- scriptStackTrace: []
14
- };
15
- });
10
+ Platform.MapUtilities.getWithDefault(map, shift, () => {
11
+ return {
12
+ unsizedMedia: [],
13
+ iframes: [],
14
+ fontChanges: [],
15
+ renderBlockingRequests: [],
16
+ scriptStackTrace: [],
17
+ };
18
+ });
16
19
  }
17
- const NON_RENDER_BLOCKING_VALUES = /* @__PURE__ */ new Set([
18
- "non_blocking",
19
- "potentially_blocking"
20
+ // Important: we purposefully treat `potentially_blocking` as
21
+ // non-render-blocking here because:
22
+ // 1. An async script can run on the main thread at any point, including before
23
+ // the page is loaded
24
+ // 2. An async script will never block the parsing and rendering process of the
25
+ // browser.
26
+ // 3. Therefore, from a developer's point of view, there is nothing more they
27
+ // can do if they've put `async` on, and within the context of Insights, we
28
+ // shouldn't report an async script as render blocking.
29
+ // In the future we may want to consider suggesting the use of `defer` over
30
+ // `async`, as it doesn't have this concern, but for now we'll allow `async`
31
+ // and not report it as an issue.
32
+ const NON_RENDER_BLOCKING_VALUES = new Set([
33
+ 'non_blocking',
34
+ 'potentially_blocking',
20
35
  ]);
21
36
  function networkRequestIsRenderBlockingInFrame(event, frameId) {
22
- const isRenderBlocking = !NON_RENDER_BLOCKING_VALUES.has(event.args.data.renderBlocking);
23
- return isRenderBlocking && event.args.data.frame === frameId;
37
+ const isRenderBlocking = !NON_RENDER_BLOCKING_VALUES.has(event.args.data.renderBlocking);
38
+ return isRenderBlocking && event.args.data.frame === frameId;
24
39
  }
25
40
  export class LayoutShiftRootCauses {
26
- #protocolInterface;
27
- #rootCauseCacheMap = /* @__PURE__ */ new Map();
28
- constructor(protocolInterface) {
29
- this.#protocolInterface = protocolInterface;
30
- }
31
- async rootCausesForEvent(modelData, event) {
32
- const cachedResult = this.#rootCauseCacheMap.get(event);
33
- if (cachedResult) {
34
- return cachedResult;
35
- }
36
- const allLayoutShifts = modelData.LayoutShifts.clusters.flatMap((cluster) => cluster.events);
37
- allLayoutShifts.forEach((shift) => setDefaultValue(this.#rootCauseCacheMap, shift));
38
- await this.blameShifts(allLayoutShifts, modelData);
39
- const resultForEvent = this.#rootCauseCacheMap.get(event);
40
- if (!resultForEvent) {
41
- return null;
42
- }
43
- return resultForEvent;
44
- }
45
- async blameShifts(layoutShifts, modelData) {
46
- await this.linkShiftsToLayoutInvalidations(layoutShifts, modelData);
47
- this.linkShiftsToLayoutEvents(layoutShifts, modelData);
48
- }
49
- async linkShiftsToLayoutInvalidations(layoutShifts, modelData) {
50
- const { prePaintEvents, layoutInvalidationEvents, scheduleStyleInvalidationEvents, backendNodeIds } = modelData.LayoutShifts;
51
- const eventsForLayoutInvalidation = [...layoutInvalidationEvents, ...scheduleStyleInvalidationEvents];
52
- const nodes = await this.#protocolInterface.pushNodesByBackendIdsToFrontend(backendNodeIds);
53
- const nodeIdsByBackendIdMap = /* @__PURE__ */ new Map();
54
- for (let i = 0; i < backendNodeIds.length; i++) {
55
- nodeIdsByBackendIdMap.set(backendNodeIds[i], nodes[i]);
41
+ #protocolInterface;
42
+ #rootCauseCacheMap = new Map();
43
+ constructor(protocolInterface) {
44
+ this.#protocolInterface = protocolInterface;
56
45
  }
57
- const shiftsByPrePaint = getShiftsByPrePaintEvents(layoutShifts, prePaintEvents);
58
- for (const layoutInvalidation of eventsForLayoutInvalidation) {
59
- const nextPrePaintIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(prePaintEvents, (prePaint) => prePaint.ts > layoutInvalidation.ts);
60
- if (nextPrePaintIndex === null) {
61
- continue;
62
- }
63
- const nextPrePaint = prePaintEvents[nextPrePaintIndex];
64
- const subsequentShifts = shiftsByPrePaint.get(nextPrePaint);
65
- if (!subsequentShifts) {
66
- continue;
67
- }
68
- const fontChangeRootCause = this.getFontChangeRootCause(layoutInvalidation, nextPrePaint, modelData);
69
- const renderBlockRootCause = this.getRenderBlockRootCause(layoutInvalidation, nextPrePaint, modelData);
70
- const layoutInvalidationNodeId = nodeIdsByBackendIdMap.get(layoutInvalidation.args.data.nodeId);
71
- const layoutInvalidationNode = layoutInvalidationNodeId !== void 0 ? await this.#protocolInterface.getNode(layoutInvalidationNodeId) : null;
72
- let unsizedMediaRootCause = null;
73
- let iframeRootCause = null;
74
- if (layoutInvalidationNode && layoutInvalidation.args.data.reason) {
75
- unsizedMediaRootCause = await this.getUnsizedMediaRootCause(layoutInvalidation.args.data.reason, layoutInvalidationNode);
76
- iframeRootCause = this.getIframeRootCause(layoutInvalidation.args.data.reason, layoutInvalidationNode);
77
- }
78
- if (!unsizedMediaRootCause && !iframeRootCause && !fontChangeRootCause && !renderBlockRootCause) {
79
- continue;
80
- }
81
- for (const shift of subsequentShifts) {
82
- const rootCausesForShift = Platform.MapUtilities.getWithDefault(this.#rootCauseCacheMap, shift, () => {
83
- return {
84
- unsizedMedia: [],
85
- iframes: [],
86
- fontChanges: [],
87
- renderBlockingRequests: [],
88
- scriptStackTrace: []
89
- };
90
- });
91
- if (unsizedMediaRootCause && !rootCausesForShift.unsizedMedia.some((media) => media.node.nodeId === unsizedMediaRootCause?.node.nodeId) && shift.args.frame === layoutInvalidation.args.data.frame) {
92
- rootCausesForShift.unsizedMedia.push(unsizedMediaRootCause);
46
+ /**
47
+ * Calculates the potential root causes for a given layout shift event. Once
48
+ * calculated, this data is cached.
49
+ * Note: because you need all layout shift data at once to calculate these
50
+ * correctly, this function will parse the root causes for _all_ layout shift
51
+ * events the first time that it's called. That then populates the cache for
52
+ * each shift, so any subsequent calls are just a constant lookup.
53
+ */
54
+ async rootCausesForEvent(modelData, event) {
55
+ const cachedResult = this.#rootCauseCacheMap.get(event);
56
+ if (cachedResult) {
57
+ return cachedResult;
93
58
  }
94
- if (iframeRootCause && !rootCausesForShift.iframes.some((injectedIframe) => injectedIframe.iframe.nodeId === iframeRootCause?.iframe.nodeId)) {
95
- rootCausesForShift.iframes.push(iframeRootCause);
59
+ const allLayoutShifts = modelData.LayoutShifts.clusters.flatMap(cluster => cluster.events);
60
+ // Make sure a value in the cache is set even for shifts that don't have a root cause,
61
+ // so that we don't have to recompute when no root causes are found. In case a cause
62
+ // for a shift is found, the default value is replaced.
63
+ allLayoutShifts.forEach(shift => setDefaultValue(this.#rootCauseCacheMap, shift));
64
+ // Populate the cache
65
+ await this.blameShifts(allLayoutShifts, modelData);
66
+ const resultForEvent = this.#rootCauseCacheMap.get(event);
67
+ if (!resultForEvent) {
68
+ // No root causes available for this layout shift.
69
+ return null;
96
70
  }
97
- if (fontChangeRootCause) {
98
- rootCausesForShift.fontChanges = fontChangeRootCause;
71
+ return resultForEvent;
72
+ }
73
+ /**
74
+ * Determines potential root causes for shifts
75
+ */
76
+ async blameShifts(layoutShifts, modelData) {
77
+ await this.linkShiftsToLayoutInvalidations(layoutShifts, modelData);
78
+ this.linkShiftsToLayoutEvents(layoutShifts, modelData);
79
+ }
80
+ /**
81
+ * "LayoutInvalidations" are a set of trace events dispatched in Blink under the name
82
+ * "layoutInvalidationTracking", which track invalidations on the "Layout"stage of the
83
+ * rendering pipeline. This function utilizes this event to flag potential root causes
84
+ * to layout shifts.
85
+ */
86
+ async linkShiftsToLayoutInvalidations(layoutShifts, modelData) {
87
+ const { prePaintEvents, layoutInvalidationEvents, scheduleStyleInvalidationEvents, backendNodeIds } = modelData.LayoutShifts;
88
+ // For the purposes of determining root causes of layout shifts, we
89
+ // consider scheduleStyleInvalidationTracking and
90
+ // LayoutInvalidationTracking events as events that could have been the
91
+ // cause of the layout shift.
92
+ const eventsForLayoutInvalidation = [...layoutInvalidationEvents, ...scheduleStyleInvalidationEvents];
93
+ const nodes = await this.#protocolInterface.pushNodesByBackendIdsToFrontend(backendNodeIds);
94
+ const nodeIdsByBackendIdMap = new Map();
95
+ for (let i = 0; i < backendNodeIds.length; i++) {
96
+ nodeIdsByBackendIdMap.set(backendNodeIds[i], nodes[i]);
99
97
  }
100
- if (renderBlockRootCause) {
101
- rootCausesForShift.renderBlockingRequests = renderBlockRootCause;
98
+ // Maps from PrePaint events to LayoutShifts that occured in each one.
99
+ const shiftsByPrePaint = getShiftsByPrePaintEvents(layoutShifts, prePaintEvents);
100
+ for (const layoutInvalidation of eventsForLayoutInvalidation) {
101
+ // Get the first PrePaint event that happened after the current LayoutInvalidation event.
102
+ const nextPrePaintIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(prePaintEvents, prePaint => prePaint.ts > layoutInvalidation.ts);
103
+ if (nextPrePaintIndex === null) {
104
+ // No PrePaint event registered after this LayoutInvalidation. Continue.
105
+ continue;
106
+ }
107
+ const nextPrePaint = prePaintEvents[nextPrePaintIndex];
108
+ const subsequentShifts = shiftsByPrePaint.get(nextPrePaint);
109
+ if (!subsequentShifts) {
110
+ // The PrePaint after the current LayoutInvalidation doesn't contain shifts.
111
+ continue;
112
+ }
113
+ const fontChangeRootCause = this.getFontChangeRootCause(layoutInvalidation, nextPrePaint, modelData);
114
+ const renderBlockRootCause = this.getRenderBlockRootCause(layoutInvalidation, nextPrePaint, modelData);
115
+ const layoutInvalidationNodeId = nodeIdsByBackendIdMap.get(layoutInvalidation.args.data.nodeId);
116
+ const layoutInvalidationNode = layoutInvalidationNodeId !== undefined ?
117
+ await this.#protocolInterface.getNode(layoutInvalidationNodeId) :
118
+ null;
119
+ let unsizedMediaRootCause = null;
120
+ let iframeRootCause = null;
121
+ if (layoutInvalidationNode && layoutInvalidation.args.data.reason) {
122
+ unsizedMediaRootCause =
123
+ await this.getUnsizedMediaRootCause(layoutInvalidation.args.data.reason, layoutInvalidationNode);
124
+ iframeRootCause = this.getIframeRootCause(layoutInvalidation.args.data.reason, layoutInvalidationNode);
125
+ }
126
+ if (!unsizedMediaRootCause && !iframeRootCause && !fontChangeRootCause && !renderBlockRootCause) {
127
+ continue;
128
+ }
129
+ // Add found potential root causes to all the shifts in this PrePaint and populate the cache.
130
+ for (const shift of subsequentShifts) {
131
+ const rootCausesForShift = Platform.MapUtilities.getWithDefault(this.#rootCauseCacheMap, shift, () => {
132
+ return {
133
+ unsizedMedia: [],
134
+ iframes: [],
135
+ fontChanges: [],
136
+ renderBlockingRequests: [],
137
+ scriptStackTrace: [],
138
+ };
139
+ });
140
+ if (unsizedMediaRootCause &&
141
+ !rootCausesForShift.unsizedMedia.some(media => media.node.nodeId === unsizedMediaRootCause?.node.nodeId) &&
142
+ shift.args.frame === layoutInvalidation.args.data.frame) {
143
+ rootCausesForShift.unsizedMedia.push(unsizedMediaRootCause);
144
+ }
145
+ if (iframeRootCause &&
146
+ !rootCausesForShift.iframes.some(injectedIframe => injectedIframe.iframe.nodeId === iframeRootCause?.iframe.nodeId)) {
147
+ rootCausesForShift.iframes.push(iframeRootCause);
148
+ }
149
+ if (fontChangeRootCause) {
150
+ // Unlike other root causes, we calculate fonts causing a shift only once,
151
+ // which means we assign the built array instead of appending new objects
152
+ // to it.
153
+ rootCausesForShift.fontChanges = fontChangeRootCause;
154
+ }
155
+ if (renderBlockRootCause) {
156
+ rootCausesForShift.renderBlockingRequests = renderBlockRootCause;
157
+ }
158
+ }
102
159
  }
103
- }
104
160
  }
105
- }
106
- linkShiftsToLayoutEvents(layoutShifts, modelData) {
107
- const { prePaintEvents } = modelData.LayoutShifts;
108
- const shiftsByPrePaint = getShiftsByPrePaintEvents(layoutShifts, prePaintEvents);
109
- const eventTriggersLayout = ({ name }) => {
110
- const knownName = name;
111
- return knownName === Types.TraceEvents.KnownEventName.Layout;
112
- };
113
- const layoutEvents = modelData.Renderer.allTraceEntries.filter(eventTriggersLayout);
114
- for (const layout of layoutEvents) {
115
- const nextPrePaintIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(prePaintEvents, (prePaint) => prePaint.ts > layout.ts + (layout.dur || 0));
116
- if (nextPrePaintIndex === null) {
117
- continue;
118
- }
119
- const nextPrePaint = prePaintEvents[nextPrePaintIndex];
120
- const subsequentShifts = shiftsByPrePaint.get(nextPrePaint);
121
- if (!subsequentShifts) {
122
- continue;
123
- }
124
- const layoutNode = modelData.Renderer.entryToNode.get(layout);
125
- const initiator = layoutNode ? modelData.Initiators.eventToInitiator.get(layoutNode.entry) : null;
126
- const stackTrace = initiator?.args?.data?.stackTrace;
127
- if (!stackTrace) {
128
- continue;
129
- }
130
- for (const shift of subsequentShifts) {
131
- const rootCausesForShift = Platform.MapUtilities.getWithDefault(this.#rootCauseCacheMap, shift, () => {
132
- return {
133
- unsizedMedia: [],
134
- iframes: [],
135
- fontChanges: [],
136
- renderBlockingRequests: [],
137
- scriptStackTrace: []
138
- };
139
- });
140
- if (rootCausesForShift.scriptStackTrace.length === 0) {
141
- rootCausesForShift.scriptStackTrace = stackTrace;
161
+ /**
162
+ * For every shift looks up the initiator of its corresponding Layout event. This initiator
163
+ * is assigned by the RendererHandler and contains the stack trace of the point in a script
164
+ * that caused a style recalculation or a relayout. This stack trace is added to the shift's
165
+ * potential root causes.
166
+ * Note that a Layout cannot always be linked to a script, in that case, we cannot add a
167
+ * "script causing reflow" as a potential root cause to the corresponding shift.
168
+ */
169
+ linkShiftsToLayoutEvents(layoutShifts, modelData) {
170
+ const { prePaintEvents } = modelData.LayoutShifts;
171
+ // Maps from PrePaint events to LayoutShifts that occured in each one.
172
+ const shiftsByPrePaint = getShiftsByPrePaintEvents(layoutShifts, prePaintEvents);
173
+ const eventTriggersLayout = ({ name }) => {
174
+ const knownName = name;
175
+ return knownName === "Layout" /* Types.TraceEvents.KnownEventName.Layout */;
176
+ };
177
+ const layoutEvents = modelData.Renderer.allTraceEntries.filter(eventTriggersLayout);
178
+ for (const layout of layoutEvents) {
179
+ // Get the first PrePaint event that happened after the current layout event.
180
+ const nextPrePaintIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(prePaintEvents, prePaint => prePaint.ts > layout.ts + (layout.dur || 0));
181
+ if (nextPrePaintIndex === null) {
182
+ // No PrePaint event registered after this LayoutInvalidation. Continue.
183
+ continue;
184
+ }
185
+ const nextPrePaint = prePaintEvents[nextPrePaintIndex];
186
+ const subsequentShifts = shiftsByPrePaint.get(nextPrePaint);
187
+ if (!subsequentShifts) {
188
+ // The PrePaint after the current LayoutInvalidation doesn't contain shifts.
189
+ continue;
190
+ }
191
+ const layoutNode = modelData.Renderer.entryToNode.get(layout);
192
+ const initiator = layoutNode ? modelData.Initiators.eventToInitiator.get(layoutNode.entry) : null;
193
+ const stackTrace = initiator?.args?.data?.stackTrace;
194
+ if (!stackTrace) {
195
+ continue;
196
+ }
197
+ // Add found potential root causes to all the shifts in this PrePaint and populate the cache.
198
+ for (const shift of subsequentShifts) {
199
+ const rootCausesForShift = Platform.MapUtilities.getWithDefault(this.#rootCauseCacheMap, shift, () => {
200
+ return {
201
+ unsizedMedia: [],
202
+ iframes: [],
203
+ fontChanges: [],
204
+ renderBlockingRequests: [],
205
+ scriptStackTrace: [],
206
+ };
207
+ });
208
+ if (rootCausesForShift.scriptStackTrace.length === 0) {
209
+ rootCausesForShift.scriptStackTrace = stackTrace;
210
+ }
211
+ }
142
212
  }
143
- }
144
- }
145
- }
146
- async getUnsizedMediaRootCause(reason, layoutInvalidationNode) {
147
- if (reason !== Types.TraceEvents.LayoutInvalidationReason.SIZE_CHANGED) {
148
- return null;
149
- }
150
- const computedStylesList = await this.#protocolInterface.getComputedStyleForNode(layoutInvalidationNode.nodeId);
151
- const computedStyles = new Map(computedStylesList.map((item) => [item.name, item.value]));
152
- if (computedStyles && !await nodeIsUnfixedMedia(layoutInvalidationNode, computedStyles)) {
153
- return null;
154
213
  }
155
- const authoredDimensions = await this.getNodeAuthoredDimensions(layoutInvalidationNode);
156
- if (dimensionsAreExplicit(authoredDimensions)) {
157
- return null;
158
- }
159
- const computedDimensions = computedStyles ? getNodeComputedDimensions(computedStyles) : {};
160
- return { node: layoutInvalidationNode, authoredDimensions, computedDimensions };
161
- }
162
- getIframeRootCause(reason, layoutInvalidationDOMNode) {
163
- if (layoutInvalidationDOMNode.nodeName !== "IFRAME" && reason !== Types.TraceEvents.LayoutInvalidationReason.STYLE_CHANGED && reason !== Types.TraceEvents.LayoutInvalidationReason.ADDED_TO_LAYOUT) {
164
- return null;
165
- }
166
- const iframe = firstIframeInDOMTree(layoutInvalidationDOMNode);
167
- if (!iframe) {
168
- return null;
169
- }
170
- return { iframe };
171
- }
172
- requestsInInvalidationWindow(layoutInvalidation, modelData) {
173
- const requestsSortedByEndTime = modelData.NetworkRequests.byTime.sort((req1, req2) => {
174
- const req1EndTime = req1.ts + req1.dur;
175
- const req2EndTime = req2.ts + req2.dur;
176
- return req1EndTime - req2EndTime;
177
- });
178
- const lastRequestIndex = Platform.ArrayUtilities.nearestIndexFromEnd(requestsSortedByEndTime, (request) => request.ts + request.dur < layoutInvalidation.ts);
179
- if (lastRequestIndex === null) {
180
- return [];
181
- }
182
- const MAX_DELTA_FOR_FONT_REQUEST = Helpers.Timing.secondsToMicroseconds(Types.Timing.Seconds(0.5));
183
- const requestsInInvalidationWindow = [];
184
- for (let i = lastRequestIndex; i > -1; i--) {
185
- const previousRequest = requestsSortedByEndTime[i];
186
- const previousRequestEndTime = previousRequest.ts + previousRequest.dur;
187
- if (layoutInvalidation.ts - previousRequestEndTime < MAX_DELTA_FOR_FONT_REQUEST) {
188
- const requestInInvalidationWindow = { request: previousRequest };
189
- const initiator = this.#protocolInterface.getInitiatorForRequest(previousRequest.args.data.url);
190
- requestInInvalidationWindow.initiator = initiator || void 0;
191
- requestsInInvalidationWindow.push(requestInInvalidationWindow);
192
- } else {
193
- break;
194
- }
195
- }
196
- return requestsInInvalidationWindow;
197
- }
198
- getFontChangeRootCause(layoutInvalidation, nextPrePaint, modelData) {
199
- if (layoutInvalidation.args.data.reason !== Types.TraceEvents.LayoutInvalidationReason.FONTS_CHANGED) {
200
- return null;
214
+ /**
215
+ * Given a LayoutInvalidation trace event, determines if it was dispatched
216
+ * because a media element without dimensions was resized.
217
+ */
218
+ async getUnsizedMediaRootCause(reason, layoutInvalidationNode) {
219
+ // Filter events to resizes only.
220
+ if (reason !== "Size changed" /* Types.TraceEvents.LayoutInvalidationReason.SIZE_CHANGED */) {
221
+ return null;
222
+ }
223
+ const computedStylesList = await this.#protocolInterface.getComputedStyleForNode(layoutInvalidationNode.nodeId);
224
+ const computedStyles = new Map(computedStylesList.map(item => [item.name, item.value]));
225
+ if (computedStyles && !(await nodeIsUnfixedMedia(layoutInvalidationNode, computedStyles))) {
226
+ return null;
227
+ }
228
+ const authoredDimensions = await this.getNodeAuthoredDimensions(layoutInvalidationNode);
229
+ if (dimensionsAreExplicit(authoredDimensions)) {
230
+ return null;
231
+ }
232
+ const computedDimensions = computedStyles ? getNodeComputedDimensions(computedStyles) : {};
233
+ return { node: layoutInvalidationNode, authoredDimensions, computedDimensions };
201
234
  }
202
- const fontRequestsForPrepaint = fontRequestsByPrePaint.get(nextPrePaint);
203
- if (fontRequestsForPrepaint !== void 0) {
204
- return fontRequestsForPrepaint;
235
+ /**
236
+ * Given a LayoutInvalidation trace event, determines if it was dispatched
237
+ * because a node, which is an ancestor to an iframe, was injected.
238
+ */
239
+ getIframeRootCause(reason, layoutInvalidationDOMNode) {
240
+ if (layoutInvalidationDOMNode.nodeName !== 'IFRAME' &&
241
+ reason !== "Style changed" /* Types.TraceEvents.LayoutInvalidationReason.STYLE_CHANGED */ &&
242
+ reason !== "Added to layout" /* Types.TraceEvents.LayoutInvalidationReason.ADDED_TO_LAYOUT */) {
243
+ return null;
244
+ }
245
+ const iframe = firstIframeInDOMTree(layoutInvalidationDOMNode);
246
+ if (!iframe) {
247
+ return null;
248
+ }
249
+ return { iframe };
205
250
  }
206
- const fontRequestsInThisPrepaint = this.getFontRequestsInInvalidationWindow(this.requestsInInvalidationWindow(layoutInvalidation, modelData));
207
- fontRequestsByPrePaint.set(nextPrePaint, fontRequestsInThisPrepaint);
208
- return fontRequestsInThisPrepaint;
209
- }
210
- getFontRequestsInInvalidationWindow(requestsInInvalidationWindow) {
211
- const fontRequests = [];
212
- for (let i = 0; i < requestsInInvalidationWindow.length; i++) {
213
- const fontRequest = requestsInInvalidationWindow[i];
214
- if (!fontRequest.request.args.data.mimeType.startsWith("font")) {
215
- continue;
216
- }
217
- const fontFace = this.#protocolInterface.fontFaceForSource(fontRequest.request.args.data.url);
218
- if (!fontFace || fontFace.fontDisplay === "optional") {
219
- continue;
220
- }
221
- fontRequest.fontFace = fontFace;
222
- fontRequests.push(fontRequest);
251
+ /**
252
+ * Given a layout invalidation event and a sorted array, returns the subset of requests that arrived within a
253
+ * 500ms window before the layout invalidation.
254
+ */
255
+ requestsInInvalidationWindow(layoutInvalidation, modelData) {
256
+ const requestsSortedByEndTime = modelData.NetworkRequests.byTime.sort((req1, req2) => {
257
+ const req1EndTime = req1.ts + req1.dur;
258
+ const req2EndTime = req2.ts + req2.dur;
259
+ return req1EndTime - req2EndTime;
260
+ });
261
+ const lastRequestIndex = Platform.ArrayUtilities.nearestIndexFromEnd(requestsSortedByEndTime, request => request.ts + request.dur < layoutInvalidation.ts);
262
+ if (lastRequestIndex === null) {
263
+ return [];
264
+ }
265
+ const MAX_DELTA_FOR_FONT_REQUEST = Helpers.Timing.secondsToMicroseconds(Types.Timing.Seconds(0.5));
266
+ const requestsInInvalidationWindow = [];
267
+ // Get all requests finished within the valid window.
268
+ for (let i = lastRequestIndex; i > -1; i--) {
269
+ const previousRequest = requestsSortedByEndTime[i];
270
+ const previousRequestEndTime = previousRequest.ts + previousRequest.dur;
271
+ if (layoutInvalidation.ts - previousRequestEndTime < MAX_DELTA_FOR_FONT_REQUEST) {
272
+ const requestInInvalidationWindow = { request: previousRequest };
273
+ const initiator = this.#protocolInterface.getInitiatorForRequest(previousRequest.args.data.url);
274
+ requestInInvalidationWindow.initiator = initiator || undefined;
275
+ requestsInInvalidationWindow.push(requestInInvalidationWindow);
276
+ }
277
+ else {
278
+ // No more requests fit in the time window.
279
+ break;
280
+ }
281
+ }
282
+ return requestsInInvalidationWindow;
223
283
  }
224
- return fontRequests;
225
- }
226
- getRenderBlockRootCause(layoutInvalidation, nextPrePaint, modelData) {
227
- const renderBlocksInPrepaint = renderBlocksByPrePaint.get(nextPrePaint);
228
- if (renderBlocksInPrepaint !== void 0) {
229
- return renderBlocksInPrepaint;
284
+ /**
285
+ * Given a LayoutInvalidation trace event, determines if it was dispatched
286
+ * because fonts were changed and if so returns the information of all network
287
+ * request with which the fonts were possibly fetched, if any. The computed
288
+ * network requests are cached for the corresponding prepaint event, meaning
289
+ * that other LayoutInvalidation events that correspond to the same prepaint
290
+ * are not processed and the cached network requests for the prepaint is
291
+ * returned instead.
292
+ */
293
+ getFontChangeRootCause(layoutInvalidation, nextPrePaint, modelData) {
294
+ if (layoutInvalidation.args.data.reason !== "Fonts changed" /* Types.TraceEvents.LayoutInvalidationReason.FONTS_CHANGED */) {
295
+ return null;
296
+ }
297
+ // Prevent computing the result of this function multiple times per PrePaint event.
298
+ const fontRequestsForPrepaint = fontRequestsByPrePaint.get(nextPrePaint);
299
+ if (fontRequestsForPrepaint !== undefined) {
300
+ return fontRequestsForPrepaint;
301
+ }
302
+ const fontRequestsInThisPrepaint = this.getFontRequestsInInvalidationWindow(this.requestsInInvalidationWindow(layoutInvalidation, modelData));
303
+ fontRequestsByPrePaint.set(nextPrePaint, fontRequestsInThisPrepaint);
304
+ return fontRequestsInThisPrepaint;
230
305
  }
231
- const renderBlocksInThisPrepaint = getRenderBlockRequestsInInvalidationWindow(this.requestsInInvalidationWindow(layoutInvalidation, modelData));
232
- renderBlocksByPrePaint.set(nextPrePaint, renderBlocksInThisPrepaint);
233
- return renderBlocksInThisPrepaint;
234
- }
235
- async nodeMatchedStylesPropertyGetter(node) {
236
- const response = await this.#protocolInterface.getMatchedStylesForNode(node.nodeId);
237
- function cssPropertyValueGetter(cssProperty) {
238
- let prop = response.inlineStyle?.cssProperties.find((prop2) => prop2.name === cssProperty);
239
- if (prop) {
240
- return prop.value;
241
- }
242
- for (const { rule } of response.matchedCSSRules || []) {
243
- const prop2 = rule.style.cssProperties.find((prop3) => prop3.name === cssProperty);
244
- if (prop2) {
245
- return prop2.value;
306
+ /**
307
+ * Given the requests that arrived within a 500ms window before the layout invalidation, returns the font
308
+ * requests of them.
309
+ */
310
+ getFontRequestsInInvalidationWindow(requestsInInvalidationWindow) {
311
+ const fontRequests = [];
312
+ // Get all requests finished within the valid window.
313
+ for (let i = 0; i < requestsInInvalidationWindow.length; i++) {
314
+ const fontRequest = requestsInInvalidationWindow[i];
315
+ if (!fontRequest.request.args.data.mimeType.startsWith('font')) {
316
+ continue;
317
+ }
318
+ const fontFace = this.#protocolInterface.fontFaceForSource(fontRequest.request.args.data.url);
319
+ if (!fontFace || fontFace.fontDisplay === 'optional') {
320
+ // Setting font-display to optional is part of what the developer
321
+ // can do to avoid layout shifts due to FOIT/FOUT, as such we cannot
322
+ // suggest any actionable insight here.
323
+ continue;
324
+ }
325
+ fontRequest.fontFace = fontFace;
326
+ fontRequests.push(fontRequest);
246
327
  }
247
- }
248
- prop = response.attributesStyle?.cssProperties.find((prop2) => prop2.name === cssProperty);
249
- if (prop) {
250
- return prop.value;
251
- }
252
- return null;
328
+ return fontRequests;
253
329
  }
254
- return cssPropertyValueGetter;
255
- }
256
- async getNodeAuthoredDimensions(node) {
257
- const authoredDimensions = {};
258
- const cssMatchedRulesGetter = await this.nodeMatchedStylesPropertyGetter(node);
259
- if (!cssMatchedRulesGetter) {
260
- return authoredDimensions;
330
+ /**
331
+ * Given a LayoutInvalidation trace event, determines if it arrived within a 500ms window before the layout
332
+ * invalidation and if so returns the information of all network request, if any. The computed network
333
+ * requests are cached for the corresponding prepaint event, meaning that other LayoutInvalidation events
334
+ * that correspond to the same prepaint are not processed and the cached network requests for the prepaint is
335
+ * returned instead.
336
+ */
337
+ getRenderBlockRootCause(layoutInvalidation, nextPrePaint, modelData) {
338
+ // Prevent computing the result of this function multiple times per PrePaint event.
339
+ const renderBlocksInPrepaint = renderBlocksByPrePaint.get(nextPrePaint);
340
+ if (renderBlocksInPrepaint !== undefined) {
341
+ return renderBlocksInPrepaint;
342
+ }
343
+ const renderBlocksInThisPrepaint = getRenderBlockRequestsInInvalidationWindow(this.requestsInInvalidationWindow(layoutInvalidation, modelData));
344
+ renderBlocksByPrePaint.set(nextPrePaint, renderBlocksInThisPrepaint);
345
+ return renderBlocksInThisPrepaint;
261
346
  }
262
- const attributesFlat = node.attributes || [];
263
- const attributes = [];
264
- for (let i = 0; i < attributesFlat.length; i += 2) {
265
- attributes.push({ name: attributesFlat[i], value: attributesFlat[i + 1] });
347
+ /**
348
+ * Returns a function that retrieves the active value of a given
349
+ * CSS property within the matched styles of the param node.
350
+ * The first occurence within the matched styles is returned and the
351
+ * value is looked up in the following order, which follows CSS
352
+ * specificity:
353
+ * 1. Inline styles.
354
+ * 2. CSS rules matching this node, from all applicable stylesheets.
355
+ * 3. Attribute defined styles.
356
+ */
357
+ async nodeMatchedStylesPropertyGetter(node) {
358
+ const response = await this.#protocolInterface.getMatchedStylesForNode(node.nodeId);
359
+ function cssPropertyValueGetter(cssProperty) {
360
+ let prop = response.inlineStyle?.cssProperties.find(prop => prop.name === cssProperty);
361
+ if (prop) {
362
+ return prop.value;
363
+ }
364
+ for (const { rule } of response.matchedCSSRules || []) {
365
+ const prop = rule.style.cssProperties.find(prop => prop.name === cssProperty);
366
+ if (prop) {
367
+ return prop.value;
368
+ }
369
+ }
370
+ prop = response.attributesStyle?.cssProperties.find(prop => prop.name === cssProperty);
371
+ if (prop) {
372
+ return prop.value;
373
+ }
374
+ return null;
375
+ }
376
+ return cssPropertyValueGetter;
266
377
  }
267
- const htmlHeight = attributes.find((attr) => attr.name === "height" && htmlAttributeIsExplicit(attr));
268
- const htmlWidth = attributes.find((attr) => attr.name === "width" && htmlAttributeIsExplicit(attr));
269
- const cssExplicitAspectRatio = cssMatchedRulesGetter("aspect-ratio") || void 0;
270
- if (htmlHeight && htmlWidth && cssExplicitAspectRatio) {
271
- return { height: htmlHeight.value, width: htmlWidth.value, aspectRatio: cssExplicitAspectRatio };
378
+ /**
379
+ * Returns the CSS dimensions set to the node from its matched styles.
380
+ */
381
+ async getNodeAuthoredDimensions(node) {
382
+ const authoredDimensions = {};
383
+ const cssMatchedRulesGetter = await this.nodeMatchedStylesPropertyGetter(node);
384
+ if (!cssMatchedRulesGetter) {
385
+ return authoredDimensions;
386
+ }
387
+ const attributesFlat = node.attributes || [];
388
+ const attributes = [];
389
+ for (let i = 0; i < attributesFlat.length; i += 2) {
390
+ attributes.push({ name: attributesFlat[i], value: attributesFlat[i + 1] });
391
+ }
392
+ const htmlHeight = attributes.find(attr => attr.name === 'height' && htmlAttributeIsExplicit(attr));
393
+ const htmlWidth = attributes.find(attr => attr.name === 'width' && htmlAttributeIsExplicit(attr));
394
+ const cssExplicitAspectRatio = cssMatchedRulesGetter('aspect-ratio') || undefined;
395
+ if (htmlHeight && htmlWidth && cssExplicitAspectRatio) {
396
+ return { height: htmlHeight.value, width: htmlWidth.value, aspectRatio: cssExplicitAspectRatio };
397
+ }
398
+ const cssHeight = cssMatchedRulesGetter('height') || undefined;
399
+ const cssWidth = cssMatchedRulesGetter('width') || undefined;
400
+ return { height: cssHeight, width: cssWidth, aspectRatio: cssExplicitAspectRatio };
272
401
  }
273
- const cssHeight = cssMatchedRulesGetter("height") || void 0;
274
- const cssWidth = cssMatchedRulesGetter("width") || void 0;
275
- return { height: cssHeight, width: cssWidth, aspectRatio: cssExplicitAspectRatio };
276
- }
277
402
  }
403
+ /**
404
+ * Given the requests that arrived within a 500ms window before the layout invalidation, returns the render
405
+ * block requests of them.
406
+ */
278
407
  function getRenderBlockRequestsInInvalidationWindow(requestsInInvalidationWindow) {
279
- const renderBlockingRequests = [];
280
- for (let i = 0; i < requestsInInvalidationWindow.length; i++) {
281
- const mainFrameId = requestsInInvalidationWindow[i].request.args.data.frame;
282
- if (!networkRequestIsRenderBlockingInFrame(requestsInInvalidationWindow[i].request, mainFrameId)) {
283
- continue;
408
+ const renderBlockingRequests = [];
409
+ // Get all requests finished within the valid window.
410
+ for (let i = 0; i < requestsInInvalidationWindow.length; i++) {
411
+ const mainFrameId = requestsInInvalidationWindow[i].request.args.data.frame;
412
+ if (!networkRequestIsRenderBlockingInFrame(requestsInInvalidationWindow[i].request, mainFrameId)) {
413
+ continue;
414
+ }
415
+ renderBlockingRequests.push(requestsInInvalidationWindow[i]);
284
416
  }
285
- renderBlockingRequests.push(requestsInInvalidationWindow[i]);
286
- }
287
- return renderBlockingRequests;
417
+ return renderBlockingRequests;
288
418
  }
289
419
  function firstIframeInDOMTree(root) {
290
- if (root.nodeName === "IFRAME") {
291
- return root;
292
- }
293
- const children = root.children;
294
- if (!children) {
295
- return null;
296
- }
297
- for (const child of children) {
298
- const iFrameInChild = firstIframeInDOMTree(child);
299
- if (iFrameInChild) {
300
- return iFrameInChild;
420
+ if (root.nodeName === 'IFRAME') {
421
+ return root;
422
+ }
423
+ const children = root.children;
424
+ if (!children) {
425
+ return null;
426
+ }
427
+ for (const child of children) {
428
+ const iFrameInChild = firstIframeInDOMTree(child);
429
+ if (iFrameInChild) {
430
+ return iFrameInChild;
431
+ }
301
432
  }
302
- }
303
- return null;
433
+ return null;
304
434
  }
305
435
  function cssPropertyIsExplicitlySet(propertyValue) {
306
- return !["auto", "initial", "unset", "inherit"].includes(propertyValue);
436
+ return !['auto', 'initial', 'unset', 'inherit'].includes(propertyValue);
307
437
  }
308
438
  function htmlAttributeIsExplicit(attr) {
309
- return parseInt(attr.value, 10) >= 0;
439
+ return parseInt(attr.value, 10) >= 0;
310
440
  }
311
441
  function computedStyleHasBackroundImage(computedStyle) {
312
- const CSS_URL_REGEX = /^url\("([^"]+)"\)$/;
313
- const backgroundImage = computedStyle.get("background-image");
314
- if (!backgroundImage) {
315
- return false;
316
- }
317
- return CSS_URL_REGEX.test(backgroundImage);
442
+ const CSS_URL_REGEX = /^url\("([^"]+)"\)$/;
443
+ const backgroundImage = computedStyle.get('background-image');
444
+ if (!backgroundImage) {
445
+ return false;
446
+ }
447
+ return CSS_URL_REGEX.test(backgroundImage);
318
448
  }
319
449
  function computedStyleHasFixedPosition(computedStyle) {
320
- const position = computedStyle.get("position");
321
- if (!position) {
322
- return false;
323
- }
324
- return position === "fixed" || position === "absolute";
450
+ const position = computedStyle.get('position');
451
+ if (!position) {
452
+ return false;
453
+ }
454
+ return position === 'fixed' || position === 'absolute';
325
455
  }
326
456
  function getNodeComputedDimensions(computedStyle) {
327
- const computedDimensions = {};
328
- computedDimensions.height = computedStyle.get("height");
329
- computedDimensions.width = computedStyle.get("width");
330
- computedDimensions.aspectRatio = computedStyle.get("aspect-ratio");
331
- return computedDimensions;
457
+ const computedDimensions = {};
458
+ computedDimensions.height = computedStyle.get('height');
459
+ computedDimensions.width = computedStyle.get('width');
460
+ computedDimensions.aspectRatio = computedStyle.get('aspect-ratio');
461
+ return computedDimensions;
332
462
  }
463
+ /**
464
+ * Determines if a node is a media element and is not fixed positioned
465
+ * (i.e. "position: fixed;" or "position: absolute;")
466
+ */
333
467
  async function nodeIsUnfixedMedia(node, computedStyle) {
334
- const localName = node.localName;
335
- const isBackgroundImage = computedStyleHasBackroundImage(computedStyle);
336
- if (localName !== "img" && localName !== "video" && !isBackgroundImage) {
337
- return false;
338
- }
339
- const isFixed = computedStyleHasFixedPosition(computedStyle);
340
- return !isFixed;
468
+ const localName = node.localName;
469
+ const isBackgroundImage = computedStyleHasBackroundImage(computedStyle);
470
+ if (localName !== 'img' && localName !== 'video' && !isBackgroundImage) {
471
+ // Not a media element.
472
+ return false;
473
+ }
474
+ const isFixed = computedStyleHasFixedPosition(computedStyle);
475
+ return !isFixed;
341
476
  }
477
+ /**
478
+ * Determines if a CSS dimensions object explicitly defines both width and height
479
+ * (i.e. not set to auto, inherit, etc.)
480
+ */
342
481
  function dimensionsAreExplicit(dimensions) {
343
- const { height, width, aspectRatio } = dimensions;
344
- const explicitHeight = Boolean(height && cssPropertyIsExplicitlySet(height));
345
- const explicitWidth = Boolean(width && cssPropertyIsExplicitlySet(width));
346
- const explicitAspectRatio = Boolean(aspectRatio && cssPropertyIsExplicitlySet(aspectRatio));
347
- const explicitWithAR = (explicitHeight || explicitWidth) && explicitAspectRatio;
348
- return explicitHeight && explicitWidth || explicitWithAR;
482
+ const { height, width, aspectRatio } = dimensions;
483
+ const explicitHeight = Boolean(height && cssPropertyIsExplicitlySet(height));
484
+ const explicitWidth = Boolean(width && cssPropertyIsExplicitlySet(width));
485
+ const explicitAspectRatio = Boolean(aspectRatio && cssPropertyIsExplicitlySet(aspectRatio));
486
+ const explicitWithAR = (explicitHeight || explicitWidth) && explicitAspectRatio;
487
+ return (explicitHeight && explicitWidth) || explicitWithAR;
349
488
  }
489
+ /**
490
+ * Given an array of layout shift and PrePaint events, returns a mapping from
491
+ * PrePaint events to layout shifts dispatched within it.
492
+ */
350
493
  function getShiftsByPrePaintEvents(layoutShifts, prePaintEvents) {
351
- const shiftsByPrePaint = /* @__PURE__ */ new Map();
352
- for (const prePaintEvent of prePaintEvents) {
353
- const firstShiftIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(layoutShifts, (shift) => shift.ts >= prePaintEvent.ts);
354
- if (firstShiftIndex === null) {
355
- continue;
356
- }
357
- for (let i = firstShiftIndex; i < layoutShifts.length; i++) {
358
- const shift = layoutShifts[i];
359
- if (shift.ts >= prePaintEvent.ts && shift.ts <= prePaintEvent.ts + prePaintEvent.dur) {
360
- const shiftsInPrePaint = Platform.MapUtilities.getWithDefault(shiftsByPrePaint, prePaintEvent, () => []);
361
- shiftsInPrePaint.push(shift);
362
- }
363
- if (shift.ts > prePaintEvent.ts + prePaintEvent.dur) {
364
- break;
365
- }
494
+ // Maps from PrePaint events to LayoutShifts that occured in each one.
495
+ const shiftsByPrePaint = new Map();
496
+ // Associate all shifts to their corresponding PrePaint.
497
+ for (const prePaintEvent of prePaintEvents) {
498
+ const firstShiftIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(layoutShifts, shift => shift.ts >= prePaintEvent.ts);
499
+ if (firstShiftIndex === null) {
500
+ // No layout shifts registered after this PrePaint start. Continue.
501
+ continue;
502
+ }
503
+ for (let i = firstShiftIndex; i < layoutShifts.length; i++) {
504
+ const shift = layoutShifts[i];
505
+ if (shift.ts >= prePaintEvent.ts && shift.ts <= prePaintEvent.ts + prePaintEvent.dur) {
506
+ const shiftsInPrePaint = Platform.MapUtilities.getWithDefault(shiftsByPrePaint, prePaintEvent, () => []);
507
+ shiftsInPrePaint.push(shift);
508
+ }
509
+ if (shift.ts > prePaintEvent.ts + prePaintEvent.dur) {
510
+ // Reached the end of this PrePaint. Continue to the next one.
511
+ break;
512
+ }
513
+ }
366
514
  }
367
- }
368
- return shiftsByPrePaint;
515
+ return shiftsByPrePaint;
369
516
  }
370
- //# sourceMappingURL=LayoutShift.js.map
517
+ //# sourceMappingURL=LayoutShift.js.map