UNPKG

chrome-devtools-frontend

Version:
265 lines (222 loc) • 12.4 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import {TraceLoader} from '../../../testing/TraceLoader.js'; import * as Trace from '../trace.js'; async function processTrace(context: Mocha.Suite|Mocha.Context|null, url: string): Promise<void> { Trace.Handlers.ModelHandlers.Meta.reset(); Trace.Handlers.ModelHandlers.LayoutShifts.reset(); try { const events = await TraceLoader.rawEvents(context, url); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); Trace.Handlers.ModelHandlers.Screenshots.handleEvent(event); Trace.Handlers.ModelHandlers.LayoutShifts.handleEvent(event); } } catch (error) { assert.fail(error); } await Trace.Handlers.ModelHandlers.Meta.finalize(); await Trace.Handlers.ModelHandlers.Screenshots.finalize(); await Trace.Handlers.ModelHandlers.LayoutShifts.finalize(); } describe('LayoutShiftsHandler', function() { beforeEach(async () => { // The layout shifts handler stores by process, so to make life easier we // run the meta handler here, too, so that later on we can get the IDs of // the main renderer process and thread. Trace.Handlers.ModelHandlers.Meta.reset(); Trace.Handlers.ModelHandlers.LayoutShifts.reset(); }); it('clusters a single frame correctly', async function() { await processTrace(this, 'cls-single-frame.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); assert.lengthOf(layoutShifts.clusters, 1); assert.strictEqual(layoutShifts.clusters[0].clusterCumulativeScore, 0.29522728495836237); }); it('creates a cluster after the maximum time gap between shifts', async function() { await processTrace(this, 'cls-cluster-max-timeout.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); assert.lengthOf(layoutShifts.clusters, 3); // The first cluster should end because the maximum time gap between // shifts ends, and thus the time between the last shift and the window // end should be exactly MAX_SHIFT_TIME_DELTA; const firstCluster = layoutShifts.clusters[0]; const firstClusterEvents = layoutShifts.clusters[0].events; assert.strictEqual( firstCluster.clusterWindow.max - firstClusterEvents[firstClusterEvents.length - 1].ts, Trace.Handlers.ModelHandlers.LayoutShifts.MAX_SHIFT_TIME_DELTA); // There are seven shifts in quick succession in the first cluster, // only one shift in the second cluster and only one shift in the // third cluster. assert.lengthOf(layoutShifts.clusters[0].events, 7); assert.lengthOf(layoutShifts.clusters[1].events, 1); assert.lengthOf(layoutShifts.clusters[2].events, 1); }); it('creates a cluster after a navigation', async function() { await processTrace(this, 'cls-cluster-navigation.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); const {navigationsByFrameId, mainFrameId} = Trace.Handlers.ModelHandlers.Meta.data(); const navigations = navigationsByFrameId.get(mainFrameId); if (!navigations || navigations.length === 0) { assert.fail('No navigations found'); } assert.strictEqual(layoutShifts.clusters[0].clusterWindow.max, navigations[0].ts); // The first cluster happens before any navigation assert.strictEqual(layoutShifts.clusters[0].navigationId, Trace.Types.Events.NO_NAVIGATION); // We should see an initial cluster here from the first layout shifts, // followed by 1 for each of the navigations themselves. assert.strictEqual(layoutShifts.clusters.length, navigations.length + 1); const secondCluster = layoutShifts.clusters[1]; // The second cluster should be marked to start at the first shift timestamp. assert.strictEqual(secondCluster.clusterWindow.min, secondCluster.events[0].ts); // The second cluster happened after the first navigation, so it should // have navigationId set to the ID of the first navigation assert.isDefined(secondCluster.navigationId); assert.strictEqual(secondCluster.navigationId, navigations[0].args.data?.navigationId); }); it('creates a cluster after exceeding the continuous shift limit', async function() { await processTrace(this, 'cls-cluster-max-duration.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); assert.lengthOf(layoutShifts.clusters, 2); // Cluster must be closed as soon as MAX_CLUSTER_DURATION is reached, even if // there is a gap greater than MAX_SHIFT_TIME_DELTA right after the max window // length happens. assert.strictEqual( layoutShifts.clusters[0].clusterWindow.max - layoutShifts.clusters[0].clusterWindow.min, Trace.Handlers.ModelHandlers.LayoutShifts.MAX_CLUSTER_DURATION); }); it('sets the end of the last session window to the trace end time correctly', async function() { await processTrace(this, 'cls-cluster-max-duration.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); assert.strictEqual( layoutShifts.clusters.at(-1)?.clusterWindow.max, Trace.Handlers.ModelHandlers.Meta.data().traceBounds.max); }); it('sets the end of the last session window to the max gap between duration correctly', async function() { await processTrace(this, 'cls-cluster-max-timeout.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); const lastWindow = layoutShifts.clusters.at(-1)?.clusterWindow; const lastShiftInWindow = layoutShifts.clusters.at(-1)?.events.at(-1); if (!lastWindow) { assert.fail('Session window not found.'); } if (!lastShiftInWindow) { assert.fail('Session window not found.'); } assert.strictEqual( lastWindow.max, lastShiftInWindow.ts + Trace.Handlers.ModelHandlers.LayoutShifts.MAX_SHIFT_TIME_DELTA); assert.isBelow(lastWindow.range, Trace.Handlers.ModelHandlers.LayoutShifts.MAX_CLUSTER_DURATION); }); it('sets the end of the last session window to the max session duration correctly', async function() { await processTrace(this, 'cls-last-cluster-max-duration.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); const lastWindow = layoutShifts.clusters.at(-1)?.clusterWindow; const lastShiftInWindow = layoutShifts.clusters.at(-1)?.events.at(-1); if (!lastWindow) { assert.fail('Session window not found.'); } if (!lastShiftInWindow) { assert.fail('Session window not found.'); } assert.strictEqual(lastWindow.range, Trace.Handlers.ModelHandlers.LayoutShifts.MAX_CLUSTER_DURATION); }); it('demarcates cluster score windows correctly', async function() { await processTrace(this, 'cls-multiple-frames.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); assert.lengthOf(layoutShifts.clusters, 5); for (const cluster of layoutShifts.clusters) { let clusterScore = 0; for (const event of cluster.events) { const scoreBeforeEvent = clusterScore; clusterScore += event.args.data ? event.args.data.weighted_score_delta : 0; // Here we've crossed the threshold from Good to NI (but not Bad) so // check that both the Good and NI windows values are set as expected. if (scoreBeforeEvent < Trace.Handlers.ModelHandlers.LayoutShifts.LayoutShiftsThreshold.NEEDS_IMPROVEMENT && clusterScore >= Trace.Handlers.ModelHandlers.LayoutShifts.LayoutShiftsThreshold.NEEDS_IMPROVEMENT && clusterScore < Trace.Handlers.ModelHandlers.LayoutShifts.LayoutShiftsThreshold.BAD) { assert.strictEqual(cluster.scoreWindows.good.max, event.ts - 1); if (!cluster.scoreWindows.needsImprovement) { assert.fail('No Needs Improvement window'); } assert.strictEqual(cluster.scoreWindows.needsImprovement.min, event.ts); } // Here we have transitioned from either Good or NI to Bad, so // again we assert that the Bad window starts when expected, // and that either the NI or Good window finishes just prior. if (scoreBeforeEvent < Trace.Handlers.ModelHandlers.LayoutShifts.LayoutShiftsThreshold.BAD && clusterScore >= Trace.Handlers.ModelHandlers.LayoutShifts.LayoutShiftsThreshold.BAD) { if (!cluster.scoreWindows.bad) { assert.fail('No Bad window'); } if (cluster.scoreWindows.needsImprovement) { assert.strictEqual(cluster.scoreWindows.needsImprovement.max, event.ts - 1); } else { assert.strictEqual(cluster.scoreWindows.good.max, event.ts - 1); } assert.strictEqual(cluster.scoreWindows.bad.min, event.ts); } } } }); it('calculates Cumulative Layout Shift correctly for multiple session windows', async function() { await processTrace(this, 'cls-cluster-max-timeout.json.gz'); const layoutShifts = Trace.Handlers.ModelHandlers.LayoutShifts.data(); assert.lengthOf(layoutShifts.clusters, 3); let globalCLS = 0; let clusterCount = 1; let clusterWithCLS = 0; for (const cluster of layoutShifts.clusters) { let clusterCumulativeScore = 0; for (const shift of cluster.events) { clusterCumulativeScore += shift.args.data?.weighted_score_delta || 0; // Test the cumulative score until this shift. assert.strictEqual(shift.parsedData.cumulativeWeightedScoreInWindow, clusterCumulativeScore); // Test the score of this shift's session window. assert.strictEqual(shift.parsedData.sessionWindowData.cumulativeWindowScore, cluster.clusterCumulativeScore); // Test the id of this shift's session window. assert.strictEqual(shift.parsedData.sessionWindowData.id, clusterCount); } clusterCount++; // Test the accumulated assert.strictEqual(cluster.clusterCumulativeScore, clusterCumulativeScore); if (cluster.clusterCumulativeScore > globalCLS) { globalCLS = cluster.clusterCumulativeScore; clusterWithCLS = clusterCount - 1; } } // Test the calculated CLS. assert.strictEqual(layoutShifts.sessionMaxScore, globalCLS); assert.strictEqual(layoutShifts.clsWindowID, clusterWithCLS); }); it('calculates worst shift correctly for clusters', async function() { await processTrace(this, 'cls-cluster-max-timeout.json.gz'); const clusters = Trace.Handlers.ModelHandlers.LayoutShifts.data().clusters; assert.isNotEmpty(clusters); for (const cluster of clusters) { // Get the max shift score from the list of layout shifts. const maxShiftScore = Math.max(...cluster.events.map(s => s.args.data?.weighted_score_delta ?? 0)); const gotShift = cluster.worstShiftEvent as Trace.Types.Events.SyntheticLayoutShift; assert.isNotNull(gotShift); // Make sure the worstShiftEvent's data matches the maxShiftScore. assert.strictEqual(gotShift.args.data?.weighted_score_delta ?? 0, maxShiftScore); } }); it('correctly calculates the duration and start time of the clusters', async function() { await processTrace(this, 'cls-cluster-max-timeout.json.gz'); const clusters = Trace.Handlers.ModelHandlers.LayoutShifts.data().clusters; assert.isNotEmpty(clusters); for (const cluster of clusters) { // Earliest and latest layout shifts should match. const earliestLayoutShiftTs = Math.min(...cluster.events.map(s => s.ts)); assert.strictEqual(cluster.events[0].ts, earliestLayoutShiftTs); const latestLayoutShiftTs = Math.max(...cluster.events.map(s => s.ts)); assert.strictEqual(cluster.events[cluster.events.length - 1].ts, latestLayoutShiftTs); // earliest layout shift ts should be the cluster's ts. assert.strictEqual(cluster.ts, earliestLayoutShiftTs); const lastShiftTimings = Trace.Helpers.Timing.eventTimingsMicroSeconds(cluster.events[cluster.events.length - 1]); const wantEndTime = lastShiftTimings.endTime + Trace.Handlers.ModelHandlers.LayoutShifts.MAX_SHIFT_TIME_DELTA; const dur = Trace.Types.Timing.Micro(wantEndTime - earliestLayoutShiftTs); assert.strictEqual(cluster.dur || 0, dur); } }); });