Skip to content

Commit f9eb4e2

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: improve performance of FramesHandler by taking subset of events
The FramesHandler took a copy of all trace events, sorted them, and then passed them into TimelineFrameModel. This was a performance hit because sorting a large trace (my test trace of Figma has over 1m events, and it's not even particularly huge) is expensive. Looking through the code, I noticed that we actually only care about a subset of the events. On my Figma test trace, about 100k of the events are relevant to the TimelineFrameModel, which is 10% of the total. Therefore, this CL updates the code to pick out the relevant events in `handleEvent` and sort just that subset. On my test trace, this has a decent performance boost: Before: - parse:Frames:finalize: 250ms After: - parse:Frames:finalize: 65ms [email protected] Bug: 436491188 Change-Id: I882fb1ff052fe983653228ec94bbf3041d4c46e1 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6850647 Commit-Queue: Connor Clark <[email protected]> Auto-Submit: Jack Franklin <[email protected]> Reviewed-by: Connor Clark <[email protected]>
1 parent 67670dd commit f9eb4e2

File tree

2 files changed

+49
-44
lines changed

2 files changed

+49
-44
lines changed

front_end/models/trace/handlers/FramesHandler.test.ts

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

5-
import {describeWithMockConnection} from '../../../testing/MockConnection.js';
65
import {TraceLoader} from '../../../testing/TraceLoader.js';
76
import * as Trace from '../trace.js';
87

@@ -31,7 +30,7 @@ async function processTrace(events: readonly Trace.Types.Events.Event[]): Promis
3130
}
3231
}
3332

34-
describeWithMockConnection('FramesHandler', () => {
33+
describe('FramesHandler', () => {
3534
it('can parse out a trace and return the frames', async function() {
3635
const rawEvents = await TraceLoader.rawEvents(this, 'web-dev-with-commit.json.gz');
3736
await processTrace(rawEvents);
@@ -66,10 +65,6 @@ describeWithMockConnection('FramesHandler', () => {
6665
});
6766

6867
it('can create LayerPaintEvents from Paint and snapshot events', async function() {
69-
// Advanced instrumentation trace file is large: allow the bots more time
70-
// to process it.
71-
this.timeout(20_000);
72-
7368
const rawEvents = await TraceLoader.rawEvents(this, 'web-dev-with-advanced-instrumentation.json.gz');
7469
await processTrace(rawEvents);
7570
const parsedFrames = Trace.Handlers.ModelHandlers.Frames.data().frames;

front_end/models/trace/handlers/FramesHandler.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {data as layerTreeHandlerData, type LayerTreeData} from './LayerTreeHandl
1111
import {data as metaHandlerData, type MetaHandlerData} from './MetaHandler.js';
1212
import {data as rendererHandlerData, type RendererHandlerData} from './RendererHandler.js';
1313
import * as Threads from './Threads.js';
14-
import type {FinalizeOptions, HandlerName} from './types.js';
14+
import type {HandlerName} from './types.js';
1515

1616
/**
1717
* IMPORTANT: this handler is slightly different to the rest. This is because
@@ -25,22 +25,61 @@ import type {FinalizeOptions, HandlerName} from './types.js';
2525
*/
2626

2727
let model: TimelineFrameModel|null = null;
28+
const relevantFrameEvents: Types.Events.Event[] = [];
29+
30+
type FrameEvent = Types.Events.BeginFrame|Types.Events.DroppedFrame|Types.Events.RequestMainThreadFrame|
31+
Types.Events.BeginMainThreadFrame|Types.Events.Commit|Types.Events.CompositeLayers|
32+
Types.Events.ActivateLayerTree|Types.Events.NeedsBeginFrameChanged|Types.Events.DrawFrame;
33+
34+
function isFrameEvent(event: Types.Events.Event): event is FrameEvent {
35+
return (
36+
Types.Events.isSetLayerId(event) || Types.Events.isBeginFrame(event) || Types.Events.isDroppedFrame(event) ||
37+
Types.Events.isRequestMainThreadFrame(event) || Types.Events.isBeginMainThreadFrame(event) ||
38+
Types.Events.isNeedsBeginFrameChanged(event) ||
39+
// Note that "Commit" is the replacement for "CompositeLayers" so in a trace
40+
// we wouldn't expect to see a combination of these. All "new" trace
41+
// recordings use "Commit", but we can easily support "CompositeLayers" too
42+
// to not break older traces being imported.
43+
Types.Events.isCommit(event) || Types.Events.isCompositeLayers(event) ||
44+
Types.Events.isActivateLayerTree(event) || Types.Events.isDrawFrame(event));
45+
}
46+
47+
function entryIsTopLevel(entry: Types.Events.Event): boolean {
48+
const devtoolsTimelineCategory = 'disabled-by-default-devtools.timeline';
49+
return entry.name === Types.Events.Name.RUN_TASK && entry.cat.includes(devtoolsTimelineCategory);
50+
}
51+
52+
const MAIN_FRAME_MARKERS = new Set<Types.Events.Name>([
53+
Types.Events.Name.SCHEDULE_STYLE_RECALCULATION,
54+
Types.Events.Name.INVALIDATE_LAYOUT,
55+
Types.Events.Name.BEGIN_MAIN_THREAD_FRAME,
56+
Types.Events.Name.SCROLL_LAYER,
57+
]);
2858

2959
export function reset(): void {
3060
model = null;
61+
relevantFrameEvents.length = 0;
3162
}
32-
export function handleEvent(): void {
63+
export function handleEvent(event: Types.Events.Event): void {
64+
// This might seem like a wide set of events to filter for, but these are all
65+
// the types of events that we care about in the TimelineFrameModel class at
66+
// the bottom of this file. Previously we would take a copy of an array of
67+
// all trace events, but on a few test traces, this set of filtered events
68+
// accounts for about 10% of the total events, so it's a big performance win
69+
// to deal with a much smaller subset of the data.
70+
if (isFrameEvent(event) || Types.Events.isLayerTreeHostImplSnapshot(event) || entryIsTopLevel(event) ||
71+
MAIN_FRAME_MARKERS.has(event.name as Types.Events.Name) || Types.Events.isPaint(event)) {
72+
relevantFrameEvents.push(event);
73+
}
3374
}
3475

35-
export async function finalize({allTraceEvents}: FinalizeOptions): Promise<void> {
36-
// Snapshot events can be emitted out of order, so we need to sort before
37-
// building the frames model, but we need to take a copy because we don't
38-
// can't mutate the raw input.
39-
// TODO(crbug.com/436491188): this is expensive...we copy and sort the entire trace each time we import it.
40-
const allEvents = allTraceEvents.toSorted(Helpers.Trace.eventTimeComparator);
76+
export async function finalize(): Promise<void> {
77+
// We have to sort the events by timestamp, because the model code expects to
78+
// process events in order.
79+
Helpers.Trace.sortTraceEventsInPlace(relevantFrameEvents);
4180

4281
const modelForTrace = new TimelineFrameModel(
43-
allEvents,
82+
relevantFrameEvents,
4483
rendererHandlerData(),
4584
auctionWorkletsData(),
4685
metaHandlerData(),
@@ -65,28 +104,6 @@ export function deps(): HandlerName[] {
65104
return ['Meta', 'Renderer', 'AuctionWorklets', 'LayerTree'];
66105
}
67106

68-
type FrameEvent = Types.Events.BeginFrame|Types.Events.DroppedFrame|Types.Events.RequestMainThreadFrame|
69-
Types.Events.BeginMainThreadFrame|Types.Events.Commit|Types.Events.CompositeLayers|
70-
Types.Events.ActivateLayerTree|Types.Events.NeedsBeginFrameChanged|Types.Events.DrawFrame;
71-
72-
function isFrameEvent(event: Types.Events.Event): event is FrameEvent {
73-
return (
74-
Types.Events.isSetLayerId(event) || Types.Events.isBeginFrame(event) || Types.Events.isDroppedFrame(event) ||
75-
Types.Events.isRequestMainThreadFrame(event) || Types.Events.isBeginMainThreadFrame(event) ||
76-
Types.Events.isNeedsBeginFrameChanged(event) ||
77-
// Note that "Commit" is the replacement for "CompositeLayers" so in a trace
78-
// we wouldn't expect to see a combination of these. All "new" trace
79-
// recordings use "Commit", but we can easily support "CompositeLayers" too
80-
// to not break older traces being imported.
81-
Types.Events.isCommit(event) || Types.Events.isCompositeLayers(event) ||
82-
Types.Events.isActivateLayerTree(event) || Types.Events.isDrawFrame(event));
83-
}
84-
85-
function entryIsTopLevel(entry: Types.Events.Event): boolean {
86-
const devtoolsTimelineCategory = 'disabled-by-default-devtools.timeline';
87-
return entry.name === Types.Events.Name.RUN_TASK && entry.cat.includes(devtoolsTimelineCategory);
88-
}
89-
90107
export class TimelineFrameModel {
91108
#frames: TimelineFrame[] = [];
92109
#frameById: Record<number, TimelineFrame> = {};
@@ -362,13 +379,6 @@ export class TimelineFrameModel {
362379
}
363380
}
364381

365-
const MAIN_FRAME_MARKERS = new Set<Types.Events.Name>([
366-
Types.Events.Name.SCHEDULE_STYLE_RECALCULATION,
367-
Types.Events.Name.INVALIDATE_LAYOUT,
368-
Types.Events.Name.BEGIN_MAIN_THREAD_FRAME,
369-
Types.Events.Name.SCROLL_LAYER,
370-
]);
371-
372382
/**
373383
* Legacy class that represents TimelineFrames that was ported from the old SDK.
374384
* This class is purposefully not exported as it breaks the abstraction that

0 commit comments

Comments
 (0)