UNPKG

chrome-devtools-frontend

Version:
152 lines (133 loc) 5.62 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 type * as Handlers from '../handlers/handlers.js'; import * as Helpers from '../helpers/helpers.js'; import {type SelectorTiming, SelectorTimingsKey} from '../types/TraceEvents.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 slow CSS selectors. */ title: 'CSS Selector costs', /** * @description Text to describe how to improve the performance of CSS selectors. */ description: 'If Recalculate Style costs remain high, selector optimization can reduce them. [Optimize the selectors](https://developer.chrome.com/docs/devtools/performance/selector-stats) with both high elapsed time and high slow-path %. Simpler selectors, fewer selectors, a smaller DOM, and a shallower DOM will all reduce matching costs.', /** *@description Column name for count of elements that the engine attempted to match against a style rule */ matchAttempts: 'Match attempts', /** *@description Column name for count of elements that matched a style rule */ matchCount: 'Match count', /** *@description Column name for elapsed time spent computing a style rule */ elapsed: 'Elapsed time', /** *@description Column name for the selectors that took the longest amount of time/effort. */ topSelectors: 'Top selectors', /** *@description Column name for a total sum. */ total: 'Total', /** * @description Text status indicating that no CSS selector data was found. */ enableSelectorData: 'No CSS selector data was found. CSS selector stats need to be enabled in the performance panel settings.', } as const; const str_ = i18n.i18n.registerUIStrings('models/trace/insights/SlowCSSSelector.ts', UIStrings); export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export type SlowCSSSelectorInsightModel = InsightModel<typeof UIStrings, { totalElapsedMs: Types.Timing.Milli, totalMatchAttempts: number, totalMatchCount: number, topElapsedMs: Types.Events.SelectorTiming[], topMatchAttempts: Types.Events.SelectorTiming[], }>; function aggregateSelectorStats( data: Map<Types.Events.UpdateLayoutTree, { timings: Types.Events.SelectorTiming[], }>, context: InsightSetContext): SelectorTiming[] { const selectorMap = new Map<String, SelectorTiming>(); for (const [event, value] of data) { if (event.args.beginData?.frame !== context.frameId) { continue; } if (!Helpers.Timing.eventIsInBounds(event, context.bounds)) { continue; } for (const timing of value.timings) { const key = timing[SelectorTimingsKey.Selector] + '_' + timing[SelectorTimingsKey.StyleSheetId]; const findTiming = selectorMap.get(key); if (findTiming !== undefined) { findTiming[SelectorTimingsKey.Elapsed] += timing[SelectorTimingsKey.Elapsed]; findTiming[SelectorTimingsKey.FastRejectCount] += timing[SelectorTimingsKey.FastRejectCount]; findTiming[SelectorTimingsKey.MatchAttempts] += timing[SelectorTimingsKey.MatchAttempts]; findTiming[SelectorTimingsKey.MatchCount] += timing[SelectorTimingsKey.MatchCount]; } else { selectorMap.set(key, {...timing}); } } } return [...selectorMap.values()]; } function finalize(partialModel: PartialInsightModel<SlowCSSSelectorInsightModel>): SlowCSSSelectorInsightModel { return { insightKey: InsightKeys.SLOW_CSS_SELECTOR, strings: UIStrings, title: i18nString(UIStrings.title), description: i18nString(UIStrings.description), category: InsightCategory.ALL, state: partialModel.topElapsedMs.length !== 0 && partialModel.topMatchAttempts.length !== 0 ? 'informative' : 'pass', ...partialModel, }; } export function generateInsight( parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): SlowCSSSelectorInsightModel { const selectorStatsData = parsedTrace.SelectorStats; if (!selectorStatsData) { throw new Error('no selector stats data'); } const selectorTimings = aggregateSelectorStats(selectorStatsData.dataForUpdateLayoutEvent, context); let totalElapsedUs = 0; let totalMatchAttempts = 0; let totalMatchCount = 0; selectorTimings.map(timing => { totalElapsedUs += timing[SelectorTimingsKey.Elapsed]; totalMatchAttempts += timing[SelectorTimingsKey.MatchAttempts]; totalMatchCount += timing[SelectorTimingsKey.MatchCount]; }); // sort by elapsed time const sortByElapsedMs = selectorTimings.toSorted((a, b) => { return b[SelectorTimingsKey.Elapsed] - a[SelectorTimingsKey.Elapsed]; }); // sort by match attempts const sortByMatchAttempts = selectorTimings.toSorted((a, b) => { return b[SelectorTimingsKey.MatchAttempts] - a[SelectorTimingsKey.MatchAttempts]; }); return finalize({ // TODO: should we identify UpdateLayout events as linked to this insight? relatedEvents: [], totalElapsedMs: Types.Timing.Milli(totalElapsedUs / 1000.0), totalMatchAttempts, totalMatchCount, topElapsedMs: sortByElapsedMs.slice(0, 3), topMatchAttempts: sortByMatchAttempts.slice(0, 3), }); }