lighthouse 12.0.0-dev.20240602 → 12.0.0-dev.20240604
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/byte-efficiency/byte-efficiency-audit.js +1 -1
- package/core/audits/byte-efficiency/offscreen-images.js +1 -1
- package/core/audits/byte-efficiency/render-blocking-resources.js +6 -6
- package/core/audits/dobetterweb/uses-http2.js +3 -3
- package/core/audits/prioritize-lcp-image.js +2 -2
- package/core/audits/redirects.js +1 -1
- package/core/audits/third-party-facades.js +1 -1
- package/core/audits/third-party-summary.js +1 -1
- package/core/audits/uses-rel-preconnect.js +2 -2
- package/core/audits/uses-rel-preload.js +9 -9
- package/core/computed/critical-request-chains.js +3 -3
- package/core/computed/metrics/lantern-metric.d.ts +18 -0
- package/core/computed/metrics/lantern-metric.js +34 -2
- package/core/computed/navigation-insights.d.ts +2 -0
- package/core/computed/page-dependency-graph.d.ts +3 -1
- package/core/computed/page-dependency-graph.js +11 -4
- package/core/computed/trace-engine-result.d.ts +1 -0
- package/core/computed/trace-engine-result.js +1 -0
- package/core/lib/lantern/metric.js +3 -3
- package/core/lib/lantern/metrics/first-contentful-paint.js +1 -1
- package/core/lib/lantern/metrics/interactive.js +4 -4
- package/core/lib/lantern/metrics/largest-contentful-paint.js +2 -2
- package/core/lib/lantern/network-node.d.ts +2 -2
- package/core/lib/lantern/network-node.js +3 -4
- package/core/lib/lantern/page-dependency-graph.d.ts +62 -6
- package/core/lib/lantern/page-dependency-graph.js +473 -30
- package/core/lib/lantern/simulator/connection-pool.d.ts +10 -10
- package/core/lib/lantern/simulator/connection-pool.js +22 -22
- package/core/lib/lantern/simulator/dns-cache.d.ts +1 -1
- package/core/lib/lantern/simulator/dns-cache.js +1 -1
- package/core/lib/lantern/simulator/network-analyzer.d.ts +12 -12
- package/core/lib/lantern/simulator/network-analyzer.js +53 -53
- package/core/lib/lantern/simulator/simulator.d.ts +5 -5
- package/core/lib/lantern/simulator/simulator.js +18 -18
- package/core/lib/lantern/types/lantern.d.ts +12 -8
- package/core/lib/lantern-trace-saver.js +1 -1
- package/core/lib/network-request.d.ts +21 -0
- package/core/lib/network-request.js +5 -2
- package/package.json +6 -4
- package/tsconfig.json +1 -0
- package/types/artifacts.d.ts +16 -0
|
@@ -27,7 +27,7 @@ export class ConnectionPool {
|
|
|
27
27
|
/** @type {Map<string, TcpConnection[]>} */
|
|
28
28
|
this._connectionsByOrigin = new Map();
|
|
29
29
|
/** @type {Map<Lantern.NetworkRequest, TcpConnection>} */
|
|
30
|
-
this.
|
|
30
|
+
this._connectionsByRequest = new Map();
|
|
31
31
|
this._connectionsInUse = new Set();
|
|
32
32
|
this._connectionReusedByRequestId = NetworkAnalyzer.estimateIfConnectionWasReused(records, {
|
|
33
33
|
forceCoarseEstimates: true,
|
|
@@ -49,16 +49,16 @@ export class ConnectionPool {
|
|
|
49
49
|
const serverResponseTimeByOrigin = this._options.serverResponseTimeByOrigin;
|
|
50
50
|
|
|
51
51
|
const recordsByOrigin = NetworkAnalyzer.groupByOrigin(this._records);
|
|
52
|
-
for (const [origin,
|
|
52
|
+
for (const [origin, requests] of recordsByOrigin.entries()) {
|
|
53
53
|
const connections = [];
|
|
54
54
|
const additionalRtt = additionalRttByOrigin.get(origin) || 0;
|
|
55
55
|
const responseTime = serverResponseTimeByOrigin.get(origin) || DEFAULT_SERVER_RESPONSE_TIME;
|
|
56
56
|
|
|
57
|
-
for (const
|
|
58
|
-
if (connectionReused.get(
|
|
57
|
+
for (const request of requests) {
|
|
58
|
+
if (connectionReused.get(request.requestId)) continue;
|
|
59
59
|
|
|
60
|
-
const isTLS = TLS_SCHEMES.includes(
|
|
61
|
-
const isH2 =
|
|
60
|
+
const isTLS = TLS_SCHEMES.includes(request.parsedURL.scheme);
|
|
61
|
+
const isH2 = request.protocol === 'h2';
|
|
62
62
|
const connection = new TcpConnection(
|
|
63
63
|
this._options.rtt + additionalRtt,
|
|
64
64
|
this._options.throughput,
|
|
@@ -106,47 +106,47 @@ export class ConnectionPool {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* This method finds an available connection to the origin specified by the network
|
|
109
|
+
* This method finds an available connection to the origin specified by the network request or null
|
|
110
110
|
* if no connection was available. If returned, connection will not be available for other network
|
|
111
111
|
* records until release is called.
|
|
112
112
|
*
|
|
113
|
-
* @param {Lantern.NetworkRequest}
|
|
113
|
+
* @param {Lantern.NetworkRequest} request
|
|
114
114
|
* @return {?TcpConnection}
|
|
115
115
|
*/
|
|
116
|
-
acquire(
|
|
117
|
-
if (this.
|
|
116
|
+
acquire(request) {
|
|
117
|
+
if (this._connectionsByRequest.has(request)) throw new Error('Record already has a connection');
|
|
118
118
|
|
|
119
|
-
const origin =
|
|
119
|
+
const origin = request.parsedURL.securityOrigin;
|
|
120
120
|
const connections = this._connectionsByOrigin.get(origin) || [];
|
|
121
121
|
const connectionToUse = this._findAvailableConnectionWithLargestCongestionWindow(connections);
|
|
122
122
|
|
|
123
123
|
if (!connectionToUse) return null;
|
|
124
124
|
|
|
125
125
|
this._connectionsInUse.add(connectionToUse);
|
|
126
|
-
this.
|
|
126
|
+
this._connectionsByRequest.set(request, connectionToUse);
|
|
127
127
|
return connectionToUse;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Return the connection currently being used to fetch a
|
|
132
|
-
* currently being used for this
|
|
131
|
+
* Return the connection currently being used to fetch a request. If no connection
|
|
132
|
+
* currently being used for this request, an error will be thrown.
|
|
133
133
|
*
|
|
134
|
-
* @param {Lantern.NetworkRequest}
|
|
134
|
+
* @param {Lantern.NetworkRequest} request
|
|
135
135
|
* @return {TcpConnection}
|
|
136
136
|
*/
|
|
137
|
-
|
|
138
|
-
const activeConnection = this.
|
|
139
|
-
if (!activeConnection) throw new Error('Could not find an active connection for
|
|
137
|
+
acquireActiveConnectionFromRequest(request) {
|
|
138
|
+
const activeConnection = this._connectionsByRequest.get(request);
|
|
139
|
+
if (!activeConnection) throw new Error('Could not find an active connection for request');
|
|
140
140
|
|
|
141
141
|
return activeConnection;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* @param {Lantern.NetworkRequest}
|
|
145
|
+
* @param {Lantern.NetworkRequest} request
|
|
146
146
|
*/
|
|
147
|
-
release(
|
|
148
|
-
const connection = this.
|
|
149
|
-
this.
|
|
147
|
+
release(request) {
|
|
148
|
+
const connection = this._connectionsByRequest.get(request);
|
|
149
|
+
this._connectionsByRequest.delete(request);
|
|
150
150
|
this._connectionsInUse.delete(connection);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -25,7 +25,7 @@ export class DNSCache {
|
|
|
25
25
|
*/
|
|
26
26
|
_updateCacheResolvedAtIfNeeded(request: Lantern.NetworkRequest, resolvedAt: number): void;
|
|
27
27
|
/**
|
|
28
|
-
* Forcefully sets the DNS resolution time for a
|
|
28
|
+
* Forcefully sets the DNS resolution time for a request.
|
|
29
29
|
* Useful for testing and alternate execution simulations.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} domain
|
|
@@ -46,14 +46,14 @@ export class NetworkAnalyzer {
|
|
|
46
46
|
* @return {Map<string, Summary>}
|
|
47
47
|
*/
|
|
48
48
|
static summarize(values: Map<string, number[]>): Map<string, Summary>;
|
|
49
|
-
/** @typedef {{
|
|
49
|
+
/** @typedef {{request: Lantern.NetworkRequest, timing: LH.Crdp.Network.ResourceTiming, connectionReused?: boolean}} RequestInfo */
|
|
50
50
|
/**
|
|
51
|
-
* @param {Lantern.NetworkRequest[]}
|
|
51
|
+
* @param {Lantern.NetworkRequest[]} requests
|
|
52
52
|
* @param {(e: RequestInfo) => number | number[] | undefined} iteratee
|
|
53
53
|
* @return {Map<string, number[]>}
|
|
54
54
|
*/
|
|
55
|
-
static _estimateValueByOrigin(
|
|
56
|
-
|
|
55
|
+
static _estimateValueByOrigin(requests: Lantern.NetworkRequest[], iteratee: (e: {
|
|
56
|
+
request: Lantern.NetworkRequest;
|
|
57
57
|
timing: LH.Crdp.Network.ResourceTiming;
|
|
58
58
|
connectionReused?: boolean | undefined;
|
|
59
59
|
}) => number | number[] | undefined): Map<string, number[]>;
|
|
@@ -69,7 +69,7 @@ export class NetworkAnalyzer {
|
|
|
69
69
|
* @return {number[]|number|undefined}
|
|
70
70
|
*/
|
|
71
71
|
static _estimateRTTViaConnectionTiming(info: {
|
|
72
|
-
|
|
72
|
+
request: Lantern.NetworkRequest;
|
|
73
73
|
timing: LH.Crdp.Network.ResourceTiming;
|
|
74
74
|
connectionReused?: boolean | undefined;
|
|
75
75
|
}): number[] | number | undefined;
|
|
@@ -82,7 +82,7 @@ export class NetworkAnalyzer {
|
|
|
82
82
|
* @return {number|undefined}
|
|
83
83
|
*/
|
|
84
84
|
static _estimateRTTViaDownloadTiming(info: {
|
|
85
|
-
|
|
85
|
+
request: Lantern.NetworkRequest;
|
|
86
86
|
timing: LH.Crdp.Network.ResourceTiming;
|
|
87
87
|
connectionReused?: boolean | undefined;
|
|
88
88
|
}): number | undefined;
|
|
@@ -96,7 +96,7 @@ export class NetworkAnalyzer {
|
|
|
96
96
|
* @return {number|undefined}
|
|
97
97
|
*/
|
|
98
98
|
static _estimateRTTViaSendStartTiming(info: {
|
|
99
|
-
|
|
99
|
+
request: Lantern.NetworkRequest;
|
|
100
100
|
timing: LH.Crdp.Network.ResourceTiming;
|
|
101
101
|
connectionReused?: boolean | undefined;
|
|
102
102
|
}): number | undefined;
|
|
@@ -110,7 +110,7 @@ export class NetworkAnalyzer {
|
|
|
110
110
|
* @return {number|undefined}
|
|
111
111
|
*/
|
|
112
112
|
static _estimateRTTViaHeadersEndTiming(info: {
|
|
113
|
-
|
|
113
|
+
request: Lantern.NetworkRequest;
|
|
114
114
|
timing: LH.Crdp.Network.ResourceTiming;
|
|
115
115
|
connectionReused?: boolean | undefined;
|
|
116
116
|
}): number | undefined;
|
|
@@ -123,10 +123,10 @@ export class NetworkAnalyzer {
|
|
|
123
123
|
*/
|
|
124
124
|
static _estimateResponseTimeByOrigin(records: Lantern.NetworkRequest[], rttByOrigin: Map<string, number>): Map<string, number[]>;
|
|
125
125
|
/**
|
|
126
|
-
* @param {Lantern.NetworkRequest[]}
|
|
126
|
+
* @param {Lantern.NetworkRequest[]} requests
|
|
127
127
|
* @return {boolean}
|
|
128
128
|
*/
|
|
129
|
-
static canTrustConnectionInformation(
|
|
129
|
+
static canTrustConnectionInformation(requests: Lantern.NetworkRequest[]): boolean;
|
|
130
130
|
/**
|
|
131
131
|
* Returns a map of requestId -> connectionReused, estimating the information if the information
|
|
132
132
|
* available in the records themselves appears untrustworthy.
|
|
@@ -160,9 +160,9 @@ export class NetworkAnalyzer {
|
|
|
160
160
|
rttByOrigin?: Map<string, number> | undefined;
|
|
161
161
|
}) | undefined): Map<string, Summary>;
|
|
162
162
|
/**
|
|
163
|
-
* Computes the average throughput for the given
|
|
163
|
+
* Computes the average throughput for the given requests in bits/second.
|
|
164
164
|
* Excludes data URI, failed or otherwise incomplete, and cached requests.
|
|
165
|
-
* Returns Infinity if there were no analyzable network
|
|
165
|
+
* Returns Infinity if there were no analyzable network requests.
|
|
166
166
|
*
|
|
167
167
|
* @param {Lantern.NetworkRequest[]} records
|
|
168
168
|
* @return {number}
|
|
@@ -88,30 +88,30 @@ class NetworkAnalyzer {
|
|
|
88
88
|
return summaryByKey;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/** @typedef {{
|
|
91
|
+
/** @typedef {{request: Lantern.NetworkRequest, timing: LH.Crdp.Network.ResourceTiming, connectionReused?: boolean}} RequestInfo */
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* @param {Lantern.NetworkRequest[]}
|
|
94
|
+
* @param {Lantern.NetworkRequest[]} requests
|
|
95
95
|
* @param {(e: RequestInfo) => number | number[] | undefined} iteratee
|
|
96
96
|
* @return {Map<string, number[]>}
|
|
97
97
|
*/
|
|
98
|
-
static _estimateValueByOrigin(
|
|
99
|
-
const connectionWasReused = NetworkAnalyzer.estimateIfConnectionWasReused(
|
|
100
|
-
const groupedByOrigin = NetworkAnalyzer.groupByOrigin(
|
|
98
|
+
static _estimateValueByOrigin(requests, iteratee) {
|
|
99
|
+
const connectionWasReused = NetworkAnalyzer.estimateIfConnectionWasReused(requests);
|
|
100
|
+
const groupedByOrigin = NetworkAnalyzer.groupByOrigin(requests);
|
|
101
101
|
|
|
102
102
|
const estimates = new Map();
|
|
103
|
-
for (const [origin,
|
|
103
|
+
for (const [origin, originRequests] of groupedByOrigin.entries()) {
|
|
104
104
|
/** @type {number[]} */
|
|
105
105
|
let originEstimates = [];
|
|
106
106
|
|
|
107
|
-
for (const
|
|
108
|
-
const timing =
|
|
107
|
+
for (const request of originRequests) {
|
|
108
|
+
const timing = request.timing;
|
|
109
109
|
if (!timing) continue;
|
|
110
110
|
|
|
111
111
|
const value = iteratee({
|
|
112
|
-
|
|
112
|
+
request,
|
|
113
113
|
timing,
|
|
114
|
-
connectionReused: connectionWasReused.get(
|
|
114
|
+
connectionReused: connectionWasReused.get(request.requestId),
|
|
115
115
|
});
|
|
116
116
|
if (typeof value !== 'undefined') {
|
|
117
117
|
originEstimates = originEstimates.concat(value);
|
|
@@ -137,11 +137,11 @@ class NetworkAnalyzer {
|
|
|
137
137
|
* @return {number[]|number|undefined}
|
|
138
138
|
*/
|
|
139
139
|
static _estimateRTTViaConnectionTiming(info) {
|
|
140
|
-
const {timing, connectionReused,
|
|
140
|
+
const {timing, connectionReused, request} = info;
|
|
141
141
|
if (connectionReused) return;
|
|
142
142
|
|
|
143
143
|
const {connectStart, sslStart, sslEnd, connectEnd} = timing;
|
|
144
|
-
if (connectEnd >= 0 && connectStart >= 0 &&
|
|
144
|
+
if (connectEnd >= 0 && connectStart >= 0 && request.protocol.startsWith('h3')) {
|
|
145
145
|
// These values are equal to sslStart and sslEnd for h3.
|
|
146
146
|
return connectEnd - connectStart;
|
|
147
147
|
} else if (sslStart >= 0 && sslEnd >= 0 && sslStart !== connectStart) {
|
|
@@ -161,17 +161,17 @@ class NetworkAnalyzer {
|
|
|
161
161
|
* @return {number|undefined}
|
|
162
162
|
*/
|
|
163
163
|
static _estimateRTTViaDownloadTiming(info) {
|
|
164
|
-
const {timing, connectionReused,
|
|
164
|
+
const {timing, connectionReused, request} = info;
|
|
165
165
|
if (connectionReused) return;
|
|
166
166
|
|
|
167
167
|
// Only look at downloads that went past the initial congestion window
|
|
168
|
-
if (
|
|
168
|
+
if (request.transferSize <= INITIAL_CWD) return;
|
|
169
169
|
if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
|
|
170
170
|
|
|
171
171
|
// Compute the amount of time downloading everything after the first congestion window took
|
|
172
|
-
const totalTime =
|
|
172
|
+
const totalTime = request.networkEndTime - request.networkRequestTime;
|
|
173
173
|
const downloadTimeAfterFirstByte = totalTime - timing.receiveHeadersEnd;
|
|
174
|
-
const numberOfRoundTrips = Math.log2(
|
|
174
|
+
const numberOfRoundTrips = Math.log2(request.transferSize / INITIAL_CWD);
|
|
175
175
|
|
|
176
176
|
// Ignore requests that required a high number of round trips since bandwidth starts to play
|
|
177
177
|
// a larger role than latency
|
|
@@ -190,7 +190,7 @@ class NetworkAnalyzer {
|
|
|
190
190
|
* @return {number|undefined}
|
|
191
191
|
*/
|
|
192
192
|
static _estimateRTTViaSendStartTiming(info) {
|
|
193
|
-
const {timing, connectionReused,
|
|
193
|
+
const {timing, connectionReused, request} = info;
|
|
194
194
|
if (connectionReused) return;
|
|
195
195
|
|
|
196
196
|
if (!Number.isFinite(timing.sendStart) || timing.sendStart < 0) return;
|
|
@@ -198,8 +198,8 @@ class NetworkAnalyzer {
|
|
|
198
198
|
// Assume everything before sendStart was just DNS + (SSL)? + TCP handshake
|
|
199
199
|
// 1 RT for DNS, 1 RT (maybe) for SSL, 1 RT for TCP
|
|
200
200
|
let roundTrips = 1;
|
|
201
|
-
if (!
|
|
202
|
-
if (
|
|
201
|
+
if (!request.protocol.startsWith('h3')) roundTrips += 1; // TCP
|
|
202
|
+
if (request.parsedURL.scheme === 'https') roundTrips += 1;
|
|
203
203
|
return timing.sendStart / roundTrips;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -213,12 +213,12 @@ class NetworkAnalyzer {
|
|
|
213
213
|
* @return {number|undefined}
|
|
214
214
|
*/
|
|
215
215
|
static _estimateRTTViaHeadersEndTiming(info) {
|
|
216
|
-
const {timing, connectionReused,
|
|
216
|
+
const {timing, connectionReused, request} = info;
|
|
217
217
|
if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
|
|
218
|
-
if (!
|
|
218
|
+
if (!request.resourceType) return;
|
|
219
219
|
|
|
220
220
|
const serverResponseTimePercentage =
|
|
221
|
-
SERVER_RESPONSE_PERCENTAGE_OF_TTFB[
|
|
221
|
+
SERVER_RESPONSE_PERCENTAGE_OF_TTFB[request.resourceType] ||
|
|
222
222
|
DEFAULT_SERVER_RESPONSE_PERCENTAGE;
|
|
223
223
|
const estimatedServerResponseTime = timing.receiveHeadersEnd * serverResponseTimePercentage;
|
|
224
224
|
|
|
@@ -230,8 +230,8 @@ class NetworkAnalyzer {
|
|
|
230
230
|
// TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
|
|
231
231
|
if (!connectionReused) {
|
|
232
232
|
roundTrips += 1; // DNS
|
|
233
|
-
if (!
|
|
234
|
-
if (
|
|
233
|
+
if (!request.protocol.startsWith('h3')) roundTrips += 1; // TCP
|
|
234
|
+
if (request.parsedURL.scheme === 'https') roundTrips += 1; // SSL
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// subtract out our estimated server response time
|
|
@@ -246,28 +246,28 @@ class NetworkAnalyzer {
|
|
|
246
246
|
* @return {Map<string, number[]>}
|
|
247
247
|
*/
|
|
248
248
|
static _estimateResponseTimeByOrigin(records, rttByOrigin) {
|
|
249
|
-
return NetworkAnalyzer._estimateValueByOrigin(records, ({
|
|
250
|
-
if (
|
|
249
|
+
return NetworkAnalyzer._estimateValueByOrigin(records, ({request, timing}) => {
|
|
250
|
+
if (request.serverResponseTime !== undefined) return request.serverResponseTime;
|
|
251
251
|
|
|
252
252
|
if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
|
|
253
253
|
if (!Number.isFinite(timing.sendEnd) || timing.sendEnd < 0) return;
|
|
254
254
|
|
|
255
255
|
const ttfb = timing.receiveHeadersEnd - timing.sendEnd;
|
|
256
|
-
const origin =
|
|
256
|
+
const origin = request.parsedURL.securityOrigin;
|
|
257
257
|
const rtt = rttByOrigin.get(origin) || rttByOrigin.get(NetworkAnalyzer.SUMMARY) || 0;
|
|
258
258
|
return Math.max(ttfb - rtt, 0);
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/**
|
|
263
|
-
* @param {Lantern.NetworkRequest[]}
|
|
263
|
+
* @param {Lantern.NetworkRequest[]} requests
|
|
264
264
|
* @return {boolean}
|
|
265
265
|
*/
|
|
266
|
-
static canTrustConnectionInformation(
|
|
266
|
+
static canTrustConnectionInformation(requests) {
|
|
267
267
|
const connectionIdWasStarted = new Map();
|
|
268
|
-
for (const
|
|
269
|
-
const started = connectionIdWasStarted.get(
|
|
270
|
-
connectionIdWasStarted.set(
|
|
268
|
+
for (const request of requests) {
|
|
269
|
+
const started = connectionIdWasStarted.get(request.connectionId) || !request.connectionReused;
|
|
270
|
+
connectionIdWasStarted.set(request.connectionId, started);
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
// We probably can't trust the network information if all the connection IDs were the same
|
|
@@ -289,10 +289,10 @@ class NetworkAnalyzer {
|
|
|
289
289
|
|
|
290
290
|
// Check if we can trust the connection information coming from the protocol
|
|
291
291
|
if (!forceCoarseEstimates && NetworkAnalyzer.canTrustConnectionInformation(records)) {
|
|
292
|
-
return new Map(records.map(
|
|
292
|
+
return new Map(records.map(request => [request.requestId, !!request.connectionReused]));
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
// Otherwise we're on our own, a
|
|
295
|
+
// Otherwise we're on our own, a request may not have needed a fresh connection if...
|
|
296
296
|
// - It was not the first request to the domain
|
|
297
297
|
// - It was H2
|
|
298
298
|
// - It was after the first request to the domain ended
|
|
@@ -300,13 +300,13 @@ class NetworkAnalyzer {
|
|
|
300
300
|
const groupedByOrigin = NetworkAnalyzer.groupByOrigin(records);
|
|
301
301
|
for (const [_, originRecords] of groupedByOrigin.entries()) {
|
|
302
302
|
const earliestReusePossible = originRecords
|
|
303
|
-
.map(
|
|
303
|
+
.map(request => request.networkEndTime)
|
|
304
304
|
.reduce((a, b) => Math.min(a, b), Infinity);
|
|
305
305
|
|
|
306
|
-
for (const
|
|
306
|
+
for (const request of originRecords) {
|
|
307
307
|
connectionWasReused.set(
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
request.requestId,
|
|
309
|
+
request.networkRequestTime >= earliestReusePossible || request.protocol === 'h2'
|
|
310
310
|
);
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -343,7 +343,7 @@ class NetworkAnalyzer {
|
|
|
343
343
|
const groupedByOrigin = NetworkAnalyzer.groupByOrigin(records);
|
|
344
344
|
|
|
345
345
|
const estimatesByOrigin = new Map();
|
|
346
|
-
for (const [origin,
|
|
346
|
+
for (const [origin, originRequests] of groupedByOrigin.entries()) {
|
|
347
347
|
/** @type {number[]} */
|
|
348
348
|
const originEstimates = [];
|
|
349
349
|
|
|
@@ -352,14 +352,14 @@ class NetworkAnalyzer {
|
|
|
352
352
|
*/
|
|
353
353
|
// eslint-disable-next-line no-inner-declarations
|
|
354
354
|
function collectEstimates(estimator, multiplier = 1) {
|
|
355
|
-
for (const
|
|
356
|
-
const timing =
|
|
355
|
+
for (const request of originRequests) {
|
|
356
|
+
const timing = request.timing;
|
|
357
357
|
if (!timing) continue;
|
|
358
358
|
|
|
359
359
|
const estimates = estimator({
|
|
360
|
-
|
|
360
|
+
request,
|
|
361
361
|
timing,
|
|
362
|
-
connectionReused: connectionWasReused.get(
|
|
362
|
+
connectionReused: connectionWasReused.get(request.requestId),
|
|
363
363
|
});
|
|
364
364
|
if (estimates === undefined) continue;
|
|
365
365
|
|
|
@@ -427,9 +427,9 @@ class NetworkAnalyzer {
|
|
|
427
427
|
|
|
428
428
|
|
|
429
429
|
/**
|
|
430
|
-
* Computes the average throughput for the given
|
|
430
|
+
* Computes the average throughput for the given requests in bits/second.
|
|
431
431
|
* Excludes data URI, failed or otherwise incomplete, and cached requests.
|
|
432
|
-
* Returns Infinity if there were no analyzable network
|
|
432
|
+
* Returns Infinity if there were no analyzable network requests.
|
|
433
433
|
*
|
|
434
434
|
* @param {Lantern.NetworkRequest[]} records
|
|
435
435
|
* @return {number}
|
|
@@ -438,21 +438,21 @@ class NetworkAnalyzer {
|
|
|
438
438
|
let totalBytes = 0;
|
|
439
439
|
|
|
440
440
|
// We will measure throughput by summing the total bytes downloaded by the total time spent
|
|
441
|
-
// downloading those bytes. We slice up all the network
|
|
441
|
+
// downloading those bytes. We slice up all the network requests into start/end boundaries, so
|
|
442
442
|
// it's easier to deal with the gaps in downloading.
|
|
443
|
-
const timeBoundaries = records.reduce((boundaries,
|
|
444
|
-
const scheme =
|
|
443
|
+
const timeBoundaries = records.reduce((boundaries, request) => {
|
|
444
|
+
const scheme = request.parsedURL?.scheme;
|
|
445
445
|
// Requests whose bodies didn't come over the network or didn't completely finish will mess
|
|
446
446
|
// with the computation, just skip over them.
|
|
447
|
-
if (scheme === 'data' ||
|
|
448
|
-
|
|
447
|
+
if (scheme === 'data' || request.failed || !request.finished ||
|
|
448
|
+
request.statusCode > 300 || !request.transferSize) {
|
|
449
449
|
return boundaries;
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
// If we've made it this far, all the times we need should be valid (i.e. not undefined/-1).
|
|
453
|
-
totalBytes +=
|
|
454
|
-
boundaries.push({time:
|
|
455
|
-
boundaries.push({time:
|
|
453
|
+
totalBytes += request.transferSize;
|
|
454
|
+
boundaries.push({time: request.responseHeadersEndTime / 1000, isStart: true});
|
|
455
|
+
boundaries.push({time: request.networkEndTime / 1000, isStart: false});
|
|
456
456
|
return boundaries;
|
|
457
457
|
}, /** @type {Array<{time: number, isStart: boolean}>} */([])).sort((a, b) => a.time - b.time);
|
|
458
458
|
|
|
@@ -10,11 +10,11 @@ export class Simulator<T = any> {
|
|
|
10
10
|
/**
|
|
11
11
|
* @param {Lantern.Simulation.Settings} settings
|
|
12
12
|
*/
|
|
13
|
-
static createSimulator(settings: Lantern.Simulation.Settings):
|
|
13
|
+
static createSimulator(settings: Lantern.Simulation.Settings): Simulator<any>;
|
|
14
14
|
/** @return {Map<string, Map<Node, CompleteNodeTiming>>} */
|
|
15
15
|
static get ALL_NODE_TIMINGS(): Map<string, Map<Node, CompleteNodeTiming>>;
|
|
16
16
|
/**
|
|
17
|
-
* We attempt to start nodes by their observed start time using the
|
|
17
|
+
* We attempt to start nodes by their observed start time using the request priority as a tie breaker.
|
|
18
18
|
* When simulating, just because a low priority image started 5ms before a high priority image doesn't mean
|
|
19
19
|
* it would have happened like that when the network was slower.
|
|
20
20
|
* @param {Node} node
|
|
@@ -73,10 +73,10 @@ export class Simulator<T = any> {
|
|
|
73
73
|
*/
|
|
74
74
|
_markNodeAsComplete(node: Node, endTime: number, connectionTiming?: import("./simulator-timing-map.js").ConnectionTiming | undefined): void;
|
|
75
75
|
/**
|
|
76
|
-
* @param {Lantern.NetworkRequest}
|
|
76
|
+
* @param {Lantern.NetworkRequest} request
|
|
77
77
|
* @return {?TcpConnection}
|
|
78
78
|
*/
|
|
79
|
-
_acquireConnection(
|
|
79
|
+
_acquireConnection(request: Lantern.NetworkRequest): TcpConnection | null;
|
|
80
80
|
/**
|
|
81
81
|
* @return {Node[]}
|
|
82
82
|
*/
|
|
@@ -136,7 +136,7 @@ export class Simulator<T = any> {
|
|
|
136
136
|
*
|
|
137
137
|
* Simulator/connection pool are allowed to deviate from what was
|
|
138
138
|
* observed in the trace/devtoolsLog and start requests as soon as they are queued (i.e. do not
|
|
139
|
-
* wait around for a warm connection to be available if the original
|
|
139
|
+
* wait around for a warm connection to be available if the original request was fetched on a warm
|
|
140
140
|
* connection).
|
|
141
141
|
*
|
|
142
142
|
* @param {Node} graph
|
|
@@ -34,7 +34,7 @@ const NodeState = {
|
|
|
34
34
|
Complete: 3,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
/** @type {Record<NetworkNode['
|
|
37
|
+
/** @type {Record<NetworkNode['request']['priority'], number>} */
|
|
38
38
|
const PriorityStartTimePenalty = {
|
|
39
39
|
VeryHigh: 0,
|
|
40
40
|
High: 0.25,
|
|
@@ -53,7 +53,7 @@ class Simulator {
|
|
|
53
53
|
/**
|
|
54
54
|
* @param {Lantern.Simulation.Settings} settings
|
|
55
55
|
*/
|
|
56
|
-
static
|
|
56
|
+
static createSimulator(settings) {
|
|
57
57
|
const {throttlingMethod, throttling, precomputedLanternData, networkAnalysis} = settings;
|
|
58
58
|
|
|
59
59
|
/** @type {Lantern.Simulation.Options} */
|
|
@@ -248,11 +248,11 @@ class Simulator {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
|
-
* @param {Lantern.NetworkRequest}
|
|
251
|
+
* @param {Lantern.NetworkRequest} request
|
|
252
252
|
* @return {?TcpConnection}
|
|
253
253
|
*/
|
|
254
|
-
_acquireConnection(
|
|
255
|
-
return this._connectionPool.acquire(
|
|
254
|
+
_acquireConnection(request) {
|
|
255
|
+
return this._connectionPool.acquire(request);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
/**
|
|
@@ -342,7 +342,7 @@ class Simulator {
|
|
|
342
342
|
* @return {number}
|
|
343
343
|
*/
|
|
344
344
|
_estimateNetworkTimeRemaining(networkNode) {
|
|
345
|
-
const
|
|
345
|
+
const request = networkNode.request;
|
|
346
346
|
const timingData = this._nodeTimings.getNetworkStarted(networkNode);
|
|
347
347
|
|
|
348
348
|
let timeElapsed = 0;
|
|
@@ -350,23 +350,23 @@ class Simulator {
|
|
|
350
350
|
// Rough access time for seeking to location on disk and reading sequentially.
|
|
351
351
|
// 8ms per seek + 20ms/MB
|
|
352
352
|
// @see http://norvig.com/21-days.html#answers
|
|
353
|
-
const sizeInMb = (
|
|
353
|
+
const sizeInMb = (request.resourceSize || 0) / 1024 / 1024;
|
|
354
354
|
timeElapsed = 8 + 20 * sizeInMb - timingData.timeElapsed;
|
|
355
355
|
} else if (networkNode.isNonNetworkProtocol) {
|
|
356
356
|
// Estimates for the overhead of a data URL in Chromium and the decoding time for base64-encoded data.
|
|
357
357
|
// 2ms per request + 10ms/MB
|
|
358
358
|
// @see traces on https://dopiaza.org/tools/datauri/examples/index.php
|
|
359
|
-
const sizeInMb = (
|
|
359
|
+
const sizeInMb = (request.resourceSize || 0) / 1024 / 1024;
|
|
360
360
|
timeElapsed = 2 + 10 * sizeInMb - timingData.timeElapsed;
|
|
361
361
|
} else {
|
|
362
|
-
const connection = this._connectionPool.
|
|
363
|
-
const dnsResolutionTime = this._dns.getTimeUntilResolution(
|
|
362
|
+
const connection = this._connectionPool.acquireActiveConnectionFromRequest(request);
|
|
363
|
+
const dnsResolutionTime = this._dns.getTimeUntilResolution(request, {
|
|
364
364
|
requestedAt: timingData.startTime,
|
|
365
365
|
shouldUpdateCache: true,
|
|
366
366
|
});
|
|
367
367
|
const timeAlreadyElapsed = timingData.timeElapsed;
|
|
368
368
|
const calculation = connection.simulateDownloadUntil(
|
|
369
|
-
|
|
369
|
+
request.transferSize - timingData.bytesDownloaded,
|
|
370
370
|
{timeAlreadyElapsed, dnsResolutionTime, maximumTimeToElapse: Infinity}
|
|
371
371
|
);
|
|
372
372
|
|
|
@@ -410,14 +410,14 @@ class Simulator {
|
|
|
410
410
|
if (node.type !== BaseNode.TYPES.NETWORK) throw new Error('Unsupported');
|
|
411
411
|
if (!('bytesDownloaded' in timingData)) throw new Error('Invalid timing data');
|
|
412
412
|
|
|
413
|
-
const
|
|
414
|
-
const connection = this._connectionPool.
|
|
415
|
-
const dnsResolutionTime = this._dns.getTimeUntilResolution(
|
|
413
|
+
const request = node.request;
|
|
414
|
+
const connection = this._connectionPool.acquireActiveConnectionFromRequest(request);
|
|
415
|
+
const dnsResolutionTime = this._dns.getTimeUntilResolution(request, {
|
|
416
416
|
requestedAt: timingData.startTime,
|
|
417
417
|
shouldUpdateCache: true,
|
|
418
418
|
});
|
|
419
419
|
const calculation = connection.simulateDownloadUntil(
|
|
420
|
-
|
|
420
|
+
request.transferSize - timingData.bytesDownloaded,
|
|
421
421
|
{
|
|
422
422
|
dnsResolutionTime,
|
|
423
423
|
timeAlreadyElapsed: timingData.timeElapsed,
|
|
@@ -430,7 +430,7 @@ class Simulator {
|
|
|
430
430
|
|
|
431
431
|
if (isFinished) {
|
|
432
432
|
connection.setWarmed(true);
|
|
433
|
-
this._connectionPool.release(
|
|
433
|
+
this._connectionPool.release(request);
|
|
434
434
|
this._markNodeAsComplete(node, totalElapsedTime, calculation.connectionTiming);
|
|
435
435
|
} else {
|
|
436
436
|
timingData.timeElapsed += calculation.timeElapsed;
|
|
@@ -480,7 +480,7 @@ class Simulator {
|
|
|
480
480
|
*
|
|
481
481
|
* Simulator/connection pool are allowed to deviate from what was
|
|
482
482
|
* observed in the trace/devtoolsLog and start requests as soon as they are queued (i.e. do not
|
|
483
|
-
* wait around for a warm connection to be available if the original
|
|
483
|
+
* wait around for a warm connection to be available if the original request was fetched on a warm
|
|
484
484
|
* connection).
|
|
485
485
|
*
|
|
486
486
|
* @param {Node} graph
|
|
@@ -580,7 +580,7 @@ class Simulator {
|
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
/**
|
|
583
|
-
* We attempt to start nodes by their observed start time using the
|
|
583
|
+
* We attempt to start nodes by their observed start time using the request priority as a tie breaker.
|
|
584
584
|
* When simulating, just because a low priority image started 5ms before a high priority image doesn't mean
|
|
585
585
|
* it would have happened like that when the network was slower.
|
|
586
586
|
* @param {Node} node
|
|
@@ -38,12 +38,13 @@ type LightriderStatistics = {
|
|
|
38
38
|
|
|
39
39
|
export class NetworkRequest<T = any> {
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Implementation-specifc canoncial data structure that this Lantern NetworkRequest
|
|
42
|
+
* was derived from.
|
|
43
|
+
* Users of Lantern create a NetworkRequest matching this interface,
|
|
44
|
+
* but can store the source-of-truth for their network model in this property.
|
|
45
|
+
* This is then accessible as a read-only property on NetworkNode.
|
|
45
46
|
*/
|
|
46
|
-
|
|
47
|
+
rawRequest?: T;
|
|
47
48
|
|
|
48
49
|
requestId: string;
|
|
49
50
|
connectionId: number;
|
|
@@ -67,11 +68,14 @@ export class NetworkRequest<T = any> {
|
|
|
67
68
|
resourceSize: number;
|
|
68
69
|
fromDiskCache: boolean;
|
|
69
70
|
fromMemoryCache: boolean;
|
|
71
|
+
isLinkPreload: boolean;
|
|
70
72
|
finished: boolean;
|
|
73
|
+
failed: boolean;
|
|
71
74
|
statusCode: number;
|
|
75
|
+
/** The network request that redirected to this one */
|
|
76
|
+
redirectSource: NetworkRequest<T> | undefined;
|
|
72
77
|
/** The network request that this one redirected to */
|
|
73
78
|
redirectDestination: NetworkRequest<T> | undefined;
|
|
74
|
-
failed: boolean;
|
|
75
79
|
initiator: LH.Crdp.Network.Initiator;
|
|
76
80
|
initiatorRequest: NetworkRequest<T> | undefined;
|
|
77
81
|
/** The chain of network requests that redirected to this one */
|
|
@@ -81,12 +85,12 @@ export class NetworkRequest<T = any> {
|
|
|
81
85
|
* Optional value for how long the server took to respond to this request.
|
|
82
86
|
* When not provided, the server response time is derived from the timing object.
|
|
83
87
|
*/
|
|
84
|
-
serverResponseTime
|
|
88
|
+
serverResponseTime?: number;
|
|
85
89
|
resourceType: LH.Crdp.Network.ResourceType | undefined;
|
|
86
90
|
mimeType: string;
|
|
87
91
|
priority: LH.Crdp.Network.ResourcePriority;
|
|
88
92
|
frameId: string | undefined;
|
|
89
|
-
|
|
93
|
+
fromWorker: boolean;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
export namespace Simulation {
|