lighthouse 11.6.0 → 11.7.0

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 (141) hide show
  1. package/cli/test/smokehouse/core-tests.js +2 -2
  2. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +2 -2
  3. package/core/audits/byte-efficiency/byte-efficiency-audit.js +7 -7
  4. package/core/audits/byte-efficiency/render-blocking-resources.d.ts +4 -3
  5. package/core/audits/byte-efficiency/render-blocking-resources.js +6 -6
  6. package/core/audits/dobetterweb/uses-http2.d.ts +3 -3
  7. package/core/audits/dobetterweb/uses-http2.js +4 -4
  8. package/core/audits/installable-manifest.d.ts +2 -0
  9. package/core/audits/installable-manifest.js +2 -0
  10. package/core/audits/metrics/interaction-to-next-paint.js +1 -3
  11. package/core/audits/prioritize-lcp-image.d.ts +1 -1
  12. package/core/audits/work-during-interaction.js +0 -8
  13. package/core/computed/document-urls.js +1 -1
  14. package/core/computed/load-simulator.d.ts +2 -2
  15. package/core/computed/load-simulator.js +3 -2
  16. package/core/computed/main-resource.js +1 -1
  17. package/core/computed/metrics/lantern-first-contentful-paint.d.ts +18 -6
  18. package/core/computed/metrics/lantern-first-contentful-paint.js +4 -4
  19. package/core/computed/metrics/lantern-first-meaningful-paint.d.ts +14 -2
  20. package/core/computed/metrics/lantern-first-meaningful-paint.js +1 -1
  21. package/core/computed/metrics/lantern-interactive.d.ts +8 -6
  22. package/core/computed/metrics/lantern-interactive.js +3 -3
  23. package/core/computed/metrics/lantern-largest-contentful-paint.d.ts +14 -2
  24. package/core/computed/metrics/lantern-largest-contentful-paint.js +1 -1
  25. package/core/computed/metrics/lantern-max-potential-fid.d.ts +8 -2
  26. package/core/computed/metrics/lantern-max-potential-fid.js +3 -3
  27. package/core/computed/metrics/lantern-metric.d.ts +4 -63
  28. package/core/computed/metrics/lantern-metric.js +6 -124
  29. package/core/computed/metrics/lantern-speed-index.d.ts +8 -2
  30. package/core/computed/metrics/lantern-speed-index.js +3 -3
  31. package/core/computed/metrics/lantern-total-blocking-time.d.ts +8 -2
  32. package/core/computed/metrics/lantern-total-blocking-time.js +3 -3
  33. package/core/computed/metrics/responsiveness.d.ts +5 -13
  34. package/core/computed/metrics/responsiveness.js +10 -14
  35. package/core/computed/network-analysis.js +1 -1
  36. package/core/computed/page-dependency-graph.d.ts +4 -57
  37. package/core/computed/page-dependency-graph.js +4 -451
  38. package/core/config/config-helpers.js +1 -1
  39. package/core/gather/driver/execution-context.d.ts +2 -1
  40. package/core/gather/driver/execution-context.js +9 -9
  41. package/core/gather/driver/wait-for-condition.js +4 -2
  42. package/core/gather/gatherers/accessibility.js +2 -1
  43. package/core/gather/gatherers/css-usage.d.ts +1 -9
  44. package/core/gather/gatherers/css-usage.js +23 -25
  45. package/core/gather/gatherers/devtools-log.d.ts +4 -0
  46. package/core/gather/gatherers/devtools-log.js +7 -0
  47. package/core/gather/gatherers/full-page-screenshot.d.ts +4 -0
  48. package/core/gather/gatherers/full-page-screenshot.js +31 -35
  49. package/core/gather/gatherers/root-causes.js +1 -3
  50. package/core/gather/gatherers/trace-elements.js +1 -1
  51. package/core/gather/gatherers/trace.d.ts +1 -0
  52. package/core/gather/gatherers/trace.js +4 -0
  53. package/core/gather/navigation-runner.js +49 -62
  54. package/core/gather/session.js +5 -0
  55. package/core/gather/snapshot-runner.js +25 -25
  56. package/core/gather/timespan-runner.js +24 -25
  57. package/core/lib/asset-saver.js +1 -1
  58. package/core/lib/emulation.js +0 -3
  59. package/core/lib/{dependency-graph → lantern}/base-node.d.ts +12 -8
  60. package/core/lib/{dependency-graph → lantern}/base-node.js +8 -3
  61. package/core/lib/{dependency-graph → lantern}/cpu-node.d.ts +5 -1
  62. package/core/lib/{dependency-graph → lantern}/cpu-node.js +4 -0
  63. package/core/lib/lantern/lantern.d.ts +8 -0
  64. package/core/lib/lantern/lantern.js +31 -0
  65. package/core/lib/lantern/metric.d.ts +71 -0
  66. package/core/lib/lantern/metric.js +143 -0
  67. package/core/lib/{dependency-graph → lantern}/network-node.d.ts +17 -10
  68. package/core/lib/lantern/network-node.js +114 -0
  69. package/core/lib/lantern/page-dependency-graph.d.ts +58 -0
  70. package/core/lib/lantern/page-dependency-graph.js +463 -0
  71. package/core/lib/{dependency-graph → lantern}/simulator/connection-pool.d.ts +14 -14
  72. package/core/lib/{dependency-graph → lantern}/simulator/connection-pool.js +7 -7
  73. package/core/lib/{dependency-graph → lantern}/simulator/dns-cache.d.ts +5 -5
  74. package/core/lib/{dependency-graph → lantern}/simulator/dns-cache.js +3 -3
  75. package/core/lib/{dependency-graph → lantern}/simulator/network-analyzer.d.ts +36 -32
  76. package/core/lib/{dependency-graph → lantern}/simulator/network-analyzer.js +21 -17
  77. package/core/lib/{dependency-graph → lantern}/simulator/simulator.d.ts +18 -15
  78. package/core/lib/{dependency-graph → lantern}/simulator/simulator.js +18 -15
  79. package/core/lib/lantern/types/lantern.d.ts +129 -0
  80. package/core/lib/lantern/types/lantern.js +7 -0
  81. package/core/lib/lantern-trace-saver.d.ts +4 -4
  82. package/core/lib/lantern-trace-saver.js +6 -4
  83. package/core/lib/navigation-error.js +1 -1
  84. package/core/lib/network-recorder.js +2 -2
  85. package/core/lib/network-request.d.ts +6 -0
  86. package/core/lib/network-request.js +12 -0
  87. package/package.json +4 -5
  88. package/shared/localization/locales/ar-XB.json +1 -1
  89. package/shared/localization/locales/ar.json +1 -1
  90. package/shared/localization/locales/bg.json +1 -1
  91. package/shared/localization/locales/ca.json +1 -1
  92. package/shared/localization/locales/cs.json +1 -1
  93. package/shared/localization/locales/da.json +2 -2
  94. package/shared/localization/locales/de.json +1 -1
  95. package/shared/localization/locales/el.json +1 -1
  96. package/shared/localization/locales/en-GB.json +1 -1
  97. package/shared/localization/locales/en-US.json +3 -0
  98. package/shared/localization/locales/en-XA.json +1 -1
  99. package/shared/localization/locales/en-XL.json +3 -0
  100. package/shared/localization/locales/es-419.json +1 -1
  101. package/shared/localization/locales/es.json +1 -1
  102. package/shared/localization/locales/fi.json +1 -1
  103. package/shared/localization/locales/fil.json +1 -1
  104. package/shared/localization/locales/he.json +1 -1
  105. package/shared/localization/locales/hi.json +1 -1
  106. package/shared/localization/locales/hr.json +1 -1
  107. package/shared/localization/locales/hu.json +1 -1
  108. package/shared/localization/locales/id.json +1 -1
  109. package/shared/localization/locales/it.json +1 -1
  110. package/shared/localization/locales/ko.json +1 -1
  111. package/shared/localization/locales/lt.json +1 -1
  112. package/shared/localization/locales/nl.json +1 -1
  113. package/shared/localization/locales/no.json +1 -1
  114. package/shared/localization/locales/pl.json +1 -1
  115. package/shared/localization/locales/pt-PT.json +1 -1
  116. package/shared/localization/locales/pt.json +1 -1
  117. package/shared/localization/locales/ro.json +1 -1
  118. package/shared/localization/locales/sk.json +1 -1
  119. package/shared/localization/locales/sl.json +1 -1
  120. package/shared/localization/locales/sr-Latn.json +1 -1
  121. package/shared/localization/locales/sr.json +1 -1
  122. package/shared/localization/locales/sv.json +1 -1
  123. package/shared/localization/locales/ta.json +1 -1
  124. package/shared/localization/locales/te.json +1 -1
  125. package/shared/localization/locales/th.json +4 -4
  126. package/shared/localization/locales/tr.json +1 -1
  127. package/shared/localization/locales/uk.json +1 -1
  128. package/shared/localization/locales/vi.json +1 -1
  129. package/shared/localization/locales/zh-HK.json +1 -1
  130. package/shared/localization/locales/zh-TW.json +1 -1
  131. package/shared/localization/locales/zh.json +1 -1
  132. package/third-party/chromium-synchronization/installability-errors-test.js +1 -1
  133. package/tsconfig.json +7 -5
  134. package/types/artifacts.d.ts +1 -1
  135. package/types/gatherer.d.ts +8 -35
  136. package/types/lhr/lhr.d.ts +1 -1
  137. package/core/lib/dependency-graph/network-node.js +0 -99
  138. /package/core/lib/{dependency-graph → lantern}/simulator/simulator-timing-map.d.ts +0 -0
  139. /package/core/lib/{dependency-graph → lantern}/simulator/simulator-timing-map.js +0 -0
  140. /package/core/lib/{dependency-graph → lantern}/simulator/tcp-connection.d.ts +0 -0
  141. /package/core/lib/{dependency-graph → lantern}/simulator/tcp-connection.js +0 -0
@@ -0,0 +1,463 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2024 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import * as Lantern from './types/lantern.js';
8
+ import {NetworkRequestTypes} from './lantern.js';
9
+ import {NetworkNode} from './network-node.js';
10
+ import {CPUNode} from './cpu-node.js';
11
+ import {TraceProcessor} from '../tracehouse/trace-processor.js';
12
+ import {NetworkAnalyzer} from './simulator/network-analyzer.js';
13
+
14
+ /** @typedef {import('./base-node.js').Node} Node */
15
+ /** @typedef {Omit<LH.Artifacts['URL'], 'finalDisplayedUrl'>} URLArtifact */
16
+
17
+ /**
18
+ * @typedef {Object} NetworkNodeOutput
19
+ * @property {Array<NetworkNode>} nodes
20
+ * @property {Map<string, NetworkNode>} idToNodeMap
21
+ * @property {Map<string, Array<NetworkNode>>} urlToNodeMap
22
+ * @property {Map<string, NetworkNode|null>} frameIdToNodeMap
23
+ */
24
+
25
+ // Shorter tasks have negligible impact on simulation results.
26
+ const SIGNIFICANT_DUR_THRESHOLD_MS = 10;
27
+
28
+ // TODO: video files tend to be enormous and throw off all graph traversals, move this ignore
29
+ // into estimation logic when we use the dependency graph for other purposes.
30
+ const IGNORED_MIME_TYPES_REGEX = /^video/;
31
+
32
+ class PageDependencyGraph {
33
+ /**
34
+ * @param {Lantern.NetworkRequest} record
35
+ * @return {Array<string>}
36
+ */
37
+ static getNetworkInitiators(record) {
38
+ if (!record.initiator) return [];
39
+ if (record.initiator.url) return [record.initiator.url];
40
+ if (record.initiator.type === 'script') {
41
+ // Script initiators have the stack of callFrames from all functions that led to this request.
42
+ // If async stacks are enabled, then the stack will also have the parent functions that asynchronously
43
+ // led to this request chained in the `parent` property.
44
+ /** @type {Set<string>} */
45
+ const scriptURLs = new Set();
46
+ let stack = record.initiator.stack;
47
+ while (stack) {
48
+ const callFrames = stack.callFrames || [];
49
+ for (const frame of callFrames) {
50
+ if (frame.url) scriptURLs.add(frame.url);
51
+ }
52
+
53
+ stack = stack.parent;
54
+ }
55
+
56
+ return Array.from(scriptURLs);
57
+ }
58
+
59
+ return [];
60
+ }
61
+
62
+ /**
63
+ * @param {Array<Lantern.NetworkRequest>} networkRecords
64
+ * @return {NetworkNodeOutput}
65
+ */
66
+ static getNetworkNodeOutput(networkRecords) {
67
+ /** @type {Array<NetworkNode>} */
68
+ const nodes = [];
69
+ /** @type {Map<string, NetworkNode>} */
70
+ const idToNodeMap = new Map();
71
+ /** @type {Map<string, Array<NetworkNode>>} */
72
+ const urlToNodeMap = new Map();
73
+ /** @type {Map<string, NetworkNode|null>} */
74
+ const frameIdToNodeMap = new Map();
75
+
76
+ networkRecords.forEach(record => {
77
+ if (IGNORED_MIME_TYPES_REGEX.test(record.mimeType)) return;
78
+ if (record.sessionTargetType === 'worker') return;
79
+
80
+ // Network record requestIds can be duplicated for an unknown reason
81
+ // Suffix all subsequent records with `:duplicate` until it's unique
82
+ // NOTE: This should never happen with modern NetworkRequest library, but old fixtures
83
+ // might still have this issue.
84
+ while (idToNodeMap.has(record.requestId)) {
85
+ record.requestId += ':duplicate';
86
+ }
87
+
88
+ const node = new NetworkNode(record);
89
+ nodes.push(node);
90
+
91
+ const urlList = urlToNodeMap.get(record.url) || [];
92
+ urlList.push(node);
93
+
94
+ idToNodeMap.set(record.requestId, node);
95
+ urlToNodeMap.set(record.url, urlList);
96
+
97
+ // If the request was for the root document of an iframe, save an entry in our
98
+ // map so we can link up the task `args.data.frame` dependencies later in graph creation.
99
+ if (record.frameId &&
100
+ record.resourceType === NetworkRequestTypes.Document &&
101
+ record.documentURL === record.url) {
102
+ // If there's ever any ambiguity, permanently set the value to `false` to avoid loops in the graph.
103
+ const value = frameIdToNodeMap.has(record.frameId) ? null : node;
104
+ frameIdToNodeMap.set(record.frameId, value);
105
+ }
106
+ });
107
+
108
+ return {nodes, idToNodeMap, urlToNodeMap, frameIdToNodeMap};
109
+ }
110
+
111
+ /**
112
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
113
+ * @return {Array<CPUNode>}
114
+ */
115
+ static getCPUNodes({mainThreadEvents}) {
116
+ /** @type {Array<CPUNode>} */
117
+ const nodes = [];
118
+ let i = 0;
119
+
120
+ TraceProcessor.assertHasToplevelEvents(mainThreadEvents);
121
+
122
+ while (i < mainThreadEvents.length) {
123
+ const evt = mainThreadEvents[i];
124
+ i++;
125
+
126
+ // Skip all trace events that aren't schedulable tasks with sizable duration
127
+ if (!TraceProcessor.isScheduleableTask(evt) || !evt.dur) {
128
+ continue;
129
+ }
130
+
131
+ // Capture all events that occurred within the task
132
+ /** @type {Array<LH.TraceEvent>} */
133
+ const children = [];
134
+ for (
135
+ const endTime = evt.ts + evt.dur;
136
+ i < mainThreadEvents.length && mainThreadEvents[i].ts < endTime;
137
+ i++
138
+ ) {
139
+ children.push(mainThreadEvents[i]);
140
+ }
141
+
142
+ nodes.push(new CPUNode(evt, children));
143
+ }
144
+
145
+ return nodes;
146
+ }
147
+
148
+ /**
149
+ * @param {NetworkNode} rootNode
150
+ * @param {NetworkNodeOutput} networkNodeOutput
151
+ */
152
+ static linkNetworkNodes(rootNode, networkNodeOutput) {
153
+ networkNodeOutput.nodes.forEach(node => {
154
+ const directInitiatorRequest = node.request.initiatorRequest || rootNode.request;
155
+ const directInitiatorNode =
156
+ networkNodeOutput.idToNodeMap.get(directInitiatorRequest.requestId) || rootNode;
157
+ const canDependOnInitiator =
158
+ !directInitiatorNode.isDependentOn(node) &&
159
+ node.canDependOn(directInitiatorNode);
160
+ const initiators = PageDependencyGraph.getNetworkInitiators(node.request);
161
+ if (initiators.length) {
162
+ initiators.forEach(initiator => {
163
+ const parentCandidates = networkNodeOutput.urlToNodeMap.get(initiator) || [];
164
+ // Only add the edge if the parent is unambiguous with valid timing and isn't circular.
165
+ if (parentCandidates.length === 1 &&
166
+ parentCandidates[0].startTime <= node.startTime &&
167
+ !parentCandidates[0].isDependentOn(node)) {
168
+ node.addDependency(parentCandidates[0]);
169
+ } else if (canDependOnInitiator) {
170
+ directInitiatorNode.addDependent(node);
171
+ }
172
+ });
173
+ } else if (canDependOnInitiator) {
174
+ directInitiatorNode.addDependent(node);
175
+ }
176
+
177
+ // Make sure the nodes are attached to the graph if the initiator information was invalid.
178
+ if (node !== rootNode && node.getDependencies().length === 0 && node.canDependOn(rootNode)) {
179
+ node.addDependency(rootNode);
180
+ }
181
+
182
+ if (!node.request.redirects) return;
183
+
184
+ const redirects = [...node.request.redirects, node.request];
185
+ for (let i = 1; i < redirects.length; i++) {
186
+ const redirectNode = networkNodeOutput.idToNodeMap.get(redirects[i - 1].requestId);
187
+ const actualNode = networkNodeOutput.idToNodeMap.get(redirects[i].requestId);
188
+ if (actualNode && redirectNode) {
189
+ actualNode.addDependency(redirectNode);
190
+ }
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * @param {Node} rootNode
197
+ * @param {NetworkNodeOutput} networkNodeOutput
198
+ * @param {Array<CPUNode>} cpuNodes
199
+ */
200
+ static linkCPUNodes(rootNode, networkNodeOutput, cpuNodes) {
201
+ /** @type {Set<LH.Crdp.Network.ResourceType|undefined>} */
202
+ const linkableResourceTypes = new Set([
203
+ NetworkRequestTypes.XHR, NetworkRequestTypes.Fetch, NetworkRequestTypes.Script,
204
+ ]);
205
+
206
+ /** @param {CPUNode} cpuNode @param {string} reqId */
207
+ function addDependentNetworkRequest(cpuNode, reqId) {
208
+ const networkNode = networkNodeOutput.idToNodeMap.get(reqId);
209
+ if (!networkNode ||
210
+ // Ignore all network nodes that started before this CPU task started
211
+ // A network request that started earlier could not possibly have been started by this task
212
+ networkNode.startTime <= cpuNode.startTime) return;
213
+ const {request} = networkNode;
214
+ const resourceType = request.resourceType ||
215
+ request.redirectDestination?.resourceType;
216
+ if (!linkableResourceTypes.has(resourceType)) {
217
+ // We only link some resources to CPU nodes because we observe LCP simulation
218
+ // regressions when including images, etc.
219
+ return;
220
+ }
221
+ cpuNode.addDependent(networkNode);
222
+ }
223
+
224
+ /**
225
+ * If the node has an associated frameId, then create a dependency on the root document request
226
+ * for the frame. The task obviously couldn't have started before the frame was even downloaded.
227
+ *
228
+ * @param {CPUNode} cpuNode
229
+ * @param {string|undefined} frameId
230
+ */
231
+ function addDependencyOnFrame(cpuNode, frameId) {
232
+ if (!frameId) return;
233
+ const networkNode = networkNodeOutput.frameIdToNodeMap.get(frameId);
234
+ if (!networkNode) return;
235
+ // Ignore all network nodes that started after this CPU task started
236
+ // A network request that started after could not possibly be required this task
237
+ if (networkNode.startTime >= cpuNode.startTime) return;
238
+ cpuNode.addDependency(networkNode);
239
+ }
240
+
241
+ /** @param {CPUNode} cpuNode @param {string} url */
242
+ function addDependencyOnUrl(cpuNode, url) {
243
+ if (!url) return;
244
+ // Allow network requests that end up to 100ms before the task started
245
+ // Some script evaluations can start before the script finishes downloading
246
+ const minimumAllowableTimeSinceNetworkNodeEnd = -100 * 1000;
247
+ const candidates = networkNodeOutput.urlToNodeMap.get(url) || [];
248
+
249
+ let minCandidate = null;
250
+ let minDistance = Infinity;
251
+ // Find the closest request that finished before this CPU task started
252
+ for (const candidate of candidates) {
253
+ // Explicitly ignore all requests that started after this CPU node
254
+ // A network request that started after this task started cannot possibly be a dependency
255
+ if (cpuNode.startTime <= candidate.startTime) return;
256
+
257
+ const distance = cpuNode.startTime - candidate.endTime;
258
+ if (distance >= minimumAllowableTimeSinceNetworkNodeEnd && distance < minDistance) {
259
+ minCandidate = candidate;
260
+ minDistance = distance;
261
+ }
262
+ }
263
+
264
+ if (!minCandidate) return;
265
+ cpuNode.addDependency(minCandidate);
266
+ }
267
+
268
+ /** @type {Map<string, CPUNode>} */
269
+ const timers = new Map();
270
+ for (const node of cpuNodes) {
271
+ for (const evt of node.childEvents) {
272
+ if (!evt.args.data) continue;
273
+
274
+ const argsUrl = evt.args.data.url;
275
+ const stackTraceUrls = (evt.args.data.stackTrace || []).map(l => l.url).filter(Boolean);
276
+
277
+ switch (evt.name) {
278
+ case 'TimerInstall':
279
+ // @ts-expect-error - 'TimerInstall' event means timerId exists.
280
+ timers.set(evt.args.data.timerId, node);
281
+ stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
282
+ break;
283
+ case 'TimerFire': {
284
+ // @ts-expect-error - 'TimerFire' event means timerId exists.
285
+ const installer = timers.get(evt.args.data.timerId);
286
+ if (!installer || installer.endTime > node.startTime) break;
287
+ installer.addDependent(node);
288
+ break;
289
+ }
290
+
291
+ case 'InvalidateLayout':
292
+ case 'ScheduleStyleRecalculation':
293
+ addDependencyOnFrame(node, evt.args.data.frame);
294
+ stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
295
+ break;
296
+
297
+ case 'EvaluateScript':
298
+ addDependencyOnFrame(node, evt.args.data.frame);
299
+ // @ts-expect-error - 'EvaluateScript' event means argsUrl is defined.
300
+ addDependencyOnUrl(node, argsUrl);
301
+ stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
302
+ break;
303
+
304
+ case 'XHRReadyStateChange':
305
+ // Only create the dependency if the request was completed
306
+ // 'XHRReadyStateChange' event means readyState is defined.
307
+ if (evt.args.data.readyState !== 4) break;
308
+
309
+ // @ts-expect-error - 'XHRReadyStateChange' event means argsUrl is defined.
310
+ addDependencyOnUrl(node, argsUrl);
311
+ stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
312
+ break;
313
+
314
+ case 'FunctionCall':
315
+ case 'v8.compile':
316
+ addDependencyOnFrame(node, evt.args.data.frame);
317
+ // @ts-expect-error - events mean argsUrl is defined.
318
+ addDependencyOnUrl(node, argsUrl);
319
+ break;
320
+
321
+ case 'ParseAuthorStyleSheet':
322
+ addDependencyOnFrame(node, evt.args.data.frame);
323
+ // @ts-expect-error - 'ParseAuthorStyleSheet' event means styleSheetUrl is defined.
324
+ addDependencyOnUrl(node, evt.args.data.styleSheetUrl);
325
+ break;
326
+
327
+ case 'ResourceSendRequest':
328
+ addDependencyOnFrame(node, evt.args.data.frame);
329
+ // @ts-expect-error - 'ResourceSendRequest' event means requestId is defined.
330
+ addDependentNetworkRequest(node, evt.args.data.requestId);
331
+ stackTraceUrls.forEach(url => addDependencyOnUrl(node, url));
332
+ break;
333
+ }
334
+ }
335
+
336
+ // Nodes starting before the root node cannot depend on it.
337
+ if (node.getNumberOfDependencies() === 0 && node.canDependOn(rootNode)) {
338
+ node.addDependency(rootNode);
339
+ }
340
+ }
341
+
342
+ // Second pass to prune the graph of short tasks.
343
+ const minimumEvtDur = SIGNIFICANT_DUR_THRESHOLD_MS * 1000;
344
+ let foundFirstLayout = false;
345
+ let foundFirstPaint = false;
346
+ let foundFirstParse = false;
347
+
348
+ for (const node of cpuNodes) {
349
+ // Don't prune if event is the first ParseHTML/Layout/Paint.
350
+ // See https://github.com/GoogleChrome/lighthouse/issues/9627#issuecomment-526699524 for more.
351
+ let isFirst = false;
352
+ if (!foundFirstLayout && node.childEvents.some(evt => evt.name === 'Layout')) {
353
+ isFirst = foundFirstLayout = true;
354
+ }
355
+ if (!foundFirstPaint && node.childEvents.some(evt => evt.name === 'Paint')) {
356
+ isFirst = foundFirstPaint = true;
357
+ }
358
+ if (!foundFirstParse && node.childEvents.some(evt => evt.name === 'ParseHTML')) {
359
+ isFirst = foundFirstParse = true;
360
+ }
361
+
362
+ if (isFirst || node.event.dur >= minimumEvtDur) {
363
+ // Don't prune this node. The task is long / important so it will impact simulation.
364
+ continue;
365
+ }
366
+
367
+ // Prune the node if it isn't highly connected to minimize graph size. Rewiring the graph
368
+ // here replaces O(M + N) edges with (M * N) edges, which is fine if either M or N is at
369
+ // most 1.
370
+ if (node.getNumberOfDependencies() === 1 || node.getNumberOfDependents() <= 1) {
371
+ PageDependencyGraph._pruneNode(node);
372
+ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Removes the given node from the graph, but retains all paths between its dependencies and
378
+ * dependents.
379
+ * @param {Node} node
380
+ */
381
+ static _pruneNode(node) {
382
+ const dependencies = node.getDependencies();
383
+ const dependents = node.getDependents();
384
+ for (const dependency of dependencies) {
385
+ node.removeDependency(dependency);
386
+ for (const dependent of dependents) {
387
+ dependency.addDependent(dependent);
388
+ }
389
+ }
390
+ for (const dependent of dependents) {
391
+ node.removeDependent(dependent);
392
+ }
393
+ }
394
+
395
+ /**
396
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
397
+ * @param {Array<Lantern.NetworkRequest>} networkRecords
398
+ * @param {URLArtifact} URL
399
+ * @return {Node}
400
+ */
401
+ static createGraph(processedTrace, networkRecords, URL) {
402
+ const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
403
+ const cpuNodes = PageDependencyGraph.getCPUNodes(processedTrace);
404
+ const {requestedUrl, mainDocumentUrl} = URL;
405
+ if (!requestedUrl) throw new Error('requestedUrl is required to get the root request');
406
+ if (!mainDocumentUrl) throw new Error('mainDocumentUrl is required to get the main resource');
407
+
408
+ const rootRequest = NetworkAnalyzer.findResourceForUrl(networkRecords, requestedUrl);
409
+ if (!rootRequest) throw new Error('rootRequest not found');
410
+ const rootNode = networkNodeOutput.idToNodeMap.get(rootRequest.requestId);
411
+ if (!rootNode) throw new Error('rootNode not found');
412
+
413
+ const mainDocumentRequest =
414
+ NetworkAnalyzer.findLastDocumentForUrl(networkRecords, mainDocumentUrl);
415
+ if (!mainDocumentRequest) throw new Error('mainDocumentRequest not found');
416
+ const mainDocumentNode = networkNodeOutput.idToNodeMap.get(mainDocumentRequest.requestId);
417
+ if (!mainDocumentNode) throw new Error('mainDocumentNode not found');
418
+
419
+ PageDependencyGraph.linkNetworkNodes(rootNode, networkNodeOutput);
420
+ PageDependencyGraph.linkCPUNodes(rootNode, networkNodeOutput, cpuNodes);
421
+ mainDocumentNode.setIsMainDocument(true);
422
+
423
+ if (NetworkNode.hasCycle(rootNode)) {
424
+ throw new Error('Invalid dependency graph created, cycle detected');
425
+ }
426
+
427
+ return rootNode;
428
+ }
429
+
430
+ /**
431
+ *
432
+ * @param {Node} rootNode
433
+ */
434
+ static printGraph(rootNode, widthInCharacters = 100) {
435
+ /** @param {string} str @param {number} target */
436
+ function padRight(str, target, padChar = ' ') {
437
+ return str + padChar.repeat(Math.max(target - str.length, 0));
438
+ }
439
+
440
+ /** @type {Array<Node>} */
441
+ const nodes = [];
442
+ rootNode.traverse(node => nodes.push(node));
443
+ nodes.sort((a, b) => a.startTime - b.startTime);
444
+
445
+ const min = nodes[0].startTime;
446
+ const max = nodes.reduce((max, node) => Math.max(max, node.endTime), 0);
447
+
448
+ const totalTime = max - min;
449
+ const timePerCharacter = totalTime / widthInCharacters;
450
+ nodes.forEach(node => {
451
+ const offset = Math.round((node.startTime - min) / timePerCharacter);
452
+ const length = Math.ceil((node.endTime - node.startTime) / timePerCharacter);
453
+ const bar = padRight('', offset) + padRight('', length, '=');
454
+
455
+ // @ts-expect-error -- disambiguate displayName from across possible Node types.
456
+ const displayName = node.request ? node.request.url : node.type;
457
+ // eslint-disable-next-line
458
+ console.log(padRight(bar, widthInCharacters), `| ${displayName.slice(0, 30)}`);
459
+ });
460
+ }
461
+ }
462
+
463
+ export {PageDependencyGraph};
@@ -1,15 +1,15 @@
1
1
  export class ConnectionPool {
2
2
  /**
3
- * @param {LH.Artifacts.NetworkRequest[]} records
4
- * @param {Required<LH.Gatherer.Simulation.Options>} options
3
+ * @param {Lantern.NetworkRequest[]} records
4
+ * @param {Required<Lantern.Simulation.Options>} options
5
5
  */
6
- constructor(records: LH.Artifacts.NetworkRequest[], options: Required<LH.Gatherer.Simulation.Options>);
7
- _options: Required<LH.Gatherer.Simulation.Options>;
8
- _records: import("../../network-request.js").NetworkRequest[];
6
+ constructor(records: Lantern.NetworkRequest[], options: Required<Lantern.Simulation.Options>);
7
+ _options: Required<Lantern.Simulation.Options>;
8
+ _records: Lantern.NetworkRequest<any>[];
9
9
  /** @type {Map<string, TcpConnection[]>} */
10
10
  _connectionsByOrigin: Map<string, TcpConnection[]>;
11
- /** @type {Map<LH.Artifacts.NetworkRequest, TcpConnection>} */
12
- _connectionsByRecord: Map<LH.Artifacts.NetworkRequest, TcpConnection>;
11
+ /** @type {Map<Lantern.NetworkRequest, TcpConnection>} */
12
+ _connectionsByRecord: Map<Lantern.NetworkRequest, TcpConnection>;
13
13
  _connectionsInUse: Set<any>;
14
14
  _connectionReusedByRequestId: Map<string, boolean>;
15
15
  /**
@@ -33,26 +33,26 @@ export class ConnectionPool {
33
33
  * If ignoreConnectionReused is true, acquire will consider all connections not in use as available.
34
34
  * Otherwise, only connections that have matching "warmth" are considered available.
35
35
  *
36
- * @param {LH.Artifacts.NetworkRequest} record
36
+ * @param {Lantern.NetworkRequest} record
37
37
  * @param {{ignoreConnectionReused?: boolean}} options
38
38
  * @return {?TcpConnection}
39
39
  */
40
- acquire(record: LH.Artifacts.NetworkRequest, options?: {
40
+ acquire(record: Lantern.NetworkRequest, options?: {
41
41
  ignoreConnectionReused?: boolean;
42
42
  }): TcpConnection | null;
43
43
  /**
44
44
  * Return the connection currently being used to fetch a record. If no connection
45
45
  * currently being used for this record, an error will be thrown.
46
46
  *
47
- * @param {LH.Artifacts.NetworkRequest} record
47
+ * @param {Lantern.NetworkRequest} record
48
48
  * @return {TcpConnection}
49
49
  */
50
- acquireActiveConnectionFromRecord(record: LH.Artifacts.NetworkRequest): TcpConnection;
50
+ acquireActiveConnectionFromRecord(record: Lantern.NetworkRequest): TcpConnection;
51
51
  /**
52
- * @param {LH.Artifacts.NetworkRequest} record
52
+ * @param {Lantern.NetworkRequest} record
53
53
  */
54
- release(record: LH.Artifacts.NetworkRequest): void;
54
+ release(record: Lantern.NetworkRequest): void;
55
55
  }
56
- import * as LH from '../../../../types/lh.js';
56
+ import * as Lantern from '../types/lantern.js';
57
57
  import { TcpConnection } from './tcp-connection.js';
58
58
  //# sourceMappingURL=connection-pool.d.ts.map
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import * as LH from '../../../../types/lh.js';
7
+ import * as Lantern from '../types/lantern.js';
8
8
  import {NetworkAnalyzer} from './network-analyzer.js';
9
9
  import {TcpConnection} from './tcp-connection.js';
10
10
 
@@ -17,8 +17,8 @@ const CONNECTIONS_PER_ORIGIN = 6;
17
17
 
18
18
  export class ConnectionPool {
19
19
  /**
20
- * @param {LH.Artifacts.NetworkRequest[]} records
21
- * @param {Required<LH.Gatherer.Simulation.Options>} options
20
+ * @param {Lantern.NetworkRequest[]} records
21
+ * @param {Required<Lantern.Simulation.Options>} options
22
22
  */
23
23
  constructor(records, options) {
24
24
  this._options = options;
@@ -26,7 +26,7 @@ export class ConnectionPool {
26
26
  this._records = records;
27
27
  /** @type {Map<string, TcpConnection[]>} */
28
28
  this._connectionsByOrigin = new Map();
29
- /** @type {Map<LH.Artifacts.NetworkRequest, TcpConnection>} */
29
+ /** @type {Map<Lantern.NetworkRequest, TcpConnection>} */
30
30
  this._connectionsByRecord = new Map();
31
31
  this._connectionsInUse = new Set();
32
32
  this._connectionReusedByRequestId = NetworkAnalyzer.estimateIfConnectionWasReused(records, {
@@ -124,7 +124,7 @@ export class ConnectionPool {
124
124
  * If ignoreConnectionReused is true, acquire will consider all connections not in use as available.
125
125
  * Otherwise, only connections that have matching "warmth" are considered available.
126
126
  *
127
- * @param {LH.Artifacts.NetworkRequest} record
127
+ * @param {Lantern.NetworkRequest} record
128
128
  * @param {{ignoreConnectionReused?: boolean}} options
129
129
  * @return {?TcpConnection}
130
130
  */
@@ -150,7 +150,7 @@ export class ConnectionPool {
150
150
  * Return the connection currently being used to fetch a record. If no connection
151
151
  * currently being used for this record, an error will be thrown.
152
152
  *
153
- * @param {LH.Artifacts.NetworkRequest} record
153
+ * @param {Lantern.NetworkRequest} record
154
154
  * @return {TcpConnection}
155
155
  */
156
156
  acquireActiveConnectionFromRecord(record) {
@@ -161,7 +161,7 @@ export class ConnectionPool {
161
161
  }
162
162
 
163
163
  /**
164
- * @param {LH.Artifacts.NetworkRequest} record
164
+ * @param {Lantern.NetworkRequest} record
165
165
  */
166
166
  release(record) {
167
167
  const connection = this._connectionsByRecord.get(record);
@@ -11,19 +11,19 @@ export class DNSCache {
11
11
  resolvedAt: number;
12
12
  }>;
13
13
  /**
14
- * @param {LH.Artifacts.NetworkRequest} request
14
+ * @param {Lantern.NetworkRequest} request
15
15
  * @param {{requestedAt: number, shouldUpdateCache: boolean}=} options
16
16
  * @return {number}
17
17
  */
18
- getTimeUntilResolution(request: LH.Artifacts.NetworkRequest, options?: {
18
+ getTimeUntilResolution(request: Lantern.NetworkRequest, options?: {
19
19
  requestedAt: number;
20
20
  shouldUpdateCache: boolean;
21
21
  } | undefined): number;
22
22
  /**
23
- * @param {LH.Artifacts.NetworkRequest} request
23
+ * @param {Lantern.NetworkRequest} request
24
24
  * @param {number} resolvedAt
25
25
  */
26
- _updateCacheResolvedAtIfNeeded(request: LH.Artifacts.NetworkRequest, resolvedAt: number): void;
26
+ _updateCacheResolvedAtIfNeeded(request: Lantern.NetworkRequest, resolvedAt: number): void;
27
27
  /**
28
28
  * Forcefully sets the DNS resolution time for a record.
29
29
  * Useful for testing and alternate execution simulations.
@@ -36,7 +36,7 @@ export class DNSCache {
36
36
  export namespace DNSCache {
37
37
  export { DNS_RESOLUTION_RTT_MULTIPLIER as RTT_MULTIPLIER };
38
38
  }
39
- import * as LH from '../../../../types/lh.js';
39
+ import * as Lantern from '../types/lantern.js';
40
40
  declare const DNS_RESOLUTION_RTT_MULTIPLIER: 2;
41
41
  export {};
42
42
  //# sourceMappingURL=dns-cache.d.ts.map
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import * as LH from '../../../../types/lh.js';
7
+ import * as Lantern from '../types/lantern.js';
8
8
 
9
9
  // A DNS lookup will usually take ~1-2 roundtrips of connection latency plus the extra DNS routing time.
10
10
  // Example: https://www.webpagetest.org/result/180703_3A_e33ec79747c002ed4d7bcbfc81462203/1/details/#waterfall_view_step1
@@ -25,7 +25,7 @@ class DNSCache {
25
25
  }
26
26
 
27
27
  /**
28
- * @param {LH.Artifacts.NetworkRequest} request
28
+ * @param {Lantern.NetworkRequest} request
29
29
  * @param {{requestedAt: number, shouldUpdateCache: boolean}=} options
30
30
  * @return {number}
31
31
  */
@@ -47,7 +47,7 @@ class DNSCache {
47
47
  }
48
48
 
49
49
  /**
50
- * @param {LH.Artifacts.NetworkRequest} request
50
+ * @param {Lantern.NetworkRequest} request
51
51
  * @param {number} resolvedAt
52
52
  */
53
53
  _updateCacheResolvedAtIfNeeded(request, resolvedAt) {