chrome-devtools-frontend
Version:
Chrome DevTools UI
176 lines (153 loc) • 6.62 kB
text/typescript
// Copyright 2024 The Chromium Authors
// 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 type {SelectorStatsData} from '../handlers/SelectorStatsHandler.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/performance/insights/slow-css-selector) 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.',
/**
* @description top CSS selector when ranked by elapsed time in ms
*/
topSelectorElapsedTime: 'Top selector elapsed time',
/**
* @description top CSS selector when ranked by match attempt
*/
topSelectorMatchAttempt: 'Top selector match attempt',
} as const;
const str_ = i18n.i18n.registerUIStrings('models/trace/insights/SlowCSSSelector.ts', UIStrings);
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const slowCSSSelectorThreshold = 500; // 500us threshold for slow selectors
export type SlowCSSSelectorInsightModel = InsightModel<typeof UIStrings, {
totalElapsedMs: Types.Timing.Milli,
totalMatchAttempts: number,
totalMatchCount: number,
topSelectorElapsedMs: Types.Events.SelectorTiming | null,
topSelectorMatchAttempts: Types.Events.SelectorTiming | null,
}>;
function aggregateSelectorStats(data: SelectorStatsData, context: InsightSetContext): SelectorTiming[] {
const selectorMap = new Map<String, SelectorTiming>();
for (const [event, value] of data.dataForRecalcStyleEvent) {
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),
docs: 'https://developer.chrome.com/docs/performance/insights/slow-css-selector',
category: InsightCategory.ALL,
state: partialModel.topSelectorElapsedMs && partialModel.topSelectorMatchAttempts ? 'informative' : 'pass',
...partialModel,
};
}
export function isSlowCSSSelectorInsight(model: InsightModel): model is SlowCSSSelectorInsightModel {
return model.insightKey === InsightKeys.SLOW_CSS_SELECTOR;
}
export function generateInsight(
data: Handlers.Types.HandlerData, context: InsightSetContext): SlowCSSSelectorInsightModel {
const selectorStatsData = data.SelectorStats;
if (!selectorStatsData) {
throw new Error('no selector stats data');
}
const selectorTimings = aggregateSelectorStats(selectorStatsData, 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];
});
let topSelectorElapsedMs: SelectorTiming|null = null;
let topSelectorMatchAttempts: SelectorTiming|null = null;
if (selectorTimings.length > 0) {
// find the selector with most elapsed time
topSelectorElapsedMs = selectorTimings.reduce((a, b) => {
return a[SelectorTimingsKey.Elapsed] > b[SelectorTimingsKey.Elapsed] ? a : b;
});
// check if the slowest selector is slow enough to trigger insights info
if (topSelectorElapsedMs && topSelectorElapsedMs[SelectorTimingsKey.Elapsed] < slowCSSSelectorThreshold) {
topSelectorElapsedMs = null;
}
// find the selector with most match attempts
topSelectorMatchAttempts = selectorTimings.reduce((a, b) => {
return a[SelectorTimingsKey.MatchAttempts] > b[SelectorTimingsKey.MatchAttempts] ? a : b;
});
}
return finalize({
// TODO: should we identify RecalcStyle events as linked to this insight?
relatedEvents: [],
totalElapsedMs: Types.Timing.Milli(totalElapsedUs / 1000.0),
totalMatchAttempts,
totalMatchCount,
topSelectorElapsedMs,
topSelectorMatchAttempts,
});
}
export function createOverlays(_: SlowCSSSelectorInsightModel): Types.Overlays.Overlay[] {
return [];
}