UNPKG

chrome-devtools-frontend

Version:
195 lines (171 loc) • 7.79 kB
// Copyright 2024 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 i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import type * as Protocol from '../../../generated/protocol.js'; import * as Extras from '../extras/extras.js'; import type * as Handlers from '../handlers/handlers.js'; import * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; import { InsightCategory, InsightKeys, type InsightModel, type InsightSetContext, type PartialInsightModel, } from './types.js'; export const UIStrings = { /** *@description Title of an insight that provides details about Forced reflow. */ title: 'Forced reflow', /** * @description Text to describe the forced reflow. */ description: 'Many APIs, typically reading layout geometry, force the rendering engine to pause script execution in order to calculate the style and layout. Learn more about [forced reflow](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts) and its mitigations.', /** *@description Title of a list to provide related stack trace data */ relatedStackTrace: 'Stack trace', /** *@description Text to describe the top time-consuming function call */ topTimeConsumingFunctionCall: 'Top function call', /** * @description Text to describe the total reflow time */ totalReflowTime: 'Total reflow time', /** * @description Text to describe CPU processor tasks that could not be attributed to any specific source code. */ unattributed: '[unattributed]', /** * @description Text for the name of anonymous functions */ anonymous: '(anonymous)', } as const; const str_ = i18n.i18n.registerUIStrings('models/trace/insights/ForcedReflow.ts', UIStrings); export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export type ForcedReflowInsightModel = InsightModel<typeof UIStrings, { topLevelFunctionCallData: ForcedReflowAggregatedData | undefined, aggregatedBottomUpData: BottomUpCallStack[], }>; export interface BottomUpCallStack { /** * `null` indicates that this data is for unattributed force reflows. */ bottomUpData: Types.Events.CallFrame|Protocol.Runtime.CallFrame|null; totalTime: number; relatedEvents: Types.Events.Event[]; } export interface ForcedReflowAggregatedData { topLevelFunctionCall: Types.Events.CallFrame|Protocol.Runtime.CallFrame; totalReflowTime: number; topLevelFunctionCallEvents: Types.Events.Event[]; } function getCallFrameId(callFrame: Types.Events.CallFrame|Protocol.Runtime.CallFrame): string { return callFrame.scriptId + ':' + callFrame.lineNumber + ':' + callFrame.columnNumber; } function getLargestTopLevelFunctionData( forcedReflowEvents: Types.Events.Event[], traceParsedData: Handlers.Types.ParsedTrace): ForcedReflowAggregatedData| undefined { const entryToNodeMap = traceParsedData.Renderer.entryToNode; const dataByTopLevelFunction = new Map<string, ForcedReflowAggregatedData>(); if (forcedReflowEvents.length === 0) { return; } for (const event of forcedReflowEvents) { // Gather the stack traces by searching in the tree const traceNode = entryToNodeMap.get(event); if (!traceNode) { continue; } let node = traceNode.parent; let topLevelFunctionCall; let topLevelFunctionCallEvent: Types.Events.Event|undefined; while (node) { const eventData = node.entry; if (Types.Events.isProfileCall(eventData)) { topLevelFunctionCall = eventData.callFrame; topLevelFunctionCallEvent = eventData; } else { // We have finished searching bottom up data if (Types.Events.isFunctionCall(eventData) && eventData.args.data && Types.Events.objectIsCallFrame(eventData.args.data)) { topLevelFunctionCall = eventData.args.data; topLevelFunctionCallEvent = eventData; } break; } node = node.parent; } if (!topLevelFunctionCall || !topLevelFunctionCallEvent) { continue; } const aggregatedDataId = getCallFrameId(topLevelFunctionCall); const aggregatedData = Platform.MapUtilities.getWithDefault(dataByTopLevelFunction, aggregatedDataId, () => ({ topLevelFunctionCall, totalReflowTime: 0, topLevelFunctionCallEvents: [], })); aggregatedData.totalReflowTime += (event.dur ?? 0); aggregatedData.topLevelFunctionCallEvents.push(topLevelFunctionCallEvent); } let topTimeConsumingData: ForcedReflowAggregatedData|undefined = undefined; dataByTopLevelFunction.forEach(data => { if (!topTimeConsumingData || data.totalReflowTime > topTimeConsumingData.totalReflowTime) { topTimeConsumingData = data; } }); return topTimeConsumingData; } function finalize(partialModel: PartialInsightModel<ForcedReflowInsightModel>): ForcedReflowInsightModel { return { insightKey: InsightKeys.FORCED_REFLOW, strings: UIStrings, title: i18nString(UIStrings.title), description: i18nString(UIStrings.description), category: InsightCategory.ALL, state: partialModel.aggregatedBottomUpData.length !== 0 ? 'fail' : 'pass', ...partialModel, }; } function getBottomCallFrameForEvent(event: Types.Events.Event, traceParsedData: Handlers.Types.ParsedTrace): Types.Events.CallFrame|Protocol.Runtime.CallFrame|null { const profileStackTrace = Extras.StackTraceForEvent.get(event, traceParsedData); const eventStackTrace = Helpers.Trace.getZeroIndexedStackTraceInEventPayload(event); return profileStackTrace?.callFrames[0] ?? eventStackTrace?.[0] ?? null; } export function generateInsight( traceParsedData: Handlers.Types.ParsedTrace, context: InsightSetContext): ForcedReflowInsightModel { const isWithinContext = (event: Types.Events.Event): boolean => { const frameId = Helpers.Trace.frameIDForEvent(event); if (frameId !== context.frameId) { return false; } return Helpers.Timing.eventIsInBounds(event, context.bounds); }; const bottomUpDataMap = new Map<string, BottomUpCallStack>(); const events = traceParsedData.Warnings.perWarning.get('FORCED_REFLOW')?.filter(isWithinContext) ?? []; for (const event of events) { const bottomCallFrame = getBottomCallFrameForEvent(event, traceParsedData); const bottomCallId = bottomCallFrame ? getCallFrameId(bottomCallFrame) : 'UNATTRIBUTED'; const bottomUpData = Platform.MapUtilities.getWithDefault(bottomUpDataMap, bottomCallId, () => ({ bottomUpData: bottomCallFrame, totalTime: 0, relatedEvents: [], })); bottomUpData.totalTime += event.dur ?? 0; bottomUpData.relatedEvents.push(event); } const topLevelFunctionCallData = getLargestTopLevelFunctionData(events, traceParsedData); return finalize({ relatedEvents: events, topLevelFunctionCallData, aggregatedBottomUpData: [...bottomUpDataMap.values()], }); }