Skip to content

Commit

Permalink
[DOMStats] Add DOM stats to sidebar insight
Browse files Browse the repository at this point in the history
This does not include element links yet.

https://screenshot.googleplex.com/JqnQRp97zxj8XGD

Bug: 372897811
Change-Id: I5a2a5d47bbfa8d32e3b05e9064243d245227ca53
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6172395
Reviewed-by: Paul Irish <[email protected]>
Commit-Queue: Adam Raine <[email protected]>
  • Loading branch information
Adam Raine authored and Devtools-frontend LUCI CQ committed Jan 16, 2025
1 parent 6b80ee8 commit 9611624
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 29 deletions.
1 change: 1 addition & 0 deletions config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,7 @@ grd_files_debug_sources = [
"front_end/models/trace/handlers/AnimationHandler.js",
"front_end/models/trace/handlers/AsyncJSCallsHandler.js",
"front_end/models/trace/handlers/AuctionWorkletsHandler.js",
"front_end/models/trace/handlers/DOMStatsHandler.js",
"front_end/models/trace/handlers/ExtensionTraceDataHandler.js",
"front_end/models/trace/handlers/FlowsHandler.js",
"front_end/models/trace/handlers/FramesHandler.js",
Expand Down
2 changes: 2 additions & 0 deletions front_end/models/trace/handlers/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ devtools_module("handlers") {
"AnimationHandler.ts",
"AsyncJSCallsHandler.ts",
"AuctionWorkletsHandler.ts",
"DOMStatsHandler.ts",
"ExtensionTraceDataHandler.ts",
"FlowsHandler.ts",
"FramesHandler.ts",
Expand Down Expand Up @@ -73,6 +74,7 @@ ts_library("unittests") {
"AnimationHandler.test.ts",
"AsyncJSCallsHandler.test.ts",
"AuctionWorkletsHandler.test.ts",
"DOMStatsHandler.test.ts",
"ExtensionTraceDataHandler.test.ts",
"FlowsHandler.test.ts",
"FramesHandler.test.ts",
Expand Down
30 changes: 30 additions & 0 deletions front_end/models/trace/handlers/DOMStatsHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2025 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 {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../trace.js';

describeWithEnvironment('DOMStatsHandler', () => {
beforeEach(() => {
Trace.Handlers.ModelHandlers.DOMStats.reset();
Trace.Handlers.ModelHandlers.Meta.reset();
});

it('should get DOM stats for each frame', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multi-frame-dom-stats.json.gz');

const {mainFrameId} = parsedTrace.Meta;
const {domStatsByFrameId} = parsedTrace.DOMStats;

assert.strictEqual(domStatsByFrameId.size, 2);
const mainFrameStats = domStatsByFrameId.get(mainFrameId)!.at(-1);
const mainFrameData = mainFrameStats!.args.data;
assert.strictEqual(mainFrameData.totalElements, 7);
assert.strictEqual(mainFrameData.maxDepth!.depth, 3);
assert.strictEqual(mainFrameData.maxDepth!.nodeName, 'DIV id=\'child\'');
assert.strictEqual(mainFrameData.maxChildren!.numChildren, 4);
assert.strictEqual(mainFrameData.maxChildren!.nodeName, 'BODY');
});
});
31 changes: 31 additions & 0 deletions front_end/models/trace/handlers/DOMStatsHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2025 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 Platform from '../../../core/platform/platform.js';
import * as Types from '../types/types.js';

export interface DOMStatsData {
domStatsByFrameId: Map<string, Types.Events.DOMStats[]>;
}

const domStatsByFrameId: DOMStatsData['domStatsByFrameId'] = new Map();

export function reset(): void {
domStatsByFrameId.clear();
}

export function handleEvent(event: Types.Events.Event): void {
if (!Types.Events.isDOMStats(event)) {
return;
}
const domStatEvents = Platform.MapUtilities.getWithDefault(domStatsByFrameId, event.args.data.frame, () => []);
domStatEvents.push(event);
}

export async function finalize(): Promise<void> {
}

export function data(): DOMStatsData {
return {domStatsByFrameId};
}
1 change: 1 addition & 0 deletions front_end/models/trace/handlers/ModelHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * as AnimationFrames from './AnimationFramesHandler.js';
export * as Animations from './AnimationHandler.js';
export * as AsyncJSCalls from './AsyncJSCallsHandler.js';
export * as AuctionWorklets from './AuctionWorkletsHandler.js';
export * as DOMStats from './DOMStatsHandler.js';
export * as ExtensionTraceData from './ExtensionTraceDataHandler.js';
export * as Flows from './FlowsHandler.js';
export * as Frames from './FramesHandler.js';
Expand Down
53 changes: 33 additions & 20 deletions front_end/models/trace/insights/DOMSize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,39 @@ import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {getFirstOrError, getInsightOrError, processTrace} from '../../../testing/InsightHelpers.js';

describeWithEnvironment('DOMSize', function() {
it('finds layout reflows and style recalcs affected by DOM size',
async () => {
const {data, insights} = await processTrace(this, 'dom-size.json.gz');
// Processing traces in this file can take a while due to a performance bottleneck
// b/38254550
this.timeout(30_000);

// 1 large DOM update was triggered before the first navigation
{
const insight = getInsightOrError('DOMSize', insights);
assert.lengthOf(insight.largeLayoutUpdates, 1);
assert.lengthOf(insight.largeStyleRecalcs, 1);
}
it('finds layout reflows and style recalcs affected by DOM size', async () => {
const {data, insights} = await processTrace(this, 'dom-size.json.gz');

// 1 large DOM update was triggered after the first navigation
{
const insight =
getInsightOrError('DOMSize', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
assert.lengthOf(insight.largeLayoutUpdates, 1);
assert.lengthOf(insight.largeStyleRecalcs, 1);
}
})
// Processing the above trace can take a while due to a performance bottleneck
// b/382545507
.timeout(30_000);
// 1 large DOM update was triggered before the first navigation
{
const insight = getInsightOrError('DOMSize', insights);
assert.lengthOf(insight.largeLayoutUpdates, 1);
assert.lengthOf(insight.largeStyleRecalcs, 1);
}

// 1 large DOM update was triggered after the first navigation
{
const insight =
getInsightOrError('DOMSize', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
assert.lengthOf(insight.largeLayoutUpdates, 1);
assert.lengthOf(insight.largeStyleRecalcs, 1);
}
});

it('finds largest DOM stats event', async () => {
const {data, insights} = await processTrace(this, 'multi-frame-dom-stats.json.gz');

const insight =
getInsightOrError('DOMSize', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
const domStats = insight.maxDOMStats!.args.data;
assert.strictEqual(domStats.totalElements, 7);
assert.strictEqual(domStats.maxDepth!.depth, 3);
assert.strictEqual(domStats.maxDepth!.nodeName, 'DIV id=\'child\'');
assert.strictEqual(domStats.maxChildren!.numChildren, 4);
assert.strictEqual(domStats.maxChildren!.nodeName, 'BODY');
});
});
16 changes: 13 additions & 3 deletions front_end/models/trace/insights/DOMSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const UIStrings = {
* @description Description of an insight that recommends reducing the size of the DOM tree as a means to improve page responsiveness. "DOM" is an acronym and should not be translated. "layout reflows" are when the browser will recompute the layout of content on the page.
*/
description:
'A large DOM will increase memory usage, cause longer style calculations, and produce costly layout reflows which impact page responsiveness. [Learn how to avoid an excessive DOM size](https://developer.chrome.com/docs/lighthouse/performance/dom-size/).',
'A large DOM can increase the duration of style calculations and layout reflows, impacting page responsiveness. A large DOM will also increase memory usage. [Learn how to avoid an excessive DOM size](https://developer.chrome.com/docs/lighthouse/performance/dom-size/).',
};

const str_ = i18n.i18n.registerUIStrings('models/trace/insights/DOMSize.ts', UIStrings);
Expand All @@ -29,10 +29,11 @@ const DOM_UPDATE_LIMIT = 800;
export type DOMSizeInsightModel = InsightModel<{
largeLayoutUpdates: Types.Events.Layout[],
largeStyleRecalcs: Types.Events.UpdateLayoutTree[],
maxDOMStats?: Types.Events.DOMStats,
}>;

export function deps(): ['Renderer', 'AuctionWorklets'] {
return ['Renderer', 'AuctionWorklets'];
export function deps(): ['Renderer', 'AuctionWorklets', 'DOMStats'] {
return ['Renderer', 'AuctionWorklets', 'DOMStats'];
}

function finalize(partialModel: Omit<DOMSizeInsightModel, 'title'|'description'|'category'|'shouldShow'>):
Expand Down Expand Up @@ -114,8 +115,17 @@ export function generateInsight(
}
}

const domStatsEvents = parsedTrace.DOMStats.domStatsByFrameId.get(context.frameId)?.filter(isWithinContext) ?? [];
let maxDOMStats: Types.Events.DOMStats|undefined;
for (const domStats of domStatsEvents) {
if (!maxDOMStats || domStats.args.data.totalElements > maxDOMStats.args.data.totalElements) {
maxDOMStats = domStats;
}
}

return finalize({
largeLayoutUpdates,
largeStyleRecalcs,
maxDOMStats,
});
}
24 changes: 24 additions & 0 deletions front_end/models/trace/types/TraceEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,26 @@ export interface Instant extends Event {
s: Scope;
}

export interface DOMStats extends Instant {
name: 'DOMStats';
args: Args&{
data: ArgsData & {
frame: string,
totalElements: number,
maxChildren?: {
nodeId: Protocol.DOM.BackendNodeId,
nodeName: string,
numChildren: number,
},
maxDepth?: {
nodeId: Protocol.DOM.BackendNodeId,
nodeName: string,
depth: number,
},
},
};
}

export interface UpdateCounters extends Instant {
name: 'UpdateCounters';
args: Args&{
Expand Down Expand Up @@ -1988,6 +2008,10 @@ export function isUpdateCounters(event: Event): event is UpdateCounters {
return event.name === 'UpdateCounters';
}

export function isDOMStats(event: Event): event is DOMStats {
return event.name === 'DOMStats';
}

export function isThreadName(
event: Event,
): event is ThreadName {
Expand Down
43 changes: 38 additions & 5 deletions front_end/panels/timeline/components/insights/DOMSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,37 @@
// found in the LICENSE file.

import '../../../../ui/components/icon_button/icon_button.js';
import './Table.js';

import * as i18n from '../../../../core/i18n/i18n.js';
import type {DOMSizeInsightModel} from '../../../../models/trace/insights/DOMSize.js';
import * as LitHtml from '../../../../ui/lit-html/lit-html.js';
import type * as Overlays from '../../overlays/overlays.js';

import {BaseInsightComponent} from './BaseInsightComponent.js';
import type {TableData} from './Table.js';

const UIStrings = {
/**
* @description Text status indicating that browser operations to re-render the page were not impacted by the size of the DOM. "DOM" is an acronym and should not be translated.
* @description Header for a column containing the names of statistics as opposed to the actual statistic values.
*/
noLargeRenderTasks: 'No rendering tasks impacted by DOM size',
statistic: 'Statistic',
/**
* @description Header for a column containing the value of a statistic.
*/
value: 'Value',
/**
* @description Label for a value representing the total number of elements on the page.
*/
totalElements: 'Total elements',
/**
* @description Label for a value representing the maximum depth of the Document Object Model (DOM). "DOM" is a acronym and should not be translated.
*/
maxDOMDepth: 'DOM depth',
/**
* @description Label for a value representing the maximum number of child elements of any parent element on the page.
*/
maxChildren: 'Most children',
};

const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/insights/DOMSize.ts', UIStrings);
Expand Down Expand Up @@ -45,11 +63,26 @@ export class DOMSize extends BaseInsightComponent<DOMSizeInsightModel> {
return LitHtml.nothing;
}

if (!this.model.largeStyleRecalcs.length && !this.model.largeLayoutUpdates.length) {
return html`<div class="insight-section">${i18nString(UIStrings.noLargeRenderTasks)}</div>`;
const domStatsData = this.model.maxDOMStats?.args.data;
if (!domStatsData) {
return LitHtml.nothing;
}

return LitHtml.nothing;
// clang-format off
return html`<div class="insight-section">
<devtools-performance-table
.data=${{
insight: this,
headers: [i18nString(UIStrings.statistic), i18nString(UIStrings.value)],
rows: [
{values: [i18nString(UIStrings.totalElements), domStatsData.totalElements]},
{values: [i18nString(UIStrings.maxDOMDepth), domStatsData.maxDepth?.depth ?? 0]},
{values: [i18nString(UIStrings.maxChildren), domStatsData.maxChildren?.numChildren ?? 0]},
],
} as TableData}>
</devtools-performance-table>
</div>`;
// clang-format on
}
}

Expand Down
2 changes: 1 addition & 1 deletion front_end/panels/timeline/components/insights/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface TableData {
}

export interface TableDataRow {
values: Array<string|LitHtml.LitTemplate>;
values: Array<number|string|LitHtml.LitTemplate>;
overlays?: Overlays.Overlays.TimelineOverlay[];
}

Expand Down
1 change: 1 addition & 0 deletions front_end/panels/timeline/fixtures/traces/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ copy_to_gen("traces") {
"missing-process-data.json.gz",
"missing-tracing-start.json.gz",
"missing-url.json.gz",
"multi-frame-dom-stats.json.gz",
"multiple-lcp-main-frame.json.gz",
"multiple-navigations-render-blocking.json.gz",
"multiple-navigations-same-id.json.gz",
Expand Down
Binary file not shown.
3 changes: 3 additions & 0 deletions front_end/testing/TraceHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,9 @@ export function getBaseTraceParseModelData(overrides: Partial<ParsedTrace> = {})
animationFrames: [],
presentationForFrame: new Map(),
},
DOMStats: {
domStatsByFrameId: new Map(),
},
LayoutShifts: {
clusters: [],
clustersByNavigationId: new Map(),
Expand Down

0 comments on commit 9611624

Please sign in to comment.