lighthouse 9.5.0 → 9.6.1

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 (62) hide show
  1. package/changelog.md +70 -0
  2. package/dist/report/bundle.esm.js +23 -366
  3. package/dist/report/flow.js +20 -25
  4. package/dist/report/standalone.js +17 -22
  5. package/lighthouse-cli/test/smokehouse/core-tests.js +2 -0
  6. package/lighthouse-cli/test/smokehouse/frontends/lib.js +1 -3
  7. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +1 -3
  8. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -0
  9. package/lighthouse-cli/test/smokehouse/readme.md +5 -4
  10. package/lighthouse-cli/test/smokehouse/report-assert.js +16 -13
  11. package/lighthouse-cli/test/smokehouse/version-check-test.js +46 -0
  12. package/lighthouse-cli/test/smokehouse/version-check.js +49 -0
  13. package/lighthouse-core/audits/bootup-time.js +2 -70
  14. package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +3 -5
  15. package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +2 -5
  16. package/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +4 -2
  17. package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +3 -1
  18. package/lighthouse-core/audits/deprecations.js +603 -28
  19. package/lighthouse-core/audits/installable-manifest.js +27 -6
  20. package/lighthouse-core/audits/long-tasks.js +3 -3
  21. package/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js +88 -0
  22. package/lighthouse-core/audits/third-party-summary.js +3 -3
  23. package/lighthouse-core/audits/work-during-interaction.js +280 -0
  24. package/lighthouse-core/computed/metrics/responsiveness.js +157 -0
  25. package/lighthouse-core/config/config-helpers.js +1 -1
  26. package/lighthouse-core/config/metrics-to-audits.js +5 -0
  27. package/lighthouse-core/fraggle-rock/config/default-config.js +9 -1
  28. package/lighthouse-core/fraggle-rock/gather/base-artifacts.js +2 -2
  29. package/lighthouse-core/fraggle-rock/gather/session.js +8 -2
  30. package/lighthouse-core/gather/gather-runner.js +2 -4
  31. package/lighthouse-core/gather/gatherers/css-usage.js +59 -34
  32. package/lighthouse-core/gather/gatherers/trace-elements.js +21 -2
  33. package/lighthouse-core/lib/arbitrary-equality-map.js +2 -2
  34. package/lighthouse-core/lib/i18n/i18n.js +2 -0
  35. package/lighthouse-core/lib/minify-trace.js +2 -0
  36. package/lighthouse-core/lib/script-helpers.js +24 -0
  37. package/lighthouse-core/lib/stack-packs.js +8 -5
  38. package/lighthouse-core/lib/tracehouse/task-groups.js +1 -0
  39. package/lighthouse-core/lib/tracehouse/task-summary.js +87 -0
  40. package/lighthouse-core/lib/tracehouse/trace-processor.js +39 -14
  41. package/lighthouse-core/runner.js +1 -1
  42. package/package.json +10 -8
  43. package/report/assets/styles.css +5 -2
  44. package/report/clients/bundle.js +1 -0
  45. package/report/renderer/components.js +1 -1
  46. package/report/renderer/details-renderer.js +4 -4
  47. package/report/renderer/performance-category-renderer.js +15 -11
  48. package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +6 -98
  49. package/report/test/renderer/details-renderer-test.js +9 -9
  50. package/report/test/renderer/performance-category-renderer-test.js +31 -0
  51. package/report/test/renderer/report-renderer-axe-test.js +3 -15
  52. package/report/test-assets/faux-psi-template.html +3 -2
  53. package/report/test-assets/faux-psi.js +22 -1
  54. package/shared/localization/locales/en-US.json +244 -4
  55. package/shared/localization/locales/en-XL.json +244 -4
  56. package/shared/localization/swap-locale.js +2 -1
  57. package/third-party/chromium-synchronization/installability-errors-test.js +1 -0
  58. package/third-party/snyk/snapshot.json +2 -3
  59. package/tsconfig.json +1 -0
  60. package/types/artifacts.d.ts +40 -1
  61. package/types/lhr/audit-details.d.ts +3 -1
  62. package/types/smokehouse.d.ts +1 -1
@@ -6,17 +6,23 @@
6
6
  'use strict';
7
7
 
8
8
  const legacyDefaultConfig = require('../../config/default-config.js');
9
+ const m2a = require('../../config/metrics-to-audits.js');
9
10
  const {deepClone} = require('../../config/config-helpers.js');
10
11
 
11
12
  /** @type {LH.Config.AuditJson[]} */
12
13
  const frAudits = [
13
14
  'byte-efficiency/uses-responsive-images-snapshot',
15
+ 'metrics/experimental-interaction-to-next-paint',
16
+ 'work-during-interaction',
14
17
  ];
15
18
 
16
19
  /** @type {Record<string, LH.Config.AuditRefJson[]>} */
17
20
  const frCategoryAuditRefExtensions = {
18
21
  'performance': [
19
22
  {id: 'uses-responsive-images-snapshot', weight: 0},
23
+ {id: 'experimental-interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP',
24
+ relevantAudits: m2a.inpRelevantAudits},
25
+ {id: 'work-during-interaction', weight: 0},
20
26
  ],
21
27
  };
22
28
 
@@ -97,7 +103,6 @@ const defaultConfig = {
97
103
  {id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'},
98
104
  {id: artifacts.FontSize, gatherer: 'seo/font-size'},
99
105
  {id: artifacts.Inputs, gatherer: 'inputs'},
100
- {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
101
106
  {id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
102
107
  {id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
103
108
  {id: artifacts.ImageElements, gatherer: 'image-elements'},
@@ -126,6 +131,9 @@ const defaultConfig = {
126
131
  // Artifact copies are renamed for compatibility with legacy artifacts.
127
132
  {id: artifacts.devtoolsLogs, gatherer: 'devtools-log-compat'},
128
133
  {id: artifacts.traces, gatherer: 'trace-compat'},
134
+
135
+ // FullPageScreenshot comes at the very end so all other node analysis is captured.
136
+ {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
129
137
  ],
130
138
  navigations: [
131
139
  {
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const log = require('lighthouse-logger');
9
- const {isEqual} = require('lodash');
9
+ const isDeepEqual = require('lodash/isEqual.js');
10
10
  const {
11
11
  getBrowserVersion,
12
12
  getBenchmarkIndex,
@@ -58,7 +58,7 @@ function deduplicateWarnings(warnings) {
58
58
  const unique = [];
59
59
 
60
60
  for (const warning of warnings) {
61
- if (unique.some(existing => isEqual(warning, existing))) continue;
61
+ if (unique.some(existing => isDeepEqual(warning, existing))) continue;
62
62
  unique.push(warning);
63
63
  }
64
64
 
@@ -96,7 +96,7 @@ class ProtocolSession {
96
96
  /** @param {import('puppeteer').CDPSession} session */
97
97
  const listener = session => callback(new ProtocolSession(session));
98
98
  this._callbackMap.set(callback, listener);
99
- this._session.connection().on('sessionattached', listener);
99
+ this._getConnection().on('sessionattached', listener);
100
100
  }
101
101
 
102
102
  /**
@@ -106,7 +106,7 @@ class ProtocolSession {
106
106
  removeSessionAttachedListener(callback) {
107
107
  const listener = this._callbackMap.get(callback);
108
108
  if (!listener) return;
109
- this._session.connection().off('sessionattached', listener);
109
+ this._getConnection().off('sessionattached', listener);
110
110
  }
111
111
 
112
112
  /**
@@ -171,6 +171,12 @@ class ProtocolSession {
171
171
  this._session.removeAllListeners();
172
172
  await this._session.detach();
173
173
  }
174
+
175
+ _getConnection() {
176
+ const connection = this._session.connection();
177
+ if (!connection) throw new Error('Connection has been closed.');
178
+ return connection;
179
+ }
174
180
  }
175
181
 
176
182
  module.exports = ProtocolSession;
@@ -438,10 +438,8 @@ class GatherRunner {
438
438
  }
439
439
 
440
440
  try {
441
- if (baseArtifacts.WebAppManifest) {
442
- baseArtifacts.InstallabilityErrors = await InstallabilityErrors.getInstallabilityErrors(
443
- passContext.driver.defaultSession);
444
- }
441
+ baseArtifacts.InstallabilityErrors = await InstallabilityErrors.getInstallabilityErrors(
442
+ passContext.driver.defaultSession);
445
443
  } catch (err) {
446
444
  log.error('GatherRunner InstallabilityErrors', err);
447
445
  baseArtifacts.InstallabilityErrors = {
@@ -5,7 +5,9 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
+ const log = require('lighthouse-logger');
8
9
  const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
10
+ const Sentry = require('../../lib/sentry.js');
9
11
 
10
12
  /**
11
13
  * @fileoverview Tracks unused CSS rules.
@@ -13,21 +15,16 @@ const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
13
15
  class CSSUsage extends FRGatherer {
14
16
  constructor() {
15
17
  super();
16
- /** @type {Array<LH.Crdp.CSS.StyleSheetAddedEvent>} */
17
- this._stylesheets = [];
18
- /** @param {LH.Crdp.CSS.StyleSheetAddedEvent} sheet */
19
- this._onStylesheetAdded = sheet => this._stylesheets.push(sheet);
20
- /** @param {LH.Crdp.CSS.StyleSheetRemovedEvent} sheet */
21
- this._onStylesheetRemoved = sheet => {
22
- // We can't fetch the content of removed stylesheets, so we ignore them completely.
23
- const styleSheetId = sheet.styleSheetId;
24
- this._stylesheets = this._stylesheets.filter(s => s.header.styleSheetId !== styleSheetId);
25
- };
18
+ /** @type {LH.Gatherer.FRProtocolSession|undefined} */
19
+ this._session = undefined;
20
+ /** @type {Map<string, Promise<LH.Artifacts.CSSStyleSheetInfo|Error>>} */
21
+ this._sheetPromises = new Map();
26
22
  /**
27
23
  * Initialize as undefined so we can assert results are fetched.
28
24
  * @type {LH.Crdp.CSS.RuleUsage[]|undefined}
29
25
  */
30
26
  this._ruleUsage = undefined;
27
+ this._onStylesheetAdded = this._onStylesheetAdded.bind(this);
31
28
  }
32
29
 
33
30
  /** @type {LH.Gatherer.GathererMeta} */
@@ -35,26 +32,49 @@ class CSSUsage extends FRGatherer {
35
32
  supportedModes: ['snapshot', 'timespan', 'navigation'],
36
33
  };
37
34
 
35
+ /**
36
+ * @param {LH.Crdp.CSS.StyleSheetAddedEvent} event
37
+ */
38
+ async _onStylesheetAdded(event) {
39
+ if (!this._session) throw new Error('Session not initialized');
40
+ const styleSheetId = event.header.styleSheetId;
41
+ const sheetPromise = this._session.sendCommand('CSS.getStyleSheetText', {styleSheetId})
42
+ .then(content => ({
43
+ header: event.header,
44
+ content: content.text,
45
+ }))
46
+ .catch(/** @param {Error} err */ (err) => {
47
+ log.warn(
48
+ 'CSSUsage',
49
+ `Error fetching content of stylesheet with URL "${event.header.sourceURL}"`
50
+ );
51
+ Sentry.captureException(err, {
52
+ tags: {
53
+ gatherer: this.name,
54
+ },
55
+ extra: {
56
+ url: event.header.sourceURL,
57
+ },
58
+ level: 'error',
59
+ });
60
+ return err;
61
+ });
62
+ this._sheetPromises.set(styleSheetId, sheetPromise);
63
+ }
64
+
38
65
  /**
39
66
  * @param {LH.Gatherer.FRTransitionalContext} context
40
67
  */
41
68
  async startCSSUsageTracking(context) {
42
69
  const session = context.driver.defaultSession;
70
+ this._session = session;
43
71
  session.on('CSS.styleSheetAdded', this._onStylesheetAdded);
44
- session.on('CSS.styleSheetRemoved', this._onStylesheetRemoved);
45
72
 
46
73
  await session.sendCommand('DOM.enable');
47
74
  await session.sendCommand('CSS.enable');
48
75
  await session.sendCommand('CSS.startRuleUsageTracking');
49
76
  }
50
77
 
51
- /**
52
- * @param {LH.Gatherer.FRTransitionalContext} context
53
- */
54
- async startInstrumentation(context) {
55
- if (context.gatherMode !== 'timespan') return;
56
- await this.startCSSUsageTracking(context);
57
- }
58
78
 
59
79
  /**
60
80
  * @param {LH.Gatherer.FRTransitionalContext} context
@@ -64,7 +84,14 @@ class CSSUsage extends FRGatherer {
64
84
  const coverageResponse = await session.sendCommand('CSS.stopRuleUsageTracking');
65
85
  this._ruleUsage = coverageResponse.ruleUsage;
66
86
  session.off('CSS.styleSheetAdded', this._onStylesheetAdded);
67
- session.off('CSS.styleSheetRemoved', this._onStylesheetRemoved);
87
+ }
88
+
89
+ /**
90
+ * @param {LH.Gatherer.FRTransitionalContext} context
91
+ */
92
+ async startInstrumentation(context) {
93
+ if (context.gatherMode !== 'timespan') return;
94
+ await this.startCSSUsageTracking(context);
68
95
  }
69
96
 
70
97
  /**
@@ -93,25 +120,23 @@ class CSSUsage extends FRGatherer {
93
120
  await this.stopCSSUsageTracking(context);
94
121
  }
95
122
 
96
- // Fetch style sheet content in parallel.
97
- const promises = this._stylesheets.map(sheet => {
98
- const styleSheetId = sheet.header.styleSheetId;
99
- return session.sendCommand('CSS.getStyleSheetText', {styleSheetId}).then(content => {
100
- return {
101
- header: sheet.header,
102
- content: content.text,
103
- };
104
- });
105
- });
106
- const styleSheetInfo = await Promise.all(promises);
123
+ /** @type {Map<string, LH.Artifacts.CSSStyleSheetInfo>} */
124
+ const dedupedStylesheets = new Map();
125
+ const sheets = await Promise.all(this._sheetPromises.values());
126
+
127
+ for (const sheet of sheets) {
128
+ // Erroneous sheets will be reported via sentry and the log.
129
+ // We can ignore them here without throwing a fatal error.
130
+ if (sheet instanceof Error) {
131
+ continue;
132
+ }
133
+
134
+ dedupedStylesheets.set(sheet.content, sheet);
135
+ }
107
136
 
108
137
  await session.sendCommand('CSS.disable');
109
138
  await session.sendCommand('DOM.disable');
110
139
 
111
- const dedupedStylesheets = new Map(styleSheetInfo.map(sheet => {
112
- return [sheet.content, sheet];
113
- }));
114
-
115
140
  if (!this._ruleUsage) throw new Error('Issue collecting rule usages');
116
141
 
117
142
  return {
@@ -22,6 +22,7 @@ const Trace = require('./trace.js');
22
22
  const ProcessedTrace = require('../../computed/processed-trace.js');
23
23
  const ProcessedNavigation = require('../../computed/processed-navigation.js');
24
24
  const LighthouseError = require('../../lib/lh-error.js');
25
+ const ComputedResponsivenes = require('../../computed/metrics/responsiveness.js');
25
26
 
26
27
  /** @typedef {{nodeId: number, score?: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[]}} TraceElementData */
27
28
 
@@ -142,6 +143,23 @@ class TraceElements extends FRGatherer {
142
143
  return topFive;
143
144
  }
144
145
 
146
+ /**
147
+ * @param {LH.Trace} trace
148
+ * @param {LH.Gatherer.FRTransitionalContext} context
149
+ * @return {Promise<TraceElementData|undefined>}
150
+ */
151
+ static async getResponsivenessElement(trace, context) {
152
+ const {settings} = context;
153
+ try {
154
+ const responsivenessEvent = await ComputedResponsivenes.request({trace, settings}, context);
155
+ if (!responsivenessEvent || responsivenessEvent.name === 'FallbackTiming') return;
156
+ return {nodeId: responsivenessEvent.args.data.nodeId};
157
+ } catch {
158
+ // Don't let responsiveness errors sink the rest of the gatherer.
159
+ return;
160
+ }
161
+ }
162
+
145
163
  /**
146
164
  * Find the node ids of elements which are animated using the Animation trace events.
147
165
  * @param {Array<LH.TraceEvent>} mainThreadEvents
@@ -237,14 +255,15 @@ class TraceElements extends FRGatherer {
237
255
 
238
256
  const lcpNodeId = largestContentfulPaintEvt?.args?.data?.nodeId;
239
257
  const clsNodeData = TraceElements.getTopLayoutShiftElements(mainThreadEvents);
240
- const animatedElementData =
241
- await this.getAnimatedElements(mainThreadEvents);
258
+ const animatedElementData = await this.getAnimatedElements(mainThreadEvents);
259
+ const responsivenessElementData = await TraceElements.getResponsivenessElement(trace, context);
242
260
 
243
261
  /** @type {Map<string, TraceElementData[]>} */
244
262
  const backendNodeDataMap = new Map([
245
263
  ['largest-contentful-paint', lcpNodeId ? [{nodeId: lcpNodeId}] : []],
246
264
  ['layout-shift', clsNodeData],
247
265
  ['animation', animatedElementData],
266
+ ['responsiveness', responsivenessElementData ? [responsivenessElementData] : []],
248
267
  ]);
249
268
 
250
269
  const traceElements = [];
@@ -5,7 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const {isEqual} = require('lodash');
8
+ const isDeepEqual = require('lodash/isEqual.js');
9
9
 
10
10
  /**
11
11
  * @fileoverview This class is designed to allow maps with arbitrary equality functions.
@@ -74,7 +74,7 @@ class ArbitraryEqualityMap {
74
74
  * @return {boolean}
75
75
  */
76
76
  static deepEquals(objA, objB) {
77
- return isEqual(objA, objB);
77
+ return isDeepEqual(objA, objB);
78
78
  }
79
79
  }
80
80
 
@@ -105,6 +105,8 @@ const UIStrings = {
105
105
  largestContentfulPaintMetric: 'Largest Contentful Paint',
106
106
  /** The name of the metric "Cumulative Layout Shift" that indicates how much the page changes its layout while it loads. If big segments of the page shift their location during load, the Cumulative Layout Shift will be higher. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
107
107
  cumulativeLayoutShiftMetric: 'Cumulative Layout Shift',
108
+ /** The name of the "Interaction to Next Paint" metric that measures the time between a user interaction and when the browser displays a response on screen. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
109
+ interactionToNextPaint: 'Interaction to Next Paint',
108
110
  /** Table item value for the severity of a small, or low impact vulnerability. Part of a ranking scale in the form: low, medium, high. */
109
111
  itemSeverityLow: 'Low',
110
112
  /** Table item value for the severity of a vulnerability. Part of a ranking scale in the form: low, medium, high. */
@@ -44,6 +44,8 @@ const traceEventsToAlwaysKeep = new Set([
44
44
  'EventDispatch',
45
45
  'LayoutShift',
46
46
  'FrameCommittedInBrowser',
47
+ 'EventTiming',
48
+ 'Responsiveness.Renderer.UserInteraction',
47
49
  // Not currently used by Lighthouse but might be used in the future for cross-frame LCP
48
50
  'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM',
49
51
  'NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM',
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ /**
9
+ * @param {LH.Artifacts.NetworkRequest[]} networkRecords
10
+ * @param {LH.Artifacts.ScriptElement|undefined} script
11
+ * @return {LH.Artifacts.NetworkRequest|undefined}
12
+ */
13
+ function getRequestForScript(networkRecords, script) {
14
+ if (!script) return;
15
+ let networkRequest = script.requestId &&
16
+ networkRecords.find(request => request.requestId === script.requestId);
17
+ if (!networkRequest) networkRequest = networkRecords.find(request => request.url === script.src);
18
+ while (networkRequest?.redirectDestination) {
19
+ networkRequest = networkRequest.redirectDestination;
20
+ }
21
+ return networkRequest;
22
+ }
23
+
24
+ module.exports = {getRequestForScript};
@@ -19,11 +19,10 @@ const stackPacksToInclude = [
19
19
  packId: 'wordpress',
20
20
  requiredStacks: ['js:wordpress'],
21
21
  },
22
- // waiting for https://github.com/johnmichel/Library-Detector-for-Chrome/pull/193
23
- // {
24
- // packId: 'ezoic',
25
- // requiredStacks: ['js:ezoic'],
26
- // },
22
+ {
23
+ packId: 'ezoic',
24
+ requiredStacks: ['js:ezoic'],
25
+ },
27
26
  {
28
27
  packId: 'drupal',
29
28
  requiredStacks: ['js:drupal'],
@@ -48,6 +47,10 @@ const stackPacksToInclude = [
48
47
  packId: 'next.js',
49
48
  requiredStacks: ['js:next'],
50
49
  },
50
+ {
51
+ packId: 'nuxt',
52
+ requiredStacks: ['js:nuxt'],
53
+ },
51
54
  {
52
55
  packId: 'angular',
53
56
  requiredStacks: ['js:@angular/core'],
@@ -50,6 +50,7 @@ const taskGroups = {
50
50
  'UpdateLayer',
51
51
  'UpdateLayerTree',
52
52
  'CompositeLayers',
53
+ 'PrePaint', // New name for UpdateLayerTree: https://crrev.com/c/3519012
53
54
  ],
54
55
  },
55
56
  scriptParseCompile: {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ /**
9
+ * @fileoverview Utility functions for grouping and summarizing tasks.
10
+ */
11
+
12
+ const NetworkRequest = require('../network-request.js');
13
+
14
+ // These trace events, when not triggered by a script inside a particular task, are just general Chrome overhead.
15
+ const BROWSER_TASK_NAMES_SET = new Set([
16
+ 'CpuProfiler::StartProfiling',
17
+ ]);
18
+
19
+ // These trace events, when not triggered by a script inside a particular task, are GC Chrome overhead.
20
+ const BROWSER_GC_TASK_NAMES_SET = new Set([
21
+ 'V8.GCCompactor',
22
+ 'MajorGC',
23
+ 'MinorGC',
24
+ ]);
25
+
26
+ /**
27
+ * @param {LH.Artifacts.NetworkRequest[]} records
28
+ */
29
+ function getJavaScriptURLs(records) {
30
+ /** @type {Set<string>} */
31
+ const urls = new Set();
32
+ for (const record of records) {
33
+ if (record.resourceType === NetworkRequest.TYPES.Script) {
34
+ urls.add(record.url);
35
+ }
36
+ }
37
+
38
+ return urls;
39
+ }
40
+
41
+ /**
42
+ * @param {LH.Artifacts.TaskNode} task
43
+ * @param {Set<string>} jsURLs
44
+ * @return {string}
45
+ */
46
+ function getAttributableURLForTask(task, jsURLs) {
47
+ const jsURL = task.attributableURLs.find(url => jsURLs.has(url));
48
+ const fallbackURL = task.attributableURLs[0];
49
+ let attributableURL = jsURL || fallbackURL;
50
+ // If we can't find what URL was responsible for this execution, attribute it to the root page
51
+ // or Chrome depending on the type of work.
52
+ if (!attributableURL || attributableURL === 'about:blank') {
53
+ if (BROWSER_TASK_NAMES_SET.has(task.event.name)) attributableURL = 'Browser';
54
+ else if (BROWSER_GC_TASK_NAMES_SET.has(task.event.name)) attributableURL = 'Browser GC';
55
+ else attributableURL = 'Unattributable';
56
+ }
57
+
58
+ return attributableURL;
59
+ }
60
+
61
+ /**
62
+ * @param {LH.Artifacts.TaskNode[]} tasks
63
+ * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
64
+ * @return {Map<string, Record<string, number>>}
65
+ */
66
+ function getExecutionTimingsByURL(tasks, networkRecords) {
67
+ const jsURLs = getJavaScriptURLs(networkRecords);
68
+
69
+ /** @type {Map<string, Record<string, number>>} */
70
+ const result = new Map();
71
+
72
+ for (const task of tasks) {
73
+ const attributableURL = getAttributableURLForTask(task, jsURLs);
74
+ const timingByGroupId = result.get(attributableURL) || {};
75
+ const originalTime = timingByGroupId[task.group.id] || 0;
76
+ timingByGroupId[task.group.id] = originalTime + task.selfTime;
77
+ result.set(attributableURL, timingByGroupId);
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ module.exports = {
84
+ getJavaScriptURLs,
85
+ getAttributableURLForTask,
86
+ getExecutionTimingsByURL,
87
+ };
@@ -82,11 +82,15 @@ class TraceProcessor {
82
82
  * Returns true if the event is a navigation start event of a document whose URL seems valid.
83
83
  *
84
84
  * @param {LH.TraceEvent} event
85
+ * @return {boolean}
85
86
  */
86
87
  static _isNavigationStartOfInterest(event) {
87
- return event.name === 'navigationStart' &&
88
- (!event.args.data || !event.args.data.documentLoaderURL ||
89
- ACCEPTABLE_NAVIGATION_URL_REGEX.test(event.args.data.documentLoaderURL));
88
+ if (event.name !== 'navigationStart') return false;
89
+ // COMPAT: support pre-m67 test traces before `args.data` added to all navStart events.
90
+ // TODO: remove next line when old test traces (e.g. progressive-app-m60.json) are updated.
91
+ if (event.args.data?.documentLoaderURL === undefined) return true;
92
+ if (!event.args.data?.documentLoaderURL) return false;
93
+ return ACCEPTABLE_NAVIGATION_URL_REGEX.test(event.args.data.documentLoaderURL);
90
94
  }
91
95
 
92
96
  /**
@@ -462,8 +466,9 @@ class TraceProcessor {
462
466
  // If we can't find either TracingStarted event, then we'll fallback to the first navStart that
463
467
  // looks like it was loading the main frame with a real URL. Because the schema for this event
464
468
  // has changed across Chrome versions, we'll be extra defensive about finding this case.
465
- const navStartEvt = events.find(e => Boolean(e.name === 'navigationStart' &&
466
- e.args?.data?.isLoadingMainFrame && e.args.data.documentLoaderURL));
469
+ const navStartEvt = events.find(e =>
470
+ this._isNavigationStartOfInterest(e) && e.args.data?.isLoadingMainFrame
471
+ );
467
472
  // Find the first resource that was requested and make sure it agrees on the id.
468
473
  const firstResourceSendEvt = events.find(e => e.name === 'ResourceSendRequest');
469
474
  // We know that these properties exist if we found the events, but TSC doesn't.
@@ -607,39 +612,59 @@ class TraceProcessor {
607
612
  // Find the inspected frame
608
613
  const mainFrameIds = this.findMainFrameIds(keyEvents);
609
614
 
610
- const frames = keyEvents
615
+ /** @type {Map<string, {id: string, url: string, parent?: string}>} */
616
+ const framesById = new Map();
617
+
618
+ // Begin collection of frame tree information with TracingStartedInBrowser,
619
+ // which should be present even without navigations.
620
+ const tracingStartedFrames = keyEvents
621
+ .find(e => e.name === 'TracingStartedInBrowser')?.args?.data?.frames;
622
+ if (tracingStartedFrames) {
623
+ for (const frame of tracingStartedFrames) {
624
+ framesById.set(frame.frame, {
625
+ id: frame.frame,
626
+ url: frame.url,
627
+ parent: frame.parent,
628
+ });
629
+ }
630
+ }
631
+
632
+ // Update known frames if FrameCommittedInBrowser events come in, typically
633
+ // with updated `url`, as well as pid, etc. Some traces (like timespans) may
634
+ // not have any committed frames.
635
+ keyEvents
611
636
  .filter(/** @return {evt is FrameCommittedEvent} */ evt => {
612
637
  return Boolean(
613
638
  evt.name === 'FrameCommittedInBrowser' &&
614
639
  evt.args.data?.frame &&
615
640
  evt.args.data.url
616
641
  );
617
- })
618
- .map(evt => {
619
- return {
642
+ }).forEach(evt => {
643
+ framesById.set(evt.args.data.frame, {
620
644
  id: evt.args.data.frame,
621
645
  url: evt.args.data.url,
622
646
  parent: evt.args.data.parent,
623
- };
647
+ });
624
648
  });
649
+ const frames = [...framesById.values()];
625
650
  const frameIdToRootFrameId = this.resolveRootFrames(frames);
626
651
 
627
652
  // Filter to just events matching the main frame ID, just to make sure.
628
653
  const frameEvents = keyEvents.filter(e => e.args.frame === mainFrameIds.frameId);
629
654
 
630
655
  // Filter to just events matching the main frame ID or any child frame IDs.
631
- // In practice, there should always be FrameCommittedInBrowser events to define the frame tree.
632
- // Unfortunately, many test traces do not include FrameCommittedInBrowser events due to minification.
633
- // This ensures there is always a minimal frame tree and events so those tests don't fail.
634
656
  let frameTreeEvents = [];
635
657
  if (frameIdToRootFrameId.has(mainFrameIds.frameId)) {
636
658
  frameTreeEvents = keyEvents.filter(e => {
637
659
  return e.args.frame && frameIdToRootFrameId.get(e.args.frame) === mainFrameIds.frameId;
638
660
  });
639
661
  } else {
662
+ // In practice, there should always be TracingStartedInBrowser/FrameCommittedInBrowser events to
663
+ // define the frame tree. Unfortunately, many test traces do not that frame info due to minification.
664
+ // This ensures there is always a minimal frame tree and events so those tests don't fail.
640
665
  log.warn(
641
666
  'trace-of-tab',
642
- 'frameTreeEvents may be incomplete, make sure the trace has FrameCommittedInBrowser events'
667
+ 'frameTreeEvents may be incomplete, make sure the trace has frame events'
643
668
  );
644
669
  frameIdToRootFrameId.set(mainFrameIds.frameId, mainFrameIds.frameId);
645
670
  frameTreeEvents = frameEvents;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const {isEqual: isDeepEqual} = require('lodash');
8
+ const isDeepEqual = require('lodash/isEqual.js');
9
9
  const Driver = require('./gather/driver.js');
10
10
  const GatherRunner = require('./gather/gather-runner.js');
11
11
  const ReportScoring = require('./scoring.js');