Skip to content

Commit d90f272

Browse files
Adam RaineDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Add DOM size insight model
This initial version of the insight does not include any DOM stats since that data isn't in the trace yet. However, the passing conditions used and highlighted events are expected to be final. Bug: 372897811 Change-Id: I10e49d0eec6705cf29b0dd86460ed3b4831789c6 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6072709 Reviewed-by: Connor Clark <[email protected]> Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Adam Raine <[email protected]>
1 parent 381ad34 commit d90f272

File tree

10 files changed

+195
-0
lines changed

10 files changed

+195
-0
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,7 @@ grd_files_debug_sources = [
10931093
"front_end/models/trace/helpers/TreeHelpers.js",
10941094
"front_end/models/trace/insights/CLSCulprits.js",
10951095
"front_end/models/trace/insights/Common.js",
1096+
"front_end/models/trace/insights/DOMSize.js",
10961097
"front_end/models/trace/insights/DocumentLatency.js",
10971098
"front_end/models/trace/insights/FontDisplay.js",
10981099
"front_end/models/trace/insights/ImageDelivery.js",

front_end/models/trace/handlers/RendererHandler.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const makeRendererThread = (): RendererThread => ({
4949
name: null,
5050
entries: [],
5151
profileCalls: [],
52+
layoutEvents: [],
53+
updateLayoutTreeEvents: [],
5254
});
5355

5456
const getOrCreateRendererProcess =
@@ -98,6 +100,18 @@ export function handleEvent(event: Types.Events.Event): void {
98100
thread.entries.push(event);
99101
allTraceEntries.push(event);
100102
}
103+
104+
if (Types.Events.isLayout(event)) {
105+
const process = getOrCreateRendererProcess(processes, event.pid);
106+
const thread = getOrCreateRendererThread(process, event.tid);
107+
thread.layoutEvents.push(event);
108+
}
109+
110+
if (Types.Events.isUpdateLayoutTree(event)) {
111+
const process = getOrCreateRendererProcess(processes, event.pid);
112+
const thread = getOrCreateRendererThread(process, event.tid);
113+
thread.updateLayoutTreeEvents.push(event);
114+
}
101115
}
102116

103117
export async function finalize(): Promise<void> {
@@ -394,5 +408,7 @@ export interface RendererThread {
394408
*/
395409
entries: Types.Events.Event[];
396410
profileCalls: Types.Events.SyntheticProfileCall[];
411+
layoutEvents: Types.Events.Layout[];
412+
updateLayoutTreeEvents: Types.Events.UpdateLayoutTree[];
397413
tree?: Helpers.TreeHelpers.TraceEntryTree;
398414
}

front_end/models/trace/insights/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ devtools_module("insights") {
1010
sources = [
1111
"CLSCulprits.ts",
1212
"Common.ts",
13+
"DOMSize.ts",
1314
"DocumentLatency.ts",
1415
"FontDisplay.ts",
1516
"ImageDelivery.ts",
@@ -48,6 +49,7 @@ ts_library("unittests") {
4849

4950
sources = [
5051
"CLSCulprits.test.ts",
52+
"DOMSize.test.ts",
5153
"DocumentLatency.test.ts",
5254
"FontDisplay.test.ts",
5355
"ImageDelivery.test.ts",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
6+
import {getFirstOrError, getInsightOrError} from '../../../testing/InsightHelpers.js';
7+
import {TraceLoader} from '../../../testing/TraceLoader.js';
8+
9+
export async function processTrace(testContext: Mocha.Suite|Mocha.Context|null, traceFile: string) {
10+
const {parsedTrace, insights} = await TraceLoader.traceEngine(testContext, traceFile);
11+
if (!insights) {
12+
throw new Error('No insights');
13+
}
14+
15+
return {data: parsedTrace, insights};
16+
}
17+
18+
describeWithEnvironment('DOMSize', function() {
19+
it('finds layout reflows and style recalcs affected by DOM size',
20+
async () => {
21+
const {data, insights} = await processTrace(this, 'dom-size.json.gz');
22+
23+
// 1 large DOM update was triggered before the first navigation
24+
{
25+
const insight = getInsightOrError('DOMSize', insights);
26+
assert.lengthOf(insight.largeLayoutUpdates, 1);
27+
assert.lengthOf(insight.largeStyleRecalcs, 1);
28+
}
29+
30+
// 1 large DOM update was triggered after the first navigation
31+
{
32+
const insight =
33+
getInsightOrError('DOMSize', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
34+
assert.lengthOf(insight.largeLayoutUpdates, 1);
35+
assert.lengthOf(insight.largeStyleRecalcs, 1);
36+
}
37+
})
38+
// Processing the above trace can take a while due to a performance bottleneck
39+
// b/382545507
40+
.timeout(30_000);
41+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as i18n from '../../../core/i18n/i18n.js';
6+
import * as Handlers from '../handlers/handlers.js';
7+
import * as Helpers from '../helpers/helpers.js';
8+
import * as Types from '../types/types.js';
9+
10+
import {InsightCategory, type InsightModel, type InsightSetContext, type RequiredData} from './types.js';
11+
12+
const UIStrings = {
13+
/**
14+
* @description Title 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.
15+
*/
16+
title: 'Optimize DOM size',
17+
/**
18+
* @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.
19+
*/
20+
description:
21+
'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/).',
22+
};
23+
24+
const str_ = i18n.i18n.registerUIStrings('models/trace/insights/DOMSize.ts', UIStrings);
25+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
26+
27+
const DOM_UPDATE_LIMIT = 800;
28+
29+
export type DOMSizeInsightModel = InsightModel<{
30+
largeLayoutUpdates: Types.Events.Layout[],
31+
largeStyleRecalcs: Types.Events.UpdateLayoutTree[],
32+
}>;
33+
34+
export function deps(): ['Renderer', 'AuctionWorklets'] {
35+
return ['Renderer', 'AuctionWorklets'];
36+
}
37+
38+
function finalize(partialModel: Omit<DOMSizeInsightModel, 'title'|'description'|'category'|'shouldShow'>):
39+
DOMSizeInsightModel {
40+
const relatedEvents = [...partialModel.largeLayoutUpdates, ...partialModel.largeStyleRecalcs];
41+
return {
42+
title: i18nString(UIStrings.title),
43+
description: i18nString(UIStrings.description),
44+
category: InsightCategory.INP,
45+
shouldShow: relatedEvents.length > 0,
46+
...partialModel,
47+
relatedEvents,
48+
};
49+
}
50+
51+
export function generateInsight(
52+
parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): DOMSizeInsightModel {
53+
const isWithinContext = (event: Types.Events.Event): boolean => Helpers.Timing.eventIsInBounds(event, context.bounds);
54+
55+
const mainTid = context.navigation?.tid;
56+
57+
const largeLayoutUpdates: Types.Events.Layout[] = [];
58+
const largeStyleRecalcs: Types.Events.UpdateLayoutTree[] = [];
59+
60+
const threads = Handlers.Threads.threadsInRenderer(parsedTrace.Renderer, parsedTrace.AuctionWorklets);
61+
for (const thread of threads) {
62+
if (thread.type !== Handlers.Threads.ThreadType.MAIN_THREAD) {
63+
continue;
64+
}
65+
66+
if (mainTid === undefined) {
67+
// We won't have a specific thread ID to reference if the context does not have a navigation.
68+
// In this case, we'll just filter out any OOPIFs threads.
69+
if (!thread.processIsOnMainFrame) {
70+
continue;
71+
}
72+
} else if (thread.tid !== mainTid) {
73+
continue;
74+
}
75+
76+
const rendererThread = parsedTrace.Renderer.processes.get(thread.pid)?.threads.get(thread.tid);
77+
if (!rendererThread) {
78+
continue;
79+
}
80+
81+
const {entries, layoutEvents, updateLayoutTreeEvents} = rendererThread;
82+
if (!entries.length) {
83+
continue;
84+
}
85+
86+
const first = entries[0];
87+
const last = entries[entries.length - 1];
88+
const timeRange =
89+
Helpers.Timing.traceWindowFromMicroSeconds(first.ts, Types.Timing.MicroSeconds(last.ts + (last.dur ?? 0)));
90+
if (!Helpers.Timing.boundsIncludeTimeRange({timeRange, bounds: context.bounds})) {
91+
continue;
92+
}
93+
94+
for (const event of layoutEvents) {
95+
if (!isWithinContext(event)) {
96+
continue;
97+
}
98+
99+
const {dirtyObjects} = event.args.beginData;
100+
if (dirtyObjects > DOM_UPDATE_LIMIT) {
101+
largeLayoutUpdates.push(event);
102+
}
103+
}
104+
105+
for (const event of updateLayoutTreeEvents) {
106+
if (!isWithinContext(event)) {
107+
continue;
108+
}
109+
110+
const {elementCount} = event.args;
111+
if (elementCount > DOM_UPDATE_LIMIT) {
112+
largeStyleRecalcs.push(event);
113+
}
114+
}
115+
}
116+
117+
return finalize({
118+
largeLayoutUpdates,
119+
largeStyleRecalcs,
120+
});
121+
}

front_end/models/trace/insights/Models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
export * as CLSCulprits from './CLSCulprits.js';
66
export * as DocumentLatency from './DocumentLatency.js';
7+
export * as DOMSize from './DOMSize.js';
78
export * as FontDisplay from './FontDisplay.js';
89
export * as ImageDelivery from './ImageDelivery.js';
910
export * as InteractionToNextPaint from './InteractionToNextPaint.js';

front_end/panels/timeline/fixtures/traces/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ copy_to_gen("traces") {
2020
"cls-multiple-frames.json.gz",
2121
"cls-no-nav.json.gz",
2222
"cls-single-frame.json.gz",
23+
"dom-size.json.gz",
2324
"enhanced-traces.json.gz",
2425
"extension-tracks-and-marks.json.gz",
2526
"fenced-frame-fledge.json.gz",

front_end/panels/timeline/fixtures/traces/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,13 @@ Generated from the [scheduler story](https://github.com/ChromeDevTools/performan
218218
### image-delivery
219219

220220
Generate from a page load [this HTML file](https://gist.github.com/adamraine/397e2bd08665f9e45f6072e446715115). Contains a series of test cases for the image delivery insight.
221+
222+
### dom-size
223+
224+
Generate from a recording of [this HTML file](https://gist.github.com/adamraine/bfdb3cecca2322bf74f1e725d9a4699d) with the following steps:
225+
1. Set CPU throttling to 4x
226+
2. Start recording without reloading the page
227+
3. Click the button once
228+
4. Reload the page
229+
5. Click the button once
230+
6. End recording
403 KB
Binary file not shown.

front_end/testing/TraceHelpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,8 @@ export function makeMockRendererHandlerData(entries: Trace.Types.Events.Event[],
418418
name: 'thread',
419419
entries,
420420
profileCalls: entries.filter(Trace.Types.Events.isProfileCall),
421+
layoutEvents: entries.filter(Trace.Types.Events.isLayout),
422+
updateLayoutTreeEvents: entries.filter(Trace.Types.Events.isUpdateLayoutTree),
421423
};
422424

423425
const mockProcess: Trace.Handlers.ModelHandlers.Renderer.RendererProcess = {

0 commit comments

Comments
 (0)