lighthouse 11.4.0-dev.20240103 → 11.4.0-dev.20240105

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 (43) hide show
  1. package/cli/test/smokehouse/core-tests.js +2 -0
  2. package/core/audits/audit.d.ts +5 -0
  3. package/core/audits/audit.js +42 -2
  4. package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
  5. package/core/audits/layout-shift-elements.js +1 -1
  6. package/core/audits/layout-shifts.d.ts +33 -0
  7. package/core/audits/layout-shifts.js +158 -0
  8. package/core/audits/viewport.js +10 -0
  9. package/core/computed/metrics/cumulative-layout-shift.d.ts +2 -2
  10. package/core/computed/trace-engine-result.d.ts +40 -0
  11. package/core/computed/trace-engine-result.js +69 -0
  12. package/core/computed/viewport-meta.d.ts +4 -0
  13. package/core/computed/viewport-meta.js +6 -1
  14. package/core/config/default-config.js +3 -0
  15. package/core/config/metrics-to-audits.js +1 -0
  16. package/core/gather/driver/wait-for-condition.js +1 -1
  17. package/core/gather/gatherers/root-causes.d.ts +20 -0
  18. package/core/gather/gatherers/root-causes.js +133 -0
  19. package/core/gather/gatherers/trace-elements.d.ts +38 -7
  20. package/core/gather/gatherers/trace-elements.js +113 -34
  21. package/core/gather/gatherers/trace.js +3 -0
  22. package/core/gather/session.js +16 -3
  23. package/core/lib/i18n/i18n.js +2 -0
  24. package/core/lib/trace-engine.d.ts +5 -2
  25. package/core/lib/trace-engine.js +3 -0
  26. package/dist/report/bundle.esm.js +9 -5
  27. package/dist/report/flow.js +6 -2
  28. package/dist/report/standalone.js +11 -7
  29. package/package.json +4 -4
  30. package/report/assets/styles.css +4 -0
  31. package/report/renderer/category-renderer.d.ts +0 -7
  32. package/report/renderer/category-renderer.js +2 -12
  33. package/report/renderer/components.js +1 -1
  34. package/report/renderer/performance-category-renderer.d.ts +2 -3
  35. package/report/renderer/performance-category-renderer.js +3 -4
  36. package/shared/localization/locales/en-US.json +24 -0
  37. package/shared/localization/locales/en-XL.json +24 -0
  38. package/types/artifacts.d.ts +10 -1
  39. package/types/audit.d.ts +9 -1
  40. package/types/config.d.ts +1 -1
  41. package/types/lhr/audit-result.d.ts +1 -4
  42. package/types/lhr/lhr.d.ts +1 -3
  43. package/types/trace-engine.d.ts +1516 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import BaseGatherer from '../base-gatherer.js';
8
+ import Trace from './trace.js';
9
+ import * as TraceEngine from '../../lib/trace-engine.js';
10
+ import {TraceEngineResult} from '../../computed/trace-engine-result.js';
11
+
12
+ class RootCauses extends BaseGatherer {
13
+ static symbol = Symbol('RootCauses');
14
+
15
+ /** @type {LH.Gatherer.GathererMeta<'Trace'>} */
16
+ meta = {
17
+ symbol: RootCauses.symbol,
18
+ supportedModes: ['timespan', 'navigation'],
19
+ dependencies: {Trace: Trace.symbol},
20
+ };
21
+
22
+ /**
23
+ * @param {LH.Gatherer.Driver} driver
24
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
25
+ * @return {Promise<LH.Artifacts.TraceEngineRootCauses>}
26
+ */
27
+ static async runRootCauseAnalysis(driver, traceEngineResult) {
28
+ await driver.defaultSession.sendCommand('DOM.enable');
29
+ await driver.defaultSession.sendCommand('CSS.enable');
30
+
31
+ // DOM.getDocument is necessary for pushNodesByBackendIdsToFrontend to properly retrieve
32
+ // nodeIds if the DOM domain was enabled before this gatherer, invoke it to be safe.
33
+ await driver.defaultSession.sendCommand('DOM.getDocument', {depth: -1, pierce: true});
34
+
35
+ const protocolInterface = {
36
+ /** @param {string} url */
37
+ // eslint-disable-next-line no-unused-vars
38
+ getInitiatorForRequest(url) {
39
+ return null;
40
+ },
41
+ /** @param {number[]} backendNodeIds */
42
+ async pushNodesByBackendIdsToFrontend(backendNodeIds) {
43
+ const response = await driver.defaultSession.sendCommand(
44
+ 'DOM.pushNodesByBackendIdsToFrontend', {backendNodeIds});
45
+ return response.nodeIds;
46
+ },
47
+ /** @param {number} nodeId */
48
+ async getNode(nodeId) {
49
+ try {
50
+ const response = await driver.defaultSession.sendCommand('DOM.describeNode', {nodeId});
51
+ // This always zero, so let's fix it here.
52
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1515175
53
+ response.node.nodeId = nodeId;
54
+ return response.node;
55
+ } catch (err) {
56
+ if (err.message.includes('Could not find node with given id')) {
57
+ // TODO: when injecting an iframe, the engine gets the node of that frame's document element.
58
+ // But we don't have a way to access that frame. We just have our default session.
59
+ // Ex:
60
+ // node cli http://localhost:10503/shift-attribution.html --quiet --only-audits layout-shifts
61
+ // To fix we must:
62
+ // - Change trace engine `getNode` protocol interface to also give frame id
63
+ // - Expand our driver.targetManager to know how to talk to a session connected to a specific frame
64
+ // When this is fixed, remove this try/catch.
65
+ // Note: this could be buggy by giving the wrong node detail if a node id meant for a non-main frame
66
+ // happens to match one from the main frame ... which is pretty likely...
67
+ return null;
68
+ }
69
+ throw err;
70
+ }
71
+ },
72
+ /** @param {number} nodeId */
73
+ async getComputedStyleForNode(nodeId) {
74
+ try {
75
+ const response = await driver.defaultSession.sendCommand(
76
+ 'CSS.getComputedStyleForNode', {nodeId});
77
+ return response.computedStyle;
78
+ } catch {
79
+ return [];
80
+ }
81
+ },
82
+ /** @param {number} nodeId */
83
+ async getMatchedStylesForNode(nodeId) {
84
+ try {
85
+ const response = await driver.defaultSession.sendCommand(
86
+ 'CSS.getMatchedStylesForNode', {nodeId});
87
+ return response;
88
+ } catch {
89
+ return [];
90
+ }
91
+ },
92
+ /** @param {string} url */
93
+ // eslint-disable-next-line no-unused-vars
94
+ async fontFaceForSource(url) {
95
+ return null;
96
+ },
97
+ };
98
+
99
+ /** @type {LH.Artifacts.TraceEngineRootCauses} */
100
+ const rootCauses = {
101
+ layoutShifts: {},
102
+ };
103
+ const rootCausesEngine = new TraceEngine.RootCauses(protocolInterface);
104
+ const layoutShiftEvents = traceEngineResult.LayoutShifts.clusters.flatMap(c => c.events);
105
+ for (const event of layoutShiftEvents) {
106
+ const r = await rootCausesEngine.layoutShifts.rootCausesForEvent(traceEngineResult, event);
107
+ for (const cause of r.fontChanges) {
108
+ // TODO: why isn't trace engine unwrapping this promise ...
109
+ cause.fontFace = await cause.fontFace;
110
+ }
111
+ rootCauses.layoutShifts[layoutShiftEvents.indexOf(event)] = r;
112
+ }
113
+
114
+ // Yeah this gatherer enabled it, and so you'd think it should disable it too...
115
+ // ...but we don't give gatherers their own session so this stomps on others.
116
+ // await driver.defaultSession.sendCommand('DOM.disable');
117
+ await driver.defaultSession.sendCommand('CSS.disable');
118
+
119
+ return rootCauses;
120
+ }
121
+
122
+ /**
123
+ * @param {LH.Gatherer.Context<'Trace'>} context
124
+ * @return {Promise<LH.Artifacts.TraceEngineRootCauses>}
125
+ */
126
+ async getArtifact(context) {
127
+ const trace = context.dependencies.Trace;
128
+ const traceEngineResult = await TraceEngineResult.request({trace}, context);
129
+ return RootCauses.runRootCauseAnalysis(context.driver, traceEngineResult);
130
+ }
131
+ }
132
+
133
+ export default RootCauses;
@@ -10,13 +10,38 @@ export type TraceElementData = {
10
10
  };
11
11
  declare class TraceElements extends BaseGatherer {
12
12
  /**
13
- * This function finds the top (up to 15) elements that contribute to the CLS score of the page.
13
+ * This function finds the top (up to 15) elements that shift on the page.
14
14
  *
15
15
  * @param {LH.Trace} trace
16
16
  * @param {LH.Gatherer.Context} context
17
- * @return {Promise<Array<TraceElementData>>}
17
+ * @return {Promise<Array<{nodeId: number}>>}
18
18
  */
19
- static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<TraceElementData>>;
19
+ static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<{
20
+ nodeId: number;
21
+ }>>;
22
+ /**
23
+ * We want to a single representative node to represent the shift, so let's pick
24
+ * the one with the largest impact (size x distance moved).
25
+ *
26
+ * @param {LH.Artifacts.TraceImpactedNode[]} impactedNodes
27
+ * @param {Map<number, number>} impactByNodeId
28
+ * @return {number|undefined}
29
+ */
30
+ static getBiggestImpactNodeForShiftEvent(impactedNodes: LH.Artifacts.TraceImpactedNode[], impactByNodeId: Map<number, number>): number | undefined;
31
+ /**
32
+ * This function finds the top (up to 15) layout shifts on the page, and returns
33
+ * the id of the largest impacted node of each shift, along with any related nodes
34
+ * that may have caused the shift.
35
+ *
36
+ * @param {LH.Trace} trace
37
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
38
+ * @param {LH.Artifacts.TraceEngineRootCauses} rootCauses
39
+ * @param {LH.Gatherer.Context} context
40
+ * @return {Promise<Array<{nodeId: number}>>}
41
+ */
42
+ static getTopLayoutShifts(trace: LH.Trace, traceEngineResult: LH.Artifacts.TraceEngineResult, rootCauses: LH.Artifacts.TraceEngineRootCauses, context: LH.Gatherer.Context): Promise<Array<{
43
+ nodeId: number;
44
+ }>>;
20
45
  /**
21
46
  * @param {LH.Trace} trace
22
47
  * @param {LH.Gatherer.Context} context
@@ -32,8 +57,8 @@ declare class TraceElements extends BaseGatherer {
32
57
  nodeId: number;
33
58
  type: string;
34
59
  } | undefined>;
35
- /** @type {LH.Gatherer.GathererMeta<'Trace'>} */
36
- meta: LH.Gatherer.GathererMeta<'Trace'>;
60
+ /** @type {LH.Gatherer.GathererMeta<'Trace'|'RootCauses'>} */
61
+ meta: LH.Gatherer.GathererMeta<'Trace' | 'RootCauses'>;
37
62
  /** @type {Map<string, string>} */
38
63
  animationIdToName: Map<string, string>;
39
64
  /** @param {LH.Crdp.Animation.AnimationStartedEvent} args */
@@ -53,11 +78,17 @@ declare class TraceElements extends BaseGatherer {
53
78
  */
54
79
  stopInstrumentation(context: LH.Gatherer.Context): Promise<void>;
55
80
  /**
56
- * @param {LH.Gatherer.Context<'Trace'>} context
81
+ * @param {LH.Gatherer.ProtocolSession} session
82
+ * @param {number} backendNodeId
83
+ */
84
+ getNodeDetails(session: LH.Gatherer.ProtocolSession, backendNodeId: number): Promise<import("devtools-protocol").Protocol.Runtime.CallFunctionOnResponse | null>;
85
+ /**
86
+ * @param {LH.Gatherer.Context<'Trace'|'RootCauses'>} context
57
87
  * @return {Promise<LH.Artifacts.TraceElement[]>}
58
88
  */
59
- getArtifact(context: LH.Gatherer.Context<'Trace'>): Promise<LH.Artifacts.TraceElement[]>;
89
+ getArtifact(context: LH.Gatherer.Context<'Trace' | 'RootCauses'>): Promise<LH.Artifacts.TraceElement[]>;
60
90
  }
61
91
  import BaseGatherer from '../base-gatherer.js';
62
92
  import Trace from './trace.js';
93
+ import { TraceEngineResult } from '../../computed/trace-engine-result.js';
63
94
  //# sourceMappingURL=trace-elements.d.ts.map
@@ -23,10 +23,13 @@ import {LighthouseError} from '../../lib/lh-error.js';
23
23
  import {Responsiveness} from '../../computed/metrics/responsiveness.js';
24
24
  import {CumulativeLayoutShift} from '../../computed/metrics/cumulative-layout-shift.js';
25
25
  import {ExecutionContext} from '../driver/execution-context.js';
26
+ import RootCauses from './root-causes.js';
27
+ import {TraceEngineResult} from '../../computed/trace-engine-result.js';
26
28
 
27
29
  /** @typedef {{nodeId: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */
28
30
 
29
31
  const MAX_LAYOUT_SHIFT_ELEMENTS = 15;
32
+ const MAX_LAYOUT_SHIFTS = 15;
30
33
 
31
34
  /**
32
35
  * @this {HTMLElement}
@@ -44,10 +47,10 @@ function getNodeDetailsData() {
44
47
  /* c8 ignore stop */
45
48
 
46
49
  class TraceElements extends BaseGatherer {
47
- /** @type {LH.Gatherer.GathererMeta<'Trace'>} */
50
+ /** @type {LH.Gatherer.GathererMeta<'Trace'|'RootCauses'>} */
48
51
  meta = {
49
52
  supportedModes: ['timespan', 'navigation'],
50
- dependencies: {Trace: Trace.symbol},
53
+ dependencies: {Trace: Trace.symbol, RootCauses: RootCauses.symbol},
51
54
  };
52
55
 
53
56
  /** @type {Map<string, string>} */
@@ -64,11 +67,11 @@ class TraceElements extends BaseGatherer {
64
67
  }
65
68
 
66
69
  /**
67
- * This function finds the top (up to 15) elements that contribute to the CLS score of the page.
70
+ * This function finds the top (up to 15) elements that shift on the page.
68
71
  *
69
72
  * @param {LH.Trace} trace
70
73
  * @param {LH.Gatherer.Context} context
71
- * @return {Promise<Array<TraceElementData>>}
74
+ * @return {Promise<Array<{nodeId: number}>>}
72
75
  */
73
76
  static async getTopLayoutShiftElements(trace, context) {
74
77
  const {impactByNodeId} = await CumulativeLayoutShift.request(trace, context);
@@ -79,6 +82,66 @@ class TraceElements extends BaseGatherer {
79
82
  .map(([nodeId]) => ({nodeId}));
80
83
  }
81
84
 
85
+ /**
86
+ * We want to a single representative node to represent the shift, so let's pick
87
+ * the one with the largest impact (size x distance moved).
88
+ *
89
+ * @param {LH.Artifacts.TraceImpactedNode[]} impactedNodes
90
+ * @param {Map<number, number>} impactByNodeId
91
+ * @return {number|undefined}
92
+ */
93
+ static getBiggestImpactNodeForShiftEvent(impactedNodes, impactByNodeId) {
94
+ let biggestImpactNodeId;
95
+ let biggestImpactNodeScore = Number.NEGATIVE_INFINITY;
96
+ for (const node of impactedNodes) {
97
+ const impactScore = impactByNodeId.get(node.node_id);
98
+ if (impactScore !== undefined && impactScore > biggestImpactNodeScore) {
99
+ biggestImpactNodeId = node.node_id;
100
+ biggestImpactNodeScore = impactScore;
101
+ }
102
+ }
103
+ return biggestImpactNodeId;
104
+ }
105
+
106
+ /**
107
+ * This function finds the top (up to 15) layout shifts on the page, and returns
108
+ * the id of the largest impacted node of each shift, along with any related nodes
109
+ * that may have caused the shift.
110
+ *
111
+ * @param {LH.Trace} trace
112
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
113
+ * @param {LH.Artifacts.TraceEngineRootCauses} rootCauses
114
+ * @param {LH.Gatherer.Context} context
115
+ * @return {Promise<Array<{nodeId: number}>>}
116
+ */
117
+ static async getTopLayoutShifts(trace, traceEngineResult, rootCauses, context) {
118
+ const {impactByNodeId} = await CumulativeLayoutShift.request(trace, context);
119
+ const clusters = traceEngineResult.LayoutShifts.clusters ?? [];
120
+ const layoutShiftEvents = clusters.flatMap(c => c.events);
121
+
122
+ return layoutShiftEvents
123
+ .sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
124
+ .slice(0, MAX_LAYOUT_SHIFTS)
125
+ .flatMap(event => {
126
+ const nodeIds = [];
127
+ const biggestImpactedNodeId =
128
+ this.getBiggestImpactNodeForShiftEvent(event.args.data.impacted_nodes, impactByNodeId);
129
+ if (biggestImpactedNodeId !== undefined) {
130
+ nodeIds.push(biggestImpactedNodeId);
131
+ }
132
+
133
+ const index = layoutShiftEvents.indexOf(event);
134
+ const shiftRootCauses = rootCauses.layoutShifts[index];
135
+ if (shiftRootCauses) {
136
+ for (const cause of shiftRootCauses.unsizedMedia) {
137
+ nodeIds.push(cause.node.backendNodeId);
138
+ }
139
+ }
140
+
141
+ return nodeIds.map(nodeId => ({nodeId}));
142
+ });
143
+ }
144
+
82
145
  /**
83
146
  * @param {LH.Trace} trace
84
147
  * @param {LH.Gatherer.Context} context
@@ -195,61 +258,77 @@ class TraceElements extends BaseGatherer {
195
258
  }
196
259
 
197
260
  /**
198
- * @param {LH.Gatherer.Context<'Trace'>} context
261
+ * @param {LH.Gatherer.ProtocolSession} session
262
+ * @param {number} backendNodeId
263
+ */
264
+ async getNodeDetails(session, backendNodeId) {
265
+ try {
266
+ const objectId = await resolveNodeIdToObjectId(session, backendNodeId);
267
+ if (!objectId) return null;
268
+
269
+ const deps = ExecutionContext.serializeDeps([
270
+ pageFunctions.getNodeDetails,
271
+ getNodeDetailsData,
272
+ ]);
273
+ return await session.sendCommand('Runtime.callFunctionOn', {
274
+ objectId,
275
+ functionDeclaration: `function () {
276
+ ${deps}
277
+ return getNodeDetailsData.call(this);
278
+ }`,
279
+ returnByValue: true,
280
+ awaitPromise: true,
281
+ });
282
+ } catch (err) {
283
+ Sentry.captureException(err, {
284
+ tags: {gatherer: 'TraceElements'},
285
+ level: 'error',
286
+ });
287
+ }
288
+
289
+ return null;
290
+ }
291
+
292
+ /**
293
+ * @param {LH.Gatherer.Context<'Trace'|'RootCauses'>} context
199
294
  * @return {Promise<LH.Artifacts.TraceElement[]>}
200
295
  */
201
296
  async getArtifact(context) {
202
297
  const session = context.driver.defaultSession;
203
298
 
204
299
  const trace = context.dependencies.Trace;
205
- if (!trace) {
206
- throw new Error('Trace is missing!');
207
- }
300
+ const traceEngineResult = await TraceEngineResult.request({trace}, context);
301
+ const rootCauses = context.dependencies.RootCauses;
208
302
 
209
303
  const processedTrace = await ProcessedTrace.request(trace, context);
210
304
  const {mainThreadEvents} = processedTrace;
211
305
 
212
306
  const lcpNodeData = await TraceElements.getLcpElement(trace, context);
213
- const clsNodeData = await TraceElements.getTopLayoutShiftElements(trace, context);
307
+ const shiftElementsNodeData = await TraceElements.getTopLayoutShiftElements(trace, context);
308
+ const shiftsData = await TraceElements.getTopLayoutShifts(
309
+ trace, traceEngineResult, rootCauses, context);
214
310
  const animatedElementData = await this.getAnimatedElements(mainThreadEvents);
215
311
  const responsivenessElementData = await TraceElements.getResponsivenessElement(trace, context);
216
312
 
217
313
  /** @type {Map<string, TraceElementData[]>} */
218
314
  const backendNodeDataMap = new Map([
219
315
  ['largest-contentful-paint', lcpNodeData ? [lcpNodeData] : []],
220
- ['layout-shift', clsNodeData],
316
+ ['layout-shift-element', shiftElementsNodeData],
317
+ ['layout-shift', shiftsData],
221
318
  ['animation', animatedElementData],
222
319
  ['responsiveness', responsivenessElementData ? [responsivenessElementData] : []],
223
320
  ]);
224
321
 
322
+ /** @type {Map<number, LH.Crdp.Runtime.CallFunctionOnResponse | null>} */
323
+ const callFunctionOnCache = new Map();
225
324
  const traceElements = [];
226
325
  for (const [traceEventType, backendNodeData] of backendNodeDataMap) {
227
326
  for (let i = 0; i < backendNodeData.length; i++) {
228
327
  const backendNodeId = backendNodeData[i].nodeId;
229
- let response;
230
- try {
231
- const objectId = await resolveNodeIdToObjectId(session, backendNodeId);
232
- if (!objectId) continue;
233
-
234
- const deps = ExecutionContext.serializeDeps([
235
- pageFunctions.getNodeDetails,
236
- getNodeDetailsData,
237
- ]);
238
- response = await session.sendCommand('Runtime.callFunctionOn', {
239
- objectId,
240
- functionDeclaration: `function () {
241
- ${deps}
242
- return getNodeDetailsData.call(this);
243
- }`,
244
- returnByValue: true,
245
- awaitPromise: true,
246
- });
247
- } catch (err) {
248
- Sentry.captureException(err, {
249
- tags: {gatherer: 'TraceElements'},
250
- level: 'error',
251
- });
252
- continue;
328
+ let response = callFunctionOnCache.get(backendNodeId);
329
+ if (response === undefined) {
330
+ response = await this.getNodeDetails(session, backendNodeId);
331
+ callFunctionOnCache.set(backendNodeId, response);
253
332
  }
254
333
 
255
334
  if (response?.result?.value) {
@@ -56,6 +56,9 @@ class Trace extends BaseGatherer {
56
56
  'disabled-by-default-devtools.timeline.frame',
57
57
  'latencyInfo',
58
58
 
59
+ // For CLS root causes.
60
+ 'disabled-by-default-devtools.timeline.invalidationTracking',
61
+
59
62
  // Not used by Lighthouse (yet) but included for users that want JS samples when looking at
60
63
  // a trace collected by Lighthouse (e.g. "View Trace" workflow in DevTools)
61
64
  'disabled-by-default-v8.cpu_profiler',
@@ -10,6 +10,17 @@ import {LighthouseError} from '../lib/lh-error.js';
10
10
 
11
11
  // Controls how long to wait for a response after sending a DevTools protocol command.
12
12
  const DEFAULT_PROTOCOL_TIMEOUT = 30000;
13
+ const PPTR_BUFFER = 50;
14
+
15
+ /**
16
+ * Puppeteer timeouts must fit into an int32 and the maximum timeout for `setTimeout` is a *signed*
17
+ * int32. However, this also needs to account for the puppeteer buffer we add to the timeout later.
18
+ *
19
+ * So this is defined as the max *signed* int32 minus PPTR_BUFFER.
20
+ *
21
+ * In human terms, this timeout is ~25 days which is as good as infinity for all practical purposes.
22
+ */
23
+ const MAX_TIMEOUT = 2147483647 - PPTR_BUFFER;
13
24
 
14
25
  /** @typedef {LH.Protocol.StrictEventEmitterClass<LH.CrdpEvents>} CrdpEventMessageEmitter */
15
26
  const CrdpEventEmitter = /** @type {CrdpEventMessageEmitter} */ (EventEmitter);
@@ -70,6 +81,7 @@ class ProtocolSession extends CrdpEventEmitter {
70
81
  * @param {number} ms
71
82
  */
72
83
  setNextProtocolTimeout(ms) {
84
+ if (ms > MAX_TIMEOUT) ms = MAX_TIMEOUT;
73
85
  this._nextProtocolTimeout = ms;
74
86
  }
75
87
 
@@ -86,15 +98,16 @@ class ProtocolSession extends CrdpEventEmitter {
86
98
  /** @type {NodeJS.Timer|undefined} */
87
99
  let timeout;
88
100
  const timeoutPromise = new Promise((resolve, reject) => {
89
- if (timeoutMs === Infinity) return;
90
-
91
101
  // eslint-disable-next-line max-len
92
102
  timeout = setTimeout(reject, timeoutMs, new LighthouseError(LighthouseError.errors.PROTOCOL_TIMEOUT, {
93
103
  protocolMethod: method,
94
104
  }));
95
105
  });
96
106
 
97
- const resultPromise = this._cdpSession.send(method, ...params);
107
+ const resultPromise = this._cdpSession.send(method, ...params, {
108
+ // Add 50ms to the Puppeteer timeout to ensure the Lighthouse timeout finishes first.
109
+ timeout: timeoutMs + PPTR_BUFFER,
110
+ });
98
111
  const resultWithTimeoutPromise = Promise.race([resultPromise, timeoutPromise]);
99
112
 
100
113
  return resultWithTimeoutPromise.finally(() => {
@@ -17,6 +17,7 @@ import {LH_ROOT} from '../../../shared/root.js';
17
17
  import {isIcuMessage, formatMessage, DEFAULT_LOCALE} from '../../../shared/localization/format.js';
18
18
  import {getModulePath} from '../../../shared/esm-utils.js';
19
19
 
20
+ /* eslint-disable max-len */
20
21
  const UIStrings = {
21
22
  /** Used to show the duration in milliseconds that something lasted. The `{timeInMs}` placeholder will be replaced with the time duration, shown in milliseconds (e.g. 63 ms) */
22
23
  ms: '{timeInMs, number, milliseconds}\xa0ms',
@@ -113,6 +114,7 @@ const UIStrings = {
113
114
  /** Table item value for the severity of a high impact, or dangerous vulnerability. Part of a ranking scale in the form: low, medium, high. */
114
115
  itemSeverityHigh: 'High',
115
116
  };
117
+ /* eslint-enable max-len */
116
118
 
117
119
  /**
118
120
  * Look up the best available locale for the requested language through these fall backs:
@@ -1,4 +1,7 @@
1
- export const TraceProcessor: any;
1
+ /** @type {import('../../types/trace-engine.js').TraceProcessor & typeof import('../../types/trace-engine.js').TraceProcessor} */
2
+ export const TraceProcessor: import('../../types/trace-engine.js').TraceProcessor & typeof import('../../types/trace-engine.js').TraceProcessor;
3
+ /** @type {import('../../types/trace-engine.js').TraceHandlers} */
2
4
  export const TraceHandlers: any;
3
- export const RootCauses: any;
5
+ /** @type {import('../../types/trace-engine.js').RootCauses & typeof import('../../types/trace-engine.js').RootCauses} */
6
+ export const RootCauses: import('../../types/trace-engine.js').RootCauses & typeof import('../../types/trace-engine.js').RootCauses;
4
7
  //# sourceMappingURL=trace-engine.d.ts.map
@@ -5,8 +5,11 @@ import {polyfillDOMRect} from './polyfill-dom-rect.js';
5
5
 
6
6
  polyfillDOMRect();
7
7
 
8
+ /** @type {import('../../types/trace-engine.js').TraceProcessor & typeof import('../../types/trace-engine.js').TraceProcessor} */
8
9
  const TraceProcessor = TraceEngine.Processor.TraceProcessor;
10
+ /** @type {import('../../types/trace-engine.js').TraceHandlers} */
9
11
  const TraceHandlers = TraceEngine.Handlers.ModelHandlers;
12
+ /** @type {import('../../types/trace-engine.js').RootCauses & typeof import('../../types/trace-engine.js').RootCauses} */
10
13
  const RootCauses = TraceEngine.RootCauses.RootCauses.RootCauses;
11
14
 
12
15
  export {