lighthouse 9.5.0-dev.20220504 → 9.5.0-dev.20220507

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 (45) hide show
  1. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +97 -22
  2. package/lighthouse-cli/test/smokehouse/version-check-test.js +0 -2
  3. package/lighthouse-core/audits/bootup-time.js +2 -70
  4. package/lighthouse-core/audits/long-tasks.js +3 -3
  5. package/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js +7 -5
  6. package/lighthouse-core/audits/third-party-summary.js +3 -3
  7. package/lighthouse-core/computed/metrics/responsiveness.js +14 -13
  8. package/lighthouse-core/fraggle-rock/gather/driver.js +1 -1
  9. package/lighthouse-core/gather/driver.js +1 -1
  10. package/lighthouse-core/gather/fetcher.js +34 -210
  11. package/lighthouse-core/gather/gather-runner.js +0 -11
  12. package/lighthouse-core/gather/gatherers/seo/robots-txt.js +0 -29
  13. package/lighthouse-core/gather/gatherers/source-maps.js +0 -1
  14. package/lighthouse-core/lib/cdt/generated/SourceMap.js +1 -1
  15. package/lighthouse-core/lib/stack-packs.js +4 -5
  16. package/lighthouse-core/lib/tracehouse/task-groups.js +1 -0
  17. package/lighthouse-core/lib/tracehouse/task-summary.js +87 -0
  18. package/package.json +5 -5
  19. package/report/test/.eslintrc.cjs +11 -0
  20. package/report/test/clients/bundle-test.js +0 -3
  21. package/report/test/generator/file-namer-test.js +0 -1
  22. package/report/test/generator/report-generator-test.js +0 -2
  23. package/report/test/renderer/category-renderer-test.js +0 -2
  24. package/report/test/renderer/components-test.js +0 -2
  25. package/report/test/renderer/crc-details-renderer-test.js +0 -2
  26. package/report/test/renderer/details-renderer-test.js +0 -2
  27. package/report/test/renderer/dom-test.js +0 -2
  28. package/report/test/renderer/element-screenshot-renderer-test.js +0 -2
  29. package/report/test/renderer/i18n-test.js +0 -2
  30. package/report/test/renderer/performance-category-renderer-test.js +0 -2
  31. package/report/test/renderer/pwa-category-renderer-test.js +0 -2
  32. package/report/test/renderer/report-renderer-axe-test.js +0 -2
  33. package/report/test/renderer/report-renderer-test.js +0 -2
  34. package/report/test/renderer/report-ui-features-test.js +0 -2
  35. package/report/test/renderer/snippet-renderer-test.js +0 -2
  36. package/report/test/renderer/text-encoding-test.js +0 -2
  37. package/report/test/renderer/util-test.js +0 -2
  38. package/shared/test/localization/.eslintrc.cjs +11 -0
  39. package/shared/test/localization/format-test.js +0 -2
  40. package/shared/test/localization/locales-test.js +0 -2
  41. package/shared/test/localization/swap-locale-test.js +0 -2
  42. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +0 -2
  43. package/third-party/chromium-synchronization/installability-errors-test.js +0 -2
  44. package/tsconfig.json +2 -0
  45. package/types/artifacts.d.ts +17 -0
@@ -8,47 +8,76 @@
8
8
  * @fileoverview A runner that launches Chrome and executes Lighthouse via a
9
9
  * bundle to test that bundling has produced correct and runnable code.
10
10
  * Currently uses `lighthouse-dt-bundle.js`.
11
+ * Runs in a worker to avoid messing up marky's global state.
11
12
  */
12
13
 
13
14
  import fs from 'fs';
15
+ import os from 'os';
16
+ import {Worker, isMainThread, parentPort, workerData} from 'worker_threads';
17
+ import {once} from 'events';
14
18
 
15
19
  import puppeteer from 'puppeteer-core';
16
20
  import ChromeLauncher from 'chrome-launcher';
17
21
 
18
22
  import ChromeProtocol from '../../../../lighthouse-core/gather/connections/cri.js';
19
23
  import {LH_ROOT} from '../../../../root.js';
24
+ import {loadArtifacts, saveArtifacts} from '../../../../lighthouse-core/lib/asset-saver.js';
20
25
 
21
- const originalBuffer = global.Buffer;
22
- const originalRequire = global.require;
23
- if (typeof globalThis === 'undefined') {
24
- // @ts-expect-error - exposing for loading of dt-bundle.
25
- global.globalThis = global;
26
+ // This runs only in the worker. The rest runs on the main thread.
27
+ if (!isMainThread && parentPort) {
28
+ (async () => {
29
+ const {url, configJson, testRunnerOptions} = workerData;
30
+ try {
31
+ const result = await runBundledLighthouse(url, configJson, testRunnerOptions);
32
+ // Save to assets directory because LighthouseError won't survive postMessage.
33
+ const assetsDir = fs.mkdtempSync(os.tmpdir() + '/smoke-bundle-assets-');
34
+ await saveArtifacts(result.artifacts, assetsDir);
35
+ const value = {
36
+ lhr: result.lhr,
37
+ assetsDir,
38
+ };
39
+ parentPort?.postMessage({type: 'result', value});
40
+ } catch (err) {
41
+ console.error(err);
42
+ parentPort?.postMessage({type: 'error', value: err});
43
+ }
44
+ })();
26
45
  }
27
46
 
28
- // Load bundle, which creates a `global.runBundledLighthouse`.
29
- eval(fs.readFileSync(LH_ROOT + '/dist/lighthouse-dt-bundle.js', 'utf-8'));
30
-
31
- global.require = originalRequire;
32
- global.Buffer = originalBuffer;
33
-
34
- /** @type {import('../../../../lighthouse-core/index.js')} */
35
- // @ts-expect-error - not worth giving test global an actual type.
36
- const lighthouse = global.runBundledLighthouse;
37
-
38
47
  /**
39
- * Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools bundle.
40
48
  * @param {string} url
41
- * @param {LH.Config.Json=} configJson
42
- * @param {{isDebug?: boolean, useFraggleRock?: boolean}=} testRunnerOptions
43
- * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
49
+ * @param {LH.Config.Json|undefined} configJson
50
+ * @param {{isDebug?: boolean, useFraggleRock?: boolean}} testRunnerOptions
51
+ * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>}
44
52
  */
45
- async function runLighthouse(url, configJson, testRunnerOptions = {}) {
53
+ async function runBundledLighthouse(url, configJson, testRunnerOptions) {
54
+ if (isMainThread || !parentPort) {
55
+ throw new Error('must be called in worker');
56
+ }
57
+
58
+ const originalBuffer = global.Buffer;
59
+ const originalRequire = global.require;
60
+ if (typeof globalThis === 'undefined') {
61
+ // @ts-expect-error - exposing for loading of dt-bundle.
62
+ global.globalThis = global;
63
+ }
64
+
65
+ // Load bundle, which creates a `global.runBundledLighthouse`.
66
+ eval(fs.readFileSync(LH_ROOT + '/dist/lighthouse-dt-bundle.js', 'utf-8'));
67
+
68
+ global.require = originalRequire;
69
+ global.Buffer = originalBuffer;
70
+
71
+ /** @type {import('../../../../lighthouse-core/index.js')} */
72
+ // @ts-expect-error - not worth giving test global an actual type.
73
+ const lighthouse = global.runBundledLighthouse;
74
+
46
75
  // Launch and connect to Chrome.
47
76
  const launchedChrome = await ChromeLauncher.launch();
48
77
  const port = launchedChrome.port;
49
78
 
79
+ // Run Lighthouse.
50
80
  try {
51
- // Run Lighthouse.
52
81
  const logLevel = testRunnerOptions.isDebug ? 'info' : undefined;
53
82
  let runnerResult;
54
83
  if (testRunnerOptions.useFraggleRock) {
@@ -66,7 +95,6 @@ async function runLighthouse(url, configJson, testRunnerOptions = {}) {
66
95
  return {
67
96
  lhr: runnerResult.lhr,
68
97
  artifacts: runnerResult.artifacts,
69
- log: '', // TODO: if want to run in parallel, need to capture lighthouse-logger output.
70
98
  };
71
99
  } finally {
72
100
  // Clean up and return results.
@@ -74,6 +102,53 @@ async function runLighthouse(url, configJson, testRunnerOptions = {}) {
74
102
  }
75
103
  }
76
104
 
105
+ /**
106
+ * Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools bundle.
107
+ * @param {string} url
108
+ * @param {LH.Config.Json=} configJson
109
+ * @param {{isDebug?: boolean, useFraggleRock?: boolean}=} testRunnerOptions
110
+ * @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
111
+ */
112
+ async function runLighthouse(url, configJson, testRunnerOptions = {}) {
113
+ /** @type {string[]} */
114
+ const logs = [];
115
+ const worker = new Worker(new URL(import.meta.url), {
116
+ stdout: true,
117
+ stderr: true,
118
+ workerData: {url, configJson, testRunnerOptions},
119
+ });
120
+ worker.stdout.setEncoding('utf8');
121
+ worker.stderr.setEncoding('utf8');
122
+ worker.stdout.addListener('data', (data) => {
123
+ process.stdout.write(data);
124
+ logs.push(`STDOUT: ${data}`);
125
+ });
126
+ worker.stderr.addListener('data', (data) => {
127
+ process.stderr.write(data);
128
+ logs.push(`STDERR: ${data}`);
129
+ });
130
+ const [workerResponse] = await once(worker, 'message');
131
+ const log = logs.join('') + '\n';
132
+
133
+ if (workerResponse.type === 'error') {
134
+ new Error(`Worker returned an error: ${workerResponse.value}\nLog:\n${log}`);
135
+ }
136
+
137
+ const result = workerResponse.value;
138
+ if (!result.lhr || !result.assetsDir) {
139
+ throw new Error(`invalid response from worker:\n${JSON.stringify(result, null, 2)}`);
140
+ }
141
+
142
+ const artifacts = loadArtifacts(result.assetsDir);
143
+ fs.rmSync(result.assetsDir, {recursive: true});
144
+
145
+ return {
146
+ lhr: result.lhr,
147
+ artifacts,
148
+ log,
149
+ };
150
+ }
151
+
77
152
  export {
78
153
  runLighthouse,
79
154
  };
@@ -6,8 +6,6 @@
6
6
 
7
7
  import {chromiumVersionCheck, compareVersions} from './version-check.js';
8
8
 
9
- /* eslint-env jest */
10
-
11
9
  describe('version check', () => {
12
10
  it('compareVersions', async () => {
13
11
  expect(compareVersions([100, 0, 0, 0], [100, 0, 0, 0])).toBe(0);
@@ -6,11 +6,11 @@
6
6
  'use strict';
7
7
 
8
8
  const Audit = require('./audit.js');
9
- const NetworkRequest = require('../lib/network-request.js');
10
9
  const {taskGroups} = require('../lib/tracehouse/task-groups.js');
11
10
  const i18n = require('../lib/i18n/i18n.js');
12
11
  const NetworkRecords = require('../computed/network-records.js');
13
12
  const MainThreadTasks = require('../computed/main-thread-tasks.js');
13
+ const {getExecutionTimingsByURL} = require('../lib/tracehouse/task-summary.js');
14
14
 
15
15
  const UIStrings = {
16
16
  /** Title of a diagnostic audit that provides detail on the time spent executing javascript files during the load. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -34,18 +34,6 @@ const UIStrings = {
34
34
 
35
35
  const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
36
36
 
37
- // These trace events, when not triggered by a script inside a particular task, are just general Chrome overhead.
38
- const BROWSER_TASK_NAMES_SET = new Set([
39
- 'CpuProfiler::StartProfiling',
40
- ]);
41
-
42
- // These trace events, when not triggered by a script inside a particular task, are GC Chrome overhead.
43
- const BROWSER_GC_TASK_NAMES_SET = new Set([
44
- 'V8.GCCompactor',
45
- 'MajorGC',
46
- 'MinorGC',
47
- ]);
48
-
49
37
  class BootupTime extends Audit {
50
38
  /**
51
39
  * @return {LH.Audit.Meta}
@@ -74,61 +62,6 @@ class BootupTime extends Audit {
74
62
  };
75
63
  }
76
64
 
77
- /**
78
- * @param {LH.Artifacts.NetworkRequest[]} records
79
- */
80
- static getJavaScriptURLs(records) {
81
- /** @type {Set<string>} */
82
- const urls = new Set();
83
- for (const record of records) {
84
- if (record.resourceType === NetworkRequest.TYPES.Script) {
85
- urls.add(record.url);
86
- }
87
- }
88
-
89
- return urls;
90
- }
91
-
92
- /**
93
- * @param {LH.Artifacts.TaskNode} task
94
- * @param {Set<string>} jsURLs
95
- * @return {string}
96
- */
97
- static getAttributableURLForTask(task, jsURLs) {
98
- const jsURL = task.attributableURLs.find(url => jsURLs.has(url));
99
- const fallbackURL = task.attributableURLs[0];
100
- let attributableURL = jsURL || fallbackURL;
101
- // If we can't find what URL was responsible for this execution, attribute it to the root page
102
- // or Chrome depending on the type of work.
103
- if (!attributableURL || attributableURL === 'about:blank') {
104
- if (BROWSER_TASK_NAMES_SET.has(task.event.name)) attributableURL = 'Browser';
105
- else if (BROWSER_GC_TASK_NAMES_SET.has(task.event.name)) attributableURL = 'Browser GC';
106
- else attributableURL = 'Unattributable';
107
- }
108
-
109
- return attributableURL;
110
- }
111
-
112
- /**
113
- * @param {LH.Artifacts.TaskNode[]} tasks
114
- * @param {Set<string>} jsURLs
115
- * @return {Map<string, Object<string, number>>}
116
- */
117
- static getExecutionTimingsByURL(tasks, jsURLs) {
118
- /** @type {Map<string, Object<string, number>>} */
119
- const result = new Map();
120
-
121
- for (const task of tasks) {
122
- const attributableURL = BootupTime.getAttributableURLForTask(task, jsURLs);
123
- const timingByGroupId = result.get(attributableURL) || {};
124
- const originalTime = timingByGroupId[task.group.id] || 0;
125
- timingByGroupId[task.group.id] = originalTime + task.selfTime;
126
- result.set(attributableURL, timingByGroupId);
127
- }
128
-
129
- return result;
130
- }
131
-
132
65
  /**
133
66
  * @param {LH.Artifacts} artifacts
134
67
  * @param {LH.Audit.Context} context
@@ -143,8 +76,7 @@ class BootupTime extends Audit {
143
76
  const multiplier = settings.throttlingMethod === 'simulate' ?
144
77
  settings.throttling.cpuSlowdownMultiplier : 1;
145
78
 
146
- const jsURLs = BootupTime.getJavaScriptURLs(networkRecords);
147
- const executionTimings = BootupTime.getExecutionTimingsByURL(tasks, jsURLs);
79
+ const executionTimings = getExecutionTimingsByURL(tasks, networkRecords);
148
80
 
149
81
  let hadExcessiveChromeExtension = false;
150
82
  let totalBootupTime = 0;
@@ -9,9 +9,9 @@ const Audit = require('./audit.js');
9
9
  const NetworkRecords = require('../computed/network-records.js');
10
10
  const i18n = require('../lib/i18n/i18n.js');
11
11
  const MainThreadTasks = require('../computed/main-thread-tasks.js');
12
- const BootupTime = require('./bootup-time.js');
13
12
  const PageDependencyGraph = require('../computed/page-dependency-graph.js');
14
13
  const LoadSimulator = require('../computed/load-simulator.js');
14
+ const {getJavaScriptURLs, getAttributableURLForTask} = require('../lib/tracehouse/task-summary.js');
15
15
 
16
16
  /** We don't always have timing data for short tasks, if we're missing timing data. Treat it as though it were 0ms. */
17
17
  const DEFAULT_TIMING = {startTime: 0, endTime: 0, duration: 0};
@@ -78,7 +78,7 @@ class LongTasks extends Audit {
78
78
  }
79
79
  }
80
80
 
81
- const jsURLs = BootupTime.getJavaScriptURLs(networkRecords);
81
+ const jsURLs = getJavaScriptURLs(networkRecords);
82
82
  // Only consider up to 20 long, top-level (no parent) tasks that have an explicit endTime
83
83
  const longtasks = tasks
84
84
  .map(t => {
@@ -91,7 +91,7 @@ class LongTasks extends Audit {
91
91
 
92
92
  // TODO(beytoven): Add start time that matches with the simulated throttling
93
93
  const results = longtasks.map(task => ({
94
- url: BootupTime.getAttributableURLForTask(task, jsURLs),
94
+ url: getAttributableURLForTask(task, jsURLs),
95
95
  duration: task.duration,
96
96
  startTime: task.startTime,
97
97
  }));
@@ -63,19 +63,21 @@ class ExperimentalInteractionToNextPaint extends Audit {
63
63
 
64
64
  const trace = artifacts.traces[Audit.DEFAULT_PASS];
65
65
  const metricData = {trace, settings};
66
- const metricResult = await ComputedResponsivenes.request(metricData, context);
66
+ const responsivenessEvent = await ComputedResponsivenes.request(metricData, context);
67
67
 
68
68
  // TODO: include the no-interaction state in the report instead of using n/a.
69
- if (metricResult === null) {
69
+ if (responsivenessEvent === null) {
70
70
  return {score: null, notApplicable: true};
71
71
  }
72
72
 
73
+ const timing = responsivenessEvent.args.data.maxDuration;
74
+
73
75
  return {
74
76
  score: Audit.computeLogNormalScore({p10: context.options.p10, median: context.options.median},
75
- metricResult.timing),
76
- numericValue: metricResult.timing,
77
+ timing),
78
+ numericValue: timing,
77
79
  numericUnit: 'millisecond',
78
- displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
80
+ displayValue: str_(i18n.UIStrings.ms, {timeInMs: timing}),
79
81
  };
80
82
  }
81
83
  }
@@ -6,11 +6,11 @@
6
6
  'use strict';
7
7
 
8
8
  const Audit = require('./audit.js');
9
- const BootupTime = require('./bootup-time.js');
10
9
  const i18n = require('../lib/i18n/i18n.js');
11
10
  const thirdPartyWeb = require('../lib/third-party-web.js');
12
11
  const NetworkRecords = require('../computed/network-records.js');
13
12
  const MainThreadTasks = require('../computed/main-thread-tasks.js');
13
+ const {getJavaScriptURLs, getAttributableURLForTask} = require('../lib/tracehouse/task-summary.js');
14
14
 
15
15
  const UIStrings = {
16
16
  /** Title of a diagnostic audit that provides details about the code on a web page that the user doesn't control (referred to as "third-party code"). This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -98,10 +98,10 @@ class ThirdPartySummary extends Audit {
98
98
  byURL.set(request.url, urlSummary);
99
99
  }
100
100
 
101
- const jsURLs = BootupTime.getJavaScriptURLs(networkRecords);
101
+ const jsURLs = getJavaScriptURLs(networkRecords);
102
102
 
103
103
  for (const task of mainThreadTasks) {
104
- const attributableURL = BootupTime.getAttributableURLForTask(task, jsURLs);
104
+ const attributableURL = getAttributableURLForTask(task, jsURLs);
105
105
 
106
106
  const urlSummary = byURL.get(attributableURL) || {...defaultSummary};
107
107
  const taskDuration = task.selfTime * cpuMultiplier;
@@ -11,24 +11,25 @@
11
11
  * user input in the provided trace).
12
12
  */
13
13
 
14
+ /** @typedef {LH.Trace.CompleteEvent & {name: 'Responsiveness.Renderer.UserInteraction', args: {frame: string, data: {interactionType: 'drag'|'keyboard'|'tapOrClick', maxDuration: number}}}} ResponsivenessEvent */
15
+
14
16
  const makeComputedArtifact = require('../computed-artifact.js');
15
17
  const ProcessedTrace = require('../processed-trace.js');
16
18
 
17
19
  class Responsiveness {
18
20
  /**
19
21
  * @param {LH.Artifacts.ProcessedTrace} processedTrace
20
- * @return {{timing: number}|null}
22
+ * @return {ResponsivenessEvent|null}
21
23
  */
22
24
  static getHighPercentileResponsiveness(processedTrace) {
23
- const durations = processedTrace.frameTreeEvents
25
+ const responsivenessEvents = processedTrace.frameTreeEvents
24
26
  // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/responsiveness_metrics.cc;l=146-150;drc=a1a2302f30b0a58f7669a41c80acdf1fa11958dd
25
- .filter(e => e.name === 'Responsiveness.Renderer.UserInteraction')
26
- .map(evt => evt.args.data?.maxDuration)
27
- .filter(/** @return {duration is number} */duration => duration !== undefined)
28
- .sort((a, b) => b - a);
27
+ .filter(/** @return {e is ResponsivenessEvent} */ e => {
28
+ return e.name === 'Responsiveness.Renderer.UserInteraction';
29
+ }).sort((a, b) => b.args.data.maxDuration - a.args.data.maxDuration);
29
30
 
30
31
  // If there were no interactions with the page, the metric is N/A.
31
- if (durations.length === 0) {
32
+ if (responsivenessEvents.length === 0) {
32
33
  return null;
33
34
  }
34
35
 
@@ -36,17 +37,15 @@ class Responsiveness {
36
37
  // keeps the 10 worst events around, so it can never be more than the 10th from
37
38
  // last array element. To keep things simpler, sort desc and pick from front.
38
39
  // See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad
39
- const index = Math.min(9, Math.floor(durations.length / 50));
40
+ const index = Math.min(9, Math.floor(responsivenessEvents.length / 50));
40
41
 
41
- return {
42
- timing: durations[index],
43
- };
42
+ return responsivenessEvents[index];
44
43
  }
45
44
 
46
45
  /**
47
46
  * @param {{trace: LH.Trace, settings: Immutable<LH.Config.Settings>}} data
48
47
  * @param {LH.Artifacts.ComputedContext} context
49
- * @return {Promise<LH.Artifacts.Metric|null>}
48
+ * @return {Promise<ResponsivenessEvent|null>}
50
49
  */
51
50
  static async compute_(data, context) {
52
51
  if (data.settings.throttlingMethod === 'simulate') {
@@ -54,7 +53,9 @@ class Responsiveness {
54
53
  }
55
54
 
56
55
  const processedTrace = await ProcessedTrace.request(data.trace, context);
57
- return Responsiveness.getHighPercentileResponsiveness(processedTrace);
56
+ const event = Responsiveness.getHighPercentileResponsiveness(processedTrace);
57
+
58
+ return JSON.parse(JSON.stringify(event));
58
59
  }
59
60
  }
60
61
 
@@ -74,7 +74,7 @@ class Driver {
74
74
  const session = await this._page.target().createCDPSession();
75
75
  this._session = this.defaultSession = new ProtocolSession(session);
76
76
  this._executionContext = new ExecutionContext(this._session);
77
- this._fetcher = new Fetcher(this._session, this._executionContext);
77
+ this._fetcher = new Fetcher(this._session);
78
78
  log.timeEnd(status);
79
79
  }
80
80
 
@@ -64,7 +64,7 @@ class Driver {
64
64
  defaultSession = this;
65
65
 
66
66
  // eslint-disable-next-line no-invalid-this
67
- fetcher = new Fetcher(this.defaultSession, this.executionContext);
67
+ fetcher = new Fetcher(this.defaultSession);
68
68
 
69
69
  /**
70
70
  * @param {Connection} connection