From b38fd2793673656372dc0310ce32cca37521a933 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Mon, 24 May 2021 18:38:38 -0500 Subject: [PATCH] core: update to windowed cumulative-layout-shift --- .../test-definitions/perf/expectations.js | 11 +- lighthouse-core/audits/metrics.js | 12 +- .../audits/metrics/cumulative-layout-shift.js | 10 +- .../computed/layout-shift-variants.js | 200 -------- .../cumulative-layout-shift-all-frames.js | 53 --- .../metrics/cumulative-layout-shift.js | 115 ++++- .../computed/metrics/timing-summary.js | 37 +- .../audits/__snapshots__/metrics-test.js.snap | 52 +-- lighthouse-core/test/audits/metrics-test.js | 70 ++- .../metrics/cumulative-layout-shift-test.js | 40 ++ .../computed/layout-shift-variants-test.js | 399 ---------------- ...cumulative-layout-shift-all-frames-test.js | 132 ------ .../metrics/cumulative-layout-shift-test.js | 437 +++++++++++++++--- .../computed/metrics/timing-summary-test.js | 16 +- .../results/artifacts/defaultPass.trace.json | 2 +- lighthouse-core/test/results/sample_v2.json | 29 +- lighthouse-treemap/app/debug.json | 28 +- types/artifacts.d.ts | 12 +- 18 files changed, 617 insertions(+), 1038 deletions(-) delete mode 100644 lighthouse-core/computed/layout-shift-variants.js delete mode 100644 lighthouse-core/computed/metrics/cumulative-layout-shift-all-frames.js create mode 100644 lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js delete mode 100644 lighthouse-core/test/computed/layout-shift-variants-test.js delete mode 100644 lighthouse-core/test/computed/metrics/cumulative-layout-shift-all-frames-test.js diff --git a/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js index 59d0d0834922..b1198ad635fe 100644 --- a/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js +++ b/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js @@ -328,14 +328,9 @@ module.exports = [ firstContentfulPaintAllFrames: '<5000', largestContentfulPaint: '>5000', largestContentfulPaintAllFrames: '<5000', - cumulativeLayoutShift: '0.001 +/- 0.0005', - cumulativeLayoutShiftAllFrames: '0.197 +/- 0.001', - layoutShiftAvgSessionGap5s: '>0', - layoutShiftMaxSessionGap1s: '>0', - layoutShiftMaxSessionGap1sLimit5s: '>0', - layoutShiftMaxSliding1s: '>0', - layoutShiftMaxSliding300ms: '>0', - layoutShiftMaxSessionGap1sLimit5sAllFrames: '0.197 +/- 0.001', + cumulativeLayoutShift: '0.197 +/- 0.001', + cumulativeLayoutShiftMainFrame: '0.001 +/- 0.0005', + oldCumulativeLayoutShift: '0.001 +/- 0.0005', }, { lcpInvalidated: false, diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js index 6a424469d716..aa3f5976e0dc 100644 --- a/lighthouse-core/audits/metrics.js +++ b/lighthouse-core/audits/metrics.js @@ -11,15 +11,11 @@ const ComputedTimingSummary = require('../computed/metrics/timing-summary.js'); /** @type {Set} */ const DECIMAL_METRIC_KEYS = new Set([ 'cumulativeLayoutShift', - 'cumulativeLayoutShiftAllFrames', + 'cumulativeLayoutShiftMainFrame', + 'oldCumulativeLayoutShift', 'observedCumulativeLayoutShift', - 'observedCumulativeLayoutShiftAllFrames', - 'layoutShiftAvgSessionGap5s', - 'layoutShiftMaxSessionGap1s', - 'layoutShiftMaxSessionGap1sLimit5s', - 'layoutShiftMaxSliding1s', - 'layoutShiftMaxSliding300ms', - 'layoutShiftMaxSessionGap1sLimit5sAllFrames', + 'observedCumulativeLayoutShiftMainFrame', + 'observedOldCumulativeLayoutShift', ]); class Metrics extends Audit { diff --git a/lighthouse-core/audits/metrics/cumulative-layout-shift.js b/lighthouse-core/audits/metrics/cumulative-layout-shift.js index cbea74685003..eb97cc531066 100644 --- a/lighthouse-core/audits/metrics/cumulative-layout-shift.js +++ b/lighthouse-core/audits/metrics/cumulative-layout-shift.js @@ -54,22 +54,22 @@ class CumulativeLayoutShift extends Audit { */ static async audit(artifacts, context) { const trace = artifacts.traces[Audit.DEFAULT_PASS]; - const metricResult = await ComputedCLS.request(trace, context); + const {cumulativeLayoutShift, ...rest} = await ComputedCLS.request(trace, context); /** @type {LH.Audit.Details.DebugData} */ const details = { type: 'debugdata', - items: [metricResult.debugInfo], + items: [rest], }; return { score: Audit.computeLogNormalScore( {p10: context.options.p10, median: context.options.median}, - metricResult.value + cumulativeLayoutShift ), - numericValue: metricResult.value, + numericValue: cumulativeLayoutShift, numericUnit: 'unitless', - displayValue: metricResult.value.toLocaleString(context.settings.locale), + displayValue: cumulativeLayoutShift.toLocaleString(context.settings.locale), details, }; } diff --git a/lighthouse-core/computed/layout-shift-variants.js b/lighthouse-core/computed/layout-shift-variants.js deleted file mode 100644 index 4f27fd6ae19d..000000000000 --- a/lighthouse-core/computed/layout-shift-variants.js +++ /dev/null @@ -1,200 +0,0 @@ -/** - * @license Copyright 2021 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 makeComputedArtifact = require('./computed-artifact.js'); -const TraceOfTab = require('./trace-of-tab.js'); - -/** @typedef {{ts: number, score: number, isMainFrame: boolean, weightedScoreDelta?: number}} LayoutShiftEvent */ - -/** - * @fileoverview An implementation of the Layout Shift variants being considered - * as successors to CLS. This file will be removed when a variant is chosen and - * promoted to a Core Web Vital. - * @see https://web.dev/better-layout-shift-metric/ - * @see https://github.com/mmocny/web-vitals/wiki/Snippets-for-LSN-using-PerformanceObserver - */ - -class LayoutShiftVariants { - /** - * Calculates the average LayoutShift score across clusters of LayoutShift - * events, where a new cluster is created when there's a gap of more than 5s - * since the last LayoutShift event. - * @param {Array} layoutShiftEvents - * @return {number} - */ - static avgSessionGap5s(layoutShiftEvents) { - if (layoutShiftEvents.length === 0) return 0; - - let score = 0; - let clusterCount = 0; - let prevTs = Number.NEGATIVE_INFINITY; - - for (const event of layoutShiftEvents) { - if (event.ts - prevTs > 5_000_000) clusterCount++; - prevTs = event.ts; - score += event.score; - } - - return score / clusterCount; - } - - /** - * Calculates cumulative layout shifts per cluster (session) of LayoutShift - * events -- where a new cluster is created when there's a gap of more than - * `gapMs` ms since the last LayoutShift event or the cluster is greater than - * `limitMs` ms long -- and returns the max LayoutShift score found. - * `limitMs` is optional if no limit is needed. - * @param {Array} layoutShiftEvents - * @param {number} gapMs - * @param {number} [limitMs] - */ - static maxSession(layoutShiftEvents, gapMs, limitMs = Number.POSITIVE_INFINITY) { - let maxScore = 0; - let currentClusterScore = 0; - let firstTs = Number.NEGATIVE_INFINITY; - let prevTs = Number.NEGATIVE_INFINITY; - - for (const event of layoutShiftEvents) { - if (event.ts - firstTs > limitMs * 1000 || event.ts - prevTs > gapMs * 1000) { - firstTs = event.ts; - currentClusterScore = 0; - } - prevTs = event.ts; - currentClusterScore += event.score; - maxScore = Math.max(maxScore, currentClusterScore); - } - - return maxScore; - } - - /** - * Calculates cumulative layout shifts per cluster (session) of LayoutShift - * events -- where a new cluster is created when there's a gap of more than - * 1000ms since the last LayoutShift event or the cluster is greater than - * 5000ms long -- and returns the max LayoutShift score found. - * @param {Array} layoutShiftEvents - * @return {number} - */ - static newCumulativeLayoutShift(layoutShiftEvents) { - const gapMicroseconds = 1_000_000; - const limitMicroseconds = 5_000_000; - let maxScore = 0; - let currentClusterScore = 0; - let firstTs = Number.NEGATIVE_INFINITY; - let prevTs = Number.NEGATIVE_INFINITY; - - for (const event of layoutShiftEvents) { - if (event.weightedScoreDelta === undefined) { - // TODO(bckenny): replace with an LHError when moving to AF by default. - return -1; - } - - if (event.ts - firstTs > limitMicroseconds || event.ts - prevTs > gapMicroseconds) { - firstTs = event.ts; - currentClusterScore = 0; - } - prevTs = event.ts; - currentClusterScore += event.weightedScoreDelta; - maxScore = Math.max(maxScore, currentClusterScore); - } - - return maxScore; - } - - /** - * Returns the maximum cumulative layout shift in any `windowMs` ms window - * (inclusive of bounds) in the trace. - * @param {Array} layoutShiftEvents - * @param {number} windowMs - * @return {number} - */ - static maxSliding(layoutShiftEvents, windowMs) { - let maxScore = 0; - let currentWindowScore = 0; - const windowEvents = []; - - for (const event of layoutShiftEvents) { - while (windowEvents.length && event.ts - windowEvents[0].ts > windowMs * 1000) { - currentWindowScore -= windowEvents[0].score; - windowEvents.shift(); - } - windowEvents.push(event); - currentWindowScore += event.score; - maxScore = Math.max(maxScore, currentWindowScore); - } - - return maxScore; - } - - /** - * Returns all LayoutShift events that had no recent input. - * @param {LH.TraceEvent[]} traceEvents - * @return {Array} - */ - static getLayoutShiftEvents(traceEvents) { - const layoutShiftEvents = []; - - // Chromium will set `had_recent_input` if there was recent user input, which - // skips shift events from contributing to CLS. This flag is also set when - // Lighthouse changes the emulation size. This results in the first few shift - // events having `had_recent_input` set, so ignore it for those events. - // See https://bugs.chromium.org/p/chromium/issues/detail?id=1094974. - let ignoreHadRecentInput = true; - - for (const event of traceEvents) { - if (event.name !== 'LayoutShift' || - !event.args.data || - event.args.data.is_main_frame === undefined || - event.args.data.score === undefined) { // Keep tsc happy, but a score-less event would be useless regardless. - continue; - } - - if (event.args.data.had_recent_input) { - // `had_recent_input` events aren't used unless currently ignoring. - if (!ignoreHadRecentInput) continue; - } else { - // After a false `had_recent_input`, stop ignoring property. - ignoreHadRecentInput = false; - } - - layoutShiftEvents.push({ - ts: event.ts, - score: event.args.data.score, - isMainFrame: event.args.data.is_main_frame, - weightedScoreDelta: event.args.data.weighted_score_delta, - }); - } - - return layoutShiftEvents; - } - - /** - * @param {LH.Trace} trace - * @param {LH.Artifacts.ComputedContext} context - * @return {Promise<{avgSessionGap5s: number, maxSessionGap1s: number, maxSessionGap1sLimit5s: number, maxSliding1s: number, maxSliding300ms: number, layoutShiftMaxSessionGap1sLimit5sAllFrames: number}>} - */ - static async compute_(trace, context) { - const traceOfTab = await TraceOfTab.request(trace, context); - const layoutShiftEvents = LayoutShiftVariants.getLayoutShiftEvents(traceOfTab.mainThreadEvents) - .filter(e => e.isMainFrame); // Only main frame for now. - - const layoutShiftEventsAllFrames = - LayoutShiftVariants.getLayoutShiftEvents(traceOfTab.frameTreeEvents); - - return { - avgSessionGap5s: LayoutShiftVariants.avgSessionGap5s(layoutShiftEvents), - maxSessionGap1s: LayoutShiftVariants.maxSession(layoutShiftEvents, 1000), - maxSessionGap1sLimit5s: LayoutShiftVariants.maxSession(layoutShiftEvents, 1000, 5000), - maxSliding1s: LayoutShiftVariants.maxSliding(layoutShiftEvents, 1000), - maxSliding300ms: LayoutShiftVariants.maxSliding(layoutShiftEvents, 300), - // eslint-disable-next-line max-len - layoutShiftMaxSessionGap1sLimit5sAllFrames: LayoutShiftVariants.newCumulativeLayoutShift(layoutShiftEventsAllFrames), - }; - } -} - -module.exports = makeComputedArtifact(LayoutShiftVariants); diff --git a/lighthouse-core/computed/metrics/cumulative-layout-shift-all-frames.js b/lighthouse-core/computed/metrics/cumulative-layout-shift-all-frames.js deleted file mode 100644 index af0042135ea0..000000000000 --- a/lighthouse-core/computed/metrics/cumulative-layout-shift-all-frames.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license Copyright 2020 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'; - -/** @typedef {Omit & {name: 'LayoutShift', args: {data: {score: number, weighted_score_delta: number, had_recent_input: boolean}}}} LayoutShiftEvent */ - -const makeComputedArtifact = require('../computed-artifact.js'); -const TraceOfTab = require('../trace-of-tab.js'); -const log = require('lighthouse-logger'); -const LayoutShiftVariants = require('../layout-shift-variants.js'); - -class CumulativeLayoutShiftAllFrames { - /** - * @param {LH.Trace} trace - * @param {LH.Artifacts.ComputedContext} context - * @return {Promise<{value: number}>} - */ - static async compute_(trace, context) { - const traceOfTab = await TraceOfTab.request(trace, context); - const layoutShiftEvents = LayoutShiftVariants.getLayoutShiftEvents(traceOfTab.frameTreeEvents); - - let traceHasWeightedScore = true; - const cumulativeShift = layoutShiftEvents - .map(e => { - // COMPAT: remove after m90 hits stable - // We should replace with a LHError at that point: - // https://github.com/GoogleChrome/lighthouse/pull/12034#discussion_r568150032 - if (e.weightedScoreDelta === undefined) { - traceHasWeightedScore = false; - return e.score; - } - - return e.weightedScoreDelta; - }) - .reduce((sum, score) => sum + score, 0); - - if (traceHasWeightedScore) { - log.warn( - 'CLS-AF', - 'Trace does not have weighted layout shift scores. CLS-AF may not be accurate.' - ); - } - - return { - value: cumulativeShift, - }; - } -} - -module.exports = makeComputedArtifact(CumulativeLayoutShiftAllFrames); diff --git a/lighthouse-core/computed/metrics/cumulative-layout-shift.js b/lighthouse-core/computed/metrics/cumulative-layout-shift.js index ce9ddcd0e6f3..fc40a06f72c9 100644 --- a/lighthouse-core/computed/metrics/cumulative-layout-shift.js +++ b/lighthouse-core/computed/metrics/cumulative-layout-shift.js @@ -7,27 +7,122 @@ const makeComputedArtifact = require('../computed-artifact.js'); const TraceOfTab = require('../trace-of-tab.js'); -const LayoutShiftVariants = require('../layout-shift-variants.js'); +const LHError = require('../../lib/lh-error.js'); + +/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */ class CumulativeLayoutShift { + /** + * Returns all LayoutShift events that had no recent input. + * Only a `weightedScore` per event is returned. For non-main-frame events, this is + * the only score that matters. For main-frame events, `weighted_score_delta === score`. + * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5 + * @param {LH.TraceEvent[]} traceEvents + * @return {Array} + */ + static getLayoutShiftEvents(traceEvents) { + const layoutShiftEvents = []; + + // Chromium will set `had_recent_input` if there was recent user input, which + // skips shift events from contributing to CLS. This flag is also set when + // Lighthouse changes the emulation size. This results in the first few shift + // events having `had_recent_input` set, so ignore it for those events. + // See https://bugs.chromium.org/p/chromium/issues/detail?id=1094974. + let ignoreHadRecentInput = true; + + for (const event of traceEvents) { + if (event.name !== 'LayoutShift' || + !event.args.data || + event.args.data.is_main_frame === undefined) { + continue; + } + + // For all-frames CLS calculation, we rely on `weighted_score_delta`, which + // was added in Chrome 90: https://crbug.com/1173139 + if (event.args.data.weighted_score_delta === undefined) { + throw new LHError( + LHError.errors.UNSUPPORTED_OLD_CHROME, + {featureName: 'Cumulative Layout Shift'} + ); + } + + if (event.args.data.had_recent_input) { + // `had_recent_input` events aren't used unless currently ignoring. + if (!ignoreHadRecentInput) continue; + } else { + // After a false `had_recent_input`, stop ignoring property. + ignoreHadRecentInput = false; + } + + layoutShiftEvents.push({ + ts: event.ts, + isMainFrame: event.args.data.is_main_frame, + weightedScore: event.args.data.weighted_score_delta, + }); + } + + return layoutShiftEvents; + } + + /** + * Calculates cumulative layout shifts per cluster (session) of LayoutShift + * events -- where a new cluster is created when there's a gap of more than + * 1000ms since the last LayoutShift event or the cluster is greater than + * 5000ms long -- and returns the max LayoutShift score found. + * @param {Array} layoutShiftEvents + * @return {number} + */ + static calculate(layoutShiftEvents) { + const gapMicroseconds = 1_000_000; + const limitMicroseconds = 5_000_000; + let maxScore = 0; + let currentClusterScore = 0; + let firstTs = Number.NEGATIVE_INFINITY; + let prevTs = Number.NEGATIVE_INFINITY; + + for (const event of layoutShiftEvents) { + if (event.ts - firstTs > limitMicroseconds || event.ts - prevTs > gapMicroseconds) { + firstTs = event.ts; + currentClusterScore = 0; + } + prevTs = event.ts; + currentClusterScore += event.weightedScore; + maxScore = Math.max(maxScore, currentClusterScore); + } + + return maxScore; + } + + /** + * Calculates the original Cumulative Layout Shift metric, summing main-frame + * layout-shift events from the entire trace. + * @param {Array} layoutShiftEvents + * @return {number} + */ + static calculateOldCumulativeLayoutShift(layoutShiftEvents) { + return layoutShiftEvents.reduce((sum, e) => sum += e.weightedScore, 0); + } + /** * @param {LH.Trace} trace * @param {LH.Artifacts.ComputedContext} context - * @return {Promise<{value: number, debugInfo: Record | null}>} + * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, oldCumulativeLayoutShift: number}>} */ static async compute_(trace, context) { const traceOfTab = await TraceOfTab.request(trace, context); - const layoutShiftEvents = LayoutShiftVariants.getLayoutShiftEvents(traceOfTab.mainThreadEvents); - const cumulativeLayoutShift = layoutShiftEvents - .filter(e => e.isMainFrame) - .reduce((sum, e) => sum += e.score, 0); + const allFrameShiftEvents = + CumulativeLayoutShift.getLayoutShiftEvents(traceOfTab.frameTreeEvents); + const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame); + + // The original Cumulative Layout Shift metric, the sum of main-frame shift events from the entire trace. + const oldCumulativeLayoutShift = mainFrameShiftEvents + .reduce((sum, e) => sum += e.weightedScore, 0); return { - value: cumulativeLayoutShift, - debugInfo: { - finalLayoutShiftTraceEventFound: Boolean(layoutShiftEvents.length), - }, + cumulativeLayoutShift: CumulativeLayoutShift.calculate(allFrameShiftEvents), + cumulativeLayoutShiftMainFrame: CumulativeLayoutShift.calculate(mainFrameShiftEvents), + oldCumulativeLayoutShift, }; } } diff --git a/lighthouse-core/computed/metrics/timing-summary.js b/lighthouse-core/computed/metrics/timing-summary.js index 626f3b649bb3..23729cdfb1b7 100644 --- a/lighthouse-core/computed/metrics/timing-summary.js +++ b/lighthouse-core/computed/metrics/timing-summary.js @@ -15,12 +15,10 @@ const LargestContentfulPaintAllFrames = require('./largest-contentful-paint-all- const FirstCPUIdle = require('./first-cpu-idle.js'); const Interactive = require('./interactive.js'); const CumulativeLayoutShift = require('./cumulative-layout-shift.js'); -const CumulativeLayoutShiftAllFrames = require('./cumulative-layout-shift-all-frames.js'); const SpeedIndex = require('./speed-index.js'); const EstimatedInputLatency = require('./estimated-input-latency.js'); const MaxPotentialFID = require('./max-potential-fid.js'); const TotalBlockingTime = require('./total-blocking-time.js'); -const LayoutShiftVariants = require('../layout-shift-variants.js'); const makeComputedArtifact = require('../computed-artifact.js'); class TimingSummary { @@ -53,19 +51,17 @@ class TimingSummary { const largestContentfulPaintAllFrames = await requestOrUndefined(LargestContentfulPaintAllFrames, metricComputationData); // eslint-disable-line max-len const firstCPUIdle = await requestOrUndefined(FirstCPUIdle, metricComputationData); const interactive = await requestOrUndefined(Interactive, metricComputationData); - const cumulativeLayoutShift = await requestOrUndefined(CumulativeLayoutShift, trace); - const cumulativeLayoutShiftAllFrames = await requestOrUndefined(CumulativeLayoutShiftAllFrames, trace); // eslint-disable-line max-len + const cumulativeLayoutShiftValues = await requestOrUndefined(CumulativeLayoutShift, trace); const maxPotentialFID = await requestOrUndefined(MaxPotentialFID, metricComputationData); const speedIndex = await requestOrUndefined(SpeedIndex, metricComputationData); const estimatedInputLatency = await EstimatedInputLatency.request(metricComputationData, context); // eslint-disable-line max-len const totalBlockingTime = await TotalBlockingTime.request(metricComputationData, context); // eslint-disable-line max-len - const layoutShiftVariants = await LayoutShiftVariants.request(trace, context); - const cumulativeLayoutShiftValue = cumulativeLayoutShift && - cumulativeLayoutShift.value !== null ? - cumulativeLayoutShift.value : undefined; - const cumulativeLayoutShiftAllFramesValue = cumulativeLayoutShiftAllFrames ? - cumulativeLayoutShiftAllFrames.value : undefined; + const { + cumulativeLayoutShift, + cumulativeLayoutShiftMainFrame, + oldCumulativeLayoutShift, + } = cumulativeLayoutShiftValues || {}; /** @type {LH.Artifacts.TimingSummary} */ const metrics = { @@ -90,8 +86,9 @@ class TimingSummary { estimatedInputLatencyTs: estimatedInputLatency.timestamp, totalBlockingTime: totalBlockingTime.timing, maxPotentialFID: maxPotentialFID && maxPotentialFID.timing, - cumulativeLayoutShift: cumulativeLayoutShiftValue, - cumulativeLayoutShiftAllFrames: cumulativeLayoutShiftAllFramesValue, + cumulativeLayoutShift, + cumulativeLayoutShiftMainFrame, + oldCumulativeLayoutShift, // Include all timestamps of interest from trace of tab observedTimeOrigin: traceOfTab.timings.timeOrigin, @@ -118,8 +115,9 @@ class TimingSummary { observedLoadTs: traceOfTab.timestamps.load, observedDomContentLoaded: traceOfTab.timings.domContentLoaded, observedDomContentLoadedTs: traceOfTab.timestamps.domContentLoaded, - observedCumulativeLayoutShift: cumulativeLayoutShiftValue, - observedCumulativeLayoutShiftAllFrames: cumulativeLayoutShiftAllFramesValue, + observedCumulativeLayoutShift: cumulativeLayoutShift, + observedCumulativeLayoutShiftMainFrame: cumulativeLayoutShiftMainFrame, + observedOldCumulativeLayoutShift: oldCumulativeLayoutShift, // Include some visual metrics from speedline observedFirstVisualChange: speedline.first, @@ -128,17 +126,6 @@ class TimingSummary { observedLastVisualChangeTs: (speedline.complete + speedline.beginning) * 1000, observedSpeedIndex: speedline.speedIndex, observedSpeedIndexTs: (speedline.speedIndex + speedline.beginning) * 1000, - - // Include experimental LayoutShift variants. - layoutShiftAvgSessionGap5s: layoutShiftVariants.avgSessionGap5s, - layoutShiftMaxSessionGap1s: layoutShiftVariants.maxSessionGap1s, - layoutShiftMaxSessionGap1sLimit5s: layoutShiftVariants.maxSessionGap1sLimit5s, - layoutShiftMaxSliding1s: layoutShiftVariants.maxSliding1s, - layoutShiftMaxSliding300ms: layoutShiftVariants.maxSliding300ms, - - // And new CLS (layoutShiftMaxSessionGap1sLimit5s) across all frames. - // eslint-disable-next-line max-len - layoutShiftMaxSessionGap1sLimit5sAllFrames: layoutShiftVariants.layoutShiftMaxSessionGap1sLimit5sAllFrames, }; /** @type {Record} */ const debugInfo = {lcpInvalidated: traceOfTab.lcpInvalidated}; diff --git a/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap b/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap index 62ed52cf4195..c0c3f88688ad 100644 --- a/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap +++ b/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap @@ -2,8 +2,8 @@ exports[`Performance: metrics evaluates valid input (with lcp from all frames) correctly 1`] = ` Object { - "cumulativeLayoutShift": 0.0011656245471340055, - "cumulativeLayoutShiftAllFrames": 0.4591700003057729, + "cumulativeLayoutShift": undefined, + "cumulativeLayoutShiftMainFrame": undefined, "estimatedInputLatency": 16, "estimatedInputLatencyTs": undefined, "firstCPUIdle": 863, @@ -20,15 +20,9 @@ Object { "largestContentfulPaintAllFrames": 683, "largestContentfulPaintAllFramesTs": 23466705983, "largestContentfulPaintTs": 23466886143, - "layoutShiftAvgSessionGap5s": 0.0011656245471340055, - "layoutShiftMaxSessionGap1s": 0.0011656245471340055, - "layoutShiftMaxSessionGap1sLimit5s": 0.0011656245471340055, - "layoutShiftMaxSessionGap1sLimit5sAllFrames": -1, - "layoutShiftMaxSliding1s": 0.0011656245471340055, - "layoutShiftMaxSliding300ms": 0.0011656245471340055, "maxPotentialFID": 16, - "observedCumulativeLayoutShift": 0.0011656245471340055, - "observedCumulativeLayoutShiftAllFrames": 0.4591700003057729, + "observedCumulativeLayoutShift": undefined, + "observedCumulativeLayoutShiftMainFrame": undefined, "observedDomContentLoaded": 596, "observedDomContentLoadedTs": 23466619325, "observedFirstContentfulPaint": 863, @@ -51,12 +45,14 @@ Object { "observedLoadTs": 23466696096, "observedNavigationStart": 0, "observedNavigationStartTs": 23466023130, + "observedOldCumulativeLayoutShift": undefined, "observedSpeedIndex": 1583, "observedSpeedIndexTs": 23467605703, "observedTimeOrigin": 0, "observedTimeOriginTs": 23466023130, "observedTraceEnd": 6006, "observedTraceEndTs": 23472029453, + "oldCumulativeLayoutShift": undefined, "speedIndex": 1583, "speedIndexTs": 23467606130, "totalBlockingTime": 0, @@ -66,7 +62,7 @@ Object { exports[`Performance: metrics evaluates valid input (with lcp) correctly 1`] = ` Object { "cumulativeLayoutShift": 0, - "cumulativeLayoutShiftAllFrames": 0, + "cumulativeLayoutShiftMainFrame": 0, "estimatedInputLatency": 543, "estimatedInputLatencyTs": undefined, "firstCPUIdle": 3790, @@ -83,15 +79,9 @@ Object { "largestContentfulPaintAllFrames": undefined, "largestContentfulPaintAllFramesTs": undefined, "largestContentfulPaintTs": undefined, - "layoutShiftAvgSessionGap5s": 0, - "layoutShiftMaxSessionGap1s": 0, - "layoutShiftMaxSessionGap1sLimit5s": 0, - "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0, - "layoutShiftMaxSliding1s": 0, - "layoutShiftMaxSliding300ms": 0, "maxPotentialFID": 1336, "observedCumulativeLayoutShift": 0, - "observedCumulativeLayoutShiftAllFrames": 0, + "observedCumulativeLayoutShiftMainFrame": 0, "observedDomContentLoaded": 1513, "observedDomContentLoadedTs": 713038536140, "observedFirstContentfulPaint": 1122, @@ -114,12 +104,14 @@ Object { "observedLoadTs": 713039182071, "observedNavigationStart": 0, "observedNavigationStartTs": 713037023064, + "observedOldCumulativeLayoutShift": 0, "observedSpeedIndex": 1393, "observedSpeedIndexTs": 713038416494, "observedTimeOrigin": 0, "observedTimeOriginTs": 713037023064, "observedTraceEnd": 7416, "observedTraceEndTs": 713044439102, + "oldCumulativeLayoutShift": 0, "speedIndex": 3681, "speedIndexTs": undefined, "totalBlockingTime": 1205, @@ -129,7 +121,7 @@ Object { exports[`Performance: metrics evaluates valid input correctly (throttlingMethod=provided) 1`] = ` Object { "cumulativeLayoutShift": 0, - "cumulativeLayoutShiftAllFrames": 0, + "cumulativeLayoutShiftMainFrame": 0, "estimatedInputLatency": 17, "estimatedInputLatencyTs": undefined, "firstCPUIdle": 1582, @@ -146,15 +138,9 @@ Object { "largestContentfulPaintAllFrames": undefined, "largestContentfulPaintAllFramesTs": undefined, "largestContentfulPaintTs": undefined, - "layoutShiftAvgSessionGap5s": 0, - "layoutShiftMaxSessionGap1s": 0, - "layoutShiftMaxSessionGap1sLimit5s": 0, - "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0, - "layoutShiftMaxSliding1s": 0, - "layoutShiftMaxSliding300ms": 0, "maxPotentialFID": 198, "observedCumulativeLayoutShift": 0, - "observedCumulativeLayoutShiftAllFrames": 0, + "observedCumulativeLayoutShiftMainFrame": 0, "observedDomContentLoaded": 560, "observedDomContentLoadedTs": 225414732309, "observedFirstContentfulPaint": 499, @@ -177,12 +163,14 @@ Object { "observedLoadTs": 225416370913, "observedNavigationStart": 0, "observedNavigationStartTs": 225414172015, + "observedOldCumulativeLayoutShift": 0, "observedSpeedIndex": 605, "observedSpeedIndexTs": 225414776724, "observedTimeOrigin": 0, "observedTimeOriginTs": 225414172015, "observedTraceEnd": 12540, "observedTraceEndTs": 225426711887, + "oldCumulativeLayoutShift": 0, "speedIndex": 605, "speedIndexTs": 225414777015, "totalBlockingTime": 48, @@ -192,7 +180,7 @@ Object { exports[`Performance: metrics evaluates valid input correctly 1`] = ` Object { "cumulativeLayoutShift": 0, - "cumulativeLayoutShiftAllFrames": 0, + "cumulativeLayoutShiftMainFrame": 0, "estimatedInputLatency": 78, "estimatedInputLatencyTs": undefined, "firstCPUIdle": 4313, @@ -209,15 +197,9 @@ Object { "largestContentfulPaintAllFrames": undefined, "largestContentfulPaintAllFramesTs": undefined, "largestContentfulPaintTs": undefined, - "layoutShiftAvgSessionGap5s": 0, - "layoutShiftMaxSessionGap1s": 0, - "layoutShiftMaxSessionGap1sLimit5s": 0, - "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0, - "layoutShiftMaxSliding1s": 0, - "layoutShiftMaxSliding300ms": 0, "maxPotentialFID": 396, "observedCumulativeLayoutShift": 0, - "observedCumulativeLayoutShiftAllFrames": 0, + "observedCumulativeLayoutShiftMainFrame": 0, "observedDomContentLoaded": 560, "observedDomContentLoadedTs": 225414732309, "observedFirstContentfulPaint": 499, @@ -240,12 +222,14 @@ Object { "observedLoadTs": 225416370913, "observedNavigationStart": 0, "observedNavigationStartTs": 225414172015, + "observedOldCumulativeLayoutShift": 0, "observedSpeedIndex": 605, "observedSpeedIndexTs": 225414776724, "observedTimeOrigin": 0, "observedTimeOriginTs": 225414172015, "observedTraceEnd": 12540, "observedTraceEndTs": 225426711887, + "oldCumulativeLayoutShift": 0, "speedIndex": 1676, "speedIndexTs": undefined, "totalBlockingTime": 777, diff --git a/lighthouse-core/test/audits/metrics-test.js b/lighthouse-core/test/audits/metrics-test.js index 3ce93624572d..a204257fb422 100644 --- a/lighthouse-core/test/audits/metrics-test.js +++ b/lighthouse-core/test/audits/metrics-test.js @@ -20,9 +20,6 @@ const lcpAllFramesDevtoolsLog = require('../fixtures/traces/frame-metrics-m89.de const clsAllFramesTrace = require('../fixtures/traces/frame-metrics-m90.json'); const clsAllFramesDevtoolsLog = require('../fixtures/traces/frame-metrics-m90.devtools.log.json'); // eslint-disable-line max-len -const artifactsTrace = require('../results/artifacts/defaultPass.trace.json'); -const artifactsDevtoolsLog = require('../results/artifacts/defaultPass.devtoolslog.json'); - const jumpyClsTrace = require('../fixtures/traces/jumpy-cls-m90.json'); const jumpyClsDevtoolsLog = require('../fixtures/traces/jumpy-cls-m90.devtoolslog.json'); @@ -89,23 +86,29 @@ describe('Performance: metrics', () => { expect(result.details.items[0]).toMatchSnapshot(); }); - it('evaluates valid input (with CLS) correctly', async () => { + it('leaves CLS undefined in an old trace without weighted scores', async () => { const artifacts = { traces: { - [MetricsAudit.DEFAULT_PASS]: artifactsTrace, + [MetricsAudit.DEFAULT_PASS]: lcpAllFramesTrace, }, devtoolsLogs: { - [MetricsAudit.DEFAULT_PASS]: artifactsDevtoolsLog, + [MetricsAudit.DEFAULT_PASS]: lcpAllFramesDevtoolsLog, }, }; const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()}; const {details} = await MetricsAudit.audit(artifacts, context); - expect(details.items[0].cumulativeLayoutShift).toMatchInlineSnapshot(`0.42`); - expect(details.items[0].observedCumulativeLayoutShift).toMatchInlineSnapshot(`0.42`); + expect(details.items[0]).toMatchObject({ + cumulativeLayoutShift: undefined, + cumulativeLayoutShiftMainFrame: undefined, + oldCumulativeLayoutShift: undefined, + observedCumulativeLayoutShift: undefined, + observedCumulativeLayoutShiftMainFrame: undefined, + observedOldCumulativeLayoutShift: undefined, + }); }); - it('evaluates valid input (with CLS from all frames) correctly', async () => { + it('evaluates new CLS correctly across all frames', async () => { const artifacts = { traces: { [MetricsAudit.DEFAULT_PASS]: clsAllFramesTrace, @@ -117,10 +120,17 @@ describe('Performance: metrics', () => { const context = {settings: {throttlingMethod: 'provided'}, computedCache: new Map()}; const {details} = await MetricsAudit.audit(artifacts, context); - expect(details.items[0].cumulativeLayoutShift).toBeCloseTo(0.0011); - expect(details.items[0].observedCumulativeLayoutShift).toBeCloseTo(0.0011); - expect(details.items[0].cumulativeLayoutShiftAllFrames).toBeCloseTo(0.0276); - expect(details.items[0].observedCumulativeLayoutShiftAllFrames).toBeCloseTo(0.0276); + + // Only a single main-frame shift event, so mfCls and oldCls are equal. + expect(details.items[0]).toMatchObject({ + cumulativeLayoutShift: expect.toBeApproximately(0.026463, 6), + cumulativeLayoutShiftMainFrame: expect.toBeApproximately(0.001166, 6), + oldCumulativeLayoutShift: expect.toBeApproximately(0.001166, 6), + + observedCumulativeLayoutShift: expect.toBeApproximately(0.026463, 6), + observedCumulativeLayoutShiftMainFrame: expect.toBeApproximately(0.001166, 6), + observedOldCumulativeLayoutShift: expect.toBeApproximately(0.001166, 6), + }); }); it('does not fail the entire audit when TTI errors', async () => { @@ -140,7 +150,7 @@ describe('Performance: metrics', () => { expect(result.details.items[0].interactive).toEqual(undefined); }); - it('evaluates LayoutShift variants correctly', async () => { + it('evaluates CLS correctly', async () => { const artifacts = { traces: { [MetricsAudit.DEFAULT_PASS]: jumpyClsTrace, @@ -153,33 +163,13 @@ describe('Performance: metrics', () => { const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()}; const {details} = await MetricsAudit.audit(artifacts, context); expect(details.items[0]).toMatchObject({ - cumulativeLayoutShift: expect.toBeApproximately(4.809794, 6), - cumulativeLayoutShiftAllFrames: expect.toBeApproximately(4.809794, 6), - layoutShiftAvgSessionGap5s: expect.toBeApproximately(4.809794, 6), - layoutShiftMaxSessionGap1s: expect.toBeApproximately(2.897995, 6), - layoutShiftMaxSessionGap1sLimit5s: expect.toBeApproximately(2.268816, 6), - layoutShiftMaxSliding1s: expect.toBeApproximately(1.911799, 6), - layoutShiftMaxSliding300ms: expect.toBeApproximately(1.436742, 6), - layoutShiftMaxSessionGap1sLimit5sAllFrames: expect.toBeApproximately(2.268816, 6), - }); - }); + cumulativeLayoutShift: expect.toBeApproximately(2.268816, 6), + cumulativeLayoutShiftMainFrame: expect.toBeApproximately(2.268816, 6), + oldCumulativeLayoutShift: expect.toBeApproximately(4.809794, 6), - it('evaluates new CLS correctly across all frames', async () => { - const artifacts = { - traces: { - [MetricsAudit.DEFAULT_PASS]: clsAllFramesTrace, - }, - devtoolsLogs: { - [MetricsAudit.DEFAULT_PASS]: clsAllFramesDevtoolsLog, - }, - }; - - const context = {settings: {throttlingMethod: 'provided'}, computedCache: new Map()}; - const {details} = await MetricsAudit.audit(artifacts, context); - expect(details.items[0]).toMatchObject({ - cumulativeLayoutShift: expect.toBeApproximately(0.001166, 6), - cumulativeLayoutShiftAllFrames: expect.toBeApproximately(0.027629, 6), - layoutShiftMaxSessionGap1sLimit5sAllFrames: expect.toBeApproximately(0.026463, 6), + observedCumulativeLayoutShift: expect.toBeApproximately(2.268816, 6), + observedCumulativeLayoutShiftMainFrame: expect.toBeApproximately(2.268816, 6), + observedOldCumulativeLayoutShift: expect.toBeApproximately(4.809794, 6), }); }); }); diff --git a/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js b/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js new file mode 100644 index 000000000000..80662bf160d2 --- /dev/null +++ b/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js @@ -0,0 +1,40 @@ +/** + * @license Copyright 2021 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 CumulativeLayoutShift = require('../../../audits/metrics/cumulative-layout-shift.js'); +const jumpyClsTrace = require('../../fixtures/traces/jumpy-cls-m90.json'); + +/* eslint-env jest */ + +describe('Cumulative Layout Shift', () => { + it('evaluates CLS correctly', async () => { + const artifacts = { + traces: { + [CumulativeLayoutShift.DEFAULT_PASS]: jumpyClsTrace, + }, + }; + + const context = { + settings: {throttlingMethod: 'simulate'}, + computedCache: new Map(), + options: CumulativeLayoutShift.defaultOptions, + }; + const result = await CumulativeLayoutShift.audit(artifacts, context); + expect(result).toMatchObject({ + score: 0, + numericValue: expect.toBeApproximately(2.268816, 6), + numericUnit: 'unitless', + details: { + type: 'debugdata', + items: [{ + cumulativeLayoutShiftMainFrame: expect.toBeApproximately(2.268816, 6), + oldCumulativeLayoutShift: expect.toBeApproximately(4.809794, 6), + }], + }, + }); + }); +}); diff --git a/lighthouse-core/test/computed/layout-shift-variants-test.js b/lighthouse-core/test/computed/layout-shift-variants-test.js deleted file mode 100644 index aee7ea89f82c..000000000000 --- a/lighthouse-core/test/computed/layout-shift-variants-test.js +++ /dev/null @@ -1,399 +0,0 @@ -/** - * @license Copyright 2021 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 LayoutShiftVariants = require('../../computed/layout-shift-variants.js'); -const jumpyClsTrace = require('../fixtures/traces/jumpy-cls-m90.json'); -const mainFrameMetricsTrace = require('../fixtures/traces/frame-metrics-m89.json'); -const allFramesMetricsTrace = require('../fixtures/traces/frame-metrics-m90.json'); -const preClsTrace = require('../fixtures/traces/progressive-app-m60.json'); -const createTestTrace = require('../create-test-trace.js'); - -/* eslint-env jest */ - -const childFrameId = 'CAF4634127666E186C9C8B35627DBF0B'; - -describe('Layout Shift Variants', () => { - const context = { - computedCache: new Map(), - }; - - describe('real traces', () => { - it('calculates CLS variants for a trace', async () => { - const variants = await LayoutShiftVariants.request(jumpyClsTrace, context); - expect(variants).toEqual({ - avgSessionGap5s: expect.toBeApproximately(4.809794, 6), - maxSessionGap1s: expect.toBeApproximately(2.897995, 6), - maxSessionGap1sLimit5s: expect.toBeApproximately(2.268816, 6), - maxSliding1s: expect.toBeApproximately(1.911799, 6), - maxSliding300ms: expect.toBeApproximately(1.436742, 6), - layoutShiftMaxSessionGap1sLimit5sAllFrames: expect.toBeApproximately(2.268816, 6), - }); - }); - - it('calculates CLS variants for a trace with a single (main frame) CLS event', async () => { - // Only a single CLS `is_main_frame` event in this trace. - const variants = await LayoutShiftVariants.request(mainFrameMetricsTrace, context); - expect(variants).toEqual({ - avgSessionGap5s: 0.0011656245471340055, - maxSessionGap1s: 0.0011656245471340055, - maxSessionGap1sLimit5s: 0.0011656245471340055, - maxSliding1s: 0.0011656245471340055, - maxSliding300ms: 0.0011656245471340055, - // No weightedScoreDeltas in this trace. - layoutShiftMaxSessionGap1sLimit5sAllFrames: -1, - }); - }); - - it('calculates CLS variants for a trace with CLS events over more than one frame', async () => { - const variants = await LayoutShiftVariants.request(allFramesMetricsTrace, context); - expect(variants).toEqual({ - avgSessionGap5s: 0.0011656245471340055, - maxSessionGap1s: 0.0011656245471340055, - maxSessionGap1sLimit5s: 0.0011656245471340055, - maxSliding1s: 0.0011656245471340055, - maxSliding300ms: 0.0011656245471340055, - // No weightedScoreDeltas in this trace. - layoutShiftMaxSessionGap1sLimit5sAllFrames: 0.026463014612806653, - }); - }); - - it('handles a trace with no CLS events', async () => { - const variants = await LayoutShiftVariants.request(preClsTrace, context); - expect(variants).toEqual({ - avgSessionGap5s: 0, - maxSessionGap1s: 0, - maxSessionGap1sLimit5s: 0, - maxSliding1s: 0, - maxSliding300ms: 0, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 0, - }); - }); - }); - - describe('constructed traces', () => { - /** - * @param {Array<{score: number, ts: number, had_recent_input?: boolean, is_main_frame?: boolean, weighted_score_delta?: number}>} shiftEventsData - */ - function makeTrace(shiftEventsData) { - // If there are non-is_main_frame events, create a child frame in trace to add those events to. - const needsChildFrame = shiftEventsData.some(e => e.is_main_frame === false); - const childFrames = needsChildFrame ? [{frame: childFrameId}] : []; - - const trace = createTestTrace({traceEnd: 30_000, childFrames}); - const navigationStartEvt = trace.traceEvents.find(e => e.name === 'navigationStart'); - const mainFrameId = navigationStartEvt.args.frame; - - let mainCumulativeScore = 0; - let childCumulativeScore = 0; - - /* eslint-disable camelcase */ - const shiftEvents = shiftEventsData.map(data => { - const { - score, - ts, - had_recent_input = false, - is_main_frame = true, - weighted_score_delta = score, - } = data; - - if (!had_recent_input) { - if (is_main_frame) mainCumulativeScore += score; - else childCumulativeScore += score; - } - - return { - name: 'LayoutShift', - cat: 'loading', - ph: 'I', - pid: 1111, - tid: 222, - ts: ts, - args: { - frame: is_main_frame ? mainFrameId : childFrameId, - data: { - is_main_frame, - had_recent_input, - score, - cumulative_score: is_main_frame ? mainCumulativeScore : childCumulativeScore, - weighted_score_delta, - }, - }, - }; - }); - /* eslint-enable camelcase */ - - trace.traceEvents.push(...shiftEvents); - return trace; - } - - describe('single frame traces', () => { - // Test numbers verified against Chrome Speed Metrics tooling. - it('calculates from a uniform distribution of layout shift events', async () => { - const shiftEvents = []; - for (let i = 0; i < 30; i++) { - shiftEvents.push({ - score: 0.125, - ts: (i + 0.5) * 1_000_000, - }); - } - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toEqual({ - avgSessionGap5s: 3.75, - maxSessionGap1s: 3.75, - maxSessionGap1sLimit5s: 0.75, - maxSliding1s: 0.25, - maxSliding300ms: 0.125, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 0.75, - }); - }); - - it('calculates from three clusters of layout shift events', async () => { - const shiftEvents = [ - {score: 0.0625, ts: 1_000_000}, - {score: 0.2500, ts: 1_200_000}, - {score: 0.0625, ts: 1_250_000}, // Still in 300ms sliding window. - {score: 0.1250, ts: 2_200_000}, // Sliding windows excluding most of cluster. - - {score: 0.0625, ts: 3_000_000}, // 1.8s gap > 1s but < 5s. - {score: 0.2500, ts: 3_400_000}, - {score: 0.2500, ts: 4_000_000}, - - {score: 0.1250, ts: 10_000_000}, // > 5s gap - {score: 0.1250, ts: 10_400_000}, - {score: 0.0625, ts: 10_680_000}, - ]; - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toEqual({ - avgSessionGap5s: 0.6875, - maxSessionGap1s: 1.0625, - maxSessionGap1sLimit5s: 1.0625, - maxSliding1s: 0.5625, - maxSliding300ms: 0.375, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 1.0625, - }); - }); - - it('calculates the same LS score from a tiny extra small cluster of events', async () => { - const shiftEvents = []; - for (let i = 0; i < 30; i++) { - shiftEvents.push({ - score: 0.125, - ts: 1_000_000 + i * 10_000, - }); - } - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toEqual({ - avgSessionGap5s: 3.75, // 30 * 0.125 - maxSessionGap1s: 3.75, - maxSessionGap1sLimit5s: 3.75, - maxSliding1s: 3.75, - maxSliding300ms: 3.75, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 3.75, - }); - }); - - it('includes events with recent input at start of trace, but ignores others', async () => { - const shiftEvents = [ - {score: 1, ts: 250_000, had_recent_input: true}, - {score: 1, ts: 500_000, had_recent_input: true}, - {score: 1, ts: 750_000, had_recent_input: true}, - {score: 1, ts: 1_000_000, had_recent_input: true}, // These first four events will still be counted. - - {score: 1, ts: 1_250_000, had_recent_input: false}, - - {score: 1, ts: 1_500_000, had_recent_input: true}, // The last four will not. - {score: 1, ts: 1_750_000, had_recent_input: true}, - {score: 1, ts: 2_000_000, had_recent_input: true}, - {score: 1, ts: 2_250_000, had_recent_input: true}, - ]; - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toEqual({ - avgSessionGap5s: 5, - maxSessionGap1s: 5, - maxSessionGap1sLimit5s: 5, - maxSliding1s: 5, - maxSliding300ms: 2, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 5, - }); - }); - }); - - describe('multi-frame traces', () => { - it('calculates layout shift events uniformly distributed across two frames', async () => { - const shiftEvents = []; - for (let i = 0; i < 30; i++) { - shiftEvents.push({ - score: 0.125, - ts: (i + 0.5) * 1_000_000, - is_main_frame: Boolean(i % 2), - }); - } - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toEqual({ - avgSessionGap5s: 1.875, // No 5s gaps, so 0.125 * 15 (main frame shifts). - layoutShiftMaxSessionGap1sLimit5sAllFrames: 0.75, // Includes all frames, so same value as when only main_frame. - maxSessionGap1s: 0.125, // These all have 2s gaps, so single 0.125 shift per cluster. - maxSessionGap1sLimit5s: 0.125, - maxSliding1s: 0.125, - maxSliding300ms: 0.125, - }); - }); - - it('includes events with recent input at start of trace, but ignores others', async () => { - const shiftEvents = [ - {score: 1, ts: 250_000, had_recent_input: true}, - {score: 1, ts: 750_000, had_recent_input: true}, // These first two events will still be counted. - - {score: 1, ts: 1_250_000, had_recent_input: false}, - - {score: 1, ts: 1_750_000, had_recent_input: true}, // The last two will not. - {score: 1, ts: 2_000_000, had_recent_input: true}, - - // Child frame - {score: 1, ts: 500_000, had_recent_input: true, is_main_frame: false}, - {score: 1, ts: 1_000_000, had_recent_input: true, is_main_frame: false}, // These first two events will still be counted. - - {score: 1, ts: 1_250_000, had_recent_input: false, is_main_frame: false}, - - {score: 1, ts: 1_500_000, had_recent_input: true, is_main_frame: false}, // The last two will not. - {score: 1, ts: 2_250_000, had_recent_input: true, is_main_frame: false}, - ]; - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toMatchObject({ - maxSessionGap1sLimit5s: 3, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 6, - }); - }); - - it('uses layout shift score weighted by frame size', async () => { - const shiftEvents = [ - {score: 2, weighted_score_delta: 2, ts: 250_000, is_main_frame: true}, - {score: 2, weighted_score_delta: 1, ts: 500_000, is_main_frame: false}, - {score: 2, weighted_score_delta: 1, ts: 750_000, is_main_frame: false}, - ]; - const trace = makeTrace(shiftEvents); - - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toMatchObject({ - maxSessionGap1sLimit5s: 2, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 4, - }); - }); - }); - - describe('variants include events on window/cluster bounds', () => { - it('avgSessionGap5s only counts gaps > 5s', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 6_000_000}, // Included since exactly 5s later. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants.avgSessionGap5s).toEqual(2); - }); - - it('maxSessionGap1s only counts gaps > 1s', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 2_000_000}, // Included since exactly 1s later. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants.maxSessionGap1s).toEqual(2); - }); - - it('maxSessionGap1sLimit5s counts gaps > 1s and limits cluster length to <= 5s', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 2_000_000}, // All of these included since exactly 1s after the last. - {score: 1, ts: 3_000_000}, - {score: 1, ts: 4_000_000}, - {score: 1, ts: 5_000_000}, - {score: 1, ts: 6_000_000}, // Included since exactly 5s after beginning of cluster. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants.maxSessionGap1sLimit5s).toEqual(6); - }); - - it('maxSliding1s includes events exactly 1s apart in window', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 2_000_000}, // Included since exactly 1s later. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants.maxSliding1s).toEqual(2); - }); - - it('maxSliding300ms includes events exactly 300ms apart in window', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 1_300_000}, // Included since exactly 300ms later. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants.maxSliding300ms).toEqual(2); - }); - - describe('layoutShiftMaxSessionGap1sLimit5sAllFrames', () => { - it('only counts gaps > 1s', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 2_000_000}, // Included since exactly 1s later. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants.layoutShiftMaxSessionGap1sLimit5sAllFrames).toEqual(2); - }); - - it('ignores gaps ≤ 1s, even across frames', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 2_000_000, is_main_frame: false}, // Included since exactly 1s later. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toMatchObject({ - maxSessionGap1sLimit5s: 1, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 2, - }); - }); - - it('counts gaps > 1s and limits cluster length to <= 5s even across frames', async () => { - const shiftEvents = [ - {score: 1, ts: 1_000_000}, - {score: 1, ts: 2_000_000, is_main_frame: false}, // All of these included since exactly 1s after the last. - {score: 1, ts: 3_000_000}, - {score: 1, ts: 4_000_000, is_main_frame: false}, - {score: 1, ts: 5_000_000}, - {score: 1, ts: 6_000_000, is_main_frame: false}, // Included since exactly 5s after beginning of cluster. - {score: 1, ts: 6_000_001}, // Not included since >5s after beginning of cluster. - ]; - const trace = makeTrace(shiftEvents); - const variants = await LayoutShiftVariants.request(trace, context); - expect(variants).toMatchObject({ - maxSessionGap1sLimit5s: 1, - layoutShiftMaxSessionGap1sLimit5sAllFrames: 6, - }); - }); - }); - }); - }); -}); diff --git a/lighthouse-core/test/computed/metrics/cumulative-layout-shift-all-frames-test.js b/lighthouse-core/test/computed/metrics/cumulative-layout-shift-all-frames-test.js deleted file mode 100644 index b6466ef2b7af..000000000000 --- a/lighthouse-core/test/computed/metrics/cumulative-layout-shift-all-frames-test.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @license Copyright 2020 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 CumulativeLayoutShiftAllFrames = require('../../../computed/metrics/cumulative-layout-shift-all-frames.js'); // eslint-disable-line max-len -const missingWeightedClsTrace = require('../../fixtures/traces/frame-metrics-m89.json'); -const frameMetricsTrace = require('../../fixtures/traces/frame-metrics-m90.json'); -const createTestTrace = require('../../create-test-trace.js'); - -/* eslint-env jest */ - -describe('Metrics: CLS All Frames', () => { - it('should compute value', async () => { - const context = {computedCache: new Map()}; - const result = await CumulativeLayoutShiftAllFrames.request(frameMetricsTrace, context); - expect(result.value).toBeCloseTo(0.0276); - }); - - it('should use unweighted score for trace missing weighted score', async () => { - const context = {computedCache: new Map()}; - const result = await CumulativeLayoutShiftAllFrames.request(missingWeightedClsTrace, context); - expect(result.value).toBeCloseTo(0.459); - }); - - it('uses weighted_score_delta instead of score', async () => { - const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); - const mainFrame = trace.traceEvents[0].args.frame; - const childFrame = 'CHILDFRAME'; - const cat = 'loading,rail,devtools.timeline'; - const context = {computedCache: new Map()}; - trace.traceEvents.push( - /* eslint-disable max-len */ - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: mainFrame, url: 'https://example.com'}}}, - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: childFrame, parent: mainFrame, url: 'https://frame.com'}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 2, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 3, weighted_score_delta: 1, is_main_frame: false}}} - /* eslint-enable max-len */ - ); - const result = await CumulativeLayoutShiftAllFrames.request(trace, context); - expect(result.value).toBe(3); - }); - - it('uses score if weighted_score_delta is unavailable', async () => { - const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); - const mainFrame = trace.traceEvents[0].args.frame; - const childFrame = 'CHILDFRAME'; - const cat = 'loading,rail,devtools.timeline'; - const context = {computedCache: new Map()}; - trace.traceEvents.push( - /* eslint-disable max-len */ - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: mainFrame, url: 'https://example.com'}}}, - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: childFrame, parent: mainFrame, url: 'https://frame.com'}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 2, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 3, is_main_frame: false}}} - /* eslint-enable max-len */ - ); - const result = await CumulativeLayoutShiftAllFrames.request(trace, context); - expect(result.value).toBe(6); - }); - - it('ignores had_recent_input for beginning of trace', async () => { - const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); - const mainFrame = trace.traceEvents[0].args.frame; - const childFrame = 'CHILDFRAME'; - const cat = 'loading,rail,devtools.timeline'; - const context = {computedCache: new Map()}; - trace.traceEvents.push( - /* eslint-disable max-len */ - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: mainFrame, url: 'https://example.com'}}}, - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: childFrame, parent: mainFrame, url: 'https://frame.com'}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: true, score: 2, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 3, weighted_score_delta: 1, is_main_frame: false}}} - /* eslint-enable max-len */ - ); - const result = await CumulativeLayoutShiftAllFrames.request(trace, context); - expect(result.value).toBe(4); - }); - - it('collects layout shift data from main frame and all child frames', async () => { - const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); - const mainFrame = trace.traceEvents[0].args.frame; - const childFrame = 'CHILDFRAME'; - const cat = 'loading,rail,devtools.timeline'; - const context = {computedCache: new Map()}; - trace.traceEvents.push( - /* eslint-disable max-len */ - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: mainFrame, url: 'https://example.com'}}}, - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: childFrame, parent: mainFrame, url: 'https://frame.com'}}}, - {name: 'LayoutShift', cat, args: {frame: mainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: mainFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: false}}} - /* eslint-enable max-len */ - ); - const result = await CumulativeLayoutShiftAllFrames.request(trace, context); - expect(result.value).toBe(3); - }); - - it('ignores layout shift data from other tabs', async () => { - const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); - const mainFrame = trace.traceEvents[0].args.frame; - const childFrame = 'CHILDFRAME'; - const otherMainFrame = 'ANOTHERTABOPEN'; - const cat = 'loading,rail,devtools.timeline'; - const context = {computedCache: new Map()}; - trace.traceEvents.push( - /* eslint-disable max-len */ - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: mainFrame, url: 'https://example.com'}}}, - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: childFrame, parent: mainFrame, url: 'https://frame.com'}}}, - {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: otherMainFrame, url: 'https://example.com'}}}, - {name: 'LayoutShift', cat, args: {frame: mainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: mainFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, - {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: otherMainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, - {name: 'LayoutShift', cat, args: {frame: otherMainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}} - /* eslint-enable max-len */ - ); - const result = await CumulativeLayoutShiftAllFrames.request(trace, context); - expect(result.value).toBe(3); - }); -}); diff --git a/lighthouse-core/test/computed/metrics/cumulative-layout-shift-test.js b/lighthouse-core/test/computed/metrics/cumulative-layout-shift-test.js index cc2a05291dac..33ef07874c78 100644 --- a/lighthouse-core/test/computed/metrics/cumulative-layout-shift-test.js +++ b/lighthouse-core/test/computed/metrics/cumulative-layout-shift-test.js @@ -6,79 +6,392 @@ 'use strict'; const CumulativeLayoutShift = require('../../../computed/metrics/cumulative-layout-shift.js'); // eslint-disable-line max-len -const trace = require('../../results/artifacts/defaultPass.trace.json'); -const invalidTrace = require('../../fixtures/traces/progressive-app-m60.json'); +const jumpyClsTrace = require('../../fixtures/traces/jumpy-cls-m90.json'); +const oldMetricsTrace = require('../../fixtures/traces/frame-metrics-m89.json'); +const allFramesMetricsTrace = require('../../fixtures/traces/frame-metrics-m90.json'); +const preClsTrace = require('../../fixtures/traces/progressive-app-m60.json'); const createTestTrace = require('../../create-test-trace.js'); /* eslint-env jest */ +const childFrameId = 'CAF4634127666E186C9C8B35627DBF0B'; + describe('Metrics: CLS', () => { - it('should compute value', async () => { - const context = {computedCache: new Map()}; - const result = await CumulativeLayoutShift.request(trace, context); - expect(result.value).toBe(0.42); - expect(result.debugInfo.finalLayoutShiftTraceEventFound).toBe(true); - }); + const context = { + computedCache: new Map(), + }; + + describe('real traces', () => { + it('calculates (all main frame) CLS for a trace', async () => { + const result = await CumulativeLayoutShift.request(jumpyClsTrace, context); + expect(result).toEqual({ + cumulativeLayoutShift: expect.toBeApproximately(2.268816, 6), + cumulativeLayoutShiftMainFrame: expect.toBeApproximately(2.268816, 6), + oldCumulativeLayoutShift: expect.toBeApproximately(4.809794, 6), + }); + }); + + it('throws if layout shift events are found without weighted_score_delta', async () => { + expect(_ => CumulativeLayoutShift.request(oldMetricsTrace, context)).rejects + .toThrow('UNSUPPORTED_OLD_CHROME'); + }); - it('should fail to compute a value for old trace', async () => { - const context = {computedCache: new Map()}; - const result = await CumulativeLayoutShift.request(invalidTrace, context); - expect(result.value).toBe(0); - expect(result.debugInfo.finalLayoutShiftTraceEventFound).toBe(false); + it('calculates CLS values for a trace with CLS events over more than one frame', async () => { + const result = await CumulativeLayoutShift.request(allFramesMetricsTrace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 0.026463014612806653, + cumulativeLayoutShiftMainFrame: 0.0011656245471340055, + oldCumulativeLayoutShift: 0.0011656245471340055, + }); + }); + + it('returns 0 for a trace with no CLS events', async () => { + const result = await CumulativeLayoutShift.request(preClsTrace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 0, + cumulativeLayoutShiftMainFrame: 0, + oldCumulativeLayoutShift: 0, + }); + }); }); - /** - * @param {Array<{score: number, had_recent_input: boolean}>} shiftEventsData - */ - function makeTrace(shiftEventsData) { - let cumulativeScore = 0; - const children = shiftEventsData.map(data => { - if (!data.had_recent_input) cumulativeScore += data.score; - return { - name: 'LayoutShift', - cat: 'loading', - ph: 'I', - pid: 1111, - tid: 222, - ts: 308559814315, - args: { - data: { - is_main_frame: true, - had_recent_input: data.had_recent_input, - score: data.score, - cumulative_score: cumulativeScore, + describe('constructed traces', () => { + /** + * @param {Array<{score: number, ts: number, had_recent_input?: boolean, is_main_frame?: boolean, weighted_score_delta?: number}>} shiftEventsData + */ + function makeTrace(shiftEventsData) { + // If there are non-is_main_frame events, create a child frame in trace to add those events to. + const needsChildFrame = shiftEventsData.some(e => e.is_main_frame === false); + const childFrames = needsChildFrame ? [{frame: childFrameId}] : []; + + const trace = createTestTrace({traceEnd: 30_000, childFrames}); + const navigationStartEvt = trace.traceEvents.find(e => e.name === 'navigationStart'); + const mainFrameId = navigationStartEvt.args.frame; + + let mainCumulativeScore = 0; + let childCumulativeScore = 0; + + /* eslint-disable camelcase */ + const shiftEvents = shiftEventsData.map(data => { + const { + score, + ts, + had_recent_input = false, + is_main_frame = true, + weighted_score_delta = score, + } = data; + + if (!had_recent_input) { + if (is_main_frame) mainCumulativeScore += score; + else childCumulativeScore += score; + } + + return { + name: 'LayoutShift', + cat: 'loading', + ph: 'I', + pid: 1111, + tid: 222, + ts: ts, + args: { + frame: is_main_frame ? mainFrameId : childFrameId, + data: { + is_main_frame, + had_recent_input, + score, + cumulative_score: is_main_frame ? mainCumulativeScore : childCumulativeScore, + weighted_score_delta, + }, }, - }, - }; + }; + }); + /* eslint-enable camelcase */ + + trace.traceEvents.push(...shiftEvents); + return trace; + } + + describe('single frame traces', () => { + it('should count initial shift events even if input is true', async () => { + const context = {computedCache: new Map()}; + const trace = makeTrace([ + {score: 1, ts: 1, had_recent_input: true}, + {score: 1, ts: 2, had_recent_input: true}, + {score: 1, ts: 3, had_recent_input: false}, + {score: 1, ts: 4, had_recent_input: false}, + ]); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 4, + cumulativeLayoutShiftMainFrame: 4, + oldCumulativeLayoutShift: 4, + }); + }); + + it('should not count later shift events if input it true', async () => { + const context = {computedCache: new Map()}; + const trace = makeTrace([ + {score: 1, ts: 1, had_recent_input: true}, + {score: 1, ts: 2, had_recent_input: false}, + {score: 1, ts: 3, had_recent_input: false}, + {score: 1, ts: 4, had_recent_input: true}, + {score: 1, ts: 5, had_recent_input: true}, + ]); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 3, + cumulativeLayoutShiftMainFrame: 3, + oldCumulativeLayoutShift: 3, + }); + }); + + it('calculates from a uniform distribution of layout shift events', async () => { + const shiftEvents = []; + for (let i = 0; i < 30; i++) { + shiftEvents.push({ + score: 0.125, + ts: (i + 0.5) * 1_000_000, + }); + } + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 0.75, + cumulativeLayoutShiftMainFrame: 0.75, + oldCumulativeLayoutShift: 3.75, // 30 * 0.125 + }); + }); + + it('calculates from three clusters of layout shift events', async () => { + const shiftEvents = [ + {score: 0.0625, ts: 1_000_000}, + {score: 0.2500, ts: 1_200_000}, + {score: 0.0625, ts: 1_250_000}, // Still in 300ms sliding window. + {score: 0.1250, ts: 2_200_000}, // Sliding windows excluding most of cluster. + + {score: 0.0625, ts: 3_000_000}, // 1.8s gap > 1s but < 5s. + {score: 0.2500, ts: 3_400_000}, + {score: 0.2500, ts: 4_000_000}, + + {score: 0.1250, ts: 10_000_000}, // > 5s gap + {score: 0.1250, ts: 10_400_000}, + {score: 0.0625, ts: 10_680_000}, + ]; + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 1.0625, + cumulativeLayoutShiftMainFrame: 1.0625, + oldCumulativeLayoutShift: 1.375, + }); + }); + + it('calculates the same LS score from a tiny extra small cluster of events', async () => { + const shiftEvents = []; + for (let i = 0; i < 30; i++) { + shiftEvents.push({ + score: 0.125, + ts: 1_000_000 + i * 10_000, + }); + } + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 3.75, // 30 * 0.125 + cumulativeLayoutShiftMainFrame: 3.75, + oldCumulativeLayoutShift: 3.75, + }); + }); + + it('includes events with recent input at start of trace, but ignores others', async () => { + const shiftEvents = [ + {score: 1, ts: 250_000, had_recent_input: true}, + {score: 1, ts: 500_000, had_recent_input: true}, + {score: 1, ts: 750_000, had_recent_input: true}, + {score: 1, ts: 1_000_000, had_recent_input: true}, // These first four events will still be counted. + + {score: 1, ts: 1_250_000, had_recent_input: false}, + + {score: 1, ts: 1_500_000, had_recent_input: true}, // The last four will not. + {score: 1, ts: 1_750_000, had_recent_input: true}, + {score: 1, ts: 2_000_000, had_recent_input: true}, + {score: 1, ts: 2_250_000, had_recent_input: true}, + ]; + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 5, + cumulativeLayoutShiftMainFrame: 5, + oldCumulativeLayoutShift: 5, + }); + }); }); - const trace = createTestTrace({}); - trace.traceEvents.push(...children); - return trace; - } - - it('should count initial shift events even if input is true', async () => { - const context = {computedCache: new Map()}; - const trace = makeTrace([ - {score: 1, had_recent_input: true}, - {score: 1, had_recent_input: true}, - {score: 1, had_recent_input: false}, - {score: 1, had_recent_input: false}, - ]); - const result = await CumulativeLayoutShift.request(trace, context); - expect(result.value).toBe(4); - }); + describe('multi-frame traces', () => { + it('calculates layout shift events uniformly distributed across two frames', async () => { + const shiftEvents = []; + for (let i = 0; i < 30; i++) { + shiftEvents.push({ + score: 0.125, + ts: (i + 0.5) * 1_000_000, + is_main_frame: Boolean(i % 2), + }); + } + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toEqual({ + cumulativeLayoutShift: 0.75, // Same value as single-frame uniformly distributed. + cumulativeLayoutShiftMainFrame: 0.125, // All 1s gaps, so only one event per cluster. + oldCumulativeLayoutShift: 1.875, // 0.125 * 15 + }); + }); + + it('includes events with recent input at start of trace, but ignores others', async () => { + const shiftEvents = [ + {score: 1, ts: 250_000, had_recent_input: true}, + {score: 1, ts: 750_000, had_recent_input: true}, // These first two events will still be counted. + + {score: 1, ts: 1_250_000, had_recent_input: false}, + + {score: 1, ts: 1_750_000, had_recent_input: true}, // The last two will not. + {score: 1, ts: 2_000_000, had_recent_input: true}, + + // Child frame + {score: 1, ts: 500_000, had_recent_input: true, is_main_frame: false}, + {score: 1, ts: 1_000_000, had_recent_input: true, is_main_frame: false}, // These first two events will still be counted. - it('should not count later shift events if input it true', async () => { - const context = {computedCache: new Map()}; - const trace = makeTrace([ - {score: 1, had_recent_input: true}, - {score: 1, had_recent_input: false}, - {score: 1, had_recent_input: false}, - {score: 1, had_recent_input: true}, - {score: 1, had_recent_input: true}, - ]); - const result = await CumulativeLayoutShift.request(trace, context); - expect(result.value).toBe(3); + {score: 1, ts: 1_250_000, had_recent_input: false, is_main_frame: false}, + + {score: 1, ts: 1_500_000, had_recent_input: true, is_main_frame: false}, // The last two will not. + {score: 1, ts: 2_250_000, had_recent_input: true, is_main_frame: false}, + ]; + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 6, + cumulativeLayoutShiftMainFrame: 3, + oldCumulativeLayoutShift: 3, + }); + }); + + it('uses layout shift score weighted by frame size', async () => { + const shiftEvents = [ + {score: 2, weighted_score_delta: 2, ts: 250_000, is_main_frame: true}, + {score: 2, weighted_score_delta: 1, ts: 500_000, is_main_frame: false}, + {score: 2, weighted_score_delta: 1, ts: 750_000, is_main_frame: false}, + ]; + const trace = makeTrace(shiftEvents); + + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 4, + cumulativeLayoutShiftMainFrame: 2, + oldCumulativeLayoutShift: 2, + }); + }); + + it('ignores layout shift data from other tabs', async () => { + const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); + const mainFrame = trace.traceEvents[0].args.frame; + const childFrame = 'CHILDFRAME'; + const otherMainFrame = 'ANOTHERTABOPEN'; + const cat = 'loading,rail,devtools.timeline'; + trace.traceEvents.push( + /* eslint-disable max-len */ + {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: mainFrame, url: 'https://example.com'}}}, + {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: childFrame, parent: mainFrame, url: 'https://frame.com'}}}, + {name: 'FrameCommittedInBrowser', cat, args: {data: {frame: otherMainFrame, url: 'https://example.com'}}}, + {name: 'LayoutShift', cat, args: {frame: mainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, + {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, + {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, + // Following two not used because of `had_recent_input: true`. + {name: 'LayoutShift', cat, args: {frame: mainFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, + {name: 'LayoutShift', cat, args: {frame: childFrame, data: {had_recent_input: true, score: 1, weighted_score_delta: 1, is_main_frame: false}}}, + // Following two not used because part of another frame tree. + {name: 'LayoutShift', cat, args: {frame: otherMainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: true}}}, + {name: 'LayoutShift', cat, args: {frame: otherMainFrame, data: {had_recent_input: false, score: 1, weighted_score_delta: 1, is_main_frame: true}}} + /* eslint-enable max-len */ + ); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 3, + cumulativeLayoutShiftMainFrame: 1, + oldCumulativeLayoutShift: 1, + }); + }); + }); + + describe('layout shift session/cluster bounds', () => { + it('counts gaps > 1s and limits cluster length to <= 5s (only main frame)', async () => { + const shiftEvents = [ + {score: 1, ts: 1_000_000}, + {score: 1, ts: 2_000_000}, // All of these included since exactly 1s after the last. + {score: 1, ts: 3_000_000}, + {score: 1, ts: 4_000_000}, + {score: 1, ts: 5_000_000}, + {score: 1, ts: 6_000_000}, // Included since exactly 5s after beginning of cluster. + ]; + const trace = makeTrace(shiftEvents); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 6, + cumulativeLayoutShiftMainFrame: 6, + oldCumulativeLayoutShift: 6, + }); + }); + + it('counts gaps > 1s and limits cluster length to <= 5s (multiple frames)', async () => { + const shiftEvents = [ + {score: 1, ts: 1_000_000}, + {score: 1, ts: 2_000_000, is_main_frame: false}, // All of these included since exactly 1s after the last. + {score: 1, ts: 3_000_000}, + {score: 1, ts: 4_000_000, is_main_frame: false}, + {score: 1, ts: 5_000_000}, + {score: 1, ts: 6_000_000, is_main_frame: false}, // Included since exactly 5s after beginning of cluster. + {score: 1, ts: 6_000_001}, // Not included since >5s after beginning of cluster. + ]; + const trace = makeTrace(shiftEvents); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 6, + cumulativeLayoutShiftMainFrame: 1, + oldCumulativeLayoutShift: 4, + }); + }); + + it('only counts gaps > 1s', async () => { + const shiftEvents = [ + {score: 1, ts: 1_000_000}, + {score: 1, ts: 2_000_000}, // Included since exactly 1s later. + ]; + const trace = makeTrace(shiftEvents); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 2, + cumulativeLayoutShiftMainFrame: 2, + oldCumulativeLayoutShift: 2, + }); + }); + + it('only counts gaps > 1s (multiple frames)', async () => { + const shiftEvents = [ + {score: 1, ts: 1_000_000}, + {score: 1, ts: 2_000_000, is_main_frame: false}, // Included since exactly 1s later. + ]; + const trace = makeTrace(shiftEvents); + const result = await CumulativeLayoutShift.request(trace, context); + expect(result).toMatchObject({ + cumulativeLayoutShift: 2, + cumulativeLayoutShiftMainFrame: 1, + oldCumulativeLayoutShift: 1, + }); + }); + }); }); }); diff --git a/lighthouse-core/test/computed/metrics/timing-summary-test.js b/lighthouse-core/test/computed/metrics/timing-summary-test.js index 1de204329213..99e213ed0bd0 100644 --- a/lighthouse-core/test/computed/metrics/timing-summary-test.js +++ b/lighthouse-core/test/computed/metrics/timing-summary-test.js @@ -19,8 +19,8 @@ describe('Timing summary', () => { expect(result.metrics).toMatchInlineSnapshot(` Object { - "cumulativeLayoutShift": 0.0011656245471340055, - "cumulativeLayoutShiftAllFrames": 0.027628639159940658, + "cumulativeLayoutShift": 0.026463014612806653, + "cumulativeLayoutShiftMainFrame": 0.0011656245471340055, "estimatedInputLatency": 16, "estimatedInputLatencyTs": undefined, "firstCPUIdle": 8654.264, @@ -37,15 +37,9 @@ describe('Timing summary', () => { "largestContentfulPaintAllFrames": 697.751, "largestContentfulPaintAllFramesTs": 10327885660, "largestContentfulPaintTs": 10332856184, - "layoutShiftAvgSessionGap5s": 0.0011656245471340055, - "layoutShiftMaxSessionGap1s": 0.0011656245471340055, - "layoutShiftMaxSessionGap1sLimit5s": 0.0011656245471340055, - "layoutShiftMaxSessionGap1sLimit5sAllFrames": 0.026463014612806653, - "layoutShiftMaxSliding1s": 0.0011656245471340055, - "layoutShiftMaxSliding300ms": 0.0011656245471340055, "maxPotentialFID": 51.056, - "observedCumulativeLayoutShift": 0.0011656245471340055, - "observedCumulativeLayoutShiftAllFrames": 0.027628639159940658, + "observedCumulativeLayoutShift": 0.026463014612806653, + "observedCumulativeLayoutShiftMainFrame": 0.0011656245471340055, "observedDomContentLoaded": 604.135, "observedDomContentLoadedTs": 10327792044, "observedFirstContentfulPaint": 5668.275, @@ -68,12 +62,14 @@ describe('Timing summary', () => { "observedLoadTs": 10327876093, "observedNavigationStart": 0, "observedNavigationStartTs": 10327187909, + "observedOldCumulativeLayoutShift": 0.0011656245471340055, "observedSpeedIndex": 1334.5801200005412, "observedSpeedIndexTs": 10328522489.12, "observedTimeOrigin": 0, "observedTimeOriginTs": 10327187909, "observedTraceEnd": 14214.313, "observedTraceEndTs": 10341402222, + "oldCumulativeLayoutShift": 0.0011656245471340055, "speedIndex": 1335, "speedIndexTs": 10328522909, "totalBlockingTime": 2.7429999999994834, diff --git a/lighthouse-core/test/results/artifacts/defaultPass.trace.json b/lighthouse-core/test/results/artifacts/defaultPass.trace.json index 2c4b1f759b71..2bd31ad1e056 100644 --- a/lighthouse-core/test/results/artifacts/defaultPass.trace.json +++ b/lighthouse-core/test/results/artifacts/defaultPass.trace.json @@ -14506,7 +14506,7 @@ {"pid":75994,"tid":17667,"ts":185608246474,"ph":"X","cat":"toplevel","name":"MessageLoop::RunTask","args":{"src_file":"../../ipc/ipc_mojo_bootstrap.cc","src_func":"SendMessage"},"dur":16,"tdur":16,"tts":78752}, {"pid":75994,"tid":17667,"ts":185608247189,"ph":"X","cat":"toplevel","name":"MessageLoop::RunTask","args":{"src_file":"../../ipc/ipc_mojo_bootstrap.cc","src_func":"SendMessage"},"dur":30,"tdur":29,"tts":78797}, {"_comment":"Manually added event to make sample lhr not error","name":"largestContentfulPaint::Candidate","pid":75994,"tid":17667,"ts":185608247190,"ph":"R","cat":"loading,rail,devtools.timeline","args":{"frame":"0x44d2861df8","data":{"size":50}}}, - {"_comment":"Manually added event to make test CLS","name":"LayoutShift","pid":75994,"tid":775,"ts":185608247190,"ph":"R","cat":"loading,rail,devtools.timeline","args":{"frame":"0x44d2861df8","data":{"is_main_frame":true,"score":0.42,"cumulative_score":0.42}}}, + {"_comment":"Manually added event to make test CLS","name":"LayoutShift","pid":75994,"tid":775,"ts":185608247190,"ph":"R","cat":"loading,rail,devtools.timeline","args":{"frame":"0x44d2861df8","data":{"is_main_frame":true,"score":0.42,"cumulative_score":0.42,"weighted_score_delta":0.42}}}, {"pid":75994,"tid":17667,"ts":185608250604,"ph":"X","cat":"toplevel","name":"MessagePumpLibevent::OnLibeventNotification","args":{"src_file":"../../mojo/edk/system/channel_posix.cc","src_func":"StartOnIOThread"},"dur":29,"tdur":30,"tts":78849}, {"pid":75994,"tid":17667,"ts":185608250671,"ph":"X","cat":"toplevel","name":"MessagePumpLibevent::OnLibeventNotification","args":{"src_file":"../../mojo/edk/system/channel_posix.cc","src_func":"StartOnIOThread"},"dur":25,"tdur":25,"tts":78903}, {"pid":75994,"tid":17667,"ts":185608251964,"ph":"X","cat":"toplevel","name":"MessageLoop::RunTask","args":{"src_file":"../../ipc/ipc_mojo_bootstrap.cc","src_func":"SendMessage"},"dur":29,"tdur":28,"tts":78957}, diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index 1a2fc9b3546d..e7a5bdea052a 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -224,7 +224,8 @@ "type": "debugdata", "items": [ { - "finalLayoutShiftTraceEventFound": true + "cumulativeLayoutShiftMainFrame": 0.42, + "oldCumulativeLayoutShift": 0.42 } ] } @@ -1472,7 +1473,8 @@ "totalBlockingTime": 117, "maxPotentialFID": 123, "cumulativeLayoutShift": 0.42, - "cumulativeLayoutShiftAllFrames": 0.42, + "cumulativeLayoutShiftMainFrame": 0.42, + "oldCumulativeLayoutShift": 0.42, "observedTimeOrigin": 0, "observedTimeOriginTs": 185603319912, "observedNavigationStart": 0, @@ -1496,19 +1498,14 @@ "observedDomContentLoaded": 4901, "observedDomContentLoadedTs": 185608220734, "observedCumulativeLayoutShift": 0.42, - "observedCumulativeLayoutShiftAllFrames": 0.42, + "observedCumulativeLayoutShiftMainFrame": 0.42, + "observedOldCumulativeLayoutShift": 0.42, "observedFirstVisualChange": 3969, "observedFirstVisualChangeTs": 185607288912, "observedLastVisualChange": 4791, "observedLastVisualChangeTs": 185608110912, "observedSpeedIndex": 4417, - "observedSpeedIndexTs": 185607736763, - "layoutShiftAvgSessionGap5s": 0.42, - "layoutShiftMaxSessionGap1s": 0.42, - "layoutShiftMaxSessionGap1sLimit5s": 0.42, - "layoutShiftMaxSliding1s": 0.42, - "layoutShiftMaxSliding300ms": 0.42, - "layoutShiftMaxSessionGap1sLimit5sAllFrames": -1 + "observedSpeedIndexTs": 185607736763 }, { "lcpInvalidated": false @@ -6361,18 +6358,6 @@ "duration": 100, "entryType": "measure" }, - { - "startTime": 0, - "name": "lh:computed:CumulativeLayoutShiftAllFrames", - "duration": 100, - "entryType": "measure" - }, - { - "startTime": 0, - "name": "lh:computed:LayoutShiftVariants", - "duration": 100, - "entryType": "measure" - }, { "startTime": 0, "name": "lh:audit:performance-budget", diff --git a/lighthouse-treemap/app/debug.json b/lighthouse-treemap/app/debug.json index 9d5c3bd31361..46e55764043d 100644 --- a/lighthouse-treemap/app/debug.json +++ b/lighthouse-treemap/app/debug.json @@ -208,7 +208,8 @@ "type": "debugdata", "items": [ { - "finalLayoutShiftTraceEventFound": true + "cumulativeLayoutShiftMainFrame": 0.15165935601128475, + "oldCumulativeLayoutShift": 0.15165935601128475 } ] } @@ -3860,7 +3861,8 @@ "totalBlockingTime": 1012, "maxPotentialFID": 493, "cumulativeLayoutShift": 0.15165935601128475, - "cumulativeLayoutShiftAllFrames": 0.15165935601128475, + "cumulativeLayoutShiftMainFrame": 0.15165935601128475, + "oldCumulativeLayoutShift": 0.15165935601128475, "observedTimeOrigin": 0, "observedTimeOriginTs": 80765068055, "observedNavigationStart": 0, @@ -3884,18 +3886,14 @@ "observedDomContentLoaded": 1364, "observedDomContentLoadedTs": 80766432500, "observedCumulativeLayoutShift": 0.15165935601128475, - "observedCumulativeLayoutShiftAllFrames": 0.15165935601128475, + "observedCumulativeLayoutShiftMainFrame": 0.15165935601128475, + "observedOldCumulativeLayoutShift": 0.15165935601128475, "observedFirstVisualChange": 875, "observedFirstVisualChangeTs": 80765943055, "observedLastVisualChange": 1558, "observedLastVisualChangeTs": 80766626055, "observedSpeedIndex": 1026, - "observedSpeedIndexTs": 80766093860, - "layoutShiftAvgSessionGap5s": 0.15165935601128475, - "layoutShiftMaxSessionGap1s": 0.15165935601128475, - "layoutShiftMaxSessionGap1sLimit5s": 0.15165935601128475, - "layoutShiftMaxSliding1s": 0.15165935601128475, - "layoutShiftMaxSliding300ms": 0.1482421875 + "observedSpeedIndexTs": 80766093860 }, { "lcpInvalidated": false @@ -21952,18 +21950,6 @@ "duration": 0.05, "entryType": "measure" }, - { - "startTime": 23634.71, - "name": "lh:computed:CumulativeLayoutShiftAllFrames", - "duration": 0.67, - "entryType": "measure" - }, - { - "startTime": 23635.43, - "name": "lh:computed:LayoutShiftVariants", - "duration": 1.51, - "entryType": "measure" - }, { "startTime": 23638.39, "name": "lh:audit:performance-budget", diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index cf2cd8420316..31b22cd84c05 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -747,14 +747,16 @@ declare global { estimatedInputLatencyTs: number | undefined; maxPotentialFID: number | undefined; cumulativeLayoutShift: number | undefined; - cumulativeLayoutShiftAllFrames: number | undefined; + cumulativeLayoutShiftMainFrame: number | undefined; + oldCumulativeLayoutShift: number | undefined; totalBlockingTime: number; observedTimeOrigin: number; observedTimeOriginTs: number; observedNavigationStart: number; observedNavigationStartTs: number; observedCumulativeLayoutShift: number | undefined; - observedCumulativeLayoutShiftAllFrames: number | undefined; + observedCumulativeLayoutShiftMainFrame: number | undefined; + observedOldCumulativeLayoutShift: number | undefined; observedFirstPaint: number | undefined; observedFirstPaintTs: number | undefined; observedFirstContentfulPaint: number; @@ -779,12 +781,6 @@ declare global { observedLastVisualChangeTs: number; observedSpeedIndex: number; observedSpeedIndexTs: number; - layoutShiftAvgSessionGap5s: number, - layoutShiftMaxSessionGap1s: number, - layoutShiftMaxSessionGap1sLimit5s: number, - layoutShiftMaxSliding1s: number, - layoutShiftMaxSliding300ms: number, - layoutShiftMaxSessionGap1sLimit5sAllFrames: number, } export interface Form {