Skip to content

Commit 5977401

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: add AnimationFramesHandler
This CL adds the first pass at AnimationFrames parsing. It pairs AnimationFrame begin & end events based on the fact that they are a stack. It also does this per PID+TID to prevent against merging incorrectly from different threads. Additionally, it uses the ID field that is on the begin event + the AnimationFrame::Presentation event to pair those up. This allows us to know the time that a given frame was shown to the user. Bug: 352242870 Change-Id: Id8a120d5c7b13cd4751e23cbccc83ccee8d4c265 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6054290 Reviewed-by: Andres Olivares <[email protected]> Commit-Queue: Jack Franklin <[email protected]>
1 parent 52255fd commit 5977401

File tree

9 files changed

+210
-0
lines changed

9 files changed

+210
-0
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,7 @@ grd_files_debug_sources = [
10511051
"front_end/models/trace/extras/TraceFilter.js",
10521052
"front_end/models/trace/extras/TraceTree.js",
10531053
"front_end/models/trace/extras/URLForEntry.js",
1054+
"front_end/models/trace/handlers/AnimationFramesHandler.js",
10541055
"front_end/models/trace/handlers/AnimationHandler.js",
10551056
"front_end/models/trace/handlers/AsyncCallStacksHandler.js",
10561057
"front_end/models/trace/handlers/AuctionWorkletsHandler.js",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
import {TraceLoader} from '../../../testing/TraceLoader.js';
5+
import * as Trace from '../trace.js';
6+
7+
async function parseEvents(events: readonly Trace.Types.Events.Event[]) {
8+
Trace.Handlers.ModelHandlers.FlowsHandler.reset();
9+
Trace.Handlers.ModelHandlers.AnimationFrames.reset();
10+
for (const event of events) {
11+
Trace.Handlers.ModelHandlers.FlowsHandler.handleEvent(event);
12+
Trace.Handlers.ModelHandlers.AnimationFrames.handleEvent(event);
13+
}
14+
await Trace.Handlers.ModelHandlers.FlowsHandler.finalize();
15+
await Trace.Handlers.ModelHandlers.AnimationFrames.finalize();
16+
}
17+
18+
describe('AnimationFramesHandler', () => {
19+
it('can group all related animation frame events', async function() {
20+
Trace.Handlers.ModelHandlers.AnimationFrames.reset();
21+
const events = await TraceLoader.rawEvents(this, 'web-dev-animation-frames.json.gz');
22+
await parseEvents(events);
23+
const data = Trace.Handlers.ModelHandlers.AnimationFrames.data();
24+
assert.lengthOf(data.animationFrames, 32);
25+
const firstFrame = data.animationFrames[0];
26+
assert.strictEqual(firstFrame.args.data.beginEvent.args?.animation_frame_timing_info.duration_ms, 76);
27+
assert.strictEqual(firstFrame.dur, Trace.Types.Timing.MicroSeconds(76038));
28+
});
29+
30+
it('links an animation frame to its presentation event', async function() {
31+
Trace.Handlers.ModelHandlers.AnimationFrames.reset();
32+
const events = await TraceLoader.rawEvents(this, 'web-dev-animation-frames.json.gz');
33+
await parseEvents(events);
34+
const data = Trace.Handlers.ModelHandlers.AnimationFrames.data();
35+
const firstFrame = data.animationFrames[0];
36+
const presentationEvent = data.presentationForFrame.get(firstFrame);
37+
assert.isDefined(presentationEvent);
38+
assert.strictEqual(presentationEvent.args?.id, firstFrame.args.data.beginEvent.args?.id);
39+
});
40+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
import * as Helpers from '../helpers/helpers.js';
5+
import * as Types from '../types/types.js';
6+
7+
import type {HandlerName} from './types.js';
8+
9+
export interface Data {
10+
animationFrames: Types.Events.SyntheticAnimationFramePair[];
11+
presentationForFrame: Map<Types.Events.SyntheticAnimationFramePair, Types.Events.AnimationFramePresentation>;
12+
}
13+
14+
function threadKey(data: Types.Events.Event): string {
15+
return `${data.pid}-${data.tid}`;
16+
}
17+
// Track all the start + end events. We key them by the PID+TID so we don't
18+
// accidentally pair across different threads.
19+
const animationFrameStarts: Map<string, Types.Events.AnimationFrameAsyncStart[]> = new Map();
20+
const animationFrameEnds: Map<string, Types.Events.AnimationFrameAsyncEnd[]> = new Map();
21+
// Store all the AnimationFrame::Presentation events. Key them by their ID for
22+
// easy look-up later on when we associate one to the AnimationFrame event.
23+
const animationFramePresentations: Map<string, Types.Events.AnimationFramePresentation> = new Map();
24+
25+
// The final list of animation frames that we return.
26+
const animationFrames: Types.Events.SyntheticAnimationFramePair[] = [];
27+
28+
const presentationForFrame: Map<Types.Events.SyntheticAnimationFramePair, Types.Events.AnimationFramePresentation> =
29+
new Map();
30+
31+
export function reset(): void {
32+
animationFrameStarts.clear();
33+
animationFrameEnds.clear();
34+
animationFrames.length = 0;
35+
presentationForFrame.clear();
36+
animationFramePresentations.clear();
37+
}
38+
39+
export function handleEvent(event: Types.Events.Event): void {
40+
if (Types.Events.isAnimationFrameAsyncStart(event)) {
41+
const key = threadKey(event);
42+
const existing = animationFrameStarts.get(key) ?? [];
43+
existing.push(event);
44+
animationFrameStarts.set(key, existing);
45+
} else if (Types.Events.isAnimationFrameAsyncEnd(event)) {
46+
const key = threadKey(event);
47+
const existing = animationFrameEnds.get(key) ?? [];
48+
existing.push(event);
49+
animationFrameEnds.set(key, existing);
50+
} else if (Types.Events.isAnimationFramePresentation(event) && event.args?.id) {
51+
animationFramePresentations.set(event.args.id, event);
52+
}
53+
}
54+
55+
export async function finalize(): Promise<void> {
56+
// AnimationFrames are represented with begin & end events on a stack; so we
57+
// can pair them by walking through the list of start events and pairing with
58+
// the same index in the list of end events, once both lists are sorted by
59+
// timestamp.
60+
// We walk through the set of begin/end events we gathered per pid+tid and
61+
// pair those up.
62+
// Unfortunately we cannot use the pairing helpers in Helpers.Trace because
63+
// only the begin event has an ID; the end event does not. But because we
64+
// know that AnimationFrames are sequential and do not overlap, we can pair
65+
// up events easily.
66+
for (const [key, startEvents] of animationFrameStarts.entries()) {
67+
const endEvents = animationFrameEnds.get(key);
68+
if (!endEvents) {
69+
continue;
70+
}
71+
72+
Helpers.Trace.sortTraceEventsInPlace(startEvents);
73+
Helpers.Trace.sortTraceEventsInPlace(endEvents);
74+
75+
for (let i = 0; i < startEvents.length; i++) {
76+
const endEvent = endEvents.at(i);
77+
if (!endEvent) {
78+
// Invalid data: break. We can't pair any other events up.
79+
break;
80+
}
81+
const startEvent = startEvents[i];
82+
83+
const syntheticEvent = Helpers.SyntheticEvents.SyntheticEventsManager
84+
.registerSyntheticEvent<Types.Events.SyntheticAnimationFramePair>({
85+
rawSourceEvent: startEvent,
86+
...startEvent,
87+
dur: Types.Timing.MicroSeconds(endEvent.ts - startEvent.ts),
88+
args: {
89+
data: {
90+
beginEvent: startEvent,
91+
endEvent,
92+
},
93+
},
94+
});
95+
animationFrames.push(syntheticEvent);
96+
97+
// AnimationFrame begin events + AnimationFrame::Presentation events share
98+
// an args.id, so we can pair them up based on that.
99+
const id = startEvent.args?.id;
100+
if (id) {
101+
const presentationEvent = animationFramePresentations.get(id);
102+
if (presentationEvent) {
103+
presentationForFrame.set(syntheticEvent, presentationEvent);
104+
}
105+
}
106+
}
107+
}
108+
}
109+
110+
export function data(): Data {
111+
return {
112+
animationFrames,
113+
presentationForFrame,
114+
};
115+
}
116+
117+
export function deps(): HandlerName[] {
118+
return ['Meta'];
119+
}

front_end/models/trace/handlers/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import("../../visibility.gni")
99

1010
devtools_module("handlers") {
1111
sources = [
12+
"AnimationFramesHandler.ts",
1213
"AnimationHandler.ts",
1314
"AsyncCallStacksHandler.ts",
1415
"AuctionWorkletsHandler.ts",
@@ -66,6 +67,7 @@ ts_library("unittests") {
6667
testonly = true
6768

6869
sources = [
70+
"AnimationFramesHandler.test.ts",
6971
"AnimationHandler.test.ts",
7072
"AsyncCallStacksHandler.test.ts",
7173
"AuctionWorkletsHandler.test.ts",

front_end/models/trace/handlers/ModelHandlers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
export * as AnimationFrames from './AnimationFramesHandler.js';
56
export * as Animations from './AnimationHandler.js';
67
export * as AsyncCallStacks from './AsyncCallStacksHandler.js';
78
export * as AuctionWorklets from './AuctionWorkletsHandler.js';

front_end/models/trace/types/TraceEvents.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,44 @@ export interface PairableAsyncEnd extends PairableAsync {
12691269
ph: Phase.ASYNC_NESTABLE_END;
12701270
}
12711271

1272+
export interface AnimationFrame extends PairableAsync {
1273+
name: Name.ANIMATION_FRAME;
1274+
args?: AnimationFrameArgs;
1275+
}
1276+
export type AnimationFrameArgs = Args&{
1277+
animation_frame_timing_info: {
1278+
blocking_duration_ms: number,
1279+
duration_ms: number,
1280+
num_scripts: number,
1281+
},
1282+
id: string,
1283+
};
1284+
1285+
export interface AnimationFrameAsyncStart extends AnimationFrame {
1286+
ph: Phase.ASYNC_NESTABLE_START;
1287+
}
1288+
export interface AnimationFrameAsyncEnd extends AnimationFrame {
1289+
ph: Phase.ASYNC_NESTABLE_END;
1290+
}
1291+
1292+
export function isAnimationFrameAsyncStart(data: Event): data is AnimationFrameAsyncStart {
1293+
return data.name === Name.ANIMATION_FRAME && data.ph === Phase.ASYNC_NESTABLE_START;
1294+
}
1295+
export function isAnimationFrameAsyncEnd(data: Event): data is AnimationFrameAsyncEnd {
1296+
return data.name === Name.ANIMATION_FRAME && data.ph === Phase.ASYNC_NESTABLE_END;
1297+
}
1298+
1299+
export interface AnimationFramePresentation extends Event {
1300+
name: Name.ANIMATION_FRAME_PRESENTATION;
1301+
ph: Phase.ASYNC_NESTABLE_INSTANT;
1302+
args?: Args&{
1303+
id: string,
1304+
};
1305+
}
1306+
export function isAnimationFramePresentation(data: Event): data is AnimationFramePresentation {
1307+
return data.name === Name.ANIMATION_FRAME_PRESENTATION;
1308+
}
1309+
12721310
export interface UserTiming extends Event {
12731311
id2?: {local?: string, global?: string};
12741312
id?: string;
@@ -1475,6 +1513,7 @@ export interface SyntheticEventPair<T extends PairableAsync = PairableAsync> ext
14751513
}
14761514

14771515
export type SyntheticPipelineReporterPair = SyntheticEventPair<PipelineReporter>;
1516+
export type SyntheticAnimationFramePair = SyntheticEventPair<AnimationFrame>;
14781517

14791518
export type SyntheticUserTimingPair = SyntheticEventPair<PerformanceMeasure>;
14801519

@@ -2891,6 +2930,9 @@ export const enum Name {
28912930

28922931
DOM_LOADING = 'domLoading',
28932932
BEGIN_REMOTE_FONT_LOAD = 'BeginRemoteFontLoad',
2933+
2934+
ANIMATION_FRAME = 'AnimationFrame',
2935+
ANIMATION_FRAME_PRESENTATION = 'AnimationFrame::Presentation',
28942936
}
28952937

28962938
// NOT AN EXHAUSTIVE LIST: just some categories we use and refer

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ copy_to_gen("traces") {
100100
"user-timings-complex.json.gz",
101101
"user-timings-details.json.gz",
102102
"user-timings.json.gz",
103+
"web-dev-animation-frames.json.gz",
103104
"web-dev-initial-url.json.gz",
104105
"web-dev-modifications.json.gz",
105106
"web-dev-outermost-frames.json.gz",
Binary file not shown.

front_end/testing/TraceHelpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,10 @@ type ParsedTrace = Trace.Handlers.Types.ParsedTrace;
575575
export function getBaseTraceParseModelData(overrides: Partial<ParsedTrace> = {}): ParsedTrace {
576576
return {
577577
Animations: {animations: []},
578+
AnimationFrames: {
579+
animationFrames: [],
580+
presentationForFrame: new Map(),
581+
},
578582
LayoutShifts: {
579583
clusters: [],
580584
clustersByNavigationId: new Map(),

0 commit comments

Comments
 (0)