Skip to content

Commit 8927c6b

Browse files
and-oliDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Speed up flows handler
By checking that we only assign events to flows once given a flow binding tuple (a token that allows us to match events to flows, consisting of the event's ts, cat, pid and tid). Perfetto's trace event format [1] only considers the first event that matches a given binding before assigning an event to a flow. By being consistent with this behavior we are able to skip subsequent events with a matching binding and save quite some time on traces with many repeated flow binding tuples. To prevent further regressions, added a perf test case that consistently fails on the previous state. [1] https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?tab=t.0#heading=h.4qqub5rv9ybk Fixed: 382545507 Change-Id: I33d0ab6e167549a6499eaf6499b5a5f10d777336 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6097891 Auto-Submit: Andres Olivares <[email protected]> Reviewed-by: Adam Raine <[email protected]> Commit-Queue: Andres Olivares <[email protected]>
1 parent e1d00f2 commit 8927c6b

File tree

8 files changed

+108
-74
lines changed

8 files changed

+108
-74
lines changed

front_end/core/platform/MapUtilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class Multimap<K, V> {
8585
export function getWithDefault<K extends {}, V>(
8686
map: WeakMap<K, V>|Map<K, V>, key: K, defaultValueFactory: (key?: K) => V): V {
8787
let value = map.get(key);
88-
if (!value) {
88+
if (value === undefined || value === null) {
8989
value = defaultValueFactory(key);
9090
map.set(key, value);
9191
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('AsyncJSCallsHandler', function() {
9696
async function() {
9797
const jsTaskScheduler = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 0, 30, cat, tid, pid);
9898
const asyncTaskScheduled =
99-
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
99+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 5, 0, cat, pid, tid);
100100

101101
const asyncTaskRun =
102102
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
@@ -122,7 +122,7 @@ describe('AsyncJSCallsHandler', function() {
122122
const jsTaskScheduler = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 0, 30, cat, tid, pid);
123123
// Two asyncTaskScheduled events right under the function call.
124124
const asyncTaskScheduled1 =
125-
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
125+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 5, 0, cat, pid, tid);
126126

127127
const asyncTaskScheduled2 =
128128
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 10, 0, cat, pid, tid);

front_end/models/trace/handlers/FlowsHandler.ts

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2024 The Chromium Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4+
import * as Platform from '../../../core/platform/platform.js';
45
import * as Types from '../types/types.js';
56

67
// A flow is a logic connection between trace events. We display this
@@ -23,9 +24,28 @@ import * as Types from '../types/types.js';
2324
// flows with the timestamps of each phase. Then, we place trace events
2425
// in the flows where their corresponding phase events were placed (if
2526
// there are any corresponding flow phase events at all).
26-
const flowDataByGroupToken = new Map<string, {flowId: number, times: Types.Timing.MicroSeconds[]}>();
27-
const flowPhaseBindingTokenToFlowId = new Map<string, Set<number>>();
28-
const flowsById = new Map<number, {times: Types.Timing.MicroSeconds[], events: Types.Events.Event[]}>();
27+
const flowDataByGroupToken = new Map<string, number>();
28+
29+
type EventFlowData = {
30+
flows: Set<number>,
31+
bindingParsed: boolean,
32+
};
33+
type FlowBindingTuple =
34+
Map<Types.Timing.MicroSeconds, Map<Types.Events.ProcessID, Map<Types.Events.ThreadID, Map<string, EventFlowData>>>>;
35+
36+
// Given a trace event's flow binding tuple (timestamp, process id,
37+
// thread id and category) we determine if there is any flow data bound
38+
// to it by using this map's content. It's built when processing flow
39+
// events in a trace.
40+
// An alternative to having a map of four levels is having single map
41+
// from a string token built from concatenating the binding data to the
42+
// corresponding flow data. However, this token would be calculated for
43+
// every event in a trace, resulting in a lot of memory overhead and
44+
// major GC triggering. So we are trading off readability for
45+
// performance.
46+
const boundFlowData: FlowBindingTuple = new Map();
47+
48+
const flowsById = new Map<number, Map<Types.Timing.MicroSeconds, Types.Events.Event>>();
2949
const flowEvents: Types.Events.FlowEvent[] = [];
3050
const nonFlowEvents: Types.Events.Event[] = [];
3151
let flows: Types.Events.Event[][] = [];
@@ -35,7 +55,7 @@ export function reset(): void {
3555
flowEvents.length = 0;
3656
nonFlowEvents.length = 0;
3757
flowDataByGroupToken.clear();
38-
flowPhaseBindingTokenToFlowId.clear();
58+
boundFlowData.clear();
3959
flowsById.clear();
4060
}
4161

@@ -48,22 +68,21 @@ export function handleEvent(event: Types.Events.Event): void {
4868
}
4969

5070
function processNonFlowEvent(event: Types.Events.Event): void {
51-
const flowPhaseBindingToken = flowPhaseBindingTokenForEvent(event);
52-
const flowIds = flowPhaseBindingTokenToFlowId.get(flowPhaseBindingToken);
53-
if (!flowIds) {
71+
const flowDataForEvent = boundFlowData.get(event.ts)?.get(event.pid)?.get(event.tid)?.get(event.cat);
72+
if (!flowDataForEvent) {
5473
return;
5574
}
56-
for (const flowId of flowIds) {
57-
const flow = flowsById.get(flowId);
58-
if (!flow) {
59-
continue;
60-
}
61-
const timeIndex = flow.times.indexOf(event.ts);
62-
if (timeIndex < 0) {
63-
continue;
64-
}
65-
flow.events[timeIndex] = event;
75+
const {flows, bindingParsed} = flowDataForEvent;
76+
if (bindingParsed) {
77+
// We only consider the first event for a given flow binding tuple.
78+
return;
6679
}
80+
for (const flowId of flows) {
81+
const flow = Platform.MapUtilities.getWithDefault(
82+
flowsById, flowId, () => new Map<Types.Timing.MicroSeconds, Types.Events.Event>());
83+
flow.set(event.ts, event);
84+
}
85+
flowDataForEvent.bindingParsed = true;
6786
}
6887

6988
/**
@@ -76,51 +95,53 @@ function processFlowEvent(flowPhaseEvent: Types.Events.FlowEvent): void {
7695
const flowGroup = flowGroupTokenForFlowPhaseEvent(flowPhaseEvent);
7796
switch (flowPhaseEvent.ph) {
7897
case (Types.Events.Phase.FLOW_START): {
79-
const flowMetadata = {flowId: flowPhaseEvent.id, times: [flowPhaseEvent.ts]};
80-
flowDataByGroupToken.set(flowGroup, flowMetadata);
81-
addFlowIdToFlowPhaseBinding(flowPhaseBindingTokenForEvent(flowPhaseEvent), flowMetadata.flowId);
98+
const flowMetadata = {flowId: flowPhaseEvent.id, times: new Map([[flowPhaseEvent.ts, undefined]])};
99+
flowDataByGroupToken.set(flowGroup, flowPhaseEvent.id);
100+
addFlowIdToEventBinding(flowPhaseEvent, flowMetadata.flowId);
82101
return;
83102
}
84103
case (Types.Events.Phase.FLOW_STEP): {
85-
const flow = flowDataByGroupToken.get(flowGroup);
86-
if (!flow || flow.times.length < 0) {
104+
const flowId = flowDataByGroupToken.get(flowGroup);
105+
if (flowId === undefined) {
87106
// Found non-start flow event with no corresponding start flow,
88-
// start event. Quietly ignore problematic event.
107+
// start event. Quietly ignore the problematic event.
89108
return;
90109
}
91-
addFlowIdToFlowPhaseBinding(flowPhaseBindingTokenForEvent(flowPhaseEvent), flow.flowId);
92-
flow.times.push(flowPhaseEvent.ts);
110+
addFlowIdToEventBinding(flowPhaseEvent, flowId);
93111
return;
94112
}
95113
case (Types.Events.Phase.FLOW_END): {
96-
const flow = flowDataByGroupToken.get(flowGroup);
97-
if (!flow || flow.times.length < 0) {
114+
const flowId = flowDataByGroupToken.get(flowGroup);
115+
if (flowId === undefined) {
98116
// Found non-start flow event with no corresponding start flow,
99-
// start event. Quietly ignore problematic event.
117+
// start event. Quietly ignore the problematic event.
100118
return;
101119
}
102-
addFlowIdToFlowPhaseBinding(flowPhaseBindingTokenForEvent(flowPhaseEvent), flow.flowId);
103-
flow.times.push(flowPhaseEvent.ts);
104-
flowsById.set(flow.flowId, {times: flow.times, events: []});
120+
addFlowIdToEventBinding(flowPhaseEvent, flowId);
105121
// We don't need this data anymore as the flow has been finished,
106122
// so we can drop it.
107123
flowDataByGroupToken.delete(flowGroup);
108124
}
109125
}
110126
}
111127

128+
type MapValueType<T extends Map<unknown, unknown>> = NonNullable<ReturnType<T['get']>>;
112129
/**
113130
* A single trace event can belong to multiple flows. This method
114-
* tracks which flows (flowId) an event belongs to (given
115-
* its flow phase binding token).
131+
* tracks which flows (flowId) an event belongs to given its flow
132+
* binding tuple (made of its ts, pid, tid and cat).
116133
*/
117-
function addFlowIdToFlowPhaseBinding(flowPhaseBinding: string, flowId: number): void {
118-
let flowIds = flowPhaseBindingTokenToFlowId.get(flowPhaseBinding);
119-
if (!flowIds) {
120-
flowIds = new Set();
121-
}
122-
flowIds.add(flowId);
123-
flowPhaseBindingTokenToFlowId.set(flowPhaseBinding, flowIds);
134+
function addFlowIdToEventBinding(event: Types.Events.Event, flowId: number): void {
135+
const flowsByPid =
136+
Platform.MapUtilities.getWithDefault<Types.Timing.MicroSeconds, MapValueType<typeof boundFlowData>>(
137+
boundFlowData, event.ts, () => new Map());
138+
const flowsByTid = Platform.MapUtilities.getWithDefault<Types.Events.ProcessID, MapValueType<typeof flowsByPid>>(
139+
flowsByPid, event.pid, () => new Map());
140+
const flowsByCat = Platform.MapUtilities.getWithDefault<Types.Events.ThreadID, MapValueType<typeof flowsByTid>>(
141+
flowsByTid, event.tid, () => new Map());
142+
const flowData =
143+
Platform.MapUtilities.getWithDefault(flowsByCat, event.cat, () => ({flows: new Set(), bindingParsed: false}));
144+
flowData.flows.add(flowId);
124145
}
125146

126147
/**
@@ -138,29 +159,14 @@ function flowGroupTokenForFlowPhaseEvent(event: Types.Events.FlowEvent): string
138159
return `${event.cat}${ID_COMPONENT_SEPARATOR}${event.name}${ID_COMPONENT_SEPARATOR}${event.id}`;
139160
}
140161

141-
/**
142-
* A flow phase binding is a token that allows us to associate a flow
143-
* phase event to its corresponding event. This association indicates
144-
* what role a trace event plays in a flow.
145-
* We can assign a trace event with a flow phase when its category,
146-
* thread id, process id and timestamp matches those of a flow phase
147-
* event.
148-
*/
149-
function flowPhaseBindingTokenForEvent(event: Types.Events.Event): string {
150-
// This function is called many times (one per event) and creating a
151-
// string every time can trigger GC. If this becomes a problem, a
152-
// possible optimization is to use a multi-key map with the
153-
// binding token components, a trade off between memory performance
154-
// and readability.
155-
return `${event.cat}${ID_COMPONENT_SEPARATOR}${event.tid}${ID_COMPONENT_SEPARATOR}${event.pid}${
156-
ID_COMPONENT_SEPARATOR}${event.ts}`;
157-
}
158-
159162
export async function finalize(): Promise<void> {
160163
// Order is important: flow events need to be handled first.
161164
flowEvents.forEach(processFlowEvent);
162165
nonFlowEvents.forEach(processNonFlowEvent);
163-
flows = [...flowsById.values()].map(f => f.events).filter(flow => flow.length > 1);
166+
flows = [...flowsById.values()]
167+
.map(flowMapping => [...flowMapping.values()])
168+
.map(flow => flow.filter(event => event !== undefined))
169+
.filter(flow => flow.length > 1);
164170
}
165171

166172
export function data(): {flows: Types.Events.Event[][]} {

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-long.json.gz",
2324
"dom-size.json.gz",
2425
"enhanced-traces.json.gz",
2526
"extension-tracks-and-marks.json.gz",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ Generate from a recording of [this HTML file](https://gist.github.com/adamraine/
229229
5. Click the button once
230230
6. End recording
231231

232+
### dom-size-long
233+
Contains a trace from a site with a large DOM. It also happens to have many flows with duplicated flow bindings, so it's useful to test the FlowsHandler remains quick.
234+
232235
### lcp-multiple-frames
233236

234237
Generated from [lcp-iframes story](https://github.com/ChromeDevTools/performance-stories/tree/main/lcp-iframes).
653 KB
Binary file not shown.

test/perf/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ These are performance tests that run benchmarks of automated userflows. The resu
55
The tests are run on devtools CI on the "Standalone Linux" bot. The result of each run is [uploaded](https://source.chromium.org/chromium/infra/infra_superproject/+/main:build/recipes/recipes/devtools/devtools-frontend.py?q=publish_performance_benchmarks) to Skia Perf so that the benchmarks can be monitored in [devtools own dashboard instance](https://devtools-frontend-perf.corp.goog/e/) .
66

77

8-
## Quick links
9-
* [Performance panel "TraceLoad" benchmark dashboard](https://devtools-frontend-perf.corp.goog/e/?queries=stats%3Dmean%26test%3DTraceLoad)
10-
* [DevTools "BootPerf" benchmark dashboard](https://devtools-frontend-perf.corp.goog/e/?queries=test%3DBootPerf)
11-
* [Peak of the GCS bucket where data is stored](https://pantheon.corp.google.com/storage/browser/devtools-frontend-perf/ingest/2024/10/08/08/client.devtools-frontend.integration/Stand-alone%20Linux/performance-tests?pageState=(%22StorageObjectListTable%22:(%22f%22:%22%255B%255D%22))&e=-13802955&mods=component_inspector&project=skia-public) (you need the "Storage Object Viewer" role for your account. You can ask the [Chrome Browser Infra team](https://g3doc.corp.google.com/company/teams/chrome/ops/engprod/browser_infra/index.md?cl=head) to grant you access).
8+
## Quick links (copy and paste)
9+
* Performance panel "TraceLoad" benchmark dashboard: https://devtools-frontend-perf.corp.goog/e/?queries=stats%3Dmean%26test%3DTraceLoad
10+
* DevTools "BootPerf" benchmark dashboard: https://devtools-frontend-perf.corp.goog/e/?queries=test%3DBootPerf)
11+
* Peak of the GCS bucket where data is stored: https://pantheon.corp.google.com/storage/browser/devtools-frontend-perf/ingest/2024/10/08/08/client.devtools-frontend.integration/Stand-alone%20Linux/performance-tests?pageState=(%22StorageObjectListTable%22:(%22f%22:%22%255B%255D%22))&e=-13802955&mods=component_inspector&project=skia-public) (you need the "Storage Object Viewer" role for your account. You can ask the [Chrome Browser Infra team](https://g3doc.corp.google.com/company/teams/chrome/ops/engprod/browser_infra/index.md?cl=head) to grant you access).
1212

test/perf/panels/performance/trace-load_test.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,20 @@ async function timeFixture(fixture: string): Promise<number> {
3434

3535
describe('Performance panel trace load performance', () => {
3636
const allTestValues: {name: string, values: number[]}[] = [];
37-
3837
// Slow test
39-
describe.skip('[crbug.com/383713603]: Total trace load time', () => {
38+
describe.skip('[crbug.com/383713603]: Large CPU profile load benchmark', () => {
4039
beforeEach(async () => {
4140
// Reload devtools to get a fresh version of the panel on each
4241
// run and prevent a skew due to caching, etc.
4342
await reloadDevTools();
4443
});
4544
const RUNS = 10;
4645
const testValues = {
47-
name: 'TraceLoad',
46+
name: 'LargeCPULoad',
4847
values: [] as number[],
4948
};
5049
for (let run = 1; run <= RUNS; run++) {
51-
it(`run ${run}/${RUNS}`, async function() {
50+
it('run large cpu profile benchmark', async function() {
5251
this.timeout(20_000);
5352
const duration = await timeFixture('large-profile.cpuprofile');
5453
// Ensure only 2 decimal places.
@@ -61,6 +60,31 @@ describe('Performance panel trace load performance', () => {
6160
});
6261
});
6362

63+
describe('Large DOM trace load benchmark', () => {
64+
beforeEach(async () => {
65+
// Reload devtools to get a fresh version of the panel on each
66+
// run and prevent a skew due to caching, etc.
67+
await reloadDevTools();
68+
});
69+
const RUNS = 10;
70+
const testValues = {
71+
name: 'LargeDOMTraceLoad',
72+
values: [] as number[],
73+
};
74+
for (let run = 1; run <= RUNS; run++) {
75+
it('run large dom trace load benchmark', async function() {
76+
this.timeout(8_000);
77+
const duration = await timeFixture('dom-size-long.json');
78+
// Ensure only 2 decimal places.
79+
const timeTaken = Number(duration.toFixed(2));
80+
testValues.values.push(timeTaken);
81+
});
82+
}
83+
after(() => {
84+
allTestValues.push(testValues);
85+
});
86+
});
87+
6488
after(async () => {
6589
// Calculate statistics for each benchmark.
6690
for (const testValues of allTestValues) {
@@ -96,10 +120,10 @@ describe('Performance panel trace load performance', () => {
96120
addBenchmarkResult(benchmark);
97121
/* eslint-disable no-console */
98122
console.log(`Benchmark name: ${testValues.name}`);
99-
console.log(`Mean boot time: ${meanMeasure}ms`);
100-
console.log(`50th percentile boot time: ${percentile50}ms`);
101-
console.log(`90th percentile boot time: ${percentile90}ms`);
102-
console.log(`99th percentile boot time: ${percentile99}ms`);
123+
console.log(`Mean time: ${meanMeasure}ms`);
124+
console.log(`50th percentile time: ${percentile50}ms`);
125+
console.log(`90th percentile time: ${percentile90}ms`);
126+
console.log(`99th percentile time: ${percentile99}ms`);
103127
/* eslint-enable no-console */
104128
}
105129
});

0 commit comments

Comments
 (0)