lighthouse 9.5.0-dev.20220426 → 9.5.0-dev.20220429

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.
@@ -12,13 +12,11 @@
12
12
 
13
13
  /* eslint-disable no-console */
14
14
 
15
- import _ from 'lodash';
15
+ import cloneDeep from 'lodash/cloneDeep.js';
16
16
 
17
17
  import smokeTests from '../core-tests.js';
18
18
  import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
19
19
 
20
- const {cloneDeep} = _;
21
-
22
20
  /**
23
21
  * @param {Smokehouse.SmokehouseLibOptions} options
24
22
  */
@@ -16,7 +16,7 @@ import path from 'path';
16
16
  import fs from 'fs';
17
17
  import url from 'url';
18
18
 
19
- import _ from 'lodash';
19
+ import cloneDeep from 'lodash/cloneDeep.js';
20
20
  import yargs from 'yargs';
21
21
  import * as yargsHelpers from 'yargs/helpers';
22
22
  import log from 'lighthouse-logger';
@@ -25,8 +25,6 @@ import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
25
25
  import {updateTestDefnFormat} from './back-compat-util.js';
26
26
  import {LH_ROOT} from '../../../../root.js';
27
27
 
28
- const {cloneDeep} = _;
29
-
30
28
  const coreTestDefnsPath =
31
29
  path.join(LH_ROOT, 'lighthouse-cli/test/smokehouse/core-tests.js');
32
30
 
@@ -9,14 +9,12 @@
9
9
  * against the results actually collected from Lighthouse.
10
10
  */
11
11
 
12
- import _ from 'lodash';
12
+ import cloneDeep from 'lodash/cloneDeep.js';
13
13
  import log from 'lighthouse-logger';
14
14
 
15
15
  import {LocalConsole} from './lib/local-console.js';
16
16
  import {chromiumVersionCheck} from './version-check.js';
17
17
 
18
- const {cloneDeep} = _;
19
-
20
18
  /**
21
19
  * @typedef Difference
22
20
  * @property {string} path
@@ -7,12 +7,8 @@
7
7
 
8
8
  /**
9
9
  * @fileoverview Audits a page to determine if it is calling deprecated APIs.
10
- * This is done by collecting console log messages and filtering them by ones
11
- * that contain deprecated API warnings sent by Chrome.
12
10
  */
13
11
 
14
- // TODO: when M97 is sufficiently old, drop support for console messages
15
-
16
12
  const Audit = require('./audit.js');
17
13
  const JsBundles = require('../computed/js-bundles.js');
18
14
  const i18n = require('../lib/i18n/i18n.js');
@@ -48,7 +44,7 @@ class Deprecations extends Audit {
48
44
  title: str_(UIStrings.title),
49
45
  failureTitle: str_(UIStrings.failureTitle),
50
46
  description: str_(UIStrings.description),
51
- requiredArtifacts: ['ConsoleMessages', 'InspectorIssues', 'SourceMaps', 'Scripts'],
47
+ requiredArtifacts: ['InspectorIssues', 'SourceMaps', 'Scripts'],
52
48
  };
53
49
  }
54
50
 
@@ -58,35 +54,21 @@ class Deprecations extends Audit {
58
54
  * @return {Promise<LH.Audit.Product>}
59
55
  */
60
56
  static async audit(artifacts, context) {
61
- const entries = artifacts.ConsoleMessages;
62
57
  const bundles = await JsBundles.request(artifacts, context);
63
58
 
64
- let deprecations;
65
- if (artifacts.InspectorIssues.deprecationIssue.length) {
66
- deprecations = artifacts.InspectorIssues.deprecationIssue
67
- // TODO: translate these strings.
68
- // see https://github.com/GoogleChrome/lighthouse/issues/13895
69
- .filter(deprecation => !deprecation.type || deprecation.type === 'Untranslated')
70
- .map(deprecation => {
71
- const {scriptId, url, lineNumber, columnNumber} = deprecation.sourceCodeLocation;
72
- const bundle = bundles.find(bundle => bundle.script.scriptId === scriptId);
73
- return {
74
- value: deprecation.message || '',
75
- // Protocol.Audits.SourceCodeLocation.columnNumber is 1-indexed, but we use 0-indexed.
76
- source: Audit.makeSourceLocation(url, lineNumber, columnNumber - 1, bundle),
77
- };
78
- });
79
- } else {
80
- // Backcompat for <M97.
81
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1248484
82
- deprecations = entries.filter(log => log.source === 'deprecation')
83
- .map(log => {
84
- return {
85
- value: log.text,
86
- source: Audit.makeSourceLocationFromConsoleMessage(log),
87
- };
88
- });
89
- }
59
+ const deprecations = artifacts.InspectorIssues.deprecationIssue
60
+ // TODO: translate these strings.
61
+ // see https://github.com/GoogleChrome/lighthouse/issues/13895
62
+ .filter(deprecation => !deprecation.type || deprecation.type === 'Untranslated')
63
+ .map(deprecation => {
64
+ const {scriptId, url, lineNumber, columnNumber} = deprecation.sourceCodeLocation;
65
+ const bundle = bundles.find(bundle => bundle.script.scriptId === scriptId);
66
+ return {
67
+ value: deprecation.message || '',
68
+ // Protocol.Audits.SourceCodeLocation.columnNumber is 1-indexed, but we use 0-indexed.
69
+ source: Audit.makeSourceLocation(url, lineNumber, columnNumber - 1, bundle),
70
+ };
71
+ });
90
72
 
91
73
  /** @type {LH.Audit.Details.Table['headings']} */
92
74
  const headings = [
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ const Audit = require('../audit.js');
9
+ const ComputedResponsivenes = require('../../computed/metrics/responsiveness.js');
10
+ const i18n = require('../../lib/i18n/i18n.js');
11
+
12
+ const UIStrings = {
13
+ /** Description of the Interaction to Next Paint metric. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
14
+ description: 'Interaction to Next Paint measures page responsiveness, how long it ' +
15
+ 'takes the page to visibly respond to user input. [Learn more](https://web.dev/inp/).',
16
+ };
17
+
18
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
19
+
20
+ /**
21
+ * @fileoverview This metric gives a high-percentile measure of responsiveness to input.
22
+ */
23
+ class ExperimentalInteractionToNextPaint extends Audit {
24
+ /**
25
+ * @return {LH.Audit.Meta}
26
+ */
27
+ static get meta() {
28
+ return {
29
+ id: 'experimental-interaction-to-next-paint',
30
+ title: str_(i18n.UIStrings.interactionToNextPaint),
31
+ description: str_(UIStrings.description),
32
+ scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
33
+ supportedModes: ['timespan'],
34
+ requiredArtifacts: ['traces'],
35
+ };
36
+ }
37
+
38
+ /**
39
+ * @return {LH.Audit.ScoreOptions}
40
+ */
41
+ static get defaultOptions() {
42
+ return {
43
+ // https://web.dev/inp/
44
+ // This is using the same threshold as field tools since only supported in
45
+ // unsimulated user flows for now.
46
+ // see https://www.desmos.com/calculator/4xtrhg51th
47
+ p10: 200,
48
+ median: 500,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {LH.Artifacts} artifacts
54
+ * @param {LH.Audit.Context} context
55
+ * @return {Promise<LH.Audit.Product>}
56
+ */
57
+ static async audit(artifacts, context) {
58
+ const {settings} = context;
59
+ // TODO: responsiveness isn't yet supported by lantern.
60
+ if (settings.throttlingMethod === 'simulate') {
61
+ return {score: null, notApplicable: true};
62
+ }
63
+
64
+ const trace = artifacts.traces[Audit.DEFAULT_PASS];
65
+ const metricData = {trace, settings};
66
+ const metricResult = await ComputedResponsivenes.request(metricData, context);
67
+
68
+ // TODO: include the no-interaction state in the report instead of using n/a.
69
+ if (metricResult === null) {
70
+ return {score: null, notApplicable: true};
71
+ }
72
+
73
+ return {
74
+ score: Audit.computeLogNormalScore({p10: context.options.p10, median: context.options.median},
75
+ metricResult.timing),
76
+ numericValue: metricResult.timing,
77
+ numericUnit: 'millisecond',
78
+ displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
79
+ };
80
+ }
81
+ }
82
+
83
+ module.exports = ExperimentalInteractionToNextPaint;
84
+ module.exports.UIStrings = UIStrings;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ /**
9
+ * @fileoverview Returns a high-percentle (usually 98th) measure of how long it
10
+ * takes the page to visibly respond to user input (or null, if there was no
11
+ * user input in the provided trace).
12
+ */
13
+
14
+ const makeComputedArtifact = require('../computed-artifact.js');
15
+ const ProcessedTrace = require('../processed-trace.js');
16
+
17
+ class Responsiveness {
18
+ /**
19
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
20
+ * @return {{timing: number}|null}
21
+ */
22
+ static getHighPercentileResponsiveness(processedTrace) {
23
+ const durations = processedTrace.frameTreeEvents
24
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/responsiveness_metrics.cc;l=146-150;drc=a1a2302f30b0a58f7669a41c80acdf1fa11958dd
25
+ .filter(e => e.name === 'Responsiveness.Renderer.UserInteraction')
26
+ .map(evt => evt.args.data?.maxDuration)
27
+ .filter(/** @return {duration is number} */duration => duration !== undefined)
28
+ .sort((a, b) => b - a);
29
+
30
+ // If there were no interactions with the page, the metric is N/A.
31
+ if (durations.length === 0) {
32
+ return null;
33
+ }
34
+
35
+ // INP is the "nearest-rank"/inverted_cdf 98th percentile, except Chrome only
36
+ // keeps the 10 worst events around, so it can never be more than the 10th from
37
+ // last array element. To keep things simpler, sort desc and pick from front.
38
+ // See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad
39
+ const index = Math.min(9, Math.floor(durations.length / 50));
40
+
41
+ return {
42
+ timing: durations[index],
43
+ };
44
+ }
45
+
46
+ /**
47
+ * @param {{trace: LH.Trace, settings: Immutable<LH.Config.Settings>}} data
48
+ * @param {LH.Artifacts.ComputedContext} context
49
+ * @return {Promise<LH.Artifacts.Metric|null>}
50
+ */
51
+ static async compute_(data, context) {
52
+ if (data.settings.throttlingMethod === 'simulate') {
53
+ throw new Error('Responsiveness currently unsupported by simulated throttling');
54
+ }
55
+
56
+ const processedTrace = await ProcessedTrace.request(data.trace, context);
57
+ return Responsiveness.getHighPercentileResponsiveness(processedTrace);
58
+ }
59
+ }
60
+
61
+ module.exports = makeComputedArtifact(Responsiveness, [
62
+ 'trace',
63
+ 'settings',
64
+ ]);
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const path = require('path');
9
- const {isEqual: isDeepEqual} = require('lodash');
9
+ const isDeepEqual = require('lodash/isEqual.js');
10
10
  const constants = require('./constants.js');
11
11
  const Budget = require('./budget.js');
12
12
  const ConfigPlugin = require('./config-plugin.js');
@@ -11,12 +11,14 @@ const {deepClone} = require('../../config/config-helpers.js');
11
11
  /** @type {LH.Config.AuditJson[]} */
12
12
  const frAudits = [
13
13
  'byte-efficiency/uses-responsive-images-snapshot',
14
+ 'metrics/experimental-interaction-to-next-paint',
14
15
  ];
15
16
 
16
17
  /** @type {Record<string, LH.Config.AuditRefJson[]>} */
17
18
  const frCategoryAuditRefExtensions = {
18
19
  'performance': [
19
20
  {id: 'uses-responsive-images-snapshot', weight: 0},
21
+ {id: 'experimental-interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'},
20
22
  ],
21
23
  };
22
24
 
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const log = require('lighthouse-logger');
9
- const {isEqual} = require('lodash');
9
+ const isDeepEqual = require('lodash/isEqual.js');
10
10
  const {
11
11
  getBrowserVersion,
12
12
  getBenchmarkIndex,
@@ -61,7 +61,7 @@ function deduplicateWarnings(warnings) {
61
61
  const unique = [];
62
62
 
63
63
  for (const warning of warnings) {
64
- if (unique.some(existing => isEqual(warning, existing))) continue;
64
+ if (unique.some(existing => isDeepEqual(warning, existing))) continue;
65
65
  unique.push(warning);
66
66
  }
67
67
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const {isEqual} = require('lodash');
8
+ const isDeepEqual = require('lodash/isEqual.js');
9
9
 
10
10
  /**
11
11
  * @fileoverview This class is designed to allow maps with arbitrary equality functions.
@@ -74,7 +74,7 @@ class ArbitraryEqualityMap {
74
74
  * @return {boolean}
75
75
  */
76
76
  static deepEquals(objA, objB) {
77
- return isEqual(objA, objB);
77
+ return isDeepEqual(objA, objB);
78
78
  }
79
79
  }
80
80
 
@@ -105,6 +105,8 @@ const UIStrings = {
105
105
  largestContentfulPaintMetric: 'Largest Contentful Paint',
106
106
  /** The name of the metric "Cumulative Layout Shift" that indicates how much the page changes its layout while it loads. If big segments of the page shift their location during load, the Cumulative Layout Shift will be higher. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
107
107
  cumulativeLayoutShiftMetric: 'Cumulative Layout Shift',
108
+ /** The name of the "Interaction to Next Paint" metric that measures the time between a user interaction and when the browser displays a response on screen. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
109
+ interactionToNextPaint: 'Interaction to Next Paint',
108
110
  /** Table item value for the severity of a small, or low impact vulnerability. Part of a ranking scale in the form: low, medium, high. */
109
111
  itemSeverityLow: 'Low',
110
112
  /** Table item value for the severity of a vulnerability. Part of a ranking scale in the form: low, medium, high. */
@@ -44,6 +44,8 @@ const traceEventsToAlwaysKeep = new Set([
44
44
  'EventDispatch',
45
45
  'LayoutShift',
46
46
  'FrameCommittedInBrowser',
47
+ 'EventTiming',
48
+ 'Responsiveness.Renderer.UserInteraction',
47
49
  // Not currently used by Lighthouse but might be used in the future for cross-frame LCP
48
50
  'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM',
49
51
  'NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM',
@@ -5,7 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const {isEqual: isDeepEqual} = require('lodash');
8
+ const isDeepEqual = require('lodash/isEqual.js');
9
9
  const Driver = require('./gather/driver.js');
10
10
  const GatherRunner = require('./gather/gather-runner.js');
11
11
  const ReportScoring = require('./scoring.js');
@@ -433,11 +433,9 @@ class Util {
433
433
  break;
434
434
  case 'devtools': {
435
435
  const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
436
- // TODO: better api in i18n formatter such that this isn't needed.
437
- const cpuGranularity = Number.isInteger(cpuSlowdownMultiplier) ? 1 : 0.1;
438
436
  // eslint-disable-next-line max-len
439
- cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier, cpuGranularity)}x slowdown (DevTools)`;
440
- networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs, 1)} HTTP RTT, ` +
437
+ cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
438
+ networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs)} HTTP RTT, ` +
441
439
  `${Util.i18n.formatKbps(throttling.downloadThroughputKbps)} down, ` +
442
440
  `${Util.i18n.formatKbps(throttling.uploadThroughputKbps)} up (DevTools)`;
443
441
 
@@ -451,10 +449,8 @@ class Util {
451
449
  }
452
450
  case 'simulate': {
453
451
  const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
454
- // TODO: better api in i18n formatter such that this isn't needed.
455
- const cpuGranularity = Number.isInteger(cpuSlowdownMultiplier) ? 1 : 0.1;
456
452
  // eslint-disable-next-line max-len
457
- cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier, cpuGranularity)}x slowdown (Simulated)`;
453
+ cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
458
454
  networkThrottling = `${Util.i18n.formatMilliseconds(rttMs)} TCP RTT, ` +
459
455
  `${Util.i18n.formatKbps(throughputKbps)} throughput (Simulated)`;
460
456
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.5.0-dev.20220426",
3
+ "version": "9.5.0-dev.20220429",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -78,9 +78,8 @@ export class DetailsRenderer {
78
78
  * @return {Element}
79
79
  */
80
80
  _renderBytes(details) {
81
- // TODO: handle displayUnit once we have something other than 'kb'
82
- // Note that 'kb' is historical and actually represents KiB.
83
- const value = Util.i18n.formatBytesToKiB(details.value, details.granularity);
81
+ // TODO: handle displayUnit once we have something other than 'KiB'
82
+ const value = Util.i18n.formatBytesToKiB(details.value, details.granularity || 0.1);
84
83
  const textEl = this._renderText(value);
85
84
  textEl.title = Util.i18n.formatBytes(details.value);
86
85
  return textEl;
@@ -91,9 +90,11 @@ export class DetailsRenderer {
91
90
  * @return {Element}
92
91
  */
93
92
  _renderMilliseconds(details) {
94
- let value = Util.i18n.formatMilliseconds(details.value, details.granularity);
93
+ let value;
95
94
  if (details.displayUnit === 'duration') {
96
95
  value = Util.i18n.formatDuration(details.value);
96
+ } else {
97
+ value = Util.i18n.formatMilliseconds(details.value, details.granularity || 10);
97
98
  }
98
99
 
99
100
  return this._renderText(value);
@@ -172,7 +173,7 @@ export class DetailsRenderer {
172
173
  * @return {Element}
173
174
  */
174
175
  _renderNumeric(details) {
175
- const value = Util.i18n.formatNumber(details.value, details.granularity);
176
+ const value = Util.i18n.formatNumber(details.value, details.granularity || 0.1);
176
177
  const element = this._dom.createElement('div', 'lh-numeric');
177
178
  element.textContent = value;
178
179
  return element;
@@ -31,26 +31,31 @@ export class I18n {
31
31
 
32
32
  /**
33
33
  * @param {number} number
34
- * @param {number} granularity
35
- * @param {Intl.NumberFormatOptions} opts
34
+ * @param {number|undefined} granularity
35
+ * @param {Intl.NumberFormatOptions=} opts
36
36
  * @return {string}
37
37
  */
38
38
  _formatNumberWithGranularity(number, granularity, opts = {}) {
39
- opts = {...opts};
40
- const log10 = -Math.log10(granularity);
41
- if (!Number.isFinite(log10) || (granularity > 1 && !Number.isInteger(log10))) {
42
- console.warn(`granularity of ${granularity} is invalid, defaulting to value of 1`);
43
- granularity = 1;
44
- }
39
+ if (granularity !== undefined) {
40
+ const log10 = -Math.log10(granularity);
41
+ if (!Number.isInteger(log10)) {
42
+ console.warn(`granularity of ${granularity} is invalid. Using 1 instead`);
43
+ granularity = 1;
44
+ }
45
45
 
46
- if (granularity < 1) {
47
- opts.minimumFractionDigits = opts.maximumFractionDigits = Math.ceil(log10);
48
- }
46
+ if (granularity < 1) {
47
+ opts = {...opts};
48
+ opts.minimumFractionDigits = opts.maximumFractionDigits = Math.ceil(log10);
49
+ }
49
50
 
50
- number = Math.round(number / granularity) * granularity;
51
+ number = Math.round(number / granularity) * granularity;
51
52
 
52
- // Avoid displaying a negative value that rounds to zero as "0".
53
- if (Object.is(number, -0)) number = 0;
53
+ // Avoid displaying a negative value that rounds to zero as "0".
54
+ if (Object.is(number, -0)) number = 0;
55
+ } else if (Math.abs(number) < 0.0005) {
56
+ // Also avoids "-0".
57
+ number = 0;
58
+ }
54
59
 
55
60
  return new Intl.NumberFormat(this._locale, opts).format(number).replace(' ', NBSP2);
56
61
  }
@@ -58,10 +63,12 @@ export class I18n {
58
63
  /**
59
64
  * Format number.
60
65
  * @param {number} number
61
- * @param {number=} granularity Number of decimal places to include. Defaults to 0.1.
66
+ * @param {number=} granularity Controls how coarse the displayed value is.
67
+ * If undefined, the number will be displayed as described
68
+ * by the Intl defaults: tinyurl.com/7s67w5x7
62
69
  * @return {string}
63
70
  */
64
- formatNumber(number, granularity = 0.1) {
71
+ formatNumber(number, granularity) {
65
72
  return this._formatNumberWithGranularity(number, granularity);
66
73
  }
67
74
 
@@ -87,25 +94,28 @@ export class I18n {
87
94
 
88
95
  /**
89
96
  * @param {number} size
90
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
97
+ * @param {number=} granularity Controls how coarse the displayed value is.
98
+ * If undefined, the number will be displayed in full.
91
99
  * @return {string}
92
100
  */
93
- formatBytesToKiB(size, granularity = 0.1) {
101
+ formatBytesToKiB(size, granularity = undefined) {
94
102
  return this._formatNumberWithGranularity(size / KiB, granularity) + `${NBSP2}KiB`;
95
103
  }
96
104
 
97
105
  /**
98
106
  * @param {number} size
99
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
107
+ * @param {number=} granularity Controls how coarse the displayed value is.
108
+ * If undefined, the number will be displayed in full.
100
109
  * @return {string}
101
110
  */
102
- formatBytesToMiB(size, granularity = 0.1) {
111
+ formatBytesToMiB(size, granularity = undefined) {
103
112
  return this._formatNumberWithGranularity(size / MiB, granularity) + `${NBSP2}MiB`;
104
113
  }
105
114
 
106
115
  /**
107
116
  * @param {number} size
108
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
117
+ * @param {number=} granularity Controls how coarse the displayed value is.
118
+ * If undefined, the number will be displayed in full.
109
119
  * @return {string}
110
120
  */
111
121
  formatBytes(size, granularity = 1) {
@@ -118,10 +128,11 @@ export class I18n {
118
128
 
119
129
  /**
120
130
  * @param {number} size
121
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
131
+ * @param {number=} granularity Controls how coarse the displayed value is.
132
+ * If undefined, the number will be displayed in full.
122
133
  * @return {string}
123
134
  */
124
- formatBytesWithBestUnit(size, granularity = 0.1) {
135
+ formatBytesWithBestUnit(size, granularity = undefined) {
125
136
  if (size >= MiB) return this.formatBytesToMiB(size, granularity);
126
137
  if (size >= KiB) return this.formatBytesToKiB(size, granularity);
127
138
  return this._formatNumberWithGranularity(size, granularity, {
@@ -133,10 +144,11 @@ export class I18n {
133
144
 
134
145
  /**
135
146
  * @param {number} size
136
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
147
+ * @param {number=} granularity Controls how coarse the displayed value is.
148
+ * If undefined, the number will be displayed in full.
137
149
  * @return {string}
138
150
  */
139
- formatKbps(size, granularity = 1) {
151
+ formatKbps(size, granularity = undefined) {
140
152
  return this._formatNumberWithGranularity(size, granularity, {
141
153
  style: 'unit',
142
154
  unit: 'kilobit-per-second',
@@ -146,10 +158,11 @@ export class I18n {
146
158
 
147
159
  /**
148
160
  * @param {number} ms
149
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 10
161
+ * @param {number=} granularity Controls how coarse the displayed value is.
162
+ * If undefined, the number will be displayed in full.
150
163
  * @return {string}
151
164
  */
152
- formatMilliseconds(ms, granularity = 10) {
165
+ formatMilliseconds(ms, granularity = undefined) {
153
166
  return this._formatNumberWithGranularity(ms, granularity, {
154
167
  style: 'unit',
155
168
  unit: 'millisecond',
@@ -159,10 +172,11 @@ export class I18n {
159
172
 
160
173
  /**
161
174
  * @param {number} ms
162
- * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
175
+ * @param {number=} granularity Controls how coarse the displayed value is.
176
+ * If undefined, the number will be displayed in full.
163
177
  * @return {string}
164
178
  */
165
- formatSeconds(ms, granularity = 0.1) {
179
+ formatSeconds(ms, granularity = undefined) {
166
180
  return this._formatNumberWithGranularity(ms / 1000, granularity, {
167
181
  style: 'unit',
168
182
  unit: 'second',
@@ -429,11 +429,9 @@ class Util {
429
429
  break;
430
430
  case 'devtools': {
431
431
  const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
432
- // TODO: better api in i18n formatter such that this isn't needed.
433
- const cpuGranularity = Number.isInteger(cpuSlowdownMultiplier) ? 1 : 0.1;
434
432
  // eslint-disable-next-line max-len
435
- cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier, cpuGranularity)}x slowdown (DevTools)`;
436
- networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs, 1)} HTTP RTT, ` +
433
+ cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
434
+ networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs)} HTTP RTT, ` +
437
435
  `${Util.i18n.formatKbps(throttling.downloadThroughputKbps)} down, ` +
438
436
  `${Util.i18n.formatKbps(throttling.uploadThroughputKbps)} up (DevTools)`;
439
437
 
@@ -447,10 +445,8 @@ class Util {
447
445
  }
448
446
  case 'simulate': {
449
447
  const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
450
- // TODO: better api in i18n formatter such that this isn't needed.
451
- const cpuGranularity = Number.isInteger(cpuSlowdownMultiplier) ? 1 : 0.1;
452
448
  // eslint-disable-next-line max-len
453
- cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier, cpuGranularity)}x slowdown (Simulated)`;
449
+ cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
454
450
  networkThrottling = `${Util.i18n.formatMilliseconds(rttMs)} TCP RTT, ` +
455
451
  `${Util.i18n.formatKbps(throughputKbps)} throughput (Simulated)`;
456
452