lighthouse 9.5.0-dev.20221129 → 9.5.0-dev.20221201

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.
@@ -39,10 +39,18 @@ import {BaseNode} from '../base-node.js';
39
39
  /** @typedef {NetworkNodeTimingStarted & Pick<NodeTimingComplete, 'estimatedTimeElapsed'>} NetworkNodeTimingInProgress */
40
40
 
41
41
  /** @typedef {CpuNodeTimingInProgress & Pick<NodeTimingComplete, 'endTime'>} CpuNodeTimingComplete */
42
- /** @typedef {NetworkNodeTimingInProgress & Pick<NodeTimingComplete, 'endTime'>} NetworkNodeTimingComplete */
42
+ /** @typedef {NetworkNodeTimingInProgress & Pick<NodeTimingComplete, 'endTime'> & {connectionTiming: ConnectionTiming}} NetworkNodeTimingComplete */
43
43
 
44
44
  /** @typedef {NodeTimingQueued | CpuNodeTimingStarted | NetworkNodeTimingStarted | CpuNodeTimingInProgress | NetworkNodeTimingInProgress | CpuNodeTimingComplete | NetworkNodeTimingComplete} NodeTimingData */
45
45
 
46
+ /**
47
+ * @typedef ConnectionTiming A breakdown of network connection timings.
48
+ * @property {number} [dnsResolutionTime]
49
+ * @property {number} [connectionTime]
50
+ * @property {number} [sslTime]
51
+ * @property {number} timeToFirstByte
52
+ */
53
+
46
54
  class SimulatorTimingMap {
47
55
  constructor() {
48
56
  /** @type {Map<Node, NodeTimingData>} */
@@ -83,12 +91,13 @@ class SimulatorTimingMap {
83
91
 
84
92
  /**
85
93
  * @param {Node} node
86
- * @param {{endTime: number}} values
94
+ * @param {{endTime: number, connectionTiming?: ConnectionTiming}} values
87
95
  */
88
96
  setCompleted(node, values) {
89
97
  const nodeTiming = {
90
98
  ...this.getInProgress(node),
91
99
  endTime: values.endTime,
100
+ connectionTiming: values.connectionTiming,
92
101
  };
93
102
 
94
103
  this._nodeTimings.set(node, nodeTiming);
@@ -16,6 +16,8 @@ const mobileSlow4G = constants.throttling.mobileSlow4G;
16
16
  /** @typedef {import('../base-node.js').Node} Node */
17
17
  /** @typedef {import('../network-node').NetworkNode} NetworkNode */
18
18
  /** @typedef {import('../cpu-node').CPUNode} CpuNode */
19
+ /** @typedef {import('./simulator-timing-map.js').CpuNodeTimingComplete | import('./simulator-timing-map.js').NetworkNodeTimingComplete} CompleteNodeTiming */
20
+ /** @typedef {import('./simulator-timing-map.js').ConnectionTiming} ConnectionTiming */
19
21
 
20
22
  // see https://cs.chromium.org/search/?q=kDefaultMaxNumDelayableRequestsPerClient&sq=package:chromium&type=cs
21
23
  const DEFAULT_MAXIMUM_CONCURRENT_REQUESTS = 10;
@@ -40,7 +42,7 @@ const PriorityStartTimePenalty = {
40
42
  VeryLow: 2,
41
43
  };
42
44
 
43
- /** @type {Map<string, LH.Gatherer.Simulation.Result['nodeTimings']>} */
45
+ /** @type {Map<string, Map<Node, CompleteNodeTiming>>} */
44
46
  const ALL_SIMULATION_NODE_TIMINGS = new Map();
45
47
 
46
48
  class Simulator {
@@ -167,12 +169,13 @@ class Simulator {
167
169
  /**
168
170
  * @param {Node} node
169
171
  * @param {number} endTime
172
+ * @param {ConnectionTiming} [connectionTiming] Optional network connection information.
170
173
  */
171
- _markNodeAsComplete(node, endTime) {
174
+ _markNodeAsComplete(node, endTime, connectionTiming) {
172
175
  this._nodes[NodeState.Complete].add(node);
173
176
  this._nodes[NodeState.InProgress].delete(node);
174
177
  this._numberInProgressByType.set(node.type, this._numberInProgress(node.type) - 1);
175
- this._nodeTimings.setCompleted(node, {endTime});
178
+ this._nodeTimings.setCompleted(node, {endTime, connectionTiming});
176
179
 
177
180
  // Try to add all its dependents to the queue
178
181
  for (const dependent of node.getDependents()) {
@@ -371,7 +374,7 @@ class Simulator {
371
374
  if (isFinished) {
372
375
  connection.setWarmed(true);
373
376
  this._connectionPool.release(record);
374
- this._markNodeAsComplete(node, totalElapsedTime);
377
+ this._markNodeAsComplete(node, totalElapsedTime, calculation.connectionTiming);
375
378
  } else {
376
379
  timingData.timeElapsed += calculation.timeElapsed;
377
380
  timingData.timeElapsedOvershoot += calculation.timeElapsed - timePeriodLength;
@@ -380,23 +383,31 @@ class Simulator {
380
383
  }
381
384
 
382
385
  /**
383
- * @return {Map<Node, LH.Gatherer.Simulation.NodeTiming>}
386
+ * @return {{nodeTimings: Map<Node, LH.Gatherer.Simulation.NodeTiming>, completeNodeTimings: Map<Node, CompleteNodeTiming>}}
384
387
  */
385
388
  _computeFinalNodeTimings() {
389
+ /** @type {Array<[Node, CompleteNodeTiming]>} */
390
+ const completeNodeTimingEntries = this._nodeTimings.getNodes().map(node => {
391
+ return [node, this._nodeTimings.getCompleted(node)];
392
+ });
393
+
394
+ // Most consumers will want the entries sorted by startTime, so insert them in that order
395
+ completeNodeTimingEntries.sort((a, b) => a[1].startTime - b[1].startTime);
396
+
397
+ // Trimmed version of type `LH.Gatherer.Simulation.NodeTiming`.
386
398
  /** @type {Array<[Node, LH.Gatherer.Simulation.NodeTiming]>} */
387
- const nodeTimingEntries = [];
388
- for (const node of this._nodeTimings.getNodes()) {
389
- const timing = this._nodeTimings.getCompleted(node);
390
- nodeTimingEntries.push([node, {
399
+ const nodeTimingEntries = completeNodeTimingEntries.map(([node, timing]) => {
400
+ return [node, {
391
401
  startTime: timing.startTime,
392
402
  endTime: timing.endTime,
393
403
  duration: timing.endTime - timing.startTime,
394
- }]);
395
- }
404
+ }];
405
+ });
396
406
 
397
- // Most consumers will want the entries sorted by startTime, so insert them in that order
398
- nodeTimingEntries.sort((a, b) => a[1].startTime - b[1].startTime);
399
- return new Map(nodeTimingEntries);
407
+ return {
408
+ nodeTimings: new Map(nodeTimingEntries),
409
+ completeNodeTimings: new Map(completeNodeTimingEntries),
410
+ };
400
411
  }
401
412
 
402
413
  /**
@@ -481,8 +492,9 @@ class Simulator {
481
492
  }
482
493
  }
483
494
 
484
- const nodeTimings = this._computeFinalNodeTimings();
485
- ALL_SIMULATION_NODE_TIMINGS.set(options.label || 'unlabeled', nodeTimings);
495
+ // `nodeTimings` are used for simulator consumers, `completeNodeTimings` kept for debugging.
496
+ const {nodeTimings, completeNodeTimings} = this._computeFinalNodeTimings();
497
+ ALL_SIMULATION_NODE_TIMINGS.set(options.label || 'unlabeled', completeNodeTimings);
486
498
 
487
499
  return {
488
500
  timeInMs: totalElapsedTime,
@@ -507,7 +519,7 @@ class Simulator {
507
519
  return wastedMs;
508
520
  }
509
521
 
510
- /** @return {Map<string, LH.Gatherer.Simulation.Result['nodeTimings']>} */
522
+ /** @return {Map<string, Map<Node, CompleteNodeTiming>>} */
511
523
  static get ALL_NODE_TIMINGS() {
512
524
  return ALL_SIMULATION_NODE_TIMINGS;
513
525
  }
@@ -4,6 +4,8 @@
4
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
5
  */
6
6
 
7
+ /** @typedef {import('./simulator-timing-map.js').ConnectionTiming} ConnectionTiming */
8
+
7
9
  const INITIAL_CONGESTION_WINDOW = 10;
8
10
  const TCP_SEGMENT_SIZE = 1460;
9
11
 
@@ -176,12 +178,34 @@ class TcpConnection {
176
178
  const extraBytesDownloaded = this._h2 ? Math.max(totalBytesDownloaded - bytesToDownload, 0) : 0;
177
179
  const bytesDownloaded = Math.max(Math.min(totalBytesDownloaded, bytesToDownload), 0);
178
180
 
181
+ /** @type {ConnectionTiming} */
182
+ let connectionTiming;
183
+ if (!this._warmed) {
184
+ connectionTiming = {
185
+ dnsResolutionTime,
186
+ connectionTime: handshakeAndRequest - dnsResolutionTime,
187
+ sslTime: this._ssl ? twoWayLatency : undefined,
188
+ timeToFirstByte,
189
+ };
190
+ } else if (this._h2) {
191
+ // TODO: timing information currently difficult to model for warm h2 connections.
192
+ connectionTiming = {
193
+ timeToFirstByte,
194
+ };
195
+ } else {
196
+ connectionTiming = {
197
+ connectionTime: handshakeAndRequest,
198
+ timeToFirstByte,
199
+ };
200
+ }
201
+
179
202
  return {
180
203
  roundTrips,
181
204
  timeElapsed,
182
205
  bytesDownloaded,
183
206
  extraBytesDownloaded,
184
207
  congestionWindow,
208
+ connectionTiming,
185
209
  };
186
210
  }
187
211
  }
@@ -202,4 +226,5 @@ export {TcpConnection};
202
226
  * @property {number} bytesDownloaded
203
227
  * @property {number} extraBytesDownloaded
204
228
  * @property {number} congestionWindow
229
+ * @property {ConnectionTiming} connectionTiming
205
230
  */
@@ -4,8 +4,11 @@
4
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
5
  */
6
6
 
7
+ /** @typedef {import('./dependency-graph/base-node.js').Node} Node */
8
+ /** @typedef {import('./dependency-graph/simulator/simulator.js').CompleteNodeTiming} CompleteNodeTiming */
9
+
7
10
  /**
8
- * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
11
+ * @param {Map<Node, CompleteNodeTiming>} nodeTimings
9
12
  * @return {LH.Trace}
10
13
  */
11
14
  function convertNodeTimingsToTrace(nodeTimings) {
@@ -31,7 +34,8 @@ function convertNodeTimingsToTrace(nodeTimings) {
31
34
  } else {
32
35
  // Ignore data URIs as they don't really add much value
33
36
  if (/^data/.test(node.record.url)) continue;
34
- traceEvents.push(...createFakeNetworkEvents(node.record, timing));
37
+ traceEvents.push(...createFakeNetworkEvents(requestId, node.record, timing));
38
+ requestId++;
35
39
  }
36
40
  }
37
41
 
@@ -117,12 +121,34 @@ function convertNodeTimingsToTrace(nodeTimings) {
117
121
  }
118
122
 
119
123
  /**
124
+ * @param {number} requestId
125
+ * @param {LH.Artifacts.NetworkRequest} record
126
+ * @param {CompleteNodeTiming} timing
127
+ * @return {LH.TraceEvent}
128
+ */
129
+ function createWillSendRequestEvent(requestId, record, timing) {
130
+ return {
131
+ ...baseEvent,
132
+ ph: 'I',
133
+ s: 't',
134
+ // No `dur` on network instant events but add to keep types happy.
135
+ dur: 0,
136
+ name: 'ResourceWillSendRequest',
137
+ ts: toMicroseconds(timing.startTime),
138
+ args: {data: {requestId: String(requestId)}},
139
+ };
140
+ }
141
+
142
+ /**
143
+ * @param {number} requestId
120
144
  * @param {LH.Artifacts.NetworkRequest} record
121
- * @param {LH.Gatherer.Simulation.NodeTiming} timing
145
+ * @param {CompleteNodeTiming} timing
122
146
  * @return {LH.TraceEvent[]}
123
147
  */
124
- function createFakeNetworkEvents(record, timing) {
125
- requestId++;
148
+ function createFakeNetworkEvents(requestId, record, timing) {
149
+ if (!('connectionTiming' in timing)) {
150
+ throw new Error('Network node timing incomplete');
151
+ }
126
152
 
127
153
  // 0ms requests get super-messed up rendering
128
154
  // Use 0.3ms instead so they're still hoverable, https://github.com/GoogleChrome/lighthouse/pull/5350#discussion_r194563201
@@ -130,6 +156,7 @@ function convertNodeTimingsToTrace(nodeTimings) {
130
156
  if (startTime === endTime) endTime += 0.3;
131
157
 
132
158
  const requestData = {requestId: requestId.toString(), frame};
159
+ // No `dur` on network instant events but add to keep types happy.
133
160
  /** @type {LH.Util.StrictOmit<LH.TraceEvent, 'name'|'ts'|'args'>} */
134
161
  const baseRequestEvent = {...baseEvent, ph: 'I', s: 't', dur: 0};
135
162
 
@@ -140,6 +167,13 @@ function convertNodeTimingsToTrace(nodeTimings) {
140
167
  priority: record.priority,
141
168
  };
142
169
 
170
+ const {dnsResolutionTime, connectionTime, sslTime, timeToFirstByte} = timing.connectionTiming;
171
+ let sslStart = -1;
172
+ let sslEnd = -1;
173
+ if (connectionTime !== undefined && sslTime !== undefined) {
174
+ sslStart = connectionTime - sslTime;
175
+ sslEnd = connectionTime;
176
+ }
143
177
  const receiveResponseData = {
144
178
  ...requestData,
145
179
  statusCode: record.statusCode,
@@ -147,17 +181,45 @@ function convertNodeTimingsToTrace(nodeTimings) {
147
181
  encodedDataLength: record.transferSize,
148
182
  fromCache: record.fromDiskCache,
149
183
  fromServiceWorker: record.fetchedViaServiceWorker,
184
+ timing: {
185
+ // `requestTime` is in seconds.
186
+ requestTime: toMicroseconds(startTime) / (1000 * 1000),
187
+ // Remaining values are milliseconds after `requestTime`.
188
+ dnsStart: dnsResolutionTime === undefined ? -1 : 0,
189
+ dnsEnd: dnsResolutionTime ?? -1,
190
+ connectStart: dnsResolutionTime ?? -1,
191
+ connectEnd: connectionTime ?? -1,
192
+ sslStart,
193
+ sslEnd,
194
+ sendStart: connectionTime ?? 0,
195
+ sendEnd: connectionTime ?? 0,
196
+ receiveHeadersEnd: timeToFirstByte,
197
+ workerStart: -1,
198
+ workerReady: -1,
199
+ proxyStart: -1,
200
+ proxyEnd: -1,
201
+ pushStart: 0,
202
+ pushEnd: 0,
203
+ },
150
204
  };
151
205
 
152
206
  const resourceFinishData = {
153
- ...requestData,
207
+ requestId: requestId.toString(),
208
+ encodedDataLength: record.transferSize,
154
209
  decodedBodyLength: record.resourceSize,
155
210
  didFail: !!record.failed,
156
211
  finishTime: toMicroseconds(endTime) / (1000 * 1000),
157
212
  };
158
213
 
159
214
  /** @type {LH.TraceEvent[]} */
160
- const events = [
215
+ const events = [];
216
+
217
+ // Navigation request needs an additional ResourceWillSendRequest event.
218
+ if (requestId === 1) {
219
+ events.push(createWillSendRequestEvent(requestId, record, timing));
220
+ }
221
+
222
+ events.push(
161
223
  {
162
224
  ...baseRequestEvent,
163
225
  name: 'ResourceSendRequest',
@@ -169,13 +231,14 @@ function convertNodeTimingsToTrace(nodeTimings) {
169
231
  name: 'ResourceFinish',
170
232
  ts: toMicroseconds(endTime),
171
233
  args: {data: resourceFinishData},
172
- },
173
- ];
234
+ }
235
+ );
174
236
 
175
237
  if (!record.failed) {
176
238
  events.push({
177
239
  ...baseRequestEvent,
178
240
  name: 'ResourceReceiveResponse',
241
+ // Event `ts` isn't meaningful, so just pick a time.
179
242
  ts: toMicroseconds((startTime + endTime) / 2),
180
243
  args: {data: receiveResponseData},
181
244
  });
@@ -4,7 +4,7 @@
4
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
5
  */
6
6
 
7
- import thirdPartyWeb from 'third-party-web/httparchive-nostats-subset.js';
7
+ import thirdPartyWeb from 'third-party-web/nostats-subset.js';
8
8
 
9
9
  /** @typedef {import("third-party-web").IEntity} ThirdPartyEntity */
10
10
  /** @typedef {import("third-party-web").IProduct} ThirdPartyProduct */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "9.5.0-dev.20221129",
4
+ "version": "9.5.0-dev.20221201",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -206,7 +206,7 @@
206
206
  "robots-parser": "^3.0.0",
207
207
  "semver": "^5.3.0",
208
208
  "speedline-core": "^1.4.3",
209
- "third-party-web": "^0.17.1",
209
+ "third-party-web": "^0.20.2",
210
210
  "ws": "^7.0.0",
211
211
  "yargs": "^17.3.1",
212
212
  "yargs-parser": "^21.0.0"