lighthouse 11.7.0-dev.20240414 → 11.7.0-dev.20240416

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.
@@ -26,6 +26,8 @@ const exclusions = {
26
26
  'metrics-tricky-tti', 'metrics-tricky-tti-late-fcp', 'screenshot',
27
27
  // Disabled because of differences that need further investigation
28
28
  'byte-efficiency', 'byte-gzip', 'perf-preload',
29
+ // Disabled because a renderer crash also breaks devtools frontend
30
+ 'crash',
29
31
  ],
30
32
  };
31
33
 
@@ -7,6 +7,7 @@
7
7
  import a11y from './test-definitions/a11y.js';
8
8
  import byteEfficiency from './test-definitions/byte-efficiency.js';
9
9
  import byteGzip from './test-definitions/byte-gzip.js';
10
+ import crash from './test-definitions/crash.js';
10
11
  import cspAllowAll from './test-definitions/csp-allow-all.js';
11
12
  import cspBlockAll from './test-definitions/csp-block-all.js';
12
13
  import dbw from './test-definitions/dobetterweb.js';
@@ -65,6 +66,7 @@ const smokeTests = [
65
66
  a11y,
66
67
  byteEfficiency,
67
68
  byteGzip,
69
+ crash,
68
70
  cspAllowAll,
69
71
  cspBlockAll,
70
72
  dbw,
@@ -84,6 +84,18 @@ const BLOCKLIST = new Set([
84
84
  'மேலும் தரவுகளுக்கு',
85
85
  'தயவுசெய்து இங்கே அழுத்தவும்',
86
86
  'இங்கே கிளிக் செய்யவும்',
87
+ // Persian
88
+ 'اطلاعات بیشتر',
89
+ 'اطلاعات',
90
+ 'این',
91
+ 'اینجا بزنید',
92
+ 'اینجا کلیک کنید',
93
+ 'اینجا',
94
+ 'برو',
95
+ 'بیشتر بخوانید',
96
+ 'بیشتر بدانید',
97
+ 'بیشتر',
98
+ 'شروع',
87
99
  ]);
88
100
 
89
101
  const UIStrings = {
@@ -9,7 +9,6 @@ import {PageDependencyGraph as LanternPageDependencyGraph} from '../lib/lantern/
9
9
  import {NetworkRequest} from '../lib/network-request.js';
10
10
  import {ProcessedTrace} from './processed-trace.js';
11
11
  import {NetworkRecords} from './network-records.js';
12
- import {DocumentUrls} from './document-urls.js';
13
12
 
14
13
  /** @typedef {import('../lib/lantern/base-node.js').Node<LH.Artifacts.NetworkRequest>} Node */
15
14
 
@@ -20,16 +19,12 @@ class PageDependencyGraph {
20
19
  * @return {Promise<Node>}
21
20
  */
22
21
  static async compute_(data, context) {
23
- const {trace, devtoolsLog} = data;
22
+ const {trace, devtoolsLog, URL} = data;
24
23
  const [processedTrace, networkRecords] = await Promise.all([
25
24
  ProcessedTrace.request(trace, context),
26
25
  NetworkRecords.request(devtoolsLog, context),
27
26
  ]);
28
27
 
29
- // COMPAT: Backport for pre-10.0 clients that don't pass the URL artifact here (e.g. pubads).
30
- // Calculates the URL artifact from the processed trace and DT log.
31
- const URL = data.URL || await DocumentUrls.request(data, context);
32
-
33
28
  const mainThreadEvents = processedTrace.mainThreadEvents;
34
29
  const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest);
35
30
  return LanternPageDependencyGraph.createGraph(mainThreadEvents, lanternRequests, URL);
@@ -122,7 +122,10 @@ async function gotoURL(driver, requestor, options) {
122
122
  throw new Error('Cannot wait for FCP without waiting for page load');
123
123
  }
124
124
 
125
- const waitConditions = await Promise.all(waitConditionPromises);
125
+ const waitConditions = await Promise.race([
126
+ driver.fatalRejection.promise,
127
+ Promise.all(waitConditionPromises),
128
+ ]);
126
129
  const timedOut = waitConditions.some(condition => condition.timedOut);
127
130
  const navigationUrls = await networkMonitor.getNavigationUrls();
128
131
 
@@ -45,7 +45,7 @@ async function enableAsyncStacks(session) {
45
45
  await enable();
46
46
 
47
47
  return async () => {
48
- await session.sendCommand('Debugger.disable');
48
+ await session.sendCommandAndIgnore('Debugger.disable');
49
49
  session.off('Debugger.paused', onDebuggerPaused);
50
50
  session.off('Page.frameNavigated', onFrameNavigated);
51
51
  };
@@ -266,8 +266,9 @@ class TargetManager extends ProtocolEventEmitter {
266
266
  cdpSession.off('sessionattached', this._onSessionAttached);
267
267
  }
268
268
 
269
- await this._rootCdpSession.send('Page.disable');
270
- await this._rootCdpSession.send('Runtime.disable');
269
+ // Ignore failures on these in case the tab has crashed.
270
+ await this._rootCdpSession.send('Page.disable').catch(_ => {});
271
+ await this._rootCdpSession.send('Runtime.disable').catch(_ => {});
271
272
 
272
273
  this._enabled = false;
273
274
  this._targetIdToTargets = new Map();
@@ -14,6 +14,10 @@ export class Driver implements LH.Gatherer.Driver {
14
14
  /** @type {Fetcher|undefined} */
15
15
  _fetcher: Fetcher | undefined;
16
16
  defaultSession: import("../../types/gatherer.js").default.ProtocolSession;
17
+ fatalRejection: {
18
+ promise: Promise<never>;
19
+ rej: (_: Error) => void;
20
+ };
17
21
  /** @return {LH.Gatherer.Driver['executionContext']} */
18
22
  get executionContext(): ExecutionContext;
19
23
  get fetcher(): any;
@@ -23,6 +27,14 @@ export class Driver implements LH.Gatherer.Driver {
23
27
  url(): Promise<string>;
24
28
  /** @return {Promise<void>} */
25
29
  connect(): Promise<void>;
30
+ /**
31
+ * If the target crashes, we can't continue gathering.
32
+ *
33
+ * FWIW, if the target unexpectedly detaches (eg the user closed the tab), pptr will
34
+ * catch that and reject into our this._cdpSession.send, which we'll alrady handle appropriately
35
+ * @return {void}
36
+ */
37
+ listenForCrashes(): void;
26
38
  /** @return {Promise<void>} */
27
39
  disconnect(): Promise<void>;
28
40
  }
@@ -8,6 +8,7 @@ import log from 'lighthouse-logger';
8
8
 
9
9
  import {ExecutionContext} from './driver/execution-context.js';
10
10
  import {TargetManager} from './driver/target-manager.js';
11
+ import {LighthouseError} from '../lib/lh-error.js';
11
12
  import {Fetcher} from './fetcher.js';
12
13
  import {NetworkMonitor} from './driver/network-monitor.js';
13
14
 
@@ -47,6 +48,12 @@ class Driver {
47
48
  this._fetcher = undefined;
48
49
 
49
50
  this.defaultSession = throwingSession;
51
+
52
+ // Poor man's Promise.withResolvers()
53
+ /** @param {Error} _ */
54
+ let rej = _ => {};
55
+ const promise = /** @type {Promise<never>} */ (new Promise((_, theRej) => rej = theRej));
56
+ this.fatalRejection = {promise, rej};
50
57
  }
51
58
 
52
59
  /** @return {LH.Gatherer.Driver['executionContext']} */
@@ -86,11 +93,30 @@ class Driver {
86
93
  this._networkMonitor = new NetworkMonitor(this._targetManager);
87
94
  await this._networkMonitor.enable();
88
95
  this.defaultSession = this._targetManager.rootSession();
96
+ this.listenForCrashes();
89
97
  this._executionContext = new ExecutionContext(this.defaultSession);
90
98
  this._fetcher = new Fetcher(this.defaultSession);
91
99
  log.timeEnd(status);
92
100
  }
93
101
 
102
+ /**
103
+ * If the target crashes, we can't continue gathering.
104
+ *
105
+ * FWIW, if the target unexpectedly detaches (eg the user closed the tab), pptr will
106
+ * catch that and reject into our this._cdpSession.send, which we'll alrady handle appropriately
107
+ * @return {void}
108
+ */
109
+ listenForCrashes() {
110
+ this.defaultSession.on('Inspector.targetCrashed', async _ => {
111
+ log.error('Driver', 'Inspector.targetCrashed');
112
+ // Manually detach so no more CDP traffic is attempted.
113
+ // Don't await, else our rejection will be a 'Target closed' protocol error on cross-talk CDP calls.
114
+ void this.defaultSession.dispose();
115
+ this.fatalRejection.rej(new LighthouseError(LighthouseError.errors.TARGET_CRASHED));
116
+ });
117
+ }
118
+
119
+
94
120
  /** @return {Promise<void>} */
95
121
  async disconnect() {
96
122
  if (this.defaultSession === throwingSession) return;
@@ -92,7 +92,9 @@ async function _navigate(navigationContext) {
92
92
  return {requestedUrl, mainDocumentUrl, navigationError: undefined};
93
93
  } catch (err) {
94
94
  if (!(err instanceof LighthouseError)) throw err;
95
- if (err.code !== 'NO_FCP' && err.code !== 'PAGE_HUNG') throw err;
95
+ if (err.code !== 'NO_FCP' && err.code !== 'PAGE_HUNG' && err.code !== 'TARGET_CRASHED') {
96
+ throw err;
97
+ }
96
98
  if (typeof requestor !== 'string') throw err;
97
99
 
98
100
  // TODO: Make the urls optional here so we don't need to throw an error with a callback requestor.
@@ -100,6 +100,7 @@ class ProtocolSession extends CrdpEventEmitter {
100
100
  /** @type {NodeJS.Timer|undefined} */
101
101
  let timeout;
102
102
  const timeoutPromise = new Promise((resolve, reject) => {
103
+ // Unexpected setTimeout invocation to preserve the error stack. https://github.com/GoogleChrome/lighthouse/issues/13332
103
104
  // eslint-disable-next-line max-len
104
105
  timeout = setTimeout(reject, timeoutMs, new LighthouseError(LighthouseError.errors.PROTOCOL_TIMEOUT, {
105
106
  protocolMethod: method,
@@ -139,7 +140,7 @@ class ProtocolSession extends CrdpEventEmitter {
139
140
  async dispose() {
140
141
  // @ts-expect-error Puppeteer expects the handler params to be type `unknown`
141
142
  this._cdpSession.off('*', this._handleProtocolEvent);
142
- await this._cdpSession.detach();
143
+ await this._cdpSession.detach().catch(e => log.verbose('session', 'detach failed', e.message));
143
144
  }
144
145
  }
145
146
 
@@ -128,7 +128,7 @@ function enableNetworkThrottling(session, throttlingSettings) {
128
128
  * @return {Promise<void>}
129
129
  */
130
130
  function clearNetworkThrottling(session) {
131
- return session.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
131
+ return session.sendCommandAndIgnore('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
132
132
  }
133
133
 
134
134
  /**
@@ -146,7 +146,7 @@ function enableCPUThrottling(session, throttlingSettings) {
146
146
  * @return {Promise<void>}
147
147
  */
148
148
  function clearCPUThrottling(session) {
149
- return session.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
149
+ return session.sendCommandAndIgnore('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
150
150
  }
151
151
 
152
152
  export {
@@ -7,8 +7,8 @@
7
7
  import * as Lantern from '../types/lantern.js';
8
8
  import {Metric} from '../metric.js';
9
9
  import {BaseNode} from '../base-node.js';
10
- // TODO(15841): move this default config value into lib/lantern
11
- import {throttling as defaultThrottling} from '../../../config/constants.js';
10
+
11
+ const mobileSlow4GRtt = 150;
12
12
 
13
13
  /** @typedef {import('../base-node.js').Node} Node */
14
14
 
@@ -44,7 +44,7 @@ class SpeedIndex extends Metric {
44
44
  // lantern test data set. See core/scripts/test-lantern.sh for more detail.
45
45
  // While the coefficients haven't been analyzed at the interpolated points, it's our current best effort.
46
46
  const defaultCoefficients = this.COEFFICIENTS;
47
- const defaultRttExcess = defaultThrottling.mobileSlow4G.rttMs - 30;
47
+ const defaultRttExcess = mobileSlow4GRtt - 30;
48
48
  const multiplier = Math.max((rttMs - 30) / defaultRttExcess, 0);
49
49
 
50
50
  return {
@@ -140,15 +140,6 @@ class NetworkAnalyzer {
140
140
  const {timing, connectionReused, record} = info;
141
141
  if (connectionReused) return;
142
142
 
143
- // In LR, network records are missing connection timing, but we've smuggled it in via headers.
144
- if (global.isLightrider && record.lrStatistics) {
145
- if (record.protocol.startsWith('h3')) {
146
- return record.lrStatistics.TCPMs;
147
- } else {
148
- return [record.lrStatistics.TCPMs / 2, record.lrStatistics.TCPMs / 2];
149
- }
150
- }
151
-
152
143
  const {connectStart, sslStart, sslEnd, connectEnd} = timing;
153
144
  if (connectEnd >= 0 && connectStart >= 0 && record.protocol.startsWith('h3')) {
154
145
  // These values are equal to sslStart and sslEnd for h3.
@@ -256,9 +247,7 @@ class NetworkAnalyzer {
256
247
  */
257
248
  static _estimateResponseTimeByOrigin(records, rttByOrigin) {
258
249
  return NetworkAnalyzer._estimateValueByOrigin(records, ({record, timing}) => {
259
- // Lightrider does not have timings for sendEnd, but we do have this timing which should be
260
- // close to the response time.
261
- if (global.isLightrider && record.lrStatistics) return record.lrStatistics.requestMs;
250
+ if (record.serverResponseTime !== undefined) return record.serverResponseTime;
262
251
 
263
252
  if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
264
253
  if (!Number.isFinite(timing.sendEnd) || timing.sendEnd < 0) return;
@@ -67,9 +67,6 @@ export class NetworkRequest<T = any> {
67
67
  resourceSize: number;
68
68
  fromDiskCache: boolean;
69
69
  fromMemoryCache: boolean;
70
- // TODO(15841): remove from lantern.
71
- /** Extra timing information available only when run in Lightrider. */
72
- lrStatistics: LightriderStatistics | undefined;
73
70
  finished: boolean;
74
71
  statusCode: number;
75
72
  /** The network request that this one redirected to */
@@ -80,6 +77,11 @@ export class NetworkRequest<T = any> {
80
77
  /** The chain of network requests that redirected to this one */
81
78
  redirects: NetworkRequest[] | undefined;
82
79
  timing: LH.Crdp.Network.ResourceTiming | undefined;
80
+ /**
81
+ * Optional value for how long the server took to respond to this request.
82
+ * When not provided, the server response time is derived from the timing object.
83
+ */
84
+ serverResponseTime: number | undefined;
83
85
  resourceType: LH.Crdp.Network.ResourceType | undefined;
84
86
  mimeType: string;
85
87
  priority: LH.Crdp.Network.ResourcePriority;
@@ -105,6 +105,7 @@ export namespace UIStrings {
105
105
  const missingRequiredArtifact: string;
106
106
  const erroredRequiredArtifact: string;
107
107
  const oldChromeDoesNotSupportFeature: string;
108
+ const targetCrashed: string;
108
109
  }
109
110
  declare namespace ERRORS {
110
111
  namespace NO_SPEEDLINE_FRAMES {
@@ -340,6 +341,14 @@ declare namespace ERRORS {
340
341
  import message_30 = UIStrings.erroredRequiredArtifact;
341
342
  export { message_30 as message };
342
343
  }
344
+ namespace TARGET_CRASHED {
345
+ const code_31: string;
346
+ export { code_31 as code };
347
+ import message_31 = UIStrings.targetCrashed;
348
+ export { message_31 as message };
349
+ const lhrRuntimeError_22: boolean;
350
+ export { lhrRuntimeError_22 as lhrRuntimeError };
351
+ }
343
352
  }
344
353
  export {};
345
354
  //# sourceMappingURL=lh-error.d.ts.map
@@ -91,6 +91,9 @@ const UIStrings = {
91
91
  * @example {Largest Contentful Paint} featureName
92
92
  * */
93
93
  oldChromeDoesNotSupportFeature: 'This version of Chrome is too old to support \'{featureName}\'. Use a newer version to see full results.',
94
+
95
+ /** Error message explaining that the browser tab that Lighthouse is inspecting has crashed. */
96
+ targetCrashed: 'Browser tab has unexpectedly crashed.',
94
97
  };
95
98
 
96
99
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
@@ -423,6 +426,13 @@ const ERRORS = {
423
426
  message: UIStrings.erroredRequiredArtifact,
424
427
  },
425
428
 
429
+ /** The page has crashed and will no longer respond to 99% of CDP commmands. */
430
+ TARGET_CRASHED: {
431
+ code: 'TARGET_CRASHED',
432
+ message: UIStrings.targetCrashed,
433
+ lhrRuntimeError: true,
434
+ },
435
+
426
436
  // Hey! When adding a new error type, update lighthouse-result.proto too.
427
437
  // Only necessary for runtime errors, which come from artifacts or pageLoadErrors.
428
438
  };
@@ -150,6 +150,9 @@ function getPageLoadError(navigationError, context) {
150
150
  let finalRecord;
151
151
  if (mainRecord) {
152
152
  finalRecord = NetworkAnalyzer.resolveRedirects(mainRecord);
153
+ } else {
154
+ // We have no network requests to process, use the navError
155
+ return navigationError;
153
156
  }
154
157
 
155
158
  if (finalRecord?.mimeType === XHTML_MIME_TYPE) {
@@ -247,6 +247,7 @@ export class NetworkRequest {
247
247
  * is passed in via X-Headers similar to 'X-TotalFetchedSize'.
248
248
  */
249
249
  _updateTimingsForLightrider(): void;
250
+ serverResponseTime: number | undefined;
250
251
  }
251
252
  export namespace NetworkRequest {
252
253
  export { HEADER_TCP };
@@ -536,6 +536,7 @@ class NetworkRequest {
536
536
  requestMs: requestMs,
537
537
  responseMs: responseMs,
538
538
  };
539
+ this.serverResponseTime = responseMs;
539
540
  }
540
541
 
541
542
  /**
@@ -574,8 +575,35 @@ class NetworkRequest {
574
575
  * @return {Lantern.NetworkRequest<NetworkRequest>}
575
576
  */
576
577
  static asLanternNetworkRequest(record) {
578
+ // In LR, network records are missing connection timing, but we've smuggled it in via headers.
579
+ let timing = record.timing;
580
+ let serverResponseTime;
581
+ if (global.isLightrider && record.lrStatistics) {
582
+ if (record.protocol.startsWith('h3')) {
583
+ // @ts-expect-error We don't need all the properties set.
584
+ timing = {
585
+ connectStart: 0,
586
+ connectEnd: record.lrStatistics.TCPMs,
587
+ };
588
+ } else {
589
+ // @ts-expect-error We don't need all the properties set.
590
+ timing = {
591
+ connectStart: 0,
592
+ sslStart: record.lrStatistics.TCPMs / 2,
593
+ connectEnd: record.lrStatistics.TCPMs,
594
+ sslEnd: record.lrStatistics.TCPMs,
595
+ };
596
+
597
+ // Lightrider does not have timings for sendEnd, but we do have this timing which should be
598
+ // close to the response time.
599
+ serverResponseTime = record.lrStatistics.requestMs;
600
+ }
601
+ }
602
+
577
603
  return {
578
604
  ...record,
605
+ timing,
606
+ serverResponseTime,
579
607
  record,
580
608
  };
581
609
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "11.7.0-dev.20240414",
4
+ "version": "11.7.0-dev.20240416",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -165,7 +165,7 @@
165
165
  "pako": "^2.0.3",
166
166
  "preact": "^10.7.2",
167
167
  "pretty-json-stringify": "^0.0.2",
168
- "puppeteer": "^22.5.0",
168
+ "puppeteer": "^22.6.5",
169
169
  "resolve": "^1.22.1",
170
170
  "rollup": "^2.52.7",
171
171
  "rollup-plugin-polyfill-node": "^0.12.0",
@@ -199,7 +199,7 @@
199
199
  "open": "^8.4.0",
200
200
  "parse-cache-control": "1.0.1",
201
201
  "ps-list": "^8.0.0",
202
- "puppeteer-core": "^22.5.0",
202
+ "puppeteer-core": "^22.6.5",
203
203
  "robots-parser": "^3.0.1",
204
204
  "semver": "^5.3.0",
205
205
  "speedline-core": "^1.4.3",
@@ -2471,6 +2471,9 @@
2471
2471
  "core/lib/lh-error.js | requestContentTimeout": {
2472
2472
  "message": "Fetching resource content has exceeded the allotted time"
2473
2473
  },
2474
+ "core/lib/lh-error.js | targetCrashed": {
2475
+ "message": "Browser tab has unexpectedly crashed."
2476
+ },
2474
2477
  "core/lib/lh-error.js | urlInvalid": {
2475
2478
  "message": "The URL you have provided appears to be invalid."
2476
2479
  },
@@ -2471,6 +2471,9 @@
2471
2471
  "core/lib/lh-error.js | requestContentTimeout": {
2472
2472
  "message": "F̂ét̂ćĥín̂ǵ r̂éŝóûŕĉé ĉón̂t́êńt̂ h́âś êx́ĉéêd́êd́ t̂h́ê ál̂ĺôt́t̂éd̂ t́îḿê"
2473
2473
  },
2474
+ "core/lib/lh-error.js | targetCrashed": {
2475
+ "message": "B̂ŕôẃŝér̂ t́âb́ ĥáŝ ún̂éx̂ṕêćt̂éd̂ĺŷ ćr̂áŝh́êd́."
2476
+ },
2474
2477
  "core/lib/lh-error.js | urlInvalid": {
2475
2478
  "message": "T̂h́ê ÚR̂Ĺ ŷóû h́âv́ê ṕr̂óv̂íd̂éd̂ áp̂ṕêár̂ś t̂ó b̂é îńv̂ál̂íd̂."
2476
2479
  },
@@ -49,6 +49,8 @@ declare module Gatherer {
49
49
  off(event: 'protocolevent', callback: (payload: Protocol.RawEventMessage) => void): void
50
50
  };
51
51
  networkMonitor: NetworkMonitor;
52
+ listenForCrashes: (() => void);
53
+ fatalRejection: {promise: Promise<never>, rej: (reason: Error) => void}
52
54
  }
53
55
 
54
56
  interface Context<TDependencies extends DependencyKey = DefaultDependenciesKey> {