lighthouse 10.3.0-dev.20230704 → 10.3.0-dev.20230705
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.
- package/core/audits/audit.d.ts +6 -0
- package/core/audits/audit.js +12 -0
- package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +22 -6
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +97 -25
- package/core/audits/unsized-images.js +3 -0
- package/core/computed/metrics/tbt-utils.d.ts +26 -0
- package/core/computed/metrics/tbt-utils.js +48 -28
- package/core/computed/metrics/total-blocking-time.js +1 -1
- package/core/computed/tbt-impact-tasks.d.ts +54 -0
- package/core/computed/tbt-impact-tasks.js +221 -0
- package/core/lib/dependency-graph/simulator/simulator.js +3 -1
- package/package.json +1 -1
- package/tsconfig.json +1 -1
- package/types/artifacts.d.ts +7 -0
package/core/audits/audit.d.ts
CHANGED
|
@@ -159,6 +159,12 @@ export class Audit {
|
|
|
159
159
|
* @return {LH.RawIcu<LH.Audit.Result>}
|
|
160
160
|
*/
|
|
161
161
|
static generateAuditResult(audit: typeof Audit, product: LH.Audit.Product): LH.RawIcu<LH.Audit.Result>;
|
|
162
|
+
/**
|
|
163
|
+
* @param {LH.Artifacts} artifacts
|
|
164
|
+
* @param {LH.Audit.Context} context
|
|
165
|
+
* @returns {LH.Artifacts.MetricComputationDataInput}
|
|
166
|
+
*/
|
|
167
|
+
static makeMetricComputationDataInput(artifacts: LH.Artifacts, context: LH.Audit.Context): LH.Artifacts.MetricComputationDataInput;
|
|
162
168
|
}
|
|
163
169
|
import * as LH from '../../types/lh.js';
|
|
164
170
|
//# sourceMappingURL=audit.d.ts.map
|
package/core/audits/audit.js
CHANGED
|
@@ -415,6 +415,18 @@ class Audit {
|
|
|
415
415
|
details: product.details,
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @param {LH.Artifacts} artifacts
|
|
421
|
+
* @param {LH.Audit.Context} context
|
|
422
|
+
* @returns {LH.Artifacts.MetricComputationDataInput}
|
|
423
|
+
*/
|
|
424
|
+
static makeMetricComputationDataInput(artifacts, context) {
|
|
425
|
+
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
426
|
+
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
427
|
+
const gatherContext = artifacts.GatherContext;
|
|
428
|
+
return {trace, devtoolsLog, gatherContext, settings: context.settings, URL: artifacts.URL};
|
|
429
|
+
}
|
|
418
430
|
}
|
|
419
431
|
|
|
420
432
|
export {Audit};
|
|
@@ -49,6 +49,23 @@ export class ByteEfficiencyAudit extends Audit {
|
|
|
49
49
|
* @return {Promise<LH.Audit.Product>}
|
|
50
50
|
*/
|
|
51
51
|
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
52
|
+
/**
|
|
53
|
+
* Computes the estimated effect of all the byte savings on the provided graph.
|
|
54
|
+
*
|
|
55
|
+
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
|
|
56
|
+
* @param {Node} graph
|
|
57
|
+
* @param {Simulator} simulator
|
|
58
|
+
* @param {{label?: string, providedWastedBytesByUrl?: Map<string, number>}=} options
|
|
59
|
+
* @return {{savings: number, simulationBeforeChanges: LH.Gatherer.Simulation.Result, simulationAfterChanges: LH.Gatherer.Simulation.Result}}
|
|
60
|
+
*/
|
|
61
|
+
static computeWasteWithGraph(results: Array<LH.Audit.ByteEfficiencyItem>, graph: Node, simulator: Simulator, options?: {
|
|
62
|
+
label?: string;
|
|
63
|
+
providedWastedBytesByUrl?: Map<string, number>;
|
|
64
|
+
} | undefined): {
|
|
65
|
+
savings: number;
|
|
66
|
+
simulationBeforeChanges: LH.Gatherer.Simulation.Result;
|
|
67
|
+
simulationAfterChanges: LH.Gatherer.Simulation.Result;
|
|
68
|
+
};
|
|
52
69
|
/**
|
|
53
70
|
* Computes the estimated effect of all the byte savings on the maximum of the following:
|
|
54
71
|
*
|
|
@@ -58,22 +75,21 @@ export class ByteEfficiencyAudit extends Audit {
|
|
|
58
75
|
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
|
|
59
76
|
* @param {Node} graph
|
|
60
77
|
* @param {Simulator} simulator
|
|
61
|
-
* @param {{includeLoad?: boolean,
|
|
78
|
+
* @param {{includeLoad?: boolean, providedWastedBytesByUrl?: Map<string, number>}=} options
|
|
62
79
|
* @return {number}
|
|
63
80
|
*/
|
|
64
81
|
static computeWasteWithTTIGraph(results: Array<LH.Audit.ByteEfficiencyItem>, graph: Node, simulator: Simulator, options?: {
|
|
65
82
|
includeLoad?: boolean;
|
|
66
|
-
label?: string;
|
|
67
83
|
providedWastedBytesByUrl?: Map<string, number>;
|
|
68
84
|
} | undefined): number;
|
|
69
85
|
/**
|
|
70
86
|
* @param {ByteEfficiencyProduct} result
|
|
71
|
-
* @param {Node|null} graph
|
|
72
87
|
* @param {Simulator} simulator
|
|
73
|
-
* @param {LH.Artifacts
|
|
74
|
-
* @
|
|
88
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationInput
|
|
89
|
+
* @param {LH.Audit.Context} context
|
|
90
|
+
* @return {Promise<LH.Audit.Product>}
|
|
75
91
|
*/
|
|
76
|
-
static createAuditProduct(result: ByteEfficiencyProduct,
|
|
92
|
+
static createAuditProduct(result: ByteEfficiencyProduct, simulator: Simulator, metricComputationInput: LH.Artifacts.MetricComputationDataInput, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
77
93
|
/**
|
|
78
94
|
* @param {LH.Artifacts} artifacts
|
|
79
95
|
* @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
|
|
@@ -10,6 +10,9 @@ import * as i18n from '../../lib/i18n/i18n.js';
|
|
|
10
10
|
import {NetworkRecords} from '../../computed/network-records.js';
|
|
11
11
|
import {LoadSimulator} from '../../computed/load-simulator.js';
|
|
12
12
|
import {PageDependencyGraph} from '../../computed/page-dependency-graph.js';
|
|
13
|
+
import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js';
|
|
14
|
+
import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js';
|
|
15
|
+
import {LCPImageRecord} from '../../computed/lcp-image-record.js';
|
|
13
16
|
|
|
14
17
|
const str_ = i18n.createIcuMessageFn(import.meta.url, {});
|
|
15
18
|
|
|
@@ -104,9 +107,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
104
107
|
*/
|
|
105
108
|
static async audit(artifacts, context) {
|
|
106
109
|
const gatherContext = artifacts.GatherContext;
|
|
107
|
-
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
108
110
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
109
|
-
const URL = artifacts.URL;
|
|
110
111
|
const settings = context?.settings || {};
|
|
111
112
|
const simulatorOptions = {
|
|
112
113
|
devtoolsLog,
|
|
@@ -125,34 +126,29 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
const
|
|
129
|
+
const metricComputationInput = Audit.makeMetricComputationDataInput(artifacts, context);
|
|
130
|
+
|
|
131
|
+
const [result, simulator] = await Promise.all([
|
|
129
132
|
this.audit_(artifacts, networkRecords, context),
|
|
130
|
-
// Page dependency graph is only used in navigation mode.
|
|
131
|
-
gatherContext.gatherMode === 'navigation' ?
|
|
132
|
-
PageDependencyGraph.request({trace, devtoolsLog, URL}, context) :
|
|
133
|
-
null,
|
|
134
133
|
LoadSimulator.request(simulatorOptions, context),
|
|
135
134
|
]);
|
|
136
135
|
|
|
137
|
-
return this.createAuditProduct(result,
|
|
136
|
+
return this.createAuditProduct(result, simulator, metricComputationInput, context);
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
/**
|
|
141
|
-
* Computes the estimated effect of all the byte savings on the
|
|
142
|
-
*
|
|
143
|
-
* - end time of the last long task in the provided graph
|
|
144
|
-
* - (if includeLoad is true or not provided) end time of the last node in the graph
|
|
140
|
+
* Computes the estimated effect of all the byte savings on the provided graph.
|
|
145
141
|
*
|
|
146
142
|
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
|
|
147
143
|
* @param {Node} graph
|
|
148
144
|
* @param {Simulator} simulator
|
|
149
|
-
* @param {{
|
|
150
|
-
* @return {number}
|
|
145
|
+
* @param {{label?: string, providedWastedBytesByUrl?: Map<string, number>}=} options
|
|
146
|
+
* @return {{savings: number, simulationBeforeChanges: LH.Gatherer.Simulation.Result, simulationAfterChanges: LH.Gatherer.Simulation.Result}}
|
|
151
147
|
*/
|
|
152
|
-
static
|
|
153
|
-
options = Object.assign({
|
|
154
|
-
const beforeLabel = `${options.label}-before`;
|
|
155
|
-
const afterLabel = `${options.label}-after`;
|
|
148
|
+
static computeWasteWithGraph(results, graph, simulator, options) {
|
|
149
|
+
options = Object.assign({label: ''}, options);
|
|
150
|
+
const beforeLabel = `${this.meta.id}-${options.label}-before`;
|
|
151
|
+
const afterLabel = `${this.meta.id}-${options.label}-after`;
|
|
156
152
|
|
|
157
153
|
const simulationBeforeChanges = simulator.simulate(graph, {label: beforeLabel});
|
|
158
154
|
|
|
@@ -187,7 +183,36 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
187
183
|
node.record.transferSize = originalTransferSize;
|
|
188
184
|
});
|
|
189
185
|
|
|
190
|
-
const
|
|
186
|
+
const savings = simulationBeforeChanges.timeInMs - simulationAfterChanges.timeInMs;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
// Round waste to nearest 10ms
|
|
190
|
+
savings: Math.round(Math.max(savings, 0) / 10) * 10,
|
|
191
|
+
simulationBeforeChanges,
|
|
192
|
+
simulationAfterChanges,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Computes the estimated effect of all the byte savings on the maximum of the following:
|
|
198
|
+
*
|
|
199
|
+
* - end time of the last long task in the provided graph
|
|
200
|
+
* - (if includeLoad is true or not provided) end time of the last node in the graph
|
|
201
|
+
*
|
|
202
|
+
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
|
|
203
|
+
* @param {Node} graph
|
|
204
|
+
* @param {Simulator} simulator
|
|
205
|
+
* @param {{includeLoad?: boolean, providedWastedBytesByUrl?: Map<string, number>}=} options
|
|
206
|
+
* @return {number}
|
|
207
|
+
*/
|
|
208
|
+
static computeWasteWithTTIGraph(results, graph, simulator, options) {
|
|
209
|
+
options = Object.assign({includeLoad: true}, options);
|
|
210
|
+
const {savings: savingsOnOverallLoad, simulationBeforeChanges, simulationAfterChanges} =
|
|
211
|
+
this.computeWasteWithGraph(results, graph, simulator, {
|
|
212
|
+
...options,
|
|
213
|
+
label: 'overallLoad',
|
|
214
|
+
});
|
|
215
|
+
|
|
191
216
|
const savingsOnTTI =
|
|
192
217
|
LanternInteractive.getLastLongTaskEndTime(simulationBeforeChanges.nodeTimings) -
|
|
193
218
|
LanternInteractive.getLastLongTaskEndTime(simulationAfterChanges.nodeTimings);
|
|
@@ -201,24 +226,63 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
201
226
|
|
|
202
227
|
/**
|
|
203
228
|
* @param {ByteEfficiencyProduct} result
|
|
204
|
-
* @param {Node|null} graph
|
|
205
229
|
* @param {Simulator} simulator
|
|
206
|
-
* @param {LH.Artifacts
|
|
207
|
-
* @
|
|
230
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationInput
|
|
231
|
+
* @param {LH.Audit.Context} context
|
|
232
|
+
* @return {Promise<LH.Audit.Product>}
|
|
208
233
|
*/
|
|
209
|
-
static createAuditProduct(result,
|
|
234
|
+
static async createAuditProduct(result, simulator, metricComputationInput, context) {
|
|
210
235
|
const results = result.items.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes);
|
|
211
236
|
|
|
212
237
|
const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
|
|
213
238
|
|
|
239
|
+
/** @type {LH.Audit.MetricSavings} */
|
|
240
|
+
const metricSavings = {
|
|
241
|
+
FCP: 0,
|
|
242
|
+
LCP: 0,
|
|
243
|
+
};
|
|
244
|
+
|
|
214
245
|
// `wastedMs` may be negative, if making the opportunity change could be detrimental.
|
|
215
246
|
// This is useful information in the LHR and should be preserved.
|
|
216
247
|
let wastedMs;
|
|
217
|
-
if (gatherContext.gatherMode === 'navigation') {
|
|
218
|
-
|
|
248
|
+
if (metricComputationInput.gatherContext.gatherMode === 'navigation') {
|
|
249
|
+
const graph = await PageDependencyGraph.request(metricComputationInput, context);
|
|
250
|
+
const {
|
|
251
|
+
pessimisticGraph: pessimisticFCPGraph,
|
|
252
|
+
} = await LanternFirstContentfulPaint.request(metricComputationInput, context);
|
|
253
|
+
const {
|
|
254
|
+
pessimisticGraph: pessimisticLCPGraph,
|
|
255
|
+
} = await LanternLargestContentfulPaint.request(metricComputationInput, context);
|
|
256
|
+
|
|
219
257
|
wastedMs = this.computeWasteWithTTIGraph(results, graph, simulator, {
|
|
220
258
|
providedWastedBytesByUrl: result.wastedBytesByUrl,
|
|
221
259
|
});
|
|
260
|
+
|
|
261
|
+
const {savings: fcpSavings} = this.computeWasteWithGraph(
|
|
262
|
+
results,
|
|
263
|
+
pessimisticFCPGraph,
|
|
264
|
+
simulator,
|
|
265
|
+
{providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'fcp'}
|
|
266
|
+
);
|
|
267
|
+
const {savings: lcpGraphSavings} = this.computeWasteWithGraph(
|
|
268
|
+
results,
|
|
269
|
+
pessimisticLCPGraph,
|
|
270
|
+
simulator,
|
|
271
|
+
{providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'lcp'}
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
|
|
275
|
+
let lcpRecordSavings = 0;
|
|
276
|
+
const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
|
|
277
|
+
if (lcpRecord) {
|
|
278
|
+
const lcpResult = results.find(result => result.url === lcpRecord.url);
|
|
279
|
+
if (lcpResult) {
|
|
280
|
+
lcpRecordSavings = simulator.computeWastedMsFromWastedBytes(lcpResult.wastedBytes);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
metricSavings.FCP = fcpSavings;
|
|
285
|
+
metricSavings.LCP = Math.max(lcpGraphSavings, lcpRecordSavings);
|
|
222
286
|
} else {
|
|
223
287
|
wastedMs = simulator.computeWastedMsFromWastedBytes(wastedBytes);
|
|
224
288
|
}
|
|
@@ -232,6 +296,13 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
232
296
|
const details = Audit.makeOpportunityDetails(result.headings, results,
|
|
233
297
|
{overallSavingsMs: wastedMs, overallSavingsBytes: wastedBytes, sortedBy});
|
|
234
298
|
|
|
299
|
+
// TODO: Remove from debug data once `metricSavings` is added to the LHR.
|
|
300
|
+
// For now, add it to debug data for visibility.
|
|
301
|
+
details.debugData = {
|
|
302
|
+
type: 'debugdata',
|
|
303
|
+
metricSavings,
|
|
304
|
+
};
|
|
305
|
+
|
|
235
306
|
return {
|
|
236
307
|
explanation: result.explanation,
|
|
237
308
|
warnings: result.warnings,
|
|
@@ -240,6 +311,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
240
311
|
numericUnit: 'millisecond',
|
|
241
312
|
score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs),
|
|
242
313
|
details,
|
|
314
|
+
metricSavings,
|
|
243
315
|
};
|
|
244
316
|
}
|
|
245
317
|
|
|
@@ -15,4 +15,30 @@ export function calculateSumOfBlockingTime(topLevelEvents: Array<{
|
|
|
15
15
|
end: number;
|
|
16
16
|
duration: number;
|
|
17
17
|
}>, startTimeMs: number, endTimeMs: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* For TBT, We only want to consider tasks that fall in our time range
|
|
20
|
+
* - FCP and TTI for navigation mode
|
|
21
|
+
* - Trace start and trace end for timespan mode
|
|
22
|
+
*
|
|
23
|
+
* FCP is picked as `startTimeMs` because there is little risk of user input happening
|
|
24
|
+
* before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
25
|
+
* optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
26
|
+
*
|
|
27
|
+
* TTI is picked as `endTimeMs` because we want a well defined end point for page load.
|
|
28
|
+
*
|
|
29
|
+
* @param {{start: number, end: number, duration: number}} event
|
|
30
|
+
* @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
|
|
31
|
+
* @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
|
|
32
|
+
* @param {{start: number, end: number, duration: number}} [topLevelEvent] Leave unset if `event` is top level. Has no effect if `event` has the same duration as `topLevelEvent`.
|
|
33
|
+
* @return {number}
|
|
34
|
+
*/
|
|
35
|
+
export function calculateTbtImpactForEvent(event: {
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
}, startTimeMs: number, endTimeMs: number, topLevelEvent?: {
|
|
40
|
+
start: number;
|
|
41
|
+
end: number;
|
|
42
|
+
duration: number;
|
|
43
|
+
} | undefined): number;
|
|
18
44
|
//# sourceMappingURL=tbt-utils.d.ts.map
|
|
@@ -6,6 +6,52 @@
|
|
|
6
6
|
|
|
7
7
|
const BLOCKING_TIME_THRESHOLD = 50;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* For TBT, We only want to consider tasks that fall in our time range
|
|
11
|
+
* - FCP and TTI for navigation mode
|
|
12
|
+
* - Trace start and trace end for timespan mode
|
|
13
|
+
*
|
|
14
|
+
* FCP is picked as `startTimeMs` because there is little risk of user input happening
|
|
15
|
+
* before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
16
|
+
* optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
17
|
+
*
|
|
18
|
+
* TTI is picked as `endTimeMs` because we want a well defined end point for page load.
|
|
19
|
+
*
|
|
20
|
+
* @param {{start: number, end: number, duration: number}} event
|
|
21
|
+
* @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
|
|
22
|
+
* @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
|
|
23
|
+
* @param {{start: number, end: number, duration: number}} [topLevelEvent] Leave unset if `event` is top level. Has no effect if `event` has the same duration as `topLevelEvent`.
|
|
24
|
+
* @return {number}
|
|
25
|
+
*/
|
|
26
|
+
function calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent) {
|
|
27
|
+
let threshold = BLOCKING_TIME_THRESHOLD;
|
|
28
|
+
|
|
29
|
+
// If a task is not top level, it doesn't make sense to subtract the entire 50ms
|
|
30
|
+
// blocking threshold from the event.
|
|
31
|
+
//
|
|
32
|
+
// e.g. A 80ms top level task with two 40ms children should attribute some blocking
|
|
33
|
+
// time to the 40ms tasks even though they do not meet the 50ms threshold.
|
|
34
|
+
//
|
|
35
|
+
// The solution is to scale the threshold for child events to be considered blocking.
|
|
36
|
+
if (topLevelEvent) threshold *= (event.duration / topLevelEvent.duration);
|
|
37
|
+
|
|
38
|
+
if (event.duration < threshold) return 0;
|
|
39
|
+
if (event.end < startTimeMs) return 0;
|
|
40
|
+
if (event.start > endTimeMs) return 0;
|
|
41
|
+
|
|
42
|
+
// Perform the clipping and then calculate Blocking Region. So if we have a 150ms task
|
|
43
|
+
// [0, 150] and `startTimeMs` is at 50ms, we first clip the task to [50, 150], and then
|
|
44
|
+
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before
|
|
45
|
+
// the start time are unimportant, so we care whether the main thread is busy more than
|
|
46
|
+
// 50ms at a time only after the start time.
|
|
47
|
+
const clippedStart = Math.max(event.start, startTimeMs);
|
|
48
|
+
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
49
|
+
const clippedDuration = clippedEnd - clippedStart;
|
|
50
|
+
if (clippedDuration < threshold) return 0;
|
|
51
|
+
|
|
52
|
+
return clippedDuration - threshold;
|
|
53
|
+
}
|
|
54
|
+
|
|
9
55
|
/**
|
|
10
56
|
* @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
|
|
11
57
|
* @param {number} startTimeMs
|
|
@@ -15,36 +61,9 @@ const BLOCKING_TIME_THRESHOLD = 50;
|
|
|
15
61
|
function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
16
62
|
if (endTimeMs <= startTimeMs) return 0;
|
|
17
63
|
|
|
18
|
-
const threshold = BLOCKING_TIME_THRESHOLD;
|
|
19
64
|
let sumBlockingTime = 0;
|
|
20
65
|
for (const event of topLevelEvents) {
|
|
21
|
-
|
|
22
|
-
if (event.duration < threshold) continue;
|
|
23
|
-
|
|
24
|
-
// We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
|
|
25
|
-
// FCP is picked as the lower bound because there is little risk of user input happening
|
|
26
|
-
// before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
|
|
27
|
-
// optimizing to reach FCP as fast as possible without having to worry about task lengths.
|
|
28
|
-
if (event.end < startTimeMs) continue;
|
|
29
|
-
|
|
30
|
-
// TTI is picked as the upper bound because we want a well defined end point for page load.
|
|
31
|
-
if (event.start > endTimeMs) continue;
|
|
32
|
-
|
|
33
|
-
// We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
|
|
34
|
-
// task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
|
|
35
|
-
// calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
|
|
36
|
-
// are unimportant, so we care whether the main thread is busy more than 50ms at a time only
|
|
37
|
-
// after FCP.
|
|
38
|
-
const clippedStart = Math.max(event.start, startTimeMs);
|
|
39
|
-
const clippedEnd = Math.min(event.end, endTimeMs);
|
|
40
|
-
const clippedDuration = clippedEnd - clippedStart;
|
|
41
|
-
if (clippedDuration < threshold) continue;
|
|
42
|
-
|
|
43
|
-
// The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
|
|
44
|
-
// Example:
|
|
45
|
-
// [ 250ms Task ]
|
|
46
|
-
// | First 50ms | Blocking Region (200ms) |
|
|
47
|
-
sumBlockingTime += clippedDuration - threshold;
|
|
66
|
+
sumBlockingTime += calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
return sumBlockingTime;
|
|
@@ -53,4 +72,5 @@ function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
|
|
|
53
72
|
export {
|
|
54
73
|
BLOCKING_TIME_THRESHOLD,
|
|
55
74
|
calculateSumOfBlockingTime,
|
|
75
|
+
calculateTbtImpactForEvent,
|
|
56
76
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export { TBTImpactTasksComputed as TBTImpactTasks };
|
|
2
|
+
export type TBTImpactTask = LH.Artifacts.TaskNode & {
|
|
3
|
+
tbtImpact: number;
|
|
4
|
+
selfTbtImpact: number;
|
|
5
|
+
};
|
|
6
|
+
declare const TBTImpactTasksComputed: typeof TBTImpactTasks & {
|
|
7
|
+
request: (dependencies: import("../index.js").Artifacts.MetricComputationDataInput, context: import("../../types/utility-types.js").default.ImmutableObject<{
|
|
8
|
+
computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
|
|
9
|
+
}>) => Promise<TBTImpactTask[]>;
|
|
10
|
+
};
|
|
11
|
+
/** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
|
|
12
|
+
declare class TBTImpactTasks {
|
|
13
|
+
/**
|
|
14
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
15
|
+
* @return {LH.Artifacts.TaskNode}
|
|
16
|
+
*/
|
|
17
|
+
static getTopLevelTask(task: LH.Artifacts.TaskNode): LH.Artifacts.TaskNode;
|
|
18
|
+
/**
|
|
19
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
20
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
21
|
+
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
|
|
22
|
+
*/
|
|
23
|
+
static getTbtBounds(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
|
|
24
|
+
startTimeMs: number;
|
|
25
|
+
endTimeMs: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
29
|
+
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
|
|
30
|
+
*/
|
|
31
|
+
static createImpactTasks(tasks: LH.Artifacts.TaskNode[], taskToImpact: Map<LH.Artifacts.TaskNode, number>): TBTImpactTask[];
|
|
32
|
+
/**
|
|
33
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
34
|
+
* @param {number} startTimeMs
|
|
35
|
+
* @param {number} endTimeMs
|
|
36
|
+
* @return {TBTImpactTask[]}
|
|
37
|
+
*/
|
|
38
|
+
static computeImpactsFromObservedTasks(tasks: LH.Artifacts.TaskNode[], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
|
|
39
|
+
/**
|
|
40
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
41
|
+
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
|
|
42
|
+
* @param {number} startTimeMs
|
|
43
|
+
* @param {number} endTimeMs
|
|
44
|
+
* @return {TBTImpactTask[]}
|
|
45
|
+
*/
|
|
46
|
+
static computeImpactsFromLantern(tasks: LH.Artifacts.TaskNode[], tbtNodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
|
|
47
|
+
/**
|
|
48
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
49
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
50
|
+
* @return {Promise<TBTImpactTask[]>}
|
|
51
|
+
*/
|
|
52
|
+
static compute_(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<TBTImpactTask[]>;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=tbt-impact-tasks.d.ts.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2023 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
|
+
|
|
7
|
+
import {makeComputedArtifact} from './computed-artifact.js';
|
|
8
|
+
import {MainThreadTasks} from './main-thread-tasks.js';
|
|
9
|
+
import {FirstContentfulPaint} from './metrics/first-contentful-paint.js';
|
|
10
|
+
import {Interactive} from './metrics/interactive.js';
|
|
11
|
+
import {TotalBlockingTime} from './metrics/total-blocking-time.js';
|
|
12
|
+
import {ProcessedTrace} from './processed-trace.js';
|
|
13
|
+
import {calculateTbtImpactForEvent} from './metrics/tbt-utils.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
|
|
16
|
+
|
|
17
|
+
class TBTImpactTasks {
|
|
18
|
+
/**
|
|
19
|
+
* @param {LH.Artifacts.TaskNode} task
|
|
20
|
+
* @return {LH.Artifacts.TaskNode}
|
|
21
|
+
*/
|
|
22
|
+
static getTopLevelTask(task) {
|
|
23
|
+
let topLevelTask = task;
|
|
24
|
+
while (topLevelTask.parent) {
|
|
25
|
+
topLevelTask = topLevelTask.parent;
|
|
26
|
+
}
|
|
27
|
+
return topLevelTask;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
32
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
33
|
+
* @return {Promise<{startTimeMs: number, endTimeMs: number}>}
|
|
34
|
+
*/
|
|
35
|
+
static async getTbtBounds(metricComputationData, context) {
|
|
36
|
+
const processedTrace = await ProcessedTrace.request(metricComputationData.trace, context);
|
|
37
|
+
if (metricComputationData.gatherContext.gatherMode !== 'navigation') {
|
|
38
|
+
return {
|
|
39
|
+
startTimeMs: 0,
|
|
40
|
+
endTimeMs: processedTrace.timings.traceEnd,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fcpResult = await FirstContentfulPaint.request(metricComputationData, context);
|
|
45
|
+
const ttiResult = await Interactive.request(metricComputationData, context);
|
|
46
|
+
|
|
47
|
+
let startTimeMs = fcpResult.timing;
|
|
48
|
+
let endTimeMs = ttiResult.timing;
|
|
49
|
+
|
|
50
|
+
// When using lantern, we want to get a pessimistic view of the long tasks.
|
|
51
|
+
// This means we assume the earliest possible start time and latest possible end time.
|
|
52
|
+
|
|
53
|
+
if ('optimisticEstimate' in fcpResult) {
|
|
54
|
+
startTimeMs = fcpResult.optimisticEstimate.timeInMs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ('pessimisticEstimate' in ttiResult) {
|
|
58
|
+
endTimeMs = ttiResult.pessimisticEstimate.timeInMs;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {startTimeMs, endTimeMs};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
66
|
+
* @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
|
|
67
|
+
*/
|
|
68
|
+
static createImpactTasks(tasks, taskToImpact) {
|
|
69
|
+
/** @type {TBTImpactTask[]} */
|
|
70
|
+
const tbtImpactTasks = [];
|
|
71
|
+
|
|
72
|
+
for (const task of tasks) {
|
|
73
|
+
const tbtImpact = taskToImpact.get(task) || 0;
|
|
74
|
+
let selfTbtImpact = tbtImpact;
|
|
75
|
+
|
|
76
|
+
for (const child of task.children) {
|
|
77
|
+
const childTbtImpact = taskToImpact.get(child) || 0;
|
|
78
|
+
selfTbtImpact -= childTbtImpact;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
tbtImpactTasks.push({
|
|
82
|
+
...task,
|
|
83
|
+
tbtImpact,
|
|
84
|
+
selfTbtImpact,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return tbtImpactTasks;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
93
|
+
* @param {number} startTimeMs
|
|
94
|
+
* @param {number} endTimeMs
|
|
95
|
+
* @return {TBTImpactTask[]}
|
|
96
|
+
*/
|
|
97
|
+
static computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs) {
|
|
98
|
+
/** @type {Map<LH.Artifacts.TaskNode, number>} */
|
|
99
|
+
const taskToImpact = new Map();
|
|
100
|
+
|
|
101
|
+
for (const task of tasks) {
|
|
102
|
+
const event = {
|
|
103
|
+
start: task.startTime,
|
|
104
|
+
end: task.endTime,
|
|
105
|
+
duration: task.duration,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const topLevelTask = this.getTopLevelTask(task);
|
|
109
|
+
const topLevelEvent = {
|
|
110
|
+
start: topLevelTask.startTime,
|
|
111
|
+
end: topLevelTask.endTime,
|
|
112
|
+
duration: topLevelTask.duration,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
|
|
116
|
+
|
|
117
|
+
taskToImpact.set(task, tbtImpact);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this.createImpactTasks(tasks, taskToImpact);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {LH.Artifacts.TaskNode[]} tasks
|
|
125
|
+
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
|
|
126
|
+
* @param {number} startTimeMs
|
|
127
|
+
* @param {number} endTimeMs
|
|
128
|
+
* @return {TBTImpactTask[]}
|
|
129
|
+
*/
|
|
130
|
+
static computeImpactsFromLantern(tasks, tbtNodeTimings, startTimeMs, endTimeMs) {
|
|
131
|
+
/** @type {Map<LH.Artifacts.TaskNode, number>} */
|
|
132
|
+
const taskToImpact = new Map();
|
|
133
|
+
|
|
134
|
+
/** @type {Map<LH.Artifacts.TaskNode, {start: number, end: number, duration: number}>} */
|
|
135
|
+
const topLevelTaskToEvent = new Map();
|
|
136
|
+
|
|
137
|
+
/** @type {Map<LH.TraceEvent, LH.Artifacts.TaskNode>} */
|
|
138
|
+
const traceEventToTask = new Map();
|
|
139
|
+
for (const task of tasks) {
|
|
140
|
+
traceEventToTask.set(task.event, task);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Use lantern TBT timings to calculate the TBT impact of top level tasks.
|
|
144
|
+
for (const [node, timing] of tbtNodeTimings) {
|
|
145
|
+
if (node.type !== 'cpu') continue;
|
|
146
|
+
|
|
147
|
+
const event = {
|
|
148
|
+
start: timing.startTime,
|
|
149
|
+
end: timing.endTime,
|
|
150
|
+
duration: timing.duration,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
|
|
154
|
+
|
|
155
|
+
const task = traceEventToTask.get(node.event);
|
|
156
|
+
if (!task) continue;
|
|
157
|
+
|
|
158
|
+
topLevelTaskToEvent.set(task, event);
|
|
159
|
+
taskToImpact.set(task, tbtImpact);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Interpolate the TBT impact of remaining tasks using the top level ancestor tasks.
|
|
163
|
+
// We don't have any lantern estimates for tasks that are not top level, so we need to estimate
|
|
164
|
+
// the lantern timing based on the task's observed timing relative to it's top level task's observed timing.
|
|
165
|
+
for (const task of tasks) {
|
|
166
|
+
if (taskToImpact.has(task)) continue;
|
|
167
|
+
|
|
168
|
+
const topLevelTask = this.getTopLevelTask(task);
|
|
169
|
+
|
|
170
|
+
const topLevelEvent = topLevelTaskToEvent.get(topLevelTask);
|
|
171
|
+
if (!topLevelEvent) continue;
|
|
172
|
+
|
|
173
|
+
const startRatio = (task.startTime - topLevelTask.startTime) / topLevelTask.duration;
|
|
174
|
+
const start = startRatio * topLevelEvent.duration + topLevelEvent.start;
|
|
175
|
+
|
|
176
|
+
const endRatio = (topLevelTask.endTime - task.endTime) / topLevelTask.duration;
|
|
177
|
+
const end = topLevelEvent.end - endRatio * topLevelEvent.duration;
|
|
178
|
+
|
|
179
|
+
const event = {
|
|
180
|
+
start,
|
|
181
|
+
end,
|
|
182
|
+
duration: end - start,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const tbtImpact = calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent);
|
|
186
|
+
|
|
187
|
+
taskToImpact.set(task, tbtImpact);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return this.createImpactTasks(tasks, taskToImpact);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
195
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
196
|
+
* @return {Promise<TBTImpactTask[]>}
|
|
197
|
+
*/
|
|
198
|
+
static async compute_(metricComputationData, context) {
|
|
199
|
+
const tbtResult = await TotalBlockingTime.request(metricComputationData, context);
|
|
200
|
+
const tasks = await MainThreadTasks.request(metricComputationData.trace, context);
|
|
201
|
+
|
|
202
|
+
const {startTimeMs, endTimeMs} = await this.getTbtBounds(metricComputationData, context);
|
|
203
|
+
|
|
204
|
+
if ('pessimisticEstimate' in tbtResult) {
|
|
205
|
+
return this.computeImpactsFromLantern(
|
|
206
|
+
tasks,
|
|
207
|
+
tbtResult.pessimisticEstimate.nodeTimings,
|
|
208
|
+
startTimeMs,
|
|
209
|
+
endTimeMs
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return this.computeImpactsFromObservedTasks(tasks, startTimeMs, endTimeMs);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const TBTImpactTasksComputed = makeComputedArtifact(
|
|
218
|
+
TBTImpactTasks,
|
|
219
|
+
['trace', 'devtoolsLog', 'URL', 'gatherContext', 'settings', 'simulator']
|
|
220
|
+
);
|
|
221
|
+
export {TBTImpactTasksComputed as TBTImpactTasks};
|
|
@@ -517,7 +517,9 @@ class Simulator {
|
|
|
517
517
|
|
|
518
518
|
const wastedBits = wastedBytes * 8;
|
|
519
519
|
const wastedMs = wastedBits / bitsPerSecond * 1000;
|
|
520
|
-
|
|
520
|
+
|
|
521
|
+
// This is an estimate of wasted time, so we won't be more precise than 10ms.
|
|
522
|
+
return Math.round(wastedMs / 10) * 10;
|
|
521
523
|
}
|
|
522
524
|
|
|
523
525
|
/** @return {Map<string, Map<Node, CompleteNodeTiming>>} */
|
package/package.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"core/test/config/config-plugin-test.js",
|
|
42
42
|
"core/test/legacy/config/config-test.js",
|
|
43
43
|
"core/test/config/default-config-test.js",
|
|
44
|
-
"core/test/create-test-trace.js",
|
|
45
44
|
"core/test/gather/driver/wait-for-condition-test.js",
|
|
46
45
|
"core/test/gather/gatherers/accessibility-test.js",
|
|
47
46
|
"core/test/gather/gatherers/cache-contents-test.js",
|
|
@@ -107,6 +106,7 @@
|
|
|
107
106
|
"core/test/audits/bf-cache-test.js",
|
|
108
107
|
"core/test/audits/script-treemap-data-test.js",
|
|
109
108
|
"core/test/computed/metrics/interactive-test.js",
|
|
109
|
+
"core/test/computed/tbt-impact-tasks-test.js",
|
|
110
110
|
"core/test/fixtures/config-plugins/lighthouse-plugin-simple/plugin-simple.js",
|
|
111
111
|
],
|
|
112
112
|
}
|
package/types/artifacts.d.ts
CHANGED
|
@@ -989,7 +989,11 @@ export interface TraceEvent {
|
|
|
989
989
|
};
|
|
990
990
|
data?: {
|
|
991
991
|
frame?: string;
|
|
992
|
+
parent?: string;
|
|
992
993
|
frameID?: string;
|
|
994
|
+
frameTreeNodeId?: number;
|
|
995
|
+
isMainFrame?: boolean;
|
|
996
|
+
persistentIds?: boolean,
|
|
993
997
|
processId?: number;
|
|
994
998
|
isLoadingMainFrame?: boolean;
|
|
995
999
|
documentLoaderURL?: string;
|
|
@@ -999,6 +1003,7 @@ export interface TraceEvent {
|
|
|
999
1003
|
url: string;
|
|
1000
1004
|
parent?: string;
|
|
1001
1005
|
processId?: number;
|
|
1006
|
+
name?: string;
|
|
1002
1007
|
}[];
|
|
1003
1008
|
page?: string;
|
|
1004
1009
|
readyState?: number;
|
|
@@ -1035,6 +1040,8 @@ export interface TraceEvent {
|
|
|
1035
1040
|
interactionType?: 'drag'|'keyboard'|'tapOrClick';
|
|
1036
1041
|
maxDuration?: number;
|
|
1037
1042
|
type?: string;
|
|
1043
|
+
functionName?: string;
|
|
1044
|
+
name?: string;
|
|
1038
1045
|
};
|
|
1039
1046
|
frame?: string;
|
|
1040
1047
|
name?: string;
|