diff --git a/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js b/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js index 2ef0c4ae2d42..1e23e3717ba1 100644 --- a/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js +++ b/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js @@ -63,19 +63,21 @@ class ExperimentalInteractionToNextPaint extends Audit { const trace = artifacts.traces[Audit.DEFAULT_PASS]; const metricData = {trace, settings}; - const metricResult = await ComputedResponsivenes.request(metricData, context); + const responsivenessEvent = await ComputedResponsivenes.request(metricData, context); // TODO: include the no-interaction state in the report instead of using n/a. - if (metricResult === null) { + if (responsivenessEvent === null) { return {score: null, notApplicable: true}; } + const timing = responsivenessEvent.args.data.maxDuration; + return { score: Audit.computeLogNormalScore({p10: context.options.p10, median: context.options.median}, - metricResult.timing), - numericValue: metricResult.timing, + timing), + numericValue: timing, numericUnit: 'millisecond', - displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}), + displayValue: str_(i18n.UIStrings.ms, {timeInMs: timing}), }; } } diff --git a/lighthouse-core/computed/metrics/responsiveness.js b/lighthouse-core/computed/metrics/responsiveness.js index 56bc33f03ce8..97a283c7d06e 100644 --- a/lighthouse-core/computed/metrics/responsiveness.js +++ b/lighthouse-core/computed/metrics/responsiveness.js @@ -11,24 +11,25 @@ * user input in the provided trace). */ +/** @typedef {LH.Trace.CompleteEvent & {name: 'Responsiveness.Renderer.UserInteraction', args: {frame: string, data: {interactionType: 'drag'|'keyboard'|'tapOrClick', maxDuration: number}}}} ResponsivenessEvent */ + const makeComputedArtifact = require('../computed-artifact.js'); const ProcessedTrace = require('../processed-trace.js'); class Responsiveness { /** * @param {LH.Artifacts.ProcessedTrace} processedTrace - * @return {{timing: number}|null} + * @return {ResponsivenessEvent|null} */ static getHighPercentileResponsiveness(processedTrace) { - const durations = processedTrace.frameTreeEvents + const responsivenessEvents = 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); + .filter(/** @return {e is ResponsivenessEvent} */ e => { + return e.name === 'Responsiveness.Renderer.UserInteraction'; + }).sort((a, b) => b.args.data.maxDuration - a.args.data.maxDuration); // If there were no interactions with the page, the metric is N/A. - if (durations.length === 0) { + if (responsivenessEvents.length === 0) { return null; } @@ -36,17 +37,15 @@ class Responsiveness { // 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)); + const index = Math.min(9, Math.floor(responsivenessEvents.length / 50)); - return { - timing: durations[index], - }; + return responsivenessEvents[index]; } /** * @param {{trace: LH.Trace, settings: Immutable}} data * @param {LH.Artifacts.ComputedContext} context - * @return {Promise} + * @return {Promise} */ static async compute_(data, context) { if (data.settings.throttlingMethod === 'simulate') { @@ -54,7 +53,9 @@ class Responsiveness { } const processedTrace = await ProcessedTrace.request(data.trace, context); - return Responsiveness.getHighPercentileResponsiveness(processedTrace); + const event = Responsiveness.getHighPercentileResponsiveness(processedTrace); + + return JSON.parse(JSON.stringify(event)); } } diff --git a/lighthouse-core/test/computed/metrics/responsiveness-test.js b/lighthouse-core/test/computed/metrics/responsiveness-test.js index 8dc9d9064f2e..94a601dcebce 100644 --- a/lighthouse-core/test/computed/metrics/responsiveness-test.js +++ b/lighthouse-core/test/computed/metrics/responsiveness-test.js @@ -74,8 +74,8 @@ describe('Metric: Responsiveness', () => { settings: {throttlingMethod: 'provided'}, }; const context = {computedCache: new Map()}; - const result = await Responsiveness.request(metricInputData, context); - expect(result).toEqual(null); + const event = await Responsiveness.request(metricInputData, context); + expect(event).toEqual(null); }); it('should select the 98th percentile interaction', async () => { @@ -100,8 +100,8 @@ describe('Metric: Responsiveness', () => { } const context = {computedCache: new Map()}; - const result = await Responsiveness.request(metricInputData, context); - assert.equal(result.timing, expectedTiming, `error at ${eventCount} events`); + const event = await Responsiveness.request(metricInputData, context); + assert.equal(event.args.data.maxDuration, expectedTiming, `error at ${eventCount} events`); } }); @@ -129,8 +129,8 @@ describe('Metric: Responsiveness', () => { settings: {throttlingMethod: 'provided'}, }; const context = {computedCache: new Map()}; - const result = await Responsiveness.request(metricInputData, context); - expect(result).toEqual({timing: 49}); + const event = await Responsiveness.request(metricInputData, context); + expect(event.args.data).toMatchObject({maxDuration: 49}); }); it('should throw on attempting with a simulated timespan', async () => { @@ -148,8 +148,8 @@ describe('Metric: Responsiveness', () => { settings: {throttlingMethod: 'provided'}, }; const context = {computedCache: new Map()}; - const result = await Responsiveness.request(metricInputData, context); - expect(result).toEqual({timing: 392}); + const event = await Responsiveness.request(metricInputData, context); + expect(event.args.data).toMatchObject({maxDuration: 392}); }); it('evaluates from a real trace with no interaction events', async () => { @@ -158,7 +158,7 @@ describe('Metric: Responsiveness', () => { settings: {throttlingMethod: 'provided'}, }; const context = {computedCache: new Map()}; - const result = await Responsiveness.request(metricInputData, context); - expect(result).toEqual(null); + const event = await Responsiveness.request(metricInputData, context); + expect(event).toEqual(null); }); }); diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 56edd41b35fc..9fe7bb05c82d 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -1020,6 +1020,23 @@ export interface TraceEvent { }; } +declare module Trace { + /** + * Base event of a `ph: 'X'` 'complete' event. Extend with `name` and `args` as + * needed. + */ + interface CompleteEvent { + ph: 'X'; + cat: string; + pid: number; + tid: number; + dur: number; + ts: number; + tdur: number; + tts: number; + } +} + /** * A record of DevTools Debugging Protocol events. */