UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

320 lines 16.3 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 * as Helpers from '../helpers/helpers.js'; import { data as metaHandlerData } from './MetaHandler.js'; import { data as screenshotsHandlerData } from './ScreenshotsHandler.js'; import * as Platform from '../../../core/platform/platform.js'; import * as Types from '../types/types.js'; // This represents the maximum #time we will allow a cluster to go before we // reset it. export const MAX_CLUSTER_DURATION = Helpers.Timing.millisecondsToMicroseconds(Types.Timing.MilliSeconds(5000)); // This represents the maximum #time we will allow between layout shift events // before considering it to be the start of a new cluster. export const MAX_SHIFT_TIME_DELTA = Helpers.Timing.millisecondsToMicroseconds(Types.Timing.MilliSeconds(1000)); // Layout shifts are reported globally to the developer, irrespective of which // frame they originated in. However, each process does have its own individual // CLS score, so we need to segment by process. This means Layout Shifts from // sites with one process (no subframes, or subframes from the same origin) // will be reported together. In the case of multiple renderers (frames across // different origins), we offer the developer the ability to switch renderer in // the UI. const layoutShiftEvents = []; // These events denote potential node resizings. We store them to link captured // layout shifts to the resizing of unsized elements. const layoutInvalidationEvents = []; const styleRecalcInvalidationEvents = []; // Layout shifts happen during PrePaint as part of the rendering lifecycle. // We determine if a LayoutInvalidation event is a potential root cause of a layout // shift if the next PrePaint after the LayoutInvalidation is the parent // node of such shift. const prePaintEvents = []; let sessionMaxScore = 0; let clsWindowID = -1; const clusters = []; // The complete timeline of LS score changes in a trace. // Includes drops to 0 when session windows end. const scoreRecords = []; let handlerState = 1 /* HandlerState.UNINITIALIZED */; export function initialize() { if (handlerState !== 1 /* HandlerState.UNINITIALIZED */) { throw new Error('LayoutShifts Handler was not reset'); } handlerState = 2 /* HandlerState.INITIALIZED */; } export function reset() { handlerState = 1 /* HandlerState.UNINITIALIZED */; layoutShiftEvents.length = 0; layoutInvalidationEvents.length = 0; prePaintEvents.length = 0; clusters.length = 0; sessionMaxScore = 0; scoreRecords.length = 0; clsWindowID = -1; } export function handleEvent(event) { if (handlerState !== 2 /* HandlerState.INITIALIZED */) { throw new Error('Handler is not initialized'); } if (Types.TraceEvents.isTraceEventLayoutShift(event) && !event.args.data?.had_recent_input) { layoutShiftEvents.push(event); return; } if (Types.TraceEvents.isTraceEventLayoutInvalidation(event)) { layoutInvalidationEvents.push(event); return; } if (Types.TraceEvents.isTraceEventStyleRecalcInvalidation(event)) { styleRecalcInvalidationEvents.push(event); } if (Types.TraceEvents.isTraceEventPrePaint(event)) { prePaintEvents.push(event); return; } } function traceWindowFromTime(time) { return { min: time, max: time, range: Types.Timing.MicroSeconds(0), }; } function updateTraceWindowMax(traceWindow, newMax) { traceWindow.max = newMax; traceWindow.range = Types.Timing.MicroSeconds(traceWindow.max - traceWindow.min); } function findNextScreenshotSource(timestamp) { const screenshots = screenshotsHandlerData(); const screenshotIndex = findNextScreenshotEventIndex(screenshots, timestamp); if (!screenshotIndex) { return undefined; } return `data:img/png;base64,${screenshots[screenshotIndex].args.snapshot}`; } export function findNextScreenshotEventIndex(screenshots, timestamp) { return Platform.ArrayUtilities.nearestIndexFromBeginning(screenshots, frame => frame.ts > timestamp); } function buildScoreRecords() { const { traceBounds } = metaHandlerData(); scoreRecords.push({ ts: traceBounds.min, score: 0 }); for (const cluster of clusters) { let clusterScore = 0; if (cluster.events[0].args.data) { scoreRecords.push({ ts: cluster.clusterWindow.min, score: cluster.events[0].args.data.weighted_score_delta }); } for (let i = 0; i < cluster.events.length; i++) { const event = cluster.events[i]; if (!event.args.data) { continue; } clusterScore += event.args.data.weighted_score_delta; scoreRecords.push({ ts: event.ts, score: clusterScore }); } scoreRecords.push({ ts: cluster.clusterWindow.max, score: 0 }); } } export async function finalize() { // Ensure the events are sorted by #time ascending. layoutShiftEvents.sort((a, b) => a.ts - b.ts); prePaintEvents.sort((a, b) => a.ts - b.ts); layoutInvalidationEvents.sort((a, b) => a.ts - b.ts); // Each function transforms the data used by the next, as such the invoke order // is important. await buildLayoutShiftsClusters(); buildScoreRecords(); handlerState = 3 /* HandlerState.FINALIZED */; } async function buildLayoutShiftsClusters() { const { navigationsByFrameId, mainFrameId, traceBounds } = metaHandlerData(); const navigations = navigationsByFrameId.get(mainFrameId) || []; if (layoutShiftEvents.length === 0) { return; } let firstShiftTime = layoutShiftEvents[0].ts; let lastShiftTime = layoutShiftEvents[0].ts; let lastShiftNavigation = null; // Now step through each and create clusters. // A cluster is equivalent to a session window (see https://web.dev/cls/#what-is-cls). // To make the line chart clear, we explicitly demark the limits of each session window // by starting the cumulative score of the window at the time of the first layout shift // and ending it (dropping the line back to 0) when the window ends according to the // thresholds (MAX_CLUSTER_DURATION, MAX_SHIFT_TIME_DELTA). for (const event of layoutShiftEvents) { // First detect if either the cluster duration or the #time between this and // the last shift has been exceeded. const clusterDurationExceeded = event.ts - firstShiftTime > MAX_CLUSTER_DURATION; const maxTimeDeltaSinceLastShiftExceeded = event.ts - lastShiftTime > MAX_SHIFT_TIME_DELTA; // Next take a look at navigations. If between this and the last shift we have navigated, // note it. const currentShiftNavigation = Platform.ArrayUtilities.nearestIndexFromEnd(navigations, nav => nav.ts < event.ts); const hasNavigated = lastShiftNavigation !== currentShiftNavigation && currentShiftNavigation !== null; // If any of the above criteria are met or if we don't have any cluster yet we should // start a new one. if (clusterDurationExceeded || maxTimeDeltaSinceLastShiftExceeded || hasNavigated || !clusters.length) { // The cluster starts #time should be the timestamp of the first layout shift in it. const clusterStartTime = event.ts; // If the last session window ended because the max delta time between shifts // was exceeded set the endtime to MAX_SHIFT_TIME_DELTA microseconds after the // last shift in the session. const endTimeByMaxSessionDuration = clusterDurationExceeded ? firstShiftTime + MAX_CLUSTER_DURATION : Infinity; // If the last session window ended because the max session duration was // surpassed, set the endtime so that the window length = MAX_CLUSTER_DURATION; const endTimeByMaxShiftGap = maxTimeDeltaSinceLastShiftExceeded ? lastShiftTime + MAX_SHIFT_TIME_DELTA : Infinity; // If there was a navigation during the last window, close it at the time // of the navigation. const endTimeByNavigation = hasNavigated ? navigations[currentShiftNavigation].ts : Infinity; // End the previous cluster at the time of the first of the criteria above that was met. const previousClusterEndTime = Math.min(endTimeByMaxSessionDuration, endTimeByMaxShiftGap, endTimeByNavigation); // If there is an existing cluster update its closing time. if (clusters.length > 0) { const currentCluster = clusters[clusters.length - 1]; updateTraceWindowMax(currentCluster.clusterWindow, Types.Timing.MicroSeconds(previousClusterEndTime)); } clusters.push({ events: [], clusterWindow: traceWindowFromTime(clusterStartTime), clusterCumulativeScore: 0, scoreWindows: { good: traceWindowFromTime(clusterStartTime), needsImprovement: null, bad: null, }, }); firstShiftTime = clusterStartTime; } // Given the above we should have a cluster available, so pick the most // recent one and append the shift, bump its score and window values accordingly. const currentCluster = clusters[clusters.length - 1]; const timeFromNavigation = currentShiftNavigation !== null ? Types.Timing.MicroSeconds(event.ts - navigations[currentShiftNavigation].ts) : undefined; currentCluster.clusterCumulativeScore += event.args.data ? event.args.data.weighted_score_delta : 0; if (!event.args.data) { continue; } const shift = { ...event, args: { frame: event.args.frame, data: { ...event.args.data, rawEvent: event, }, }, parsedData: { screenshotSource: findNextScreenshotSource(event.ts), timeFromNavigation, cumulativeWeightedScoreInWindow: currentCluster.clusterCumulativeScore, // The score of the session window is temporarily set to 0 just // to initialize it. Since we need to get the score of all shifts // in the session window to determine its value, its definite // value is set when stepping through the built clusters. sessionWindowData: { cumulativeWindowScore: 0, id: clusters.length }, }, }; currentCluster.events.push(shift); updateTraceWindowMax(currentCluster.clusterWindow, event.ts); lastShiftTime = event.ts; lastShiftNavigation = currentShiftNavigation; } // Now step through each cluster and set up the times at which the value // goes from Good, to needs improvement, to Bad. Note that if there is a // large jump we may go from Good to Bad without ever creating a Needs // Improvement window at all. for (const cluster of clusters) { let weightedScore = 0; let windowID = -1; // If this is the last cluster update its window. The cluster duration is determined // by the minimum between: time to next navigation, trace end time, time to maximum // cluster duration and time to maximum gap between layout shifts. if (cluster === clusters[clusters.length - 1]) { const clusterEndByMaxDuration = MAX_CLUSTER_DURATION + cluster.clusterWindow.min; const clusterEndByMaxGap = cluster.clusterWindow.max + MAX_SHIFT_TIME_DELTA; const nextNavigationIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(navigations, nav => nav.ts > cluster.clusterWindow.max); const nextNavigationTime = nextNavigationIndex ? navigations[nextNavigationIndex].ts : Infinity; const clusterEnd = Math.min(clusterEndByMaxDuration, clusterEndByMaxGap, traceBounds.max, nextNavigationTime); updateTraceWindowMax(cluster.clusterWindow, Types.Timing.MicroSeconds(clusterEnd)); } for (const shift of cluster.events) { weightedScore += shift.args.data ? shift.args.data.weighted_score_delta : 0; windowID = shift.parsedData.sessionWindowData.id; const ts = shift.ts; // Update the the CLS score of this shift's session window now that // we have it. shift.parsedData.sessionWindowData.cumulativeWindowScore = cluster.clusterCumulativeScore; if (weightedScore < 0.1 /* LayoutShiftsThreshold.NEEDS_IMPROVEMENT */) { // Expand the Good window. updateTraceWindowMax(cluster.scoreWindows.good, ts); } else if (weightedScore >= 0.1 /* LayoutShiftsThreshold.NEEDS_IMPROVEMENT */ && weightedScore < 0.25 /* LayoutShiftsThreshold.BAD */) { if (!cluster.scoreWindows.needsImprovement) { // Close the Good window, and open the needs improvement window. updateTraceWindowMax(cluster.scoreWindows.good, Types.Timing.MicroSeconds(ts - 1)); cluster.scoreWindows.needsImprovement = traceWindowFromTime(ts); } // Expand the needs improvement window. updateTraceWindowMax(cluster.scoreWindows.needsImprovement, ts); } else if (weightedScore >= 0.25 /* LayoutShiftsThreshold.BAD */) { if (!cluster.scoreWindows.bad) { // We may jump from Good to Bad here, so update whichever window is open. if (cluster.scoreWindows.needsImprovement) { updateTraceWindowMax(cluster.scoreWindows.needsImprovement, Types.Timing.MicroSeconds(ts - 1)); } else { updateTraceWindowMax(cluster.scoreWindows.good, Types.Timing.MicroSeconds(ts - 1)); } cluster.scoreWindows.bad = traceWindowFromTime(shift.ts); } // Expand the Bad window. updateTraceWindowMax(cluster.scoreWindows.bad, ts); } // At this point the windows are set by the timestamps of the events, but the // next cluster begins at the timestamp of its first event. As such we now // need to expand the score window to the end of the cluster, and we do so // by using the Bad widow if it's there, or the NI window, or finally the // Good window. if (cluster.scoreWindows.bad) { updateTraceWindowMax(cluster.scoreWindows.bad, cluster.clusterWindow.max); } else if (cluster.scoreWindows.needsImprovement) { updateTraceWindowMax(cluster.scoreWindows.needsImprovement, cluster.clusterWindow.max); } else { updateTraceWindowMax(cluster.scoreWindows.good, cluster.clusterWindow.max); } } if (weightedScore > sessionMaxScore) { clsWindowID = windowID; sessionMaxScore = weightedScore; } } } export function data() { if (handlerState !== 3 /* HandlerState.FINALIZED */) { throw new Error('Layout Shifts Handler is not finalized'); } return { clusters: [...clusters], sessionMaxScore: sessionMaxScore, clsWindowID, prePaintEvents: [...prePaintEvents], layoutInvalidationEvents: [...layoutInvalidationEvents], styleRecalcInvalidationEvents: [], scoreRecords: [...scoreRecords], }; } export function deps() { return ['Screenshots', 'Meta']; } export function stateForLayoutShiftScore(score) { let state = "good" /* ScoreClassification.GOOD */; if (score >= 0.1 /* LayoutShiftsThreshold.NEEDS_IMPROVEMENT */) { state = "ok" /* ScoreClassification.OK */; } if (score >= 0.25 /* LayoutShiftsThreshold.BAD */) { state = "bad" /* ScoreClassification.BAD */; } return state; } //# sourceMappingURL=LayoutShiftsHandler.js.map