lighthouse 10.3.0-dev.20230709 → 10.4.0

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 (52) hide show
  1. package/cli/test/smokehouse/frontends/smokehouse-bin.js +19 -13
  2. package/cli/test/smokehouse/lighthouse-runners/bundle.js +3 -5
  3. package/core/audits/long-tasks.d.ts +48 -0
  4. package/core/audits/long-tasks.js +146 -19
  5. package/dist/report/flow.js +1 -1
  6. package/package.json +1 -1
  7. package/shared/localization/locales/ar-XB.json +233 -2
  8. package/shared/localization/locales/ar.json +233 -2
  9. package/shared/localization/locales/bg.json +233 -2
  10. package/shared/localization/locales/ca.json +233 -2
  11. package/shared/localization/locales/cs.json +246 -15
  12. package/shared/localization/locales/da.json +239 -8
  13. package/shared/localization/locales/de.json +238 -7
  14. package/shared/localization/locales/el.json +234 -3
  15. package/shared/localization/locales/en-GB.json +233 -2
  16. package/shared/localization/locales/en-XA.json +233 -2
  17. package/shared/localization/locales/es-419.json +233 -2
  18. package/shared/localization/locales/es.json +233 -2
  19. package/shared/localization/locales/fi.json +233 -2
  20. package/shared/localization/locales/fil.json +236 -5
  21. package/shared/localization/locales/fr.json +235 -4
  22. package/shared/localization/locales/he.json +233 -2
  23. package/shared/localization/locales/hi.json +236 -5
  24. package/shared/localization/locales/hr.json +234 -3
  25. package/shared/localization/locales/hu.json +233 -2
  26. package/shared/localization/locales/id.json +233 -2
  27. package/shared/localization/locales/it.json +234 -3
  28. package/shared/localization/locales/ja.json +233 -2
  29. package/shared/localization/locales/ko.json +233 -2
  30. package/shared/localization/locales/lt.json +233 -2
  31. package/shared/localization/locales/lv.json +250 -19
  32. package/shared/localization/locales/nl.json +233 -2
  33. package/shared/localization/locales/no.json +235 -4
  34. package/shared/localization/locales/pl.json +233 -2
  35. package/shared/localization/locales/pt-PT.json +233 -2
  36. package/shared/localization/locales/pt.json +234 -3
  37. package/shared/localization/locales/ro.json +233 -2
  38. package/shared/localization/locales/ru.json +233 -2
  39. package/shared/localization/locales/sk.json +233 -2
  40. package/shared/localization/locales/sl.json +233 -2
  41. package/shared/localization/locales/sr-Latn.json +233 -2
  42. package/shared/localization/locales/sr.json +233 -2
  43. package/shared/localization/locales/sv.json +233 -2
  44. package/shared/localization/locales/ta.json +233 -2
  45. package/shared/localization/locales/te.json +233 -2
  46. package/shared/localization/locales/th.json +233 -2
  47. package/shared/localization/locales/tr.json +233 -2
  48. package/shared/localization/locales/uk.json +233 -2
  49. package/shared/localization/locales/vi.json +233 -2
  50. package/shared/localization/locales/zh-HK.json +233 -2
  51. package/shared/localization/locales/zh-TW.json +235 -4
  52. package/shared/localization/locales/zh.json +239 -8
@@ -25,6 +25,8 @@ import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
25
25
  import {updateTestDefnFormat} from './back-compat-util.js';
26
26
  import {LH_ROOT} from '../../../../root.js';
27
27
  import exclusions from '../config/exclusions.js';
28
+ import {saveArtifacts} from '../../../../core/lib/asset-saver.js';
29
+ import {saveLhr} from '../../../../core/lib/asset-saver.js';
28
30
 
29
31
  const coreTestDefnsPath =
30
32
  path.join(LH_ROOT, 'cli/test/smokehouse/core-tests.js');
@@ -230,19 +232,23 @@ async function begin() {
230
232
  if (!smokehouseResult.success) {
231
233
  const failedTestResults = smokehouseResult.testResults.filter(r => r.failed);
232
234
 
233
- // For CI, save failed runs to directory to be uploaded.
234
- if (process.env.CI) {
235
- const failuresDir = `${LH_ROOT}/.tmp/smokehouse-ci-failures`;
236
- fs.mkdirSync(failuresDir, {recursive: true});
237
-
238
- for (const testResult of failedTestResults) {
239
- for (let i = 0; i < testResult.runs.length; i++) {
240
- const run = testResult.runs[i];
241
- fs.writeFileSync(`${failuresDir}/${testResult.id}-${i}.json`, JSON.stringify({
242
- ...run,
243
- lighthouseLog: run.lighthouseLog.split('\n'),
244
- assertionLog: run.assertionLog.split('\n'),
245
- }, null, 2));
235
+ // Save failed runs to directory. In CI, this is uploaded as an artifact.
236
+ const failuresDir = `${LH_ROOT}/.tmp/smokehouse-failures`;
237
+ fs.rmSync(failuresDir, {recursive: true, force: true});
238
+ fs.mkdirSync(failuresDir);
239
+
240
+ for (const testResult of failedTestResults) {
241
+ for (let i = 0; i < testResult.runs.length; i++) {
242
+ const runDir = `${failuresDir}/${i}/${testResult.id}`;
243
+ fs.mkdirSync(runDir, {recursive: true});
244
+
245
+ const run = testResult.runs[i];
246
+ await saveArtifacts(run.artifacts, runDir);
247
+ await saveLhr(run.lhr, runDir);
248
+ fs.writeFileSync(`${runDir}/assertionLog.txt`, run.assertionLog);
249
+ fs.writeFileSync(`${runDir}/lighthouseLog.txt`, run.lighthouseLog);
250
+ if (run.networkRequests) {
251
+ fs.writeFileSync(`${runDir}/networkRequests.txt`, run.networkRequests.join('\n'));
246
252
  }
247
253
  }
248
254
  }
@@ -82,7 +82,7 @@ async function runBundledLighthouse(url, config, testRunnerOptions) {
82
82
 
83
83
  // Run Lighthouse.
84
84
  try {
85
- const logLevel = testRunnerOptions.isDebug ? 'info' : undefined;
85
+ const logLevel = testRunnerOptions.isDebug ? 'verbose' : 'info';
86
86
  let runnerResult;
87
87
  if (testRunnerOptions.useLegacyNavigation) {
88
88
  const connection = new CriConnection(port);
@@ -124,12 +124,10 @@ async function runLighthouse(url, config, testRunnerOptions = {}) {
124
124
  worker.stdout.setEncoding('utf8');
125
125
  worker.stderr.setEncoding('utf8');
126
126
  worker.stdout.addListener('data', (data) => {
127
- process.stdout.write(data);
128
- logs.push(`STDOUT: ${data}`);
127
+ logs.push(`[STDOUT] ${data}`);
129
128
  });
130
129
  worker.stderr.addListener('data', (data) => {
131
- process.stderr.write(data);
132
- logs.push(`STDERR: ${data}`);
130
+ logs.push(`[STDERR] ${data}`);
133
131
  });
134
132
  const [workerResponse] = await once(worker, 'message');
135
133
  const log = logs.join('') + '\n';
@@ -1,5 +1,53 @@
1
1
  export default LongTasks;
2
+ export type TaskGroupIds = import('../lib/tracehouse/task-groups.js').TaskGroupIds;
3
+ export type Timing = {
4
+ startTime: number;
5
+ duration: number;
6
+ };
7
+ export type DebugTask = Timing & {
8
+ [p: string]: number;
9
+ urlIndex: number;
10
+ };
11
+ /** @typedef {import('../lib/tracehouse/task-groups.js').TaskGroupIds} TaskGroupIds */
12
+ /** @typedef {{startTime: number, duration: number}} Timing */
13
+ /** @typedef {Timing & {urlIndex: number, [p: string]: number}} DebugTask */
2
14
  declare class LongTasks extends Audit {
15
+ /**
16
+ * Returns the timing information for the given task, recursively walking the
17
+ * task's children and adding up time spent in each type of task activity.
18
+ * If `taskTimingsByEvent` is present, it will be used for task timing instead
19
+ * of the timings on the tasks themselves.
20
+ * If `timeByTaskGroup` is not provided, a new Map will be populated with
21
+ * timing breakdown; if one is provided, timing breakdown will be added to the
22
+ * existing breakdown.
23
+ *
24
+ * TODO: when simulated, a significant number of child tasks are dropped, so
25
+ * most time will be attributed to 'other' (the category of the top-level
26
+ * RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
27
+ * @param {LH.Artifacts.TaskNode} task
28
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
29
+ * @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
30
+ * @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
31
+ */
32
+ static getTimingBreakdown(task: LH.Artifacts.TaskNode, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined, timeByTaskGroup?: Map<import("../lib/tracehouse/task-groups.js").TaskGroupIds, number> | undefined): {
33
+ startTime: number;
34
+ duration: number;
35
+ timeByTaskGroup: Map<TaskGroupIds, number>;
36
+ };
37
+ /**
38
+ * @param {Array<LH.Artifacts.TaskNode>} longTasks
39
+ * @param {Set<string>} jsUrls
40
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
41
+ * @return {LH.Audit.Details.DebugData}
42
+ */
43
+ static makeDebugData(longTasks: Array<LH.Artifacts.TaskNode>, jsUrls: Set<string>, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined): LH.Audit.Details.DebugData;
44
+ /**
45
+ * Get timing from task, overridden by taskTimingsByEvent if provided.
46
+ * @param {LH.Artifacts.TaskNode} task
47
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
48
+ * @return {Timing}
49
+ */
50
+ static getTiming(task: LH.Artifacts.TaskNode, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined): Timing;
3
51
  /**
4
52
  * @param {LH.Artifacts} artifacts
5
53
  * @param {LH.Audit.Context} context
@@ -14,6 +14,7 @@ import {getJavaScriptURLs, getAttributableURLForTask} from '../lib/tracehouse/ta
14
14
 
15
15
  /** We don't always have timing data for short tasks, if we're missing timing data. Treat it as though it were 0ms. */
16
16
  const DEFAULT_TIMING = {startTime: 0, endTime: 0, duration: 0};
17
+ const DISPLAYED_TASK_COUNT = 20;
17
18
 
18
19
  const UIStrings = {
19
20
  /** Title of a diagnostic LH audit that provides details on the longest running tasks that occur when the page loads. */
@@ -31,6 +32,30 @@ const UIStrings = {
31
32
 
32
33
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
33
34
 
35
+ /**
36
+ * Insert `url` into `urls` array if not already present. Returns
37
+ * the index of `url` in `urls` for later lookup.
38
+ * @param {Array<string>} urls
39
+ * @param {string} url
40
+ */
41
+ function insertUrl(urls, url) {
42
+ const index = urls.indexOf(url);
43
+ if (index > -1) return index;
44
+ return urls.push(url) - 1;
45
+ }
46
+
47
+ /**
48
+ * @param {number} value
49
+ * @return {number}
50
+ */
51
+ function roundTenths(value) {
52
+ return Math.round(value * 10) / 10;
53
+ }
54
+
55
+ /** @typedef {import('../lib/tracehouse/task-groups.js').TaskGroupIds} TaskGroupIds */
56
+ /** @typedef {{startTime: number, duration: number}} Timing */
57
+ /** @typedef {Timing & {urlIndex: number, [p: string]: number}} DebugTask */
58
+
34
59
  class LongTasks extends Audit {
35
60
  /**
36
61
  * @return {LH.Audit.Meta}
@@ -45,6 +70,102 @@ class LongTasks extends Audit {
45
70
  };
46
71
  }
47
72
 
73
+ /**
74
+ * Returns the timing information for the given task, recursively walking the
75
+ * task's children and adding up time spent in each type of task activity.
76
+ * If `taskTimingsByEvent` is present, it will be used for task timing instead
77
+ * of the timings on the tasks themselves.
78
+ * If `timeByTaskGroup` is not provided, a new Map will be populated with
79
+ * timing breakdown; if one is provided, timing breakdown will be added to the
80
+ * existing breakdown.
81
+ *
82
+ * TODO: when simulated, a significant number of child tasks are dropped, so
83
+ * most time will be attributed to 'other' (the category of the top-level
84
+ * RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
85
+ * @param {LH.Artifacts.TaskNode} task
86
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
87
+ * @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
88
+ * @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
89
+ */
90
+ static getTimingBreakdown(task, taskTimingsByEvent, timeByTaskGroup = new Map()) {
91
+ const taskTiming = LongTasks.getTiming(task, taskTimingsByEvent);
92
+
93
+ // Add up child time, while recursively stepping in to accumulate group times.
94
+ let childrenTime = 0;
95
+ if (taskTiming.duration > 0) {
96
+ for (const child of task.children) {
97
+ const {duration} = LongTasks.getTimingBreakdown(child, taskTimingsByEvent, timeByTaskGroup);
98
+ childrenTime += duration;
99
+ }
100
+ }
101
+
102
+ // Add this task's selfTime to its group's total time.
103
+ const selfTime = taskTiming.duration - childrenTime;
104
+ const taskGroupTime = timeByTaskGroup.get(task.group.id) || 0;
105
+ timeByTaskGroup.set(task.group.id, taskGroupTime + selfTime);
106
+
107
+ return {
108
+ startTime: taskTiming.startTime,
109
+ duration: taskTiming.duration,
110
+ timeByTaskGroup,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * @param {Array<LH.Artifacts.TaskNode>} longTasks
116
+ * @param {Set<string>} jsUrls
117
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
118
+ * @return {LH.Audit.Details.DebugData}
119
+ */
120
+ static makeDebugData(longTasks, jsUrls, taskTimingsByEvent) {
121
+ /** @type {Array<string>} */
122
+ const urls = [];
123
+ /** @type {Array<DebugTask>} */
124
+ const tasks = [];
125
+
126
+ for (const longTask of longTasks) {
127
+ const attributableUrl = getAttributableURLForTask(longTask, jsUrls);
128
+
129
+ const {startTime, duration, timeByTaskGroup} =
130
+ LongTasks.getTimingBreakdown(longTask, taskTimingsByEvent);
131
+
132
+ // Round time per group and sort entries so order is consistent.
133
+ const timeByTaskGroupEntries = [...timeByTaskGroup]
134
+ .map(/** @return {[TaskGroupIds, number]} */ ([group, time]) => [group, roundTenths(time)])
135
+ .sort((a, b) => a[0].localeCompare(b[0]));
136
+
137
+ tasks.push({
138
+ urlIndex: insertUrl(urls, attributableUrl),
139
+ startTime: roundTenths(startTime),
140
+ duration: roundTenths(duration),
141
+ ...Object.fromEntries(timeByTaskGroupEntries),
142
+ });
143
+ }
144
+
145
+ return {
146
+ type: 'debugdata',
147
+ urls,
148
+ tasks,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Get timing from task, overridden by taskTimingsByEvent if provided.
154
+ * @param {LH.Artifacts.TaskNode} task
155
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
156
+ * @return {Timing}
157
+ */
158
+ static getTiming(task, taskTimingsByEvent) {
159
+ /** @type {Timing} */
160
+ let timing = task;
161
+ if (taskTimingsByEvent) {
162
+ timing = taskTimingsByEvent.get(task.event) || DEFAULT_TIMING;
163
+ }
164
+
165
+ const {duration, startTime} = timing;
166
+ return {duration, startTime};
167
+ }
168
+
48
169
  /**
49
170
  * @param {LH.Artifacts} artifacts
50
171
  * @param {LH.Audit.Context} context
@@ -58,10 +179,12 @@ class LongTasks extends Audit {
58
179
  const devtoolsLog = artifacts.devtoolsLogs[LongTasks.DEFAULT_PASS];
59
180
  const networkRecords = await NetworkRecords.request(devtoolsLog, context);
60
181
 
61
- /** @type {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>} */
62
- const taskTimingsByEvent = new Map();
182
+ /** @type {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} */
183
+ let taskTimingsByEvent;
63
184
 
64
185
  if (settings.throttlingMethod === 'simulate') {
186
+ taskTimingsByEvent = new Map();
187
+
65
188
  const simulatorOptions = {devtoolsLog, settings: context.settings};
66
189
  const pageGraph = await PageDependencyGraph.request({trace, devtoolsLog, URL}, context);
67
190
  const simulator = await LoadSimulator.request(simulatorOptions, context);
@@ -70,30 +193,32 @@ class LongTasks extends Audit {
70
193
  if (node.type !== 'cpu') continue;
71
194
  taskTimingsByEvent.set(node.event, timing);
72
195
  }
73
- } else {
74
- for (const task of tasks) {
75
- if (task.unbounded || task.parent) continue;
76
- taskTimingsByEvent.set(task.event, task);
77
- }
78
196
  }
79
197
 
80
198
  const jsURLs = getJavaScriptURLs(networkRecords);
81
- // Only consider up to 20 long, top-level (no parent) tasks that have an explicit endTime
82
- const longtasks = tasks
83
- .map(t => {
84
- const timing = taskTimingsByEvent.get(t.event) || DEFAULT_TIMING;
85
- return {...t, duration: timing.duration, startTime: timing.startTime};
199
+
200
+ // Only consider top-level (no parent) long tasks that have an explicit endTime.
201
+ const longTasks = tasks
202
+ .map(task => {
203
+ // Use duration from simulation, if available.
204
+ const {duration} = LongTasks.getTiming(task, taskTimingsByEvent);
205
+ return {task, duration};
206
+ })
207
+ .filter(({task, duration}) => {
208
+ return duration >= 50 && !task.unbounded && !task.parent;
86
209
  })
87
- .filter(t => t.duration >= 50 && !t.unbounded && !t.parent)
88
210
  .sort((a, b) => b.duration - a.duration)
89
- .slice(0, 20);
211
+ .map(({task}) => task);
90
212
 
91
213
  // TODO(beytoven): Add start time that matches with the simulated throttling
92
- const results = longtasks.map(task => ({
93
- url: getAttributableURLForTask(task, jsURLs),
94
- duration: task.duration,
95
- startTime: task.startTime,
96
- }));
214
+ const results = longTasks.map(task => {
215
+ const timing = LongTasks.getTiming(task, taskTimingsByEvent);
216
+ return {
217
+ url: getAttributableURLForTask(task, jsURLs),
218
+ duration: timing.duration,
219
+ startTime: timing.startTime,
220
+ };
221
+ }).slice(0, DISPLAYED_TASK_COUNT);
97
222
 
98
223
  /** @type {LH.Audit.Details.Table['headings']} */
99
224
  const headings = [
@@ -107,6 +232,8 @@ class LongTasks extends Audit {
107
232
  const tableDetails = Audit.makeTableDetails(headings, results,
108
233
  {sortedBy: ['duration'], skipSumming: ['startTime']});
109
234
 
235
+ tableDetails.debugData = LongTasks.makeDebugData(longTasks, jsURLs, taskTimingsByEvent);
236
+
110
237
  let displayValue;
111
238
  if (results.length > 0) {
112
239
  displayValue = str_(UIStrings.displayValue, {itemCount: results.length});