lighthouse 9.5.0-dev.20221128 → 9.5.0-dev.20221130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/cli/test/smokehouse/lighthouse-runners/devtools.js +1 -1
  2. package/core/audits/byte-efficiency/offscreen-images.js +1 -1
  3. package/core/audits/byte-efficiency/render-blocking-resources.js +1 -1
  4. package/core/audits/critical-request-chains.js +6 -6
  5. package/core/audits/font-display.js +1 -1
  6. package/core/audits/network-requests.js +5 -5
  7. package/core/audits/redirects.js +2 -2
  8. package/core/audits/uses-rel-preconnect.js +5 -5
  9. package/core/config/config-helpers.js +12 -3
  10. package/core/config/default-config.js +1 -0
  11. package/core/gather/base-gatherer.js +0 -1
  12. package/core/gather/driver/target-manager.js +12 -5
  13. package/core/gather/driver.js +1 -1
  14. package/core/gather/gatherers/bf-cache-failures.js +158 -0
  15. package/core/gather/gatherers/seo/font-size.js +9 -52
  16. package/core/legacy/config/legacy-default-config.js +1 -0
  17. package/core/lib/asset-saver.js +1 -1
  18. package/core/lib/dependency-graph/base-node.js +2 -0
  19. package/core/lib/dependency-graph/network-node.js +2 -2
  20. package/core/lib/dependency-graph/simulator/network-analyzer.js +3 -3
  21. package/core/lib/dependency-graph/simulator/simulator-timing-map.js +11 -2
  22. package/core/lib/dependency-graph/simulator/simulator.js +33 -18
  23. package/core/lib/dependency-graph/simulator/tcp-connection.js +25 -0
  24. package/core/lib/lantern-trace-saver.js +72 -9
  25. package/core/lib/network-request.js +16 -11
  26. package/core/runner.js +0 -1
  27. package/dist/report/bundle.esm.js +19 -1
  28. package/dist/report/flow.js +1 -1
  29. package/dist/report/standalone.js +5 -5
  30. package/package.json +3 -2
  31. package/report/renderer/i18n.js +19 -1
  32. package/types/artifacts.d.ts +12 -0
@@ -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()) {
@@ -236,8 +239,11 @@ class Simulator {
236
239
  * currently in flight.
237
240
  */
238
241
  _updateNetworkCapacity() {
242
+ const inFlight = this._numberInProgress(BaseNode.TYPES.NETWORK);
243
+ if (inFlight === 0) return;
244
+
239
245
  for (const connection of this._connectionPool.connectionsInUse()) {
240
- connection.setThroughput(this._throughput / this._nodes[NodeState.InProgress].size);
246
+ connection.setThroughput(this._throughput / inFlight);
241
247
  }
242
248
  }
243
249
 
@@ -368,7 +374,7 @@ class Simulator {
368
374
  if (isFinished) {
369
375
  connection.setWarmed(true);
370
376
  this._connectionPool.release(record);
371
- this._markNodeAsComplete(node, totalElapsedTime);
377
+ this._markNodeAsComplete(node, totalElapsedTime, calculation.connectionTiming);
372
378
  } else {
373
379
  timingData.timeElapsed += calculation.timeElapsed;
374
380
  timingData.timeElapsedOvershoot += calculation.timeElapsed - timePeriodLength;
@@ -377,23 +383,31 @@ class Simulator {
377
383
  }
378
384
 
379
385
  /**
380
- * @return {Map<Node, LH.Gatherer.Simulation.NodeTiming>}
386
+ * @return {{nodeTimings: Map<Node, LH.Gatherer.Simulation.NodeTiming>, completeNodeTimings: Map<Node, CompleteNodeTiming>}}
381
387
  */
382
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`.
383
398
  /** @type {Array<[Node, LH.Gatherer.Simulation.NodeTiming]>} */
384
- const nodeTimingEntries = [];
385
- for (const node of this._nodeTimings.getNodes()) {
386
- const timing = this._nodeTimings.getCompleted(node);
387
- nodeTimingEntries.push([node, {
399
+ const nodeTimingEntries = completeNodeTimingEntries.map(([node, timing]) => {
400
+ return [node, {
388
401
  startTime: timing.startTime,
389
402
  endTime: timing.endTime,
390
403
  duration: timing.endTime - timing.startTime,
391
- }]);
392
- }
404
+ }];
405
+ });
393
406
 
394
- // Most consumers will want the entries sorted by startTime, so insert them in that order
395
- nodeTimingEntries.sort((a, b) => a[1].startTime - b[1].startTime);
396
- return new Map(nodeTimingEntries);
407
+ return {
408
+ nodeTimings: new Map(nodeTimingEntries),
409
+ completeNodeTimings: new Map(completeNodeTimingEntries),
410
+ };
397
411
  }
398
412
 
399
413
  /**
@@ -478,8 +492,9 @@ class Simulator {
478
492
  }
479
493
  }
480
494
 
481
- const nodeTimings = this._computeFinalNodeTimings();
482
- 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);
483
498
 
484
499
  return {
485
500
  timeInMs: totalElapsedTime,
@@ -504,7 +519,7 @@ class Simulator {
504
519
  return wastedMs;
505
520
  }
506
521
 
507
- /** @return {Map<string, LH.Gatherer.Simulation.Result['nodeTimings']>} */
522
+ /** @return {Map<string, Map<Node, CompleteNodeTiming>>} */
508
523
  static get ALL_NODE_TIMINGS() {
509
524
  return ALL_SIMULATION_NODE_TIMINGS;
510
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
  });
@@ -124,10 +124,14 @@ class NetworkRequest {
124
124
  this.parsedURL = /** @type {ParsedURL} */ ({scheme: ''});
125
125
  this.documentURL = '';
126
126
 
127
+ /**
128
+ * When the network service is about to handle a request, ie. just before going to the
129
+ * HTTP cache or going to the network for DNS/connection setup, in milliseconds.
130
+ */
127
131
  this.startTime = -1;
128
- /** @type {number} */
132
+ /** When the last byte of the response body is received, in milliseconds. */
129
133
  this.endTime = -1;
130
- /** @type {number} */
134
+ /** When the last byte of the response headers is received, in milliseconds. */
131
135
  this.responseReceivedTime = -1;
132
136
 
133
137
  // Go read the comment on _updateTransferSizeForLightrider.
@@ -217,7 +221,8 @@ class NetworkRequest {
217
221
  };
218
222
  this.isSecure = UrlUtils.isSecureScheme(this.parsedURL.scheme);
219
223
 
220
- this.startTime = data.timestamp;
224
+ // Expected to be overriden with better value in `_recomputeTimesWithResourceTiming`.
225
+ this.startTime = data.timestamp * 1000;
221
226
 
222
227
  this.requestMethod = data.request.method;
223
228
 
@@ -261,7 +266,7 @@ class NetworkRequest {
261
266
  if (this.finished) return;
262
267
 
263
268
  this.finished = true;
264
- this.endTime = data.timestamp;
269
+ this.endTime = data.timestamp * 1000;
265
270
  if (data.encodedDataLength >= 0) {
266
271
  this.transferSize = data.encodedDataLength;
267
272
  }
@@ -279,7 +284,7 @@ class NetworkRequest {
279
284
  if (this.finished) return;
280
285
 
281
286
  this.finished = true;
282
- this.endTime = data.timestamp;
287
+ this.endTime = data.timestamp * 1000;
283
288
 
284
289
  this.failed = true;
285
290
  this.resourceType = data.type && RESOURCE_TYPES[data.type];
@@ -305,7 +310,7 @@ class NetworkRequest {
305
310
  this._onResponse(data.redirectResponse, data.timestamp, data.type);
306
311
  this.resourceType = undefined;
307
312
  this.finished = true;
308
- this.endTime = data.timestamp;
313
+ this.endTime = data.timestamp * 1000;
309
314
 
310
315
  this._updateResponseReceivedTimeIfNecessary();
311
316
  }
@@ -319,7 +324,7 @@ class NetworkRequest {
319
324
 
320
325
  /**
321
326
  * @param {LH.Crdp.Network.Response} response
322
- * @param {number} timestamp
327
+ * @param {number} timestamp in seconds
323
328
  * @param {LH.Crdp.Network.ResponseReceivedEvent['type']=} resourceType
324
329
  */
325
330
  _onResponse(response, timestamp, resourceType) {
@@ -330,7 +335,7 @@ class NetworkRequest {
330
335
 
331
336
  if (response.protocol) this.protocol = response.protocol;
332
337
 
333
- this.responseReceivedTime = timestamp;
338
+ this.responseReceivedTime = timestamp * 1000;
334
339
 
335
340
  this.transferSize = response.encodedDataLength;
336
341
  if (typeof response.fromDiskCache === 'boolean') this.fromDiskCache = response.fromDiskCache;
@@ -364,8 +369,8 @@ class NetworkRequest {
364
369
  // Take startTime and responseReceivedTime from timing data for better accuracy.
365
370
  // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
366
371
  // TODO: This skips the "queuing time" before the netstack has taken over ... is this a mistake?
367
- this.startTime = timing.requestTime;
368
- const headersReceivedTime = timing.requestTime + timing.receiveHeadersEnd / 1000;
372
+ this.startTime = timing.requestTime * 1000;
373
+ const headersReceivedTime = this.startTime + timing.receiveHeadersEnd;
369
374
  if (!this.responseReceivedTime || this.responseReceivedTime < 0) {
370
375
  this.responseReceivedTime = headersReceivedTime;
371
376
  }
@@ -478,7 +483,7 @@ class NetworkRequest {
478
483
  }
479
484
 
480
485
  this.lrStatistics = {
481
- endTimeDeltaMs: (this.endTime - (this.startTime + (totalMs / 1000))) * 1000,
486
+ endTimeDeltaMs: this.endTime - (this.startTime + totalMs),
482
487
  TCPMs: TCPMs,
483
488
  requestMs: requestMs,
484
489
  responseMs: responseMs,
package/core/runner.js CHANGED
@@ -521,5 +521,4 @@ class Runner {
521
521
  }
522
522
  }
523
523
 
524
- // TODO(esmodules): make this not a class.
525
524
  export {Runner};
@@ -3438,6 +3438,7 @@ class I18n {
3438
3438
 
3439
3439
  this._locale = locale;
3440
3440
  this._strings = strings;
3441
+ this._cachedNumberFormatters = new Map();
3441
3442
  }
3442
3443
 
3443
3444
  get strings() {
@@ -3472,7 +3473,24 @@ class I18n {
3472
3473
  number = 0;
3473
3474
  }
3474
3475
 
3475
- return new Intl.NumberFormat(this._locale, opts).format(number).replace(' ', NBSP2);
3476
+ let formatter;
3477
+ // eslint-disable-next-line max-len
3478
+ const cacheKey = [
3479
+ opts.minimumFractionDigits,
3480
+ opts.maximumFractionDigits,
3481
+ opts.style,
3482
+ opts.unit,
3483
+ opts.unitDisplay,
3484
+ this._locale,
3485
+ ].join('');
3486
+
3487
+ formatter = this._cachedNumberFormatters.get(cacheKey);
3488
+ if (!formatter) {
3489
+ formatter = new Intl.NumberFormat(this._locale, opts);
3490
+ this._cachedNumberFormatters.set(cacheKey, formatter);
3491
+ }
3492
+
3493
+ return formatter.format(number).replace(' ', NBSP2);
3476
3494
  }
3477
3495
 
3478
3496
  /**
@@ -26,7 +26,7 @@
26
26
  * 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
27
27
  * 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.
28
28
  */
29
- const ta=1024,ia=1048576;class oa{constructor(e,a){"en-XA"===e&&(e="de"),this._locale=e,this._strings=a}get strings(){return this._strings}_formatNumberWithGranularity(e,a,n={}){if(void 0!==a){const t=-Math.log10(a);Number.isInteger(t)||(console.warn(`granularity of ${a} is invalid. Using 1 instead`),a=1),a<1&&((n={...n}).minimumFractionDigits=n.maximumFractionDigits=Math.ceil(t)),e=Math.round(e/a)*a,Object.is(e,-0)&&(e=0)}else Math.abs(e)<5e-4&&(e=0);return new Intl.NumberFormat(this._locale,n).format(e).replace(" "," ")}formatNumber(e,a){return this._formatNumberWithGranularity(e,a)}formatInteger(e){return this._formatNumberWithGranularity(e,1)}formatPercent(e){return new Intl.NumberFormat(this._locale,{style:"percent"}).format(e)}formatBytesToKiB(e,a){return this._formatNumberWithGranularity(e/ta,a)+" KiB"}formatBytesToMiB(e,a){return this._formatNumberWithGranularity(e/ia,a)+" MiB"}formatBytes(e,a=1){return this._formatNumberWithGranularity(e,a,{style:"unit",unit:"byte",unitDisplay:"long"})}formatBytesWithBestUnit(e,a){return e>=ia?this.formatBytesToMiB(e,a):e>=ta?this.formatBytesToKiB(e,a):this._formatNumberWithGranularity(e,a,{style:"unit",unit:"byte",unitDisplay:"narrow"})}formatKbps(e,a){return this._formatNumberWithGranularity(e,a,{style:"unit",unit:"kilobit-per-second",unitDisplay:"short"})}formatMilliseconds(e,a){return this._formatNumberWithGranularity(e,a,{style:"unit",unit:"millisecond",unitDisplay:"short"})}formatSeconds(e,a){return this._formatNumberWithGranularity(e/1e3,a,{style:"unit",unit:"second",unitDisplay:"short"})}formatDateTime(e){const a={month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"numeric",timeZoneName:"short"};let n;try{n=new Intl.DateTimeFormat(this._locale,a)}catch(e){a.timeZone="UTC",n=new Intl.DateTimeFormat(this._locale,a)}return n.format(new Date(e))}formatDuration(e){let a=e/1e3;if(0===Math.round(a))return"None";const n=[],t={day:86400,hour:3600,minute:60,second:1};return Object.keys(t).forEach((e=>{const i=t[e],o=Math.floor(a/i);if(o>0){a-=o*i;const t=this._formatNumberWithGranularity(o,1,{style:"unit",unit:e,unitDisplay:"narrow"});n.push(t)}})),n.join(" ")}}
29
+ const ta=1024,ia=1048576;class oa{constructor(e,a){"en-XA"===e&&(e="de"),this._locale=e,this._strings=a,this._cachedNumberFormatters=new Map}get strings(){return this._strings}_formatNumberWithGranularity(e,a,n={}){if(void 0!==a){const t=-Math.log10(a);Number.isInteger(t)||(console.warn(`granularity of ${a} is invalid. Using 1 instead`),a=1),a<1&&((n={...n}).minimumFractionDigits=n.maximumFractionDigits=Math.ceil(t)),e=Math.round(e/a)*a,Object.is(e,-0)&&(e=0)}else Math.abs(e)<5e-4&&(e=0);let t;const i=[n.minimumFractionDigits,n.maximumFractionDigits,n.style,n.unit,n.unitDisplay,this._locale].join("");return t=this._cachedNumberFormatters.get(i),t||(t=new Intl.NumberFormat(this._locale,n),this._cachedNumberFormatters.set(i,t)),t.format(e).replace(" "," ")}formatNumber(e,a){return this._formatNumberWithGranularity(e,a)}formatInteger(e){return this._formatNumberWithGranularity(e,1)}formatPercent(e){return new Intl.NumberFormat(this._locale,{style:"percent"}).format(e)}formatBytesToKiB(e,a){return this._formatNumberWithGranularity(e/ta,a)+" KiB"}formatBytesToMiB(e,a){return this._formatNumberWithGranularity(e/ia,a)+" MiB"}formatBytes(e,a=1){return this._formatNumberWithGranularity(e,a,{style:"unit",unit:"byte",unitDisplay:"long"})}formatBytesWithBestUnit(e,a){return e>=ia?this.formatBytesToMiB(e,a):e>=ta?this.formatBytesToKiB(e,a):this._formatNumberWithGranularity(e,a,{style:"unit",unit:"byte",unitDisplay:"narrow"})}formatKbps(e,a){return this._formatNumberWithGranularity(e,a,{style:"unit",unit:"kilobit-per-second",unitDisplay:"short"})}formatMilliseconds(e,a){return this._formatNumberWithGranularity(e,a,{style:"unit",unit:"millisecond",unitDisplay:"short"})}formatSeconds(e,a){return this._formatNumberWithGranularity(e/1e3,a,{style:"unit",unit:"second",unitDisplay:"short"})}formatDateTime(e){const a={month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"numeric",timeZoneName:"short"};let n;try{n=new Intl.DateTimeFormat(this._locale,a)}catch(e){a.timeZone="UTC",n=new Intl.DateTimeFormat(this._locale,a)}return n.format(new Date(e))}formatDuration(e){let a=e/1e3;if(0===Math.round(a))return"None";const n=[],t={day:86400,hour:3600,minute:60,second:1};return Object.keys(t).forEach((e=>{const i=t[e],o=Math.floor(a/i);if(o>0){a-=o*i;const t=this._formatNumberWithGranularity(o,1,{style:"unit",unit:e,unitDisplay:"narrow"});n.push(t)}})),n.join(" ")}}
30
30
  /**
31
31
  * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
32
32
  * 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