lighthouse 10.4.0-dev.20230717 → 10.4.0-dev.20230719
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/cli/sentry-prompt.js +4 -2
- package/core/audits/uses-rel-preconnect.js +23 -8
- package/core/gather/driver/execution-context.d.ts +9 -9
- package/core/gather/driver/execution-context.js +24 -24
- package/core/lib/dependency-graph/simulator/network-analyzer.d.ts +28 -12
- package/core/lib/dependency-graph/simulator/network-analyzer.js +128 -100
- package/core/lib/page-functions.js +5 -6
- package/package.json +1 -1
- package/tsconfig.json +1 -0
package/cli/sentry-prompt.js
CHANGED
|
@@ -10,10 +10,12 @@ import log from 'lighthouse-logger';
|
|
|
10
10
|
|
|
11
11
|
const MAXIMUM_WAIT_TIME = 20 * 1000;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/* eslint-disable max-len */
|
|
14
14
|
const MESSAGE = `${log.reset}We're constantly trying to improve Lighthouse and its reliability.\n ` +
|
|
15
15
|
`${log.reset}Learn more: https://github.com/GoogleChrome/lighthouse/blob/main/docs/error-reporting.md \n ` +
|
|
16
|
-
` ${log.bold}May we anonymously report runtime exceptions to improve the tool over time?${log.reset}
|
|
16
|
+
` ${log.bold}May we anonymously report runtime exceptions to improve the tool over time?${log.reset}\n ` +
|
|
17
|
+
`We'll remember your choice, but you can also use the flag --[no-]enable-error-reporting`;
|
|
18
|
+
/* eslint-enable max-len */
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* @return {Promise<boolean>}
|
|
@@ -14,6 +14,7 @@ import {LoadSimulator} from '../computed/load-simulator.js';
|
|
|
14
14
|
import {ProcessedNavigation} from '../computed/processed-navigation.js';
|
|
15
15
|
import {PageDependencyGraph} from '../computed/page-dependency-graph.js';
|
|
16
16
|
import {LanternLargestContentfulPaint} from '../computed/metrics/lantern-largest-contentful-paint.js';
|
|
17
|
+
import {LanternFirstContentfulPaint} from '../computed/metrics/lantern-first-contentful-paint.js';
|
|
17
18
|
|
|
18
19
|
// Preconnect establishes a "clean" socket. Chrome's socket manager will keep an unused socket
|
|
19
20
|
// around for 10s. Meaning, the time delta between processing preconnect a request should be <10s,
|
|
@@ -125,7 +126,8 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
125
126
|
const devtoolsLog = artifacts.devtoolsLogs[UsesRelPreconnectAudit.DEFAULT_PASS];
|
|
126
127
|
const settings = context.settings;
|
|
127
128
|
|
|
128
|
-
let
|
|
129
|
+
let maxWastedLcp = 0;
|
|
130
|
+
let maxWastedFcp = 0;
|
|
129
131
|
/** @type {Array<LH.IcuMessage>} */
|
|
130
132
|
const warnings = [];
|
|
131
133
|
|
|
@@ -144,7 +146,14 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
144
146
|
/** @type {Set<string>} */
|
|
145
147
|
const lcpGraphURLs = new Set();
|
|
146
148
|
lcpGraph.traverse(node => {
|
|
147
|
-
if (node.type === 'network'
|
|
149
|
+
if (node.type === 'network') lcpGraphURLs.add(node.record.url);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const fcpGraph =
|
|
153
|
+
await LanternFirstContentfulPaint.getPessimisticGraph(pageGraph, processedNavigation);
|
|
154
|
+
const fcpGraphURLs = new Set();
|
|
155
|
+
fcpGraph.traverse(node => {
|
|
156
|
+
if (node.type === 'network') fcpGraphURLs.add(node.record.url);
|
|
148
157
|
});
|
|
149
158
|
|
|
150
159
|
/** @type {Map<string, LH.Artifacts.NetworkRequest[]>} */
|
|
@@ -216,7 +225,11 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
216
225
|
return;
|
|
217
226
|
}
|
|
218
227
|
|
|
219
|
-
|
|
228
|
+
maxWastedLcp = Math.max(wastedMs, maxWastedLcp);
|
|
229
|
+
|
|
230
|
+
if (fcpGraphURLs.has(firstRecordOfOrigin.url)) {
|
|
231
|
+
maxWastedFcp = Math.max(wastedMs, maxWastedLcp);
|
|
232
|
+
}
|
|
220
233
|
results.push({
|
|
221
234
|
url: securityOrigin,
|
|
222
235
|
wastedMs: wastedMs,
|
|
@@ -240,6 +253,7 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
240
253
|
score: 1,
|
|
241
254
|
warnings: preconnectLinks.length >= 3 ?
|
|
242
255
|
[...warnings, str_(UIStrings.tooManyPreconnectLinksWarning)] : warnings,
|
|
256
|
+
metricSavings: {LCP: maxWastedLcp, FCP: maxWastedFcp},
|
|
243
257
|
};
|
|
244
258
|
}
|
|
245
259
|
|
|
@@ -250,17 +264,18 @@ class UsesRelPreconnectAudit extends Audit {
|
|
|
250
264
|
];
|
|
251
265
|
|
|
252
266
|
const details = Audit.makeOpportunityDetails(headings, results,
|
|
253
|
-
{overallSavingsMs:
|
|
267
|
+
{overallSavingsMs: maxWastedLcp, sortedBy: ['wastedMs']});
|
|
254
268
|
|
|
255
269
|
return {
|
|
256
|
-
score: ByteEfficiencyAudit.scoreForWastedMs(
|
|
257
|
-
numericValue:
|
|
270
|
+
score: ByteEfficiencyAudit.scoreForWastedMs(maxWastedLcp),
|
|
271
|
+
numericValue: maxWastedLcp,
|
|
258
272
|
numericUnit: 'millisecond',
|
|
259
|
-
displayValue:
|
|
260
|
-
str_(i18n.UIStrings.displayValueMsSavings, {wastedMs:
|
|
273
|
+
displayValue: maxWastedLcp ?
|
|
274
|
+
str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: maxWastedLcp}) :
|
|
261
275
|
'',
|
|
262
276
|
warnings,
|
|
263
277
|
details,
|
|
278
|
+
metricSavings: {LCP: maxWastedLcp, FCP: maxWastedFcp},
|
|
264
279
|
};
|
|
265
280
|
}
|
|
266
281
|
}
|
|
@@ -11,6 +11,15 @@ export class ExecutionContext {
|
|
|
11
11
|
* @return {string}
|
|
12
12
|
*/
|
|
13
13
|
static serializeArguments(args: unknown[]): string;
|
|
14
|
+
/**
|
|
15
|
+
* Serializes an array of functions or strings.
|
|
16
|
+
*
|
|
17
|
+
* Also makes sure that an esbuild-bundled version of Lighthouse will
|
|
18
|
+
* continue to create working code to be executed within the browser.
|
|
19
|
+
* @param {Array<Function|string>=} deps
|
|
20
|
+
* @return {string}
|
|
21
|
+
*/
|
|
22
|
+
static serializeDeps(deps?: Array<Function | string> | undefined): string;
|
|
14
23
|
/** @param {LH.Gatherer.ProtocolSession} session */
|
|
15
24
|
constructor(session: LH.Gatherer.ProtocolSession);
|
|
16
25
|
_session: LH.Gatherer.ProtocolSession;
|
|
@@ -47,15 +56,6 @@ export class ExecutionContext {
|
|
|
47
56
|
* @return {Promise<*>}
|
|
48
57
|
*/
|
|
49
58
|
_evaluateInContext(expression: string, contextId: number | undefined): Promise<any>;
|
|
50
|
-
/**
|
|
51
|
-
* Serializes an array of functions or strings.
|
|
52
|
-
*
|
|
53
|
-
* Also makes sure that an esbuild-bundled version of Lighthouse will
|
|
54
|
-
* continue to create working code to be executed within the browser.
|
|
55
|
-
* @param {Array<Function|string>=} deps
|
|
56
|
-
* @return {string}
|
|
57
|
-
*/
|
|
58
|
-
_serializeDeps(deps?: Array<Function | string> | undefined): string;
|
|
59
59
|
/**
|
|
60
60
|
* Note: Prefer `evaluate` instead.
|
|
61
61
|
* Evaluate an expression in the context of the current page. If useIsolation is true, the expression
|
|
@@ -155,28 +155,6 @@ class ExecutionContext {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/**
|
|
159
|
-
* Serializes an array of functions or strings.
|
|
160
|
-
*
|
|
161
|
-
* Also makes sure that an esbuild-bundled version of Lighthouse will
|
|
162
|
-
* continue to create working code to be executed within the browser.
|
|
163
|
-
* @param {Array<Function|string>=} deps
|
|
164
|
-
* @return {string}
|
|
165
|
-
*/
|
|
166
|
-
_serializeDeps(deps) {
|
|
167
|
-
deps = [pageFunctions.esbuildFunctionNameStubString, ...deps || []];
|
|
168
|
-
return deps.map(dep => {
|
|
169
|
-
if (typeof dep === 'function') {
|
|
170
|
-
// esbuild will change the actual function name (ie. function actualName() {})
|
|
171
|
-
// always, despite minification settings, but preserve the real name in `actualName.name`
|
|
172
|
-
// (see esbuildFunctionNameStubString).
|
|
173
|
-
return `const ${dep.name} = ${dep}`;
|
|
174
|
-
} else {
|
|
175
|
-
return dep;
|
|
176
|
-
}
|
|
177
|
-
}).join('\n');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
158
|
/**
|
|
181
159
|
* Note: Prefer `evaluate` instead.
|
|
182
160
|
* Evaluate an expression in the context of the current page. If useIsolation is true, the expression
|
|
@@ -219,7 +197,7 @@ class ExecutionContext {
|
|
|
219
197
|
*/
|
|
220
198
|
evaluate(mainFn, options) {
|
|
221
199
|
const argsSerialized = ExecutionContext.serializeArguments(options.args);
|
|
222
|
-
const depsSerialized =
|
|
200
|
+
const depsSerialized = ExecutionContext.serializeDeps(options.deps);
|
|
223
201
|
|
|
224
202
|
const expression = `(() => {
|
|
225
203
|
${depsSerialized}
|
|
@@ -239,7 +217,7 @@ class ExecutionContext {
|
|
|
239
217
|
*/
|
|
240
218
|
async evaluateOnNewDocument(mainFn, options) {
|
|
241
219
|
const argsSerialized = ExecutionContext.serializeArguments(options.args);
|
|
242
|
-
const depsSerialized =
|
|
220
|
+
const depsSerialized = ExecutionContext.serializeDeps(options.deps);
|
|
243
221
|
|
|
244
222
|
const expression = `(() => {
|
|
245
223
|
${ExecutionContext._cachedNativesPreamble};
|
|
@@ -289,6 +267,28 @@ class ExecutionContext {
|
|
|
289
267
|
static serializeArguments(args) {
|
|
290
268
|
return args.map(arg => arg === undefined ? 'undefined' : JSON.stringify(arg)).join(',');
|
|
291
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Serializes an array of functions or strings.
|
|
273
|
+
*
|
|
274
|
+
* Also makes sure that an esbuild-bundled version of Lighthouse will
|
|
275
|
+
* continue to create working code to be executed within the browser.
|
|
276
|
+
* @param {Array<Function|string>=} deps
|
|
277
|
+
* @return {string}
|
|
278
|
+
*/
|
|
279
|
+
static serializeDeps(deps) {
|
|
280
|
+
deps = [pageFunctions.esbuildFunctionNameStubString, ...deps || []];
|
|
281
|
+
return deps.map(dep => {
|
|
282
|
+
if (typeof dep === 'function') {
|
|
283
|
+
// esbuild will change the actual function name (ie. function actualName() {})
|
|
284
|
+
// always, despite minification settings, but preserve the real name in `actualName.name`
|
|
285
|
+
// (see esbuildFunctionNameStubString).
|
|
286
|
+
return `const ${dep.name} = ${dep}`;
|
|
287
|
+
} else {
|
|
288
|
+
return dep;
|
|
289
|
+
}
|
|
290
|
+
}).join('\n');
|
|
291
|
+
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
export {ExecutionContext};
|
|
@@ -65,39 +65,55 @@ export class NetworkAnalyzer {
|
|
|
65
65
|
* single handshake.
|
|
66
66
|
* This is the most accurate and preferred method of measurement when the data is available.
|
|
67
67
|
*
|
|
68
|
-
* @param {
|
|
69
|
-
* @return {
|
|
68
|
+
* @param {RequestInfo} info
|
|
69
|
+
* @return {number[]|number|undefined}
|
|
70
70
|
*/
|
|
71
|
-
static
|
|
71
|
+
static _estimateRTTViaConnectionTiming(info: {
|
|
72
|
+
record: LH.Artifacts.NetworkRequest;
|
|
73
|
+
timing: LH.Crdp.Network.ResourceTiming;
|
|
74
|
+
connectionReused?: boolean | undefined;
|
|
75
|
+
}): number[] | number | undefined;
|
|
72
76
|
/**
|
|
73
77
|
* Estimates the observed RTT to each origin based on how long a download took on a fresh connection.
|
|
74
78
|
* NOTE: this will tend to overestimate the actual RTT quite significantly as the download can be
|
|
75
79
|
* slow for other reasons as well such as bandwidth constraints.
|
|
76
80
|
*
|
|
77
|
-
* @param {
|
|
78
|
-
* @return {
|
|
81
|
+
* @param {RequestInfo} info
|
|
82
|
+
* @return {number|undefined}
|
|
79
83
|
*/
|
|
80
|
-
static
|
|
84
|
+
static _estimateRTTViaDownloadTiming(info: {
|
|
85
|
+
record: LH.Artifacts.NetworkRequest;
|
|
86
|
+
timing: LH.Crdp.Network.ResourceTiming;
|
|
87
|
+
connectionReused?: boolean | undefined;
|
|
88
|
+
}): number | undefined;
|
|
81
89
|
/**
|
|
82
90
|
* Estimates the observed RTT to each origin based on how long it took until Chrome could
|
|
83
91
|
* start sending the actual request when a new connection was required.
|
|
84
92
|
* NOTE: this will tend to overestimate the actual RTT as the request can be delayed for other
|
|
85
93
|
* reasons as well such as more SSL handshakes if TLS False Start is not enabled.
|
|
86
94
|
*
|
|
87
|
-
* @param {
|
|
88
|
-
* @return {
|
|
95
|
+
* @param {RequestInfo} info
|
|
96
|
+
* @return {number|undefined}
|
|
89
97
|
*/
|
|
90
|
-
static
|
|
98
|
+
static _estimateRTTViaSendStartTiming(info: {
|
|
99
|
+
record: LH.Artifacts.NetworkRequest;
|
|
100
|
+
timing: LH.Crdp.Network.ResourceTiming;
|
|
101
|
+
connectionReused?: boolean | undefined;
|
|
102
|
+
}): number | undefined;
|
|
91
103
|
/**
|
|
92
104
|
* Estimates the observed RTT to each origin based on how long it took until Chrome received the
|
|
93
105
|
* headers of the response (~TTFB).
|
|
94
106
|
* NOTE: this is the most inaccurate way to estimate the RTT, but in some environments it's all
|
|
95
107
|
* we have access to :(
|
|
96
108
|
*
|
|
97
|
-
* @param {
|
|
98
|
-
* @return {
|
|
109
|
+
* @param {RequestInfo} info
|
|
110
|
+
* @return {number|undefined}
|
|
99
111
|
*/
|
|
100
|
-
static
|
|
112
|
+
static _estimateRTTViaHeadersEndTiming(info: {
|
|
113
|
+
record: LH.Artifacts.NetworkRequest;
|
|
114
|
+
timing: LH.Crdp.Network.ResourceTiming;
|
|
115
|
+
connectionReused?: boolean | undefined;
|
|
116
|
+
}): number | undefined;
|
|
101
117
|
/**
|
|
102
118
|
* Given the RTT to each origin, estimates the observed server response times.
|
|
103
119
|
*
|
|
@@ -132,33 +132,32 @@ class NetworkAnalyzer {
|
|
|
132
132
|
* single handshake.
|
|
133
133
|
* This is the most accurate and preferred method of measurement when the data is available.
|
|
134
134
|
*
|
|
135
|
-
* @param {
|
|
136
|
-
* @return {
|
|
135
|
+
* @param {RequestInfo} info
|
|
136
|
+
* @return {number[]|number|undefined}
|
|
137
137
|
*/
|
|
138
|
-
static
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
138
|
+
static _estimateRTTViaConnectionTiming(info) {
|
|
139
|
+
const {timing, connectionReused, record} = info;
|
|
140
|
+
if (connectionReused) return;
|
|
141
|
+
|
|
142
|
+
// In LR, network records are missing connection timing, but we've smuggled it in via headers.
|
|
143
|
+
if (global.isLightrider && record.lrStatistics) {
|
|
144
|
+
if (record.protocol.startsWith('h3')) {
|
|
145
|
+
return record.lrStatistics.TCPMs;
|
|
146
|
+
} else {
|
|
147
|
+
return [record.lrStatistics.TCPMs / 2, record.lrStatistics.TCPMs / 2];
|
|
149
148
|
}
|
|
149
|
+
}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
151
|
+
const {connectStart, sslStart, sslEnd, connectEnd} = timing;
|
|
152
|
+
if (connectEnd >= 0 && connectStart >= 0 && record.protocol.startsWith('h3')) {
|
|
153
|
+
// These values are equal to sslStart and sslEnd for h3.
|
|
154
|
+
return connectEnd - connectStart;
|
|
155
|
+
} else if (sslStart >= 0 && sslEnd >= 0 && sslStart !== connectStart) {
|
|
156
|
+
// SSL can also be more than 1 RT but assume False Start was used.
|
|
157
|
+
return [connectEnd - sslStart, sslStart - connectStart];
|
|
158
|
+
} else if (connectStart >= 0 && connectEnd >= 0) {
|
|
159
|
+
return connectEnd - connectStart;
|
|
160
|
+
}
|
|
162
161
|
}
|
|
163
162
|
|
|
164
163
|
/**
|
|
@@ -166,26 +165,27 @@ class NetworkAnalyzer {
|
|
|
166
165
|
* NOTE: this will tend to overestimate the actual RTT quite significantly as the download can be
|
|
167
166
|
* slow for other reasons as well such as bandwidth constraints.
|
|
168
167
|
*
|
|
169
|
-
* @param {
|
|
170
|
-
* @return {
|
|
168
|
+
* @param {RequestInfo} info
|
|
169
|
+
* @return {number|undefined}
|
|
171
170
|
*/
|
|
172
|
-
static
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Only look at downloads that went past the initial congestion window
|
|
176
|
-
if (record.transferSize <= INITIAL_CWD) return;
|
|
177
|
-
if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
|
|
171
|
+
static _estimateRTTViaDownloadTiming(info) {
|
|
172
|
+
const {timing, connectionReused, record} = info;
|
|
173
|
+
if (connectionReused) return;
|
|
178
174
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const numberOfRoundTrips = Math.log2(record.transferSize / INITIAL_CWD);
|
|
175
|
+
// Only look at downloads that went past the initial congestion window
|
|
176
|
+
if (record.transferSize <= INITIAL_CWD) return;
|
|
177
|
+
if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
|
|
183
178
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
// Compute the amount of time downloading everything after the first congestion window took
|
|
180
|
+
const totalTime = record.networkEndTime - record.networkRequestTime;
|
|
181
|
+
const downloadTimeAfterFirstByte = totalTime - timing.receiveHeadersEnd;
|
|
182
|
+
const numberOfRoundTrips = Math.log2(record.transferSize / INITIAL_CWD);
|
|
183
|
+
|
|
184
|
+
// Ignore requests that required a high number of round trips since bandwidth starts to play
|
|
185
|
+
// a larger role than latency
|
|
186
|
+
if (numberOfRoundTrips > 5) return;
|
|
187
|
+
|
|
188
|
+
return downloadTimeAfterFirstByte / numberOfRoundTrips;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/**
|
|
@@ -194,21 +194,21 @@ class NetworkAnalyzer {
|
|
|
194
194
|
* NOTE: this will tend to overestimate the actual RTT as the request can be delayed for other
|
|
195
195
|
* reasons as well such as more SSL handshakes if TLS False Start is not enabled.
|
|
196
196
|
*
|
|
197
|
-
* @param {
|
|
198
|
-
* @return {
|
|
197
|
+
* @param {RequestInfo} info
|
|
198
|
+
* @return {number|undefined}
|
|
199
199
|
*/
|
|
200
|
-
static
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
200
|
+
static _estimateRTTViaSendStartTiming(info) {
|
|
201
|
+
const {timing, connectionReused, record} = info;
|
|
202
|
+
if (connectionReused) return;
|
|
203
|
+
|
|
204
|
+
if (!Number.isFinite(timing.sendStart) || timing.sendStart < 0) return;
|
|
205
|
+
|
|
206
|
+
// Assume everything before sendStart was just DNS + (SSL)? + TCP handshake
|
|
207
|
+
// 1 RT for DNS, 1 RT (maybe) for SSL, 1 RT for TCP
|
|
208
|
+
let roundTrips = 1;
|
|
209
|
+
if (!record.protocol.startsWith('h3')) roundTrips += 1; // TCP
|
|
210
|
+
if (record.parsedURL.scheme === 'https') roundTrips += 1;
|
|
211
|
+
return timing.sendStart / roundTrips;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
@@ -217,34 +217,33 @@ class NetworkAnalyzer {
|
|
|
217
217
|
* NOTE: this is the most inaccurate way to estimate the RTT, but in some environments it's all
|
|
218
218
|
* we have access to :(
|
|
219
219
|
*
|
|
220
|
-
* @param {
|
|
221
|
-
* @return {
|
|
220
|
+
* @param {RequestInfo} info
|
|
221
|
+
* @return {number|undefined}
|
|
222
222
|
*/
|
|
223
|
-
static
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
223
|
+
static _estimateRTTViaHeadersEndTiming(info) {
|
|
224
|
+
const {timing, connectionReused, record} = info;
|
|
225
|
+
if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
|
|
226
|
+
if (!record.resourceType) return;
|
|
227
|
+
|
|
228
|
+
const serverResponseTimePercentage =
|
|
229
|
+
SERVER_RESPONSE_PERCENTAGE_OF_TTFB[record.resourceType] ||
|
|
230
|
+
DEFAULT_SERVER_RESPONSE_PERCENTAGE;
|
|
231
|
+
const estimatedServerResponseTime = timing.receiveHeadersEnd * serverResponseTimePercentage;
|
|
232
|
+
|
|
233
|
+
// When connection was reused...
|
|
234
|
+
// TTFB = 1 RT for request + server response time
|
|
235
|
+
let roundTrips = 1;
|
|
236
|
+
|
|
237
|
+
// When connection was fresh...
|
|
238
|
+
// TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
|
|
239
|
+
if (!connectionReused) {
|
|
240
|
+
roundTrips += 1; // DNS
|
|
241
|
+
if (!record.protocol.startsWith('h3')) roundTrips += 1; // TCP
|
|
242
|
+
if (record.parsedURL.scheme === 'https') roundTrips += 1; // SSL
|
|
243
|
+
}
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
245
|
+
// subtract out our estimated server response time
|
|
246
|
+
return Math.max((timing.receiveHeadersEnd - estimatedServerResponseTime) / roundTrips, 3);
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
/**
|
|
@@ -350,32 +349,61 @@ class NetworkAnalyzer {
|
|
|
350
349
|
useHeadersEndEstimates = true,
|
|
351
350
|
} = options || {};
|
|
352
351
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
estimatesByOrigin = new Map();
|
|
356
|
-
const estimatesViaDownload = NetworkAnalyzer._estimateRTTByOriginViaDownloadTiming(records);
|
|
357
|
-
const estimatesViaSendStart = NetworkAnalyzer._estimateRTTByOriginViaSendStartTiming(records);
|
|
358
|
-
const estimatesViaTTFB = NetworkAnalyzer._estimateRTTByOriginViaHeadersEndTiming(records);
|
|
352
|
+
const connectionWasReused = NetworkAnalyzer.estimateIfConnectionWasReused(records);
|
|
353
|
+
const groupedByOrigin = NetworkAnalyzer.groupByOrigin(records);
|
|
359
354
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
355
|
+
const estimatesByOrigin = new Map();
|
|
356
|
+
for (const [origin, originRecords] of groupedByOrigin.entries()) {
|
|
357
|
+
/** @type {number[]} */
|
|
358
|
+
const originEstimates = [];
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @param {(e: RequestInfo) => number[]|number|undefined} estimator
|
|
362
|
+
*/
|
|
363
|
+
// eslint-disable-next-line no-inner-declarations
|
|
364
|
+
function collectEstimates(estimator, multiplier = 1) {
|
|
365
|
+
for (const record of originRecords) {
|
|
366
|
+
const timing = record.timing;
|
|
367
|
+
if (!timing) continue;
|
|
368
|
+
|
|
369
|
+
const estimates = estimator({
|
|
370
|
+
record,
|
|
371
|
+
timing,
|
|
372
|
+
connectionReused: connectionWasReused.get(record.requestId),
|
|
373
|
+
});
|
|
374
|
+
if (estimates === undefined) continue;
|
|
375
|
+
|
|
376
|
+
if (!Array.isArray(estimates)) {
|
|
377
|
+
originEstimates.push(estimates * multiplier);
|
|
378
|
+
} else {
|
|
379
|
+
originEstimates.push(...estimates.map(e => e * multiplier));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
363
382
|
}
|
|
364
383
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const existing = estimatesByOrigin.get(origin) || [];
|
|
368
|
-
estimatesByOrigin.set(origin, existing.concat(estimates));
|
|
384
|
+
if (!forceCoarseEstimates) {
|
|
385
|
+
collectEstimates(this._estimateRTTViaConnectionTiming);
|
|
369
386
|
}
|
|
370
387
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
388
|
+
// Connection timing can be missing for a few reasons:
|
|
389
|
+
// - Origin was preconnected, which we don't have instrumentation for.
|
|
390
|
+
// - Trace began recording after a connection has already been established (for example, in timespan mode)
|
|
391
|
+
// - Perhaps Chrome established a connection already in the background (service worker? Just guessing here)
|
|
392
|
+
// - Not provided in LR netstack.
|
|
393
|
+
if (!originEstimates.length) {
|
|
394
|
+
if (useDownloadEstimates) {
|
|
395
|
+
collectEstimates(this._estimateRTTViaDownloadTiming, coarseEstimateMultiplier);
|
|
396
|
+
}
|
|
397
|
+
if (useSendStartEstimates) {
|
|
398
|
+
collectEstimates(this._estimateRTTViaSendStartTiming, coarseEstimateMultiplier);
|
|
399
|
+
}
|
|
400
|
+
if (useHeadersEndEstimates) {
|
|
401
|
+
collectEstimates(this._estimateRTTViaHeadersEndTiming, coarseEstimateMultiplier);
|
|
402
|
+
}
|
|
375
403
|
}
|
|
376
404
|
|
|
377
|
-
|
|
378
|
-
|
|
405
|
+
if (originEstimates.length) {
|
|
406
|
+
estimatesByOrigin.set(origin, originEstimates);
|
|
379
407
|
}
|
|
380
408
|
}
|
|
381
409
|
|
|
@@ -514,12 +514,11 @@ function truncate(string, characterLimit) {
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
// This is to support bundled lighthouse.
|
|
517
|
-
// esbuild calls every function with a builtin `__name
|
|
518
|
-
// real name of the function so that esbuild can rename it to avoid
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// of the bundle, which is irrelevant for code executed in the browser).
|
|
517
|
+
// esbuild calls every function with a builtin `__name` (since we set keepNames: true),
|
|
518
|
+
// whose purpose is to store the real name of the function so that esbuild can rename it to avoid
|
|
519
|
+
// collisions. Anywhere we inject dynamically generated code at runtime for the browser to process,
|
|
520
|
+
// we must manually include this function (because esbuild only does so once at the top scope of
|
|
521
|
+
// the bundle, which is irrelevant for code executed in the browser).
|
|
523
522
|
const esbuildFunctionNameStubString = 'var __name=(fn)=>fn;';
|
|
524
523
|
|
|
525
524
|
/** @type {string} */
|
package/package.json
CHANGED