Skip to content

Commit

Permalink
new_audit: add responsiveness metric for timespans (#13917)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendankenny authored Apr 27, 2022
1 parent 1fd9f3e commit f2db965
Show file tree
Hide file tree
Showing 15 changed files with 953 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';

const Audit = require('../audit.js');
const ComputedResponsivenes = require('../../computed/metrics/responsiveness.js');
const i18n = require('../../lib/i18n/i18n.js');

const UIStrings = {
/** 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. */
description: 'Interaction to Next Paint measures page responsiveness, how long it ' +
'takes the page to visibly respond to user input. [Learn more](https://web.dev/inp/).',
};

const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);

/**
* @fileoverview This metric gives a high-percentile measure of responsiveness to input.
*/
class ExperimentalInteractionToNextPaint extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'experimental-interaction-to-next-paint',
title: str_(i18n.UIStrings.interactionToNextPaint),
description: str_(UIStrings.description),
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
supportedModes: ['timespan'],
requiredArtifacts: ['traces'],
};
}

/**
* @return {LH.Audit.ScoreOptions}
*/
static get defaultOptions() {
return {
// https://web.dev/inp/
// This is using the same threshold as field tools since only supported in
// unsimulated user flows for now.
// see https://www.desmos.com/calculator/4xtrhg51th
p10: 200,
median: 500,
};
}

/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts, context) {
const {settings} = context;
// TODO: responsiveness isn't yet supported by lantern.
if (settings.throttlingMethod === 'simulate') {
return {score: null, notApplicable: true};
}

const trace = artifacts.traces[Audit.DEFAULT_PASS];
const metricData = {trace, settings};
const metricResult = await ComputedResponsivenes.request(metricData, context);

// TODO: include the no-interaction state in the report instead of using n/a.
if (metricResult === null) {
return {score: null, notApplicable: true};
}

return {
score: Audit.computeLogNormalScore({p10: context.options.p10, median: context.options.median},
metricResult.timing),
numericValue: metricResult.timing,
numericUnit: 'millisecond',
displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
};
}
}

module.exports = ExperimentalInteractionToNextPaint;
module.exports.UIStrings = UIStrings;
64 changes: 64 additions & 0 deletions lighthouse-core/computed/metrics/responsiveness.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';

/**
* @fileoverview Returns a high-percentle (usually 98th) measure of how long it
* takes the page to visibly respond to user input (or null, if there was no
* user input in the provided trace).
*/

const makeComputedArtifact = require('../computed-artifact.js');
const ProcessedTrace = require('../processed-trace.js');

class Responsiveness {
/**
* @param {LH.Artifacts.ProcessedTrace} processedTrace
* @return {{timing: number}|null}
*/
static getHighPercentileResponsiveness(processedTrace) {
const durations = processedTrace.frameTreeEvents
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/responsiveness_metrics.cc;l=146-150;drc=a1a2302f30b0a58f7669a41c80acdf1fa11958dd
.filter(e => e.name === 'Responsiveness.Renderer.UserInteraction')
.map(evt => evt.args.data?.maxDuration)
.filter(/** @return {duration is number} */duration => duration !== undefined)
.sort((a, b) => b - a);

// If there were no interactions with the page, the metric is N/A.
if (durations.length === 0) {
return null;
}

// INP is the "nearest-rank"/inverted_cdf 98th percentile, except Chrome only
// keeps the 10 worst events around, so it can never be more than the 10th from
// last array element. To keep things simpler, sort desc and pick from front.
// See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad
const index = Math.min(9, Math.floor(durations.length / 50));

return {
timing: durations[index],
};
}

/**
* @param {{trace: LH.Trace, settings: Immutable<LH.Config.Settings>}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.Metric|null>}
*/
static async compute_(data, context) {
if (data.settings.throttlingMethod === 'simulate') {
throw new Error('Responsiveness currently unsupported by simulated throttling');
}

const processedTrace = await ProcessedTrace.request(data.trace, context);
return Responsiveness.getHighPercentileResponsiveness(processedTrace);
}
}

module.exports = makeComputedArtifact(Responsiveness, [
'trace',
'settings',
]);
2 changes: 2 additions & 0 deletions lighthouse-core/fraggle-rock/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ const {deepClone} = require('../../config/config-helpers.js');
/** @type {LH.Config.AuditJson[]} */
const frAudits = [
'byte-efficiency/uses-responsive-images-snapshot',
'metrics/experimental-interaction-to-next-paint',
];

/** @type {Record<string, LH.Config.AuditRefJson[]>} */
const frCategoryAuditRefExtensions = {
'performance': [
{id: 'uses-responsive-images-snapshot', weight: 0},
{id: 'experimental-interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'},
],
};

Expand Down
2 changes: 2 additions & 0 deletions lighthouse-core/lib/i18n/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ const UIStrings = {
largestContentfulPaintMetric: 'Largest Contentful Paint',
/** 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. */
cumulativeLayoutShiftMetric: 'Cumulative Layout Shift',
/** 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. */
interactionToNextPaint: 'Interaction to Next Paint',
/** Table item value for the severity of a small, or low impact vulnerability. Part of a ranking scale in the form: low, medium, high. */
itemSeverityLow: 'Low',
/** Table item value for the severity of a vulnerability. Part of a ranking scale in the form: low, medium, high. */
Expand Down
2 changes: 2 additions & 0 deletions lighthouse-core/lib/minify-trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const traceEventsToAlwaysKeep = new Set([
'EventDispatch',
'LayoutShift',
'FrameCommittedInBrowser',
'EventTiming',
'Responsiveness.Renderer.UserInteraction',
// Not currently used by Lighthouse but might be used in the future for cross-frame LCP
'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM',
'NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';

const ExperimentalInteractionToNextPaint =
require('../../../audits/metrics/experimental-interaction-to-next-paint.js');
const interactionTrace = require('../../fixtures/traces/timespan-responsiveness-m103.trace.json');
const noInteractionTrace = require('../../fixtures/traces/jumpy-cls-m90.json');

/* eslint-env jest */

describe('Interaction to Next Paint', () => {
function getTestData() {
const artifacts = {
traces: {
[ExperimentalInteractionToNextPaint.DEFAULT_PASS]: interactionTrace,
},
};

const context = {
settings: {throttlingMethod: 'devtools'},
computedCache: new Map(),
options: ExperimentalInteractionToNextPaint.defaultOptions,
};

return {artifacts, context};
}

it('evaluates INP correctly', async () => {
const {artifacts, context} = getTestData();
const result = await ExperimentalInteractionToNextPaint.audit(artifacts, context);
expect(result).toEqual({
score: 0.63,
numericValue: 392,
numericUnit: 'millisecond',
displayValue: expect.toBeDisplayString('390 ms'),
});
});

it('is not applicable if using simulated throttling', async () => {
const {artifacts, context} = getTestData();
context.settings.throttlingMethod = 'simulate';
const result = await ExperimentalInteractionToNextPaint.audit(artifacts, context);
expect(result).toMatchObject({
score: null,
notApplicable: true,
});
});

it('is not applicable if no interactions occurred in trace', async () => {
const {artifacts, context} = getTestData();
artifacts.traces[ExperimentalInteractionToNextPaint.DEFAULT_PASS] = noInteractionTrace;
const result = await ExperimentalInteractionToNextPaint.audit(artifacts, context);
expect(result).toMatchObject({
score: null,
notApplicable: true,
});
});
});
Loading

0 comments on commit f2db965

Please sign in to comment.