chrome-devtools-frontend
Version:
Chrome DevTools UI
152 lines (133 loc) • 5.62 kB
text/typescript
// 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),
});
}