Skip to content

Commit a9b959d

Browse files
and-oliDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Add AsyncCallStacksHandler
The handler will generalize async JS calls in the perf panel. Its output will be the source for the InitiatorHandler's JS scheduling relationships, so that they don't need to be hardcoded as they are currently. The output of this handler will also be the source for async call stacks that will be shown in the details of JS entries (ProfileCalls) but this require some reworking on it, which will happen in follow up CLs. Bug: 381391955 Change-Id: If1b3a845dccf35bbfd6ae2aa06e7baf4a282d5e0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6055479 Reviewed-by: Jack Franklin <[email protected]> Commit-Queue: Andres Olivares <[email protected]>
1 parent a51a025 commit a9b959d

File tree

11 files changed

+331
-22
lines changed

11 files changed

+331
-22
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/TraceTree.js",
10521052
"front_end/models/trace/extras/URLForEntry.js",
10531053
"front_end/models/trace/handlers/AnimationHandler.js",
1054+
"front_end/models/trace/handlers/AsyncCallStacksHandler.js",
10541055
"front_end/models/trace/handlers/AuctionWorkletsHandler.js",
10551056
"front_end/models/trace/handlers/ExtensionTraceDataHandler.js",
10561057
"front_end/models/trace/handlers/FlowsHandler.js",
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 {
6+
makeCompleteEvent,
7+
makeFlowEvents,
8+
makeProfileCall,
9+
} from '../../../testing/TraceHelpers.js';
10+
import * as Trace from '../trace.js';
11+
const cat = 'mewtwo';
12+
const pid = 1;
13+
const tid = 1;
14+
async function buildAsyncCallStacksHandlerData(events: Trace.Types.Events.Event[]):
15+
Promise<ReturnType<typeof Trace.Handlers.ModelHandlers.AsyncCallStacks.data>> {
16+
Trace.Handlers.ModelHandlers.Renderer.reset();
17+
Trace.Handlers.ModelHandlers.Flows.reset();
18+
Trace.Handlers.ModelHandlers.Flows.reset();
19+
for (const event of events) {
20+
Trace.Handlers.ModelHandlers.Renderer.handleEvent(event);
21+
Trace.Handlers.ModelHandlers.Flows.handleEvent(event);
22+
}
23+
await Trace.Handlers.ModelHandlers.Renderer.finalize();
24+
await Trace.Handlers.ModelHandlers.Flows.finalize();
25+
await Trace.Handlers.ModelHandlers.AsyncCallStacks.finalize();
26+
return Trace.Handlers.ModelHandlers.AsyncCallStacks.data();
27+
}
28+
describe('AsyncCallStacksHandler', function() {
29+
describe('Resolving JS task schedulers to task run entrypoints', function() {
30+
it('associates a JS task scheduler profile call with the corresponding task run entry point', async function() {
31+
const jsTaskScheduler = makeProfileCall('setTimeout', 0, 50, pid, tid);
32+
const asyncTaskScheduled =
33+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
34+
35+
const asyncTaskRun = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
36+
const jsTaskRunEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
37+
38+
const flowEvents = makeFlowEvents([asyncTaskScheduled, asyncTaskRun]);
39+
const rendererEvents = [jsTaskScheduler, asyncTaskScheduled, asyncTaskRun, jsTaskRunEntryPoint];
40+
const allEvents = [...rendererEvents, ...flowEvents];
41+
42+
const asyncCallStacksData = await buildAsyncCallStacksHandlerData(allEvents);
43+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
44+
assert.strictEqual(testRunEntryPoints?.length, 1);
45+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
46+
});
47+
48+
it('uses the nearest profile call ancestor of a debuggerTaskScheduled as JS task scheduler', async function() {
49+
// Three profile call ancestors to the debuggerTaskScheduled event.
50+
// Test the one closest to the debuggerTaskScheduled in the tree
51+
// is picked.
52+
const foo = makeProfileCall('foo', 0, 50, pid, tid);
53+
const bar = makeProfileCall('bar', 0, 40, pid, tid);
54+
const jsTaskScheduler = makeProfileCall('setTimeout', 0, 30, pid, tid);
55+
const asyncTaskScheduled =
56+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
57+
58+
const asyncTaskRun = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
59+
const jsTaskRunEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
60+
61+
const flowEvents = makeFlowEvents([asyncTaskScheduled, asyncTaskRun]);
62+
const rendererEvents = [foo, bar, jsTaskScheduler, asyncTaskScheduled, asyncTaskRun, jsTaskRunEntryPoint];
63+
const allEvents = [...rendererEvents, ...flowEvents];
64+
65+
const asyncCallStacksData = await buildAsyncCallStacksHandlerData(allEvents);
66+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
67+
assert.strictEqual(testRunEntryPoints?.length, 1);
68+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
69+
});
70+
71+
it('uses the nearest JS entry point descendant of a debuggerTaskRun as async task run', async function() {
72+
const jsTaskScheduler = makeProfileCall('setTimeout', 0, 30, pid, tid);
73+
const asyncTaskScheduled =
74+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
75+
76+
const asyncTaskRun = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
77+
78+
// Two JS entry points belonging to the same subtree are
79+
// descendants to the debuggerTaskRun event. Test the one closest
80+
// to the debuggerTaskRun in the global tree is picked.
81+
const jsTaskRunEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
82+
const secondFakeEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 71, 10, cat, tid, pid);
83+
84+
const flowEvents = makeFlowEvents([asyncTaskScheduled, asyncTaskRun]);
85+
const rendererEvents =
86+
[jsTaskScheduler, asyncTaskScheduled, asyncTaskRun, jsTaskRunEntryPoint, secondFakeEntryPoint];
87+
const allEvents = [...rendererEvents, ...flowEvents];
88+
89+
const asyncCallStacksData = await buildAsyncCallStacksHandlerData(allEvents);
90+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
91+
assert.strictEqual(testRunEntryPoints?.length, 1);
92+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
93+
});
94+
it('returns multiple JS entry point descendants of a debuggerTaskRun when they are not in the same subtree',
95+
async function() {
96+
const jsTaskScheduler = makeProfileCall('setTimeout', 0, 30, pid, tid);
97+
const asyncTaskScheduled =
98+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
99+
100+
const asyncTaskRun =
101+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
102+
103+
// Two JS entry points belonging to different subtrees are
104+
// descendants to the debuggerTaskRun event. Test both are
105+
// used.
106+
const firstJSTaskRunEntryPoint =
107+
makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
108+
const secondJSTaskRunEntryPoint =
109+
makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 90, 10, cat, tid, pid);
110+
111+
const flowEvents = makeFlowEvents([asyncTaskScheduled, asyncTaskRun]);
112+
const rendererEvents =
113+
[jsTaskScheduler, asyncTaskScheduled, asyncTaskRun, firstJSTaskRunEntryPoint, secondJSTaskRunEntryPoint];
114+
const allEvents = [...rendererEvents, ...flowEvents];
115+
116+
const asyncCallStacksData = await buildAsyncCallStacksHandlerData(allEvents);
117+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
118+
assert.strictEqual(testRunEntryPoints?.length, 2);
119+
assert.strictEqual(testRunEntryPoints?.[0], firstJSTaskRunEntryPoint);
120+
assert.strictEqual(testRunEntryPoints?.[1], secondJSTaskRunEntryPoint);
121+
});
122+
});
123+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 type * as Helpers from '../helpers/helpers.js';
5+
import * as Types from '../types/types.js';
6+
7+
import {data as flowsHandlerData} from './FlowsHandler.js';
8+
import {data as rendererHandlerData} from './RendererHandler.js';
9+
10+
const schedulerToRunEntryPoints: Map<Types.Events.SyntheticProfileCall, Types.Events.Event[]> = new Map();
11+
12+
export function reset(): void {
13+
schedulerToRunEntryPoints.clear();
14+
}
15+
16+
export function handleEvent(_: Types.Events.Event): void {
17+
}
18+
19+
export async function finalize(): Promise<void> {
20+
const {flows} = flowsHandlerData();
21+
const {entryToNode} = rendererHandlerData();
22+
// Process async task flows
23+
for (const flow of flows) {
24+
const asyncTaskScheduled = flow.at(0);
25+
if (!asyncTaskScheduled || !Types.Events.isDebuggerAsyncTaskScheduled(asyncTaskScheduled)) {
26+
continue;
27+
}
28+
const asyncTaskRun = flow.at(1);
29+
if (!asyncTaskRun || !Types.Events.isDebuggerAsyncTaskRun(asyncTaskRun)) {
30+
// Unexpected flow shape, ignore.
31+
continue;
32+
}
33+
const asyncCaller = findNearestProfileCallAncestor(asyncTaskScheduled, entryToNode);
34+
if (!asyncCaller) {
35+
// Unexpected async call trace data shape, ignore.
36+
continue;
37+
}
38+
const asyncEntryPoints = findNearestJsInvocationDescendants(asyncTaskRun, entryToNode);
39+
if (!asyncEntryPoints) {
40+
// Unexpected async call trace data shape, ignore.
41+
continue;
42+
}
43+
schedulerToRunEntryPoints.set(asyncCaller, asyncEntryPoints);
44+
}
45+
}
46+
/**
47+
* Given a DebuggerAsyncTaskScheduled event, returns its closest
48+
* ProfileCall ancestor, which represents the JS call that scheduled
49+
* the async task.
50+
*/
51+
function findNearestProfileCallAncestor(
52+
asyncTaskScheduled: Types.Events.DebuggerAsyncTaskScheduled,
53+
entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>): Types.Events.SyntheticProfileCall|null {
54+
let node = entryToNode.get(asyncTaskScheduled)?.parent;
55+
while (node) {
56+
if (Types.Events.isProfileCall(node.entry)) {
57+
return node.entry;
58+
}
59+
node = node.parent;
60+
}
61+
return null;
62+
}
63+
/**
64+
* Entrypoints to JS execution in the timeline. We ignore those starting
65+
* with 'v8' because they aren't shown in the timeline, and ultimately
66+
* this function's output results in "initiated" events, so ideally this
67+
* returns events that end up in the flame chart.
68+
*/
69+
function acceptJSInvocationsPredicate(event: Types.Events.Event): event is Types.Events.Event {
70+
return Types.Events.isJSInvocationEvent(event) && !event.name.startsWith('v8') && !event.name.startsWith('V8');
71+
}
72+
73+
/**
74+
* Given a DebuggerAsyncTaskRun event, returns its closest JS entry
75+
* point descendants, which represent the task being scheduled.
76+
*
77+
* We return multiple entry points beacuse some of these are built
78+
* from samples (like `consoleTask.run()` ). Because of limitations with
79+
* sampling, multiple entry points can mistakenly be made from a single
80+
* entry point, so we return all of them to ensure the async stack is
81+
* in every event that applies.
82+
*/
83+
function findNearestJsInvocationDescendants(
84+
asyncTaskRun: Types.Events.DebuggerAsyncTaskRun,
85+
entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>): Types.Events.Event[] {
86+
// Ignore descendants of other DebuggerAsyncTaskRuns since they
87+
// are part of another async task and have to be handled separately
88+
return findFirstDescendantsOfType(
89+
asyncTaskRun, entryToNode, acceptJSInvocationsPredicate, Types.Events.isDebuggerAsyncTaskRun);
90+
}
91+
92+
/**
93+
* Given a root event returns all the top level descendants that meet a
94+
* predicate condition (predicateAccept) while ignoring subtrees whose
95+
* top event meets an ignore condition (predicateIgnore).
96+
*/
97+
function findFirstDescendantsOfType<T extends Types.Events.Event>(
98+
root: Types.Events.Event, entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>,
99+
predicateAccept: (event: Types.Events.Event) => event is T,
100+
predicateIgnore: (event: Types.Events.Event) => boolean): T[] {
101+
const node = entryToNode.get(root);
102+
if (!node) {
103+
return [];
104+
}
105+
const childrenGroups = [[...node.children]];
106+
const firstDescendants = [];
107+
for (let i = 0; i < childrenGroups.length; i++) {
108+
const siblings = childrenGroups[i];
109+
for (let j = 0; j < siblings.length; j++) {
110+
const node = siblings[j];
111+
if (predicateAccept(node.entry)) {
112+
firstDescendants.push(node.entry);
113+
} else if (!predicateIgnore(node.entry)) {
114+
childrenGroups.push([...node.children]);
115+
}
116+
}
117+
}
118+
return firstDescendants;
119+
}
120+
121+
export function data(): {
122+
// Given a profile call, returns the JS entrypoints it scheduled (if any).
123+
// For example, given a setTimeout call, returns the JS entry point
124+
// trace event for the timeout callback run event (usually a
125+
// FunctionCall event).
126+
schedulerToRunEntryPoints: Map<Types.Events.SyntheticProfileCall, Types.Events.Event[]>,
127+
} {
128+
return {
129+
schedulerToRunEntryPoints,
130+
};
131+
}
132+
133+
export function deps(): ['Renderer', 'Flows'] {
134+
return ['Renderer', 'Flows'];
135+
}

front_end/models/trace/handlers/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import("../../visibility.gni")
1010
devtools_module("handlers") {
1111
sources = [
1212
"AnimationHandler.ts",
13+
"AsyncCallStacksHandler.ts",
1314
"AuctionWorkletsHandler.ts",
1415
"ExtensionTraceDataHandler.ts",
1516
"FlowsHandler.ts",
@@ -66,6 +67,7 @@ ts_library("unittests") {
6667

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

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import {
99
import * as Trace from '../trace.js';
1010

1111
async function getFlowsHandlerData(events: Trace.Types.Events.Event[]):
12-
Promise<ReturnType<typeof Trace.Handlers.ModelHandlers.FlowsHandler.data>> {
13-
Trace.Handlers.ModelHandlers.FlowsHandler.reset();
12+
Promise<ReturnType<typeof Trace.Handlers.ModelHandlers.Flows.data>> {
13+
Trace.Handlers.ModelHandlers.Flows.reset();
1414
for (const event of events) {
15-
Trace.Handlers.ModelHandlers.FlowsHandler.handleEvent(event);
15+
Trace.Handlers.ModelHandlers.Flows.handleEvent(event);
1616
}
17-
await Trace.Handlers.ModelHandlers.FlowsHandler.finalize();
18-
return Trace.Handlers.ModelHandlers.FlowsHandler.data();
17+
await Trace.Handlers.ModelHandlers.Flows.finalize();
18+
return Trace.Handlers.ModelHandlers.Flows.data();
1919
}
2020
const cat = 'mewtwo';
2121
const pid = 0;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ describe('InitiatorsHandler', () => {
194194
it('for a PostMessage Handler event the initiator is the PostMessage Dispatch event', async function() {
195195
const traceEvents = await TraceLoader.rawEvents(this, 'postmessage-initiators.json.gz');
196196
for (const event of traceEvents) {
197-
Trace.Handlers.ModelHandlers.FlowsHandler.handleEvent(event);
197+
Trace.Handlers.ModelHandlers.Flows.handleEvent(event);
198198
Trace.Handlers.ModelHandlers.Initiators.handleEvent(event);
199199
}
200-
await Trace.Handlers.ModelHandlers.FlowsHandler.finalize();
200+
await Trace.Handlers.ModelHandlers.Flows.finalize();
201201
await Trace.Handlers.ModelHandlers.Initiators.finalize();
202202
const data = Trace.Handlers.ModelHandlers.Initiators.data();
203203

front_end/models/trace/handlers/InitiatorsHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,6 @@ export function data(): InitiatorsData {
188188
};
189189
}
190190

191-
export function deps(): ['FlowsHandler'] {
192-
return ['FlowsHandler'];
191+
export function deps(): ['Flows'] {
192+
return ['Flows'];
193193
}

front_end/models/trace/handlers/ModelHandlers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
// found in the LICENSE file.
44

55
export * as Animations from './AnimationHandler.js';
6+
export * as AsyncCallStacks from './AsyncCallStacksHandler.js';
67
export * as AuctionWorklets from './AuctionWorkletsHandler.js';
78
export * as ExtensionTraceData from './ExtensionTraceDataHandler.js';
8-
export * as FlowsHandler from './FlowsHandler.js';
9+
export * as Flows from './FlowsHandler.js';
910
export * as Frames from './FramesHandler.js';
1011
export * as GPU from './GPUHandler.js';
1112
export * as ImagePainting from './ImagePaintingHandler.js';

front_end/models/trace/types/TraceEvents.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1832,6 +1832,22 @@ export function isInvalidateLayout(event: Event): event is InvalidateLayout {
18321832
return event.name === Name.INVALIDATE_LAYOUT;
18331833
}
18341834

1835+
export interface DebuggerAsyncTaskScheduled extends Event {
1836+
name: Name.DEBUGGER_ASYNC_TASK_SCHEDULED;
1837+
}
1838+
1839+
export function isDebuggerAsyncTaskScheduled(event: Event): event is DebuggerAsyncTaskScheduled {
1840+
return event.name === Name.DEBUGGER_ASYNC_TASK_SCHEDULED;
1841+
}
1842+
1843+
export interface DebuggerAsyncTaskRun extends Event {
1844+
name: Name.DEBUGGER_ASYNC_TASK_RUN;
1845+
}
1846+
1847+
export function isDebuggerAsyncTaskRun(event: Event): event is DebuggerAsyncTaskRun {
1848+
return event.name === Name.DEBUGGER_ASYNC_TASK_RUN;
1849+
}
1850+
18351851
class ProfileIdTag {
18361852
readonly #profileIdTag: (symbol|undefined);
18371853
}
@@ -2635,7 +2651,8 @@ export function isAbortPostTaskCallback(event: Event): event is RunPostTaskCallb
26352651
/**
26362652
* Generally, before JS is executed, a trace event is dispatched that
26372653
* parents the JS calls. These we call "invocation" events. This
2638-
* function determines if an event is one of such.
2654+
* function determines if an event is one of such. Note: these are also
2655+
* commonly referred to as "JS entry points".
26392656
*/
26402657
export function isJSInvocationEvent(event: Event): boolean {
26412658
switch (event.name) {
@@ -2740,6 +2757,8 @@ export const enum Name {
27402757
SCHEDULE_POST_TASK_CALLBACK = 'SchedulePostTaskCallback',
27412758
RUN_POST_TASK_CALLBACK = 'RunPostTaskCallback',
27422759
ABORT_POST_TASK_CALLBACK = 'AbortPostTaskCallback',
2760+
DEBUGGER_ASYNC_TASK_RUN = 'v8::Debugger::AsyncTaskRun',
2761+
DEBUGGER_ASYNC_TASK_SCHEDULED = 'v8::Debugger::AsyncTaskScheduled',
27432762

27442763
/* Gc */
27452764
GC = 'GCEvent',

front_end/panels/timeline/TimelineController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class TimelineController implements Trace.TracingManager.TracingManagerCl
9797
disabledByDefault('devtools.timeline.frame'),
9898
disabledByDefault('devtools.timeline.stack'),
9999
disabledByDefault('v8.compile'),
100+
disabledByDefault('v8.inspector'),
100101
disabledByDefault('v8.cpu_profiler.hires'),
101102
disabledByDefault('lighthouse'),
102103
'v8.execute',

0 commit comments

Comments
 (0)