lighthouse 9.5.0-dev.20220426 → 9.5.0-dev.20220427
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/dist/report/bundle.esm.js +52 -41
- package/dist/report/flow.js +3 -3
- package/dist/report/standalone.js +8 -8
- package/lighthouse-cli/test/smokehouse/frontends/lib.js +1 -3
- package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +1 -3
- package/lighthouse-cli/test/smokehouse/report-assert.js +1 -3
- package/lighthouse-core/audits/deprecations.js +14 -32
- package/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js +84 -0
- package/lighthouse-core/computed/metrics/responsiveness.js +64 -0
- package/lighthouse-core/config/config-helpers.js +1 -1
- package/lighthouse-core/fraggle-rock/config/default-config.js +2 -0
- package/lighthouse-core/fraggle-rock/gather/base-artifacts.js +2 -2
- package/lighthouse-core/lib/arbitrary-equality-map.js +2 -2
- package/lighthouse-core/lib/i18n/i18n.js +2 -0
- package/lighthouse-core/lib/minify-trace.js +2 -0
- package/lighthouse-core/runner.js +1 -1
- package/lighthouse-core/util-commonjs.js +3 -7
- package/package.json +1 -1
- package/report/renderer/details-renderer.js +6 -5
- package/report/renderer/i18n.js +43 -29
- package/report/renderer/util.js +3 -7
- package/report/test/renderer/details-renderer-test.js +49 -0
- package/report/test/renderer/i18n-test.js +49 -20
- package/shared/localization/locales/en-US.json +6 -0
- package/shared/localization/locales/en-XL.json +6 -0
- package/shared/localization/swap-locale.js +2 -1
- package/types/artifacts.d.ts +3 -0
|
@@ -12,13 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
/* eslint-disable no-console */
|
|
14
14
|
|
|
15
|
-
import
|
|
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
|
|
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
|
|
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: ['
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
440
|
-
networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs
|
|
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
|
|
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
|
@@ -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 '
|
|
82
|
-
|
|
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
|
|
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;
|
package/report/renderer/i18n.js
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if (granularity < 1) {
|
|
47
|
+
opts = {...opts};
|
|
48
|
+
opts.minimumFractionDigits = opts.maximumFractionDigits = Math.ceil(log10);
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
number = Math.round(number / granularity) * granularity;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
179
|
+
formatSeconds(ms, granularity = undefined) {
|
|
166
180
|
return this._formatNumberWithGranularity(ms / 1000, granularity, {
|
|
167
181
|
style: 'unit',
|
|
168
182
|
unit: 'second',
|
package/report/renderer/util.js
CHANGED
|
@@ -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
|
|
436
|
-
networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs
|
|
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
|
|
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
|
|