Skip to content

Commit 23f287b

Browse files
Adam RaineDevtools-frontend LUCI CQ
authored andcommitted
[ForcedReflow] Highlight all relevant forced reflow events
- Only include events from the main frame and in the context window - Layout events can have stack traces but we weren't detecting them. This can be helpful for LH uses cases where JS sampling is disabled. - Forced reflow events that still lack any JS stack information are now highlighted and listed under a "Unattributed" item. - Bottum-up attribution is determined using common helper functions - `Extras.StackTraceForEvent.get` - `Helpers.Trace.getZeroIndexedStackTraceForEvent` - `relatedEvents` is all of the forced reflow events. No longer the top level function calls. Bug: 401538867 Change-Id: I9d80e2593be0f242fbf1c1a442d2d6ce658b7466 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6341691 Reviewed-by: Andres Olivares <[email protected]> Commit-Queue: Adam Raine <[email protected]>
1 parent 094b6c3 commit 23f287b

File tree

8 files changed

+135
-155
lines changed

8 files changed

+135
-155
lines changed

front_end/models/trace/extras/StackTraceForEvent.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export function get(event: Types.Events.Event, parsedTrace: Handlers.Types.Parse
3737
result = getForExtensionEntry(event, parsedTrace);
3838
} else if (Types.Events.isUserTiming(event)) {
3939
result = getForUserTiming(event, parsedTrace);
40+
} else if (Types.Events.isLayout(event) || Types.Events.isUpdateLayoutTree(event)) {
41+
const node = parsedTrace.Renderer.entryToNode.get(event);
42+
const parent = node?.parent?.entry;
43+
if (parent) {
44+
result = get(parent, parsedTrace);
45+
}
4046
}
4147
if (result) {
4248
cacheForTrace.set(event, result);

front_end/models/trace/helpers/Trace.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export function stackTraceInEvent(event: Types.Events.Event): Types.Events.CallF
3939
if (Types.Events.isUpdateLayoutTree(event)) {
4040
return event.args.beginData?.stackTrace || null;
4141
}
42+
if (Types.Events.isLayout(event)) {
43+
return event.args.beginData.stackTrace ?? null;
44+
}
4245
if (Types.Events.isFunctionCall(event)) {
4346
const data = event.args.data;
4447
if (!data) {
@@ -442,6 +445,7 @@ export function getZeroIndexedStackTraceForEvent(event: Types.Events.Event): Typ
442445
case Types.Events.Name.SCHEDULE_STYLE_RECALCULATION:
443446
case Types.Events.Name.INVALIDATE_LAYOUT:
444447
case Types.Events.Name.FUNCTION_CALL:
448+
case Types.Events.Name.LAYOUT:
445449
case Types.Events.Name.UPDATE_LAYOUT_TREE: {
446450
return makeZeroBasedCallFrame(callFrame);
447451
}

front_end/models/trace/insights/ForcedReflow.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ describeWithEnvironment('ForcedReflow', function() {
2828
assert.strictEqual(insight.topLevelFunctionCallData?.totalReflowTime, 26052);
2929

3030
const callStack = insight.aggregatedBottomUpData[1];
31-
assert.strictEqual(callStack.bottomUpData.columnNumber, 137906);
32-
assert.strictEqual(callStack.bottomUpData.lineNumber, 6);
31+
assert.strictEqual(callStack.bottomUpData!.columnNumber, 197203);
32+
assert.strictEqual(callStack.bottomUpData!.lineNumber, 32);
3333
assert.lengthOf(callStack.relatedEvents, 2);
3434
});
3535
});

front_end/models/trace/insights/ForcedReflow.ts

Lines changed: 87 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
// found in the LICENSE file.
44

55
import * as i18n from '../../../core/i18n/i18n.js';
6+
import * as Platform from '../../../core/platform/platform.js';
67
import type * as Protocol from '../../../generated/protocol.js';
8+
import * as Extras from '../extras/extras.js';
79
import type * as Handlers from '../handlers/handlers.js';
8-
import type {Warning} from '../handlers/WarningsHandler.js';
910
import * as Helpers from '../helpers/helpers.js';
1011
import * as Types from '../types/types.js';
1112

1213
import {
13-
type BottomUpCallStack,
14-
type ForcedReflowAggregatedData,
1514
InsightCategory,
1615
InsightKeys,
1716
type InsightModel,
@@ -41,6 +40,10 @@ export const UIStrings = {
4140
* @description Text to describe the total reflow time
4241
*/
4342
totalReflowTime: 'Total reflow time',
43+
/**
44+
* @description Text to describe CPU processor tasks that could not be attributed to any specific source code.
45+
*/
46+
unattributed: 'Unattributed',
4447
} as const;
4548

4649
const str_ = i18n.i18n.registerUIStrings('models/trace/insights/ForcedReflow.ts', UIStrings);
@@ -51,49 +54,39 @@ export type ForcedReflowInsightModel = InsightModel<typeof UIStrings, {
5154
aggregatedBottomUpData: BottomUpCallStack[],
5255
}>;
5356

54-
function aggregateForcedReflow(
55-
data: Map<Warning, Types.Events.Event[]>,
56-
entryToNodeMap: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>):
57-
[ForcedReflowAggregatedData|undefined, BottomUpCallStack[]] {
57+
export interface BottomUpCallStack {
58+
/**
59+
* `null` indicates that this data is for unattributed force reflows.
60+
*/
61+
bottomUpData: Types.Events.CallFrame|Protocol.Runtime.CallFrame|null;
62+
totalTime: number;
63+
relatedEvents: Types.Events.Event[];
64+
}
65+
66+
export interface ForcedReflowAggregatedData {
67+
topLevelFunctionCall: Types.Events.CallFrame|Protocol.Runtime.CallFrame;
68+
totalReflowTime: number;
69+
topLevelFunctionCallEvents: Types.Events.Event[];
70+
}
71+
72+
function getCallFrameId(callFrame: Types.Events.CallFrame|Protocol.Runtime.CallFrame): string {
73+
return callFrame.scriptId + ':' + callFrame.lineNumber + ':' + callFrame.columnNumber;
74+
}
75+
76+
function getLargestTopLevelFunctionData(
77+
forcedReflowEvents: Types.Events.Event[], traceParsedData: Handlers.Types.ParsedTrace): ForcedReflowAggregatedData|
78+
undefined {
79+
const entryToNodeMap = traceParsedData.Renderer.entryToNode;
5880
const dataByTopLevelFunction = new Map<string, ForcedReflowAggregatedData>();
59-
const bottomUpDataMap = new Map<string, BottomUpCallStack>();
60-
const forcedReflowEvents = data.get('FORCED_REFLOW');
61-
if (!forcedReflowEvents || forcedReflowEvents.length === 0) {
62-
return [undefined, []];
81+
if (forcedReflowEvents.length === 0) {
82+
return;
6383
}
6484

65-
forcedReflowEvents.forEach(e => {
85+
for (const event of forcedReflowEvents) {
6686
// Gather the stack traces by searching in the tree
67-
const traceNode = entryToNodeMap.get(e);
68-
87+
const traceNode = entryToNodeMap.get(event);
6988
if (!traceNode) {
70-
return;
71-
}
72-
// Compute call stack fully
73-
const bottomUpData: Array<Types.Events.CallFrame|Protocol.Runtime.CallFrame> = [];
74-
let currentNode = traceNode;
75-
let previousNode;
76-
const childStack: Protocol.Runtime.CallFrame[] = [];
77-
78-
// Some profileCalls maybe constructed as its children in hierarchy tree
79-
while (currentNode.children.length > 0) {
80-
const childNode = currentNode.children[0];
81-
if (!previousNode) {
82-
previousNode = childNode;
83-
}
84-
const eventData = childNode.entry;
85-
if (Types.Events.isProfileCall(eventData)) {
86-
childStack.push(eventData.callFrame);
87-
}
88-
currentNode = childNode;
89-
}
90-
91-
// In order to avoid too much information, we only contain 2 levels bottomUp data,
92-
while (childStack.length > 0 && bottomUpData.length < 2) {
93-
const traceData = childStack.pop();
94-
if (traceData) {
95-
bottomUpData.push(traceData);
96-
}
89+
continue;
9790
}
9891

9992
let node = traceNode.parent;
@@ -102,88 +95,43 @@ function aggregateForcedReflow(
10295
while (node) {
10396
const eventData = node.entry;
10497
if (Types.Events.isProfileCall(eventData)) {
105-
if (bottomUpData.length < 2) {
106-
bottomUpData.push(eventData.callFrame);
107-
}
98+
topLevelFunctionCall = eventData.callFrame;
99+
topLevelFunctionCallEvent = eventData;
108100
} else {
109101
// We have finished searching bottom up data
110102
if (Types.Events.isFunctionCall(eventData) && eventData.args.data &&
111103
Types.Events.objectIsCallFrame(eventData.args.data)) {
112104
topLevelFunctionCall = eventData.args.data;
113105
topLevelFunctionCallEvent = eventData;
114-
if (bottomUpData.length === 0) {
115-
bottomUpData.push(topLevelFunctionCall);
116-
}
117-
} else {
118-
// Sometimes the top level task can be other JSInvocation event
119-
// then we use the top level profile call as topLevelFunctionCall's data
120-
const previousData = previousNode?.entry;
121-
if (previousData && Types.Events.isProfileCall(previousData)) {
122-
topLevelFunctionCall = previousData.callFrame;
123-
topLevelFunctionCallEvent = previousNode?.entry;
124-
}
125106
}
126107
break;
127108
}
128-
previousNode = node;
129109
node = node.parent;
130110
}
131111

132-
if (!topLevelFunctionCall || !topLevelFunctionCallEvent || bottomUpData.length === 0) {
133-
return;
134-
}
135-
const bottomUpDataId =
136-
bottomUpData[0].scriptId + ':' + bottomUpData[0].lineNumber + ':' + bottomUpData[0].columnNumber + ':';
137-
138-
const data = bottomUpDataMap.get(bottomUpDataId) ?? {
139-
bottomUpData: bottomUpData[0],
140-
totalTime: 0,
141-
relatedEvents: [],
142-
};
143-
data.totalTime += (e.dur ?? 0);
144-
data.relatedEvents.push(e);
145-
bottomUpDataMap.set(bottomUpDataId, data);
146-
147-
const aggregatedDataId =
148-
topLevelFunctionCall.scriptId + ':' + topLevelFunctionCall.lineNumber + ':' + topLevelFunctionCall.columnNumber;
149-
if (!dataByTopLevelFunction.has(aggregatedDataId)) {
150-
dataByTopLevelFunction.set(aggregatedDataId, {
151-
topLevelFunctionCall,
152-
totalReflowTime: 0,
153-
bottomUpData: new Set<string>(),
154-
topLevelFunctionCallEvents: [],
155-
});
112+
if (!topLevelFunctionCall || !topLevelFunctionCallEvent) {
113+
continue;
156114
}
157-
const aggregatedData = dataByTopLevelFunction.get(aggregatedDataId);
158-
if (aggregatedData) {
159-
aggregatedData.totalReflowTime += (e.dur ?? 0);
160-
aggregatedData.bottomUpData.add(bottomUpDataId);
161-
aggregatedData.topLevelFunctionCallEvents.push(topLevelFunctionCallEvent);
162-
}
163-
});
164115

165-
let topTimeConsumingDataId = '';
166-
let maxTime = 0;
167-
dataByTopLevelFunction.forEach((value, key) => {
168-
if (value.totalReflowTime > maxTime) {
169-
maxTime = value.totalReflowTime;
170-
topTimeConsumingDataId = key;
116+
const aggregatedDataId = getCallFrameId(topLevelFunctionCall);
117+
const aggregatedData =
118+
Platform.MapUtilities.getWithDefault(dataByTopLevelFunction, aggregatedDataId, () => ({
119+
topLevelFunctionCall,
120+
totalReflowTime: 0,
121+
topLevelFunctionCallEvents: [],
122+
}));
123+
aggregatedData.totalReflowTime += (event.dur ?? 0);
124+
aggregatedData.topLevelFunctionCallEvents.push(topLevelFunctionCallEvent);
125+
}
126+
127+
let topTimeConsumingData: ForcedReflowAggregatedData|undefined = undefined;
128+
dataByTopLevelFunction.forEach(data => {
129+
if (!topTimeConsumingData || data.totalReflowTime > topTimeConsumingData.totalReflowTime) {
130+
topTimeConsumingData = data;
171131
}
172132
});
173133

174-
const aggregatedBottomUpData: BottomUpCallStack[] = [];
175-
const topLevelFunctionCallData = dataByTopLevelFunction.get(topTimeConsumingDataId);
176-
const dataSet = dataByTopLevelFunction.get(topTimeConsumingDataId)?.bottomUpData;
177-
if (dataSet) {
178-
dataSet.forEach(value => {
179-
const callStackData = bottomUpDataMap.get(value);
180-
if (callStackData && callStackData.totalTime > Helpers.Timing.milliToMicro(Types.Timing.Milli(1))) {
181-
aggregatedBottomUpData.push(callStackData);
182-
}
183-
});
184-
}
185-
186-
return [topLevelFunctionCallData, aggregatedBottomUpData];
134+
return topTimeConsumingData;
187135
}
188136

189137
function finalize(partialModel: PartialInsightModel<ForcedReflowInsightModel>): ForcedReflowInsightModel {
@@ -193,32 +141,50 @@ function finalize(partialModel: PartialInsightModel<ForcedReflowInsightModel>):
193141
title: i18nString(UIStrings.title),
194142
description: i18nString(UIStrings.description),
195143
category: InsightCategory.ALL,
196-
state: partialModel.topLevelFunctionCallData !== undefined && partialModel.aggregatedBottomUpData.length !== 0 ?
197-
'fail' :
198-
'pass',
144+
state: partialModel.aggregatedBottomUpData.length !== 0 ? 'fail' : 'pass',
199145
...partialModel,
200146
};
201147
}
202148

149+
function getBottomCallFrameForEvent(event: Types.Events.Event, traceParsedData: Handlers.Types.ParsedTrace):
150+
Types.Events.CallFrame|Protocol.Runtime.CallFrame|null {
151+
const profileStackTrace = Extras.StackTraceForEvent.get(event, traceParsedData);
152+
const eventStackTrace = Helpers.Trace.getZeroIndexedStackTraceForEvent(event);
153+
154+
return profileStackTrace?.callFrames[0] ?? eventStackTrace?.[0] ?? null;
155+
}
156+
203157
export function generateInsight(
204-
traceParsedData: Handlers.Types.ParsedTrace, _context: InsightSetContext): ForcedReflowInsightModel {
205-
const warningsData = traceParsedData.Warnings;
206-
const entryToNodeMap = traceParsedData.Renderer.entryToNode;
158+
traceParsedData: Handlers.Types.ParsedTrace, context: InsightSetContext): ForcedReflowInsightModel {
159+
const isWithinContext = (event: Types.Events.Event): boolean => {
160+
const frameId = Helpers.Trace.frameIDForEvent(event);
161+
if (frameId !== context.frameId) {
162+
return false;
163+
}
207164

208-
if (!warningsData) {
209-
throw new Error('no warnings data');
210-
}
165+
return Helpers.Timing.eventIsInBounds(event, context.bounds);
166+
};
211167

212-
if (!entryToNodeMap) {
213-
throw new Error('no renderer data');
168+
const bottomUpDataMap = new Map<string, BottomUpCallStack>();
169+
const events = traceParsedData.Warnings.perWarning.get('FORCED_REFLOW')?.filter(isWithinContext) ?? [];
170+
for (const event of events) {
171+
const bottomCallFrame = getBottomCallFrameForEvent(event, traceParsedData);
172+
const bottomCallId = bottomCallFrame ? getCallFrameId(bottomCallFrame) : 'UNATTRIBUTED';
173+
const bottomUpData =
174+
Platform.MapUtilities.getWithDefault(bottomUpDataMap, bottomCallId, () => ({
175+
bottomUpData: bottomCallFrame,
176+
totalTime: 0,
177+
relatedEvents: [],
178+
}));
179+
bottomUpData.totalTime += event.dur ?? 0;
180+
bottomUpData.relatedEvents.push(event);
214181
}
215182

216-
const [topLevelFunctionCallData, aggregatedBottomUpData] =
217-
aggregateForcedReflow(warningsData.perWarning, entryToNodeMap);
183+
const topLevelFunctionCallData = getLargestTopLevelFunctionData(events, traceParsedData);
218184

219185
return finalize({
220-
relatedEvents: topLevelFunctionCallData?.topLevelFunctionCallEvents,
186+
relatedEvents: events,
221187
topLevelFunctionCallData,
222-
aggregatedBottomUpData,
188+
aggregatedBottomUpData: [...bottomUpDataMap.values()],
223189
});
224190
}

front_end/models/trace/insights/types.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// found in the LICENSE file.
44

55
import type * as Common from '../../../core/common/common.js';
6-
import type * as Protocol from '../../../generated/protocol.js';
76
import type * as Lantern from '../lantern/lantern.js';
87
import type * as Types from '../types/types.js';
98

@@ -34,19 +33,6 @@ export interface LanternContext {
3433
metrics: Record<string, Lantern.Metrics.MetricResult>;
3534
}
3635

37-
export interface ForcedReflowAggregatedData {
38-
topLevelFunctionCall: Types.Events.CallFrame|Protocol.Runtime.CallFrame;
39-
totalReflowTime: number;
40-
bottomUpData: Set<string>;
41-
topLevelFunctionCallEvents: Types.Events.Event[];
42-
}
43-
44-
export interface BottomUpCallStack {
45-
bottomUpData: Types.Events.CallFrame|Protocol.Runtime.CallFrame;
46-
totalTime: number;
47-
relatedEvents: Types.Events.Event[];
48-
}
49-
5036
export type InsightModelsType = typeof Models;
5137

5238
export enum InsightWarning {

front_end/models/trace/types/TraceEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,6 +1908,7 @@ export interface Layout extends Complete {
19081908
args: Args&{
19091909
beginData: {
19101910
sampleTraceId?: number, frame: string, dirtyObjects: number, partialLayout: boolean, totalObjects: number,
1911+
stackTrace?: CallFrame[],
19111912
},
19121913
// endData is not reliably populated.
19131914
// Why? TBD. https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/local_frame_view.cc;l=847-851;drc=8b6aaad8027390ce6b32d82d57328e93f34bb8e5

front_end/panels/timeline/TimelineUIUtils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ describeWithMockConnection('TimelineUIUtils', function() {
652652
// The "Recalculation forced" Stack trace
653653
title: undefined,
654654
value:
655-
'testFuncs.changeAttributeAndDisplay @ chromedevtools.github.io/performance-stories/style-invalidations/app.js:47:40',
655+
'testFuncs.changeAttributeAndDisplay @ chromedevtools.github.io/performance-stories/style-invalidations/app.js:44:47\n(anonymous) @ chromedevtools.github.io/performance-stories/style-invalidations/app.js:64:36',
656656
},
657657
{
658658
title: 'Initiated by',

0 commit comments

Comments
 (0)