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.
- package/cli/test/smokehouse/core-tests.js +2 -0
- package/core/audits/audit.d.ts +5 -0
- package/core/audits/audit.js +42 -2
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
- package/core/audits/layout-shift-elements.js +1 -1
- package/core/audits/layout-shifts.d.ts +33 -0
- package/core/audits/layout-shifts.js +158 -0
- package/core/audits/viewport.js +10 -0
- package/core/computed/metrics/cumulative-layout-shift.d.ts +2 -2
- package/core/computed/trace-engine-result.d.ts +40 -0
- package/core/computed/trace-engine-result.js +69 -0
- package/core/computed/viewport-meta.d.ts +4 -0
- package/core/computed/viewport-meta.js +6 -1
- package/core/config/default-config.js +3 -0
- package/core/config/metrics-to-audits.js +1 -0
- package/core/gather/driver/wait-for-condition.js +1 -1
- package/core/gather/gatherers/root-causes.d.ts +20 -0
- package/core/gather/gatherers/root-causes.js +133 -0
- package/core/gather/gatherers/trace-elements.d.ts +38 -7
- package/core/gather/gatherers/trace-elements.js +113 -34
- package/core/gather/gatherers/trace.js +3 -0
- package/core/gather/session.js +16 -3
- package/core/lib/i18n/i18n.js +2 -0
- package/core/lib/trace-engine.d.ts +5 -2
- package/core/lib/trace-engine.js +3 -0
- package/dist/report/bundle.esm.js +9 -5
- package/dist/report/flow.js +6 -2
- package/dist/report/standalone.js +11 -7
- package/package.json +4 -4
- package/report/assets/styles.css +4 -0
- package/report/renderer/category-renderer.d.ts +0 -7
- package/report/renderer/category-renderer.js +2 -12
- package/report/renderer/components.js +1 -1
- package/report/renderer/performance-category-renderer.d.ts +2 -3
- package/report/renderer/performance-category-renderer.js +3 -4
- package/shared/localization/locales/en-US.json +24 -0
- package/shared/localization/locales/en-XL.json +24 -0
- package/types/artifacts.d.ts +10 -1
- package/types/audit.d.ts +9 -1
- package/types/config.d.ts +1 -1
- package/types/lhr/audit-result.d.ts +1 -4
- package/types/lhr/lhr.d.ts +1 -3
- 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
|
|
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<
|
|
17
|
+
* @return {Promise<Array<{nodeId: number}>>}
|
|
18
18
|
*/
|
|
19
|
-
static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<
|
|
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.
|
|
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
|
|
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<
|
|
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.
|
|
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
|
-
|
|
206
|
-
|
|
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
|
|
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',
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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',
|
package/core/gather/session.js
CHANGED
|
@@ -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(() => {
|
package/core/lib/i18n/i18n.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/core/lib/trace-engine.js
CHANGED
|
@@ -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 {
|