lighthouse 10.2.0-dev.20230518 → 10.2.0-dev.20230520

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.
@@ -2,9 +2,10 @@ export default LayoutShiftElements;
2
2
  declare class LayoutShiftElements extends Audit {
3
3
  /**
4
4
  * @param {LH.Artifacts} artifacts
5
- * @return {LH.Audit.Product}
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
6
7
  */
7
- static audit(artifacts: LH.Artifacts): LH.Audit.Product;
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
8
9
  }
9
10
  export namespace UIStrings {
10
11
  const title: string;
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {Audit} from './audit.js';
8
8
  import * as i18n from '../lib/i18n/i18n.js';
9
+ import {CumulativeLayoutShift} from '../computed/metrics/cumulative-layout-shift.js';
9
10
 
10
11
  const UIStrings = {
11
12
  /** Descriptive title of a diagnostic audit that provides up to the top five elements contributing to Cumulative Layout Shift. */
@@ -34,9 +35,10 @@ class LayoutShiftElements extends Audit {
34
35
 
35
36
  /**
36
37
  * @param {LH.Artifacts} artifacts
37
- * @return {LH.Audit.Product}
38
+ * @param {LH.Audit.Context} context
39
+ * @return {Promise<LH.Audit.Product>}
38
40
  */
39
- static audit(artifacts) {
41
+ static async audit(artifacts, context) {
40
42
  const clsElements = artifacts.TraceElements
41
43
  .filter(element => element.traceEventType === 'layout-shift');
42
44
 
@@ -61,8 +63,14 @@ class LayoutShiftElements extends Audit {
61
63
  {nodeCount: clsElementData.length});
62
64
  }
63
65
 
66
+ const {cumulativeLayoutShift: clsSavings} =
67
+ await CumulativeLayoutShift.request(artifacts.traces[Audit.DEFAULT_PASS], context);
68
+
64
69
  return {
65
70
  score: 1,
71
+ metricSavings: {
72
+ CLS: clsSavings,
73
+ },
66
74
  notApplicable: details.items.length === 0,
67
75
  displayValue,
68
76
  details,
@@ -18,12 +18,15 @@ export class TargetManager extends TargetManager_base {
18
18
  constructor(cdpSession: LH.Puppeteer.CDPSession);
19
19
  _enabled: boolean;
20
20
  _rootCdpSession: import("../../../types/puppeteer.js").default.CDPSession;
21
+ _mainFrameId: string;
21
22
  /**
22
23
  * A map of target id to target/session information. Used to ensure unique
23
24
  * attached targets.
24
25
  * @type {Map<string, TargetWithSession>}
25
26
  */
26
27
  _targetIdToTargets: Map<string, TargetWithSession>;
28
+ /** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
29
+ _executionContextIdToDescriptions: Map<string, LH.Crdp.Runtime.ExecutionContextDescription>;
27
30
  /**
28
31
  * @param {LH.Puppeteer.CDPSession} cdpSession
29
32
  */
@@ -32,6 +35,15 @@ export class TargetManager extends TargetManager_base {
32
35
  * @param {LH.Crdp.Page.FrameNavigatedEvent} frameNavigatedEvent
33
36
  */
34
37
  _onFrameNavigated(frameNavigatedEvent: LH.Crdp.Page.FrameNavigatedEvent): Promise<void>;
38
+ /**
39
+ * @param {LH.Crdp.Runtime.ExecutionContextCreatedEvent} event
40
+ */
41
+ _onExecutionContextCreated(event: LH.Crdp.Runtime.ExecutionContextCreatedEvent): void;
42
+ /**
43
+ * @param {LH.Crdp.Runtime.ExecutionContextDestroyedEvent} event
44
+ */
45
+ _onExecutionContextDestroyed(event: LH.Crdp.Runtime.ExecutionContextDestroyedEvent): void;
46
+ _onExecutionContextsCleared(): void;
35
47
  /**
36
48
  * @param {string} sessionId
37
49
  * @return {LH.Gatherer.FRProtocolSession}
@@ -42,6 +54,7 @@ export class TargetManager extends TargetManager_base {
42
54
  * @return {LH.Gatherer.FRProtocolSession}
43
55
  */
44
56
  rootSession(): LH.Gatherer.FRProtocolSession;
57
+ mainFrameExecutionContexts(): import("devtools-protocol").Protocol.Runtime.ExecutionContextDescription[];
45
58
  /**
46
59
  * Returns a listener for all protocol events from session, and augments the
47
60
  * event with the sessionId.
@@ -40,6 +40,7 @@ class TargetManager extends ProtocolEventEmitter {
40
40
 
41
41
  this._enabled = false;
42
42
  this._rootCdpSession = cdpSession;
43
+ this._mainFrameId = '';
43
44
 
44
45
  /**
45
46
  * A map of target id to target/session information. Used to ensure unique
@@ -47,9 +48,14 @@ class TargetManager extends ProtocolEventEmitter {
47
48
  * @type {Map<string, TargetWithSession>}
48
49
  */
49
50
  this._targetIdToTargets = new Map();
51
+ /** @type {Map<string, LH.Crdp.Runtime.ExecutionContextDescription>} */
52
+ this._executionContextIdToDescriptions = new Map();
50
53
 
51
54
  this._onSessionAttached = this._onSessionAttached.bind(this);
52
55
  this._onFrameNavigated = this._onFrameNavigated.bind(this);
56
+ this._onExecutionContextCreated = this._onExecutionContextCreated.bind(this);
57
+ this._onExecutionContextDestroyed = this._onExecutionContextDestroyed.bind(this);
58
+ this._onExecutionContextsCleared = this._onExecutionContextsCleared.bind(this);
53
59
  }
54
60
 
55
61
  /**
@@ -97,6 +103,12 @@ class TargetManager extends ProtocolEventEmitter {
97
103
  return this._findSession(rootSessionId);
98
104
  }
99
105
 
106
+ mainFrameExecutionContexts() {
107
+ return [...this._executionContextIdToDescriptions.values()].filter(executionContext => {
108
+ return executionContext.auxData.frameId === this._mainFrameId;
109
+ });
110
+ }
111
+
100
112
  /**
101
113
  * @param {LH.Puppeteer.CDPSession} cdpSession
102
114
  */
@@ -153,6 +165,27 @@ class TargetManager extends ProtocolEventEmitter {
153
165
  }
154
166
  }
155
167
 
168
+ /**
169
+ * @param {LH.Crdp.Runtime.ExecutionContextCreatedEvent} event
170
+ */
171
+ _onExecutionContextCreated(event) {
172
+ if (event.context.name === '__puppeteer_utility_world__') return;
173
+ if (event.context.name === 'lighthouse_isolated_context') return;
174
+
175
+ this._executionContextIdToDescriptions.set(event.context.uniqueId, event.context);
176
+ }
177
+
178
+ /**
179
+ * @param {LH.Crdp.Runtime.ExecutionContextDestroyedEvent} event
180
+ */
181
+ _onExecutionContextDestroyed(event) {
182
+ this._executionContextIdToDescriptions.delete(event.executionContextUniqueId);
183
+ }
184
+
185
+ _onExecutionContextsCleared() {
186
+ this._executionContextIdToDescriptions.clear();
187
+ }
188
+
156
189
  /**
157
190
  * Returns a listener for all protocol events from session, and augments the
158
191
  * event with the sessionId.
@@ -183,10 +216,17 @@ class TargetManager extends ProtocolEventEmitter {
183
216
 
184
217
  this._enabled = true;
185
218
  this._targetIdToTargets = new Map();
219
+ this._executionContextIdToDescriptions = new Map();
186
220
 
187
221
  this._rootCdpSession.on('Page.frameNavigated', this._onFrameNavigated);
222
+ this._rootCdpSession.on('Runtime.executionContextCreated', this._onExecutionContextCreated);
223
+ this._rootCdpSession.on('Runtime.executionContextDestroyed', this._onExecutionContextDestroyed);
224
+ this._rootCdpSession.on('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
188
225
 
189
226
  await this._rootCdpSession.send('Page.enable');
227
+ await this._rootCdpSession.send('Runtime.enable');
228
+
229
+ this._mainFrameId = (await this._rootCdpSession.send('Page.getFrameTree')).frameTree.frame.id;
190
230
 
191
231
  // Start with the already attached root session.
192
232
  await this._onSessionAttached(this._rootCdpSession);
@@ -197,14 +237,23 @@ class TargetManager extends ProtocolEventEmitter {
197
237
  */
198
238
  async disable() {
199
239
  this._rootCdpSession.off('Page.frameNavigated', this._onFrameNavigated);
240
+ this._rootCdpSession.off('Runtime.executionContextCreated', this._onExecutionContextCreated);
241
+ this._rootCdpSession.off('Runtime.executionContextDestroyed',
242
+ this._onExecutionContextDestroyed);
243
+ this._rootCdpSession.off('Runtime.executionContextsCleared', this._onExecutionContextsCleared);
200
244
 
201
245
  for (const {cdpSession, protocolListener} of this._targetIdToTargets.values()) {
202
246
  cdpSession.off('*', protocolListener);
203
247
  cdpSession.off('sessionattached', this._onSessionAttached);
204
248
  }
205
249
 
250
+ await this._rootCdpSession.send('Page.disable');
251
+ await this._rootCdpSession.send('Runtime.disable');
252
+
206
253
  this._enabled = false;
207
254
  this._targetIdToTargets = new Map();
255
+ this._executionContextIdToDescriptions = new Map();
256
+ this._mainFrameId = '';
208
257
  }
209
258
  }
210
259
 
@@ -11,6 +11,8 @@
11
11
  * around page unload, but this can be expanded in the future.
12
12
  */
13
13
 
14
+ import log from 'lighthouse-logger';
15
+
14
16
  import FRGatherer from '../base-gatherer.js';
15
17
 
16
18
  class GlobalListeners extends FRGatherer {
@@ -61,30 +63,45 @@ class GlobalListeners extends FRGatherer {
61
63
  async getArtifact(passContext) {
62
64
  const session = passContext.driver.defaultSession;
63
65
 
64
- // Get a RemoteObject handle to `window`.
65
- const {result: {objectId}} = await session.sendCommand('Runtime.evaluate', {
66
- expression: 'window',
67
- returnByValue: false,
68
- });
69
- if (!objectId) {
70
- throw new Error('Error fetching information about the global object');
71
- }
66
+ /** @type {Array<LH.Artifacts.GlobalListener>} */
67
+ const listeners = [];
72
68
 
73
- // And get all its listeners of interest.
74
- const {listeners} = await session.sendCommand('DOMDebugger.getEventListeners', {objectId});
75
- const filteredListeners = listeners.filter(GlobalListeners._filterForAllowlistedTypes)
76
- .map(listener => {
77
- const {type, scriptId, lineNumber, columnNumber} = listener;
78
- return {
79
- type,
80
- scriptId,
81
- lineNumber,
82
- columnNumber,
83
- };
84
- });
69
+ for (const executionContext of passContext.driver.targetManager.mainFrameExecutionContexts()) {
70
+ // Get a RemoteObject handle to `window`.
71
+ let objectId;
72
+ try {
73
+ const {result} = await session.sendCommand('Runtime.evaluate', {
74
+ expression: 'window',
75
+ returnByValue: false,
76
+ uniqueContextId: executionContext.uniqueId,
77
+ });
78
+ if (!result.objectId) {
79
+ throw new Error('Error fetching information about the global object');
80
+ }
81
+ objectId = result.objectId;
82
+ } catch (err) {
83
+ // Execution context is no longer valid, but don't let that fail the gatherer.
84
+ log.warn('Execution context is no longer valid', executionContext, err);
85
+ continue;
86
+ }
87
+
88
+ // And get all its listeners of interest.
89
+ const response = await session.sendCommand('DOMDebugger.getEventListeners', {objectId});
90
+ for (const listener of response.listeners) {
91
+ if (GlobalListeners._filterForAllowlistedTypes(listener)) {
92
+ const {type, scriptId, lineNumber, columnNumber} = listener;
93
+ listeners.push({
94
+ type,
95
+ scriptId,
96
+ lineNumber,
97
+ columnNumber,
98
+ });
99
+ }
100
+ }
101
+ }
85
102
 
86
103
  // Dedupe listeners with same underlying data.
87
- return this.dedupeListeners(filteredListeners);
104
+ return this.dedupeListeners(listeners);
88
105
  }
89
106
  }
90
107
 
@@ -43,6 +43,7 @@ export class Driver implements LH.Gatherer.FRTransitionalDriver {
43
43
  private evaluateAsync;
44
44
  targetManager: {
45
45
  rootSession: () => Driver;
46
+ mainFrameExecutionContexts: () => LH.Crdp.Runtime.ExecutionContextDescription[];
46
47
  /**
47
48
  * Bind to *any* protocol event.
48
49
  * @param {'protocolevent'} event
@@ -101,6 +101,17 @@ class Driver {
101
101
  rootSession: () => {
102
102
  return this.defaultSession;
103
103
  },
104
+ // For legacy driver, only bother supporting access to the default execution context.
105
+ mainFrameExecutionContexts: () => {
106
+ // @ts-expect-error - undefined ids are OK for purposes of calling protocol commands like Runtime.evaluate.
107
+ return [/** @type {LH.Crdp.Runtime.ExecutionContextDescription} */({
108
+ id: undefined,
109
+ uniqueId: undefined,
110
+ origin: '',
111
+ name: '',
112
+ auxData: {isDefault: true, type: 'default', frameId: ''},
113
+ })];
114
+ },
104
115
  /**
105
116
  * Bind to *any* protocol event.
106
117
  * @param {'protocolevent'} event
@@ -52,11 +52,22 @@ class NetworkAnalyzer {
52
52
  static getSummary(values) {
53
53
  values.sort((a, b) => a - b);
54
54
 
55
+ let median;
56
+ if (values.length === 0) {
57
+ median = values[0];
58
+ } else if (values.length % 2 === 0) {
59
+ const a = values[Math.floor((values.length - 1) / 2)];
60
+ const b = values[Math.floor((values.length - 1) / 2) + 1];
61
+ median = (a + b) / 2;
62
+ } else {
63
+ median = values[Math.floor((values.length - 1) / 2)];
64
+ }
65
+
55
66
  return {
56
67
  min: values[0],
57
68
  max: values[values.length - 1],
58
69
  avg: values.reduce((a, b) => a + b, 0) / values.length,
59
- median: values[Math.floor((values.length - 1) / 2)],
70
+ median,
60
71
  };
61
72
  }
62
73
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "10.2.0-dev.20230518",
4
+ "version": "10.2.0-dev.20230520",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -47,6 +47,7 @@ declare module Gatherer {
47
47
  url: () => Promise<string>;
48
48
  targetManager: {
49
49
  rootSession(): FRProtocolSession;
50
+ mainFrameExecutionContexts(): Array<Crdp.Runtime.ExecutionContextDescription>;
50
51
  on(event: 'protocolevent', callback: (payload: Protocol.RawEventMessage) => void): void
51
52
  off(event: 'protocolevent', callback: (payload: Protocol.RawEventMessage) => void): void
52
53
  };