Skip to content

Commit 13548c8

Browse files
and-oliDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Also use JS invocation trace events as async JS task schedulers
Before this CL, only profile calls were computed as async JS callers. However, due to limitations with sampling, it's possible that no profile call is built at all for an async call. In these cases we must fall back to the corresponding FunctionCall trace event, which unlike profile calls, is certain to happen for a given async call because these events come from tracing instead of sampling. Fixed: 384657567 Change-Id: I2a6364be37f64dabc3d7689015e6257410bc5ab6 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6097454 Commit-Queue: Andres Olivares <[email protected]> Reviewed-by: Alina Varkki <[email protected]>
1 parent 148fd10 commit 13548c8

File tree

3 files changed

+93
-21
lines changed

3 files changed

+93
-21
lines changed

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ describe('AsyncJSCallsHandler', function() {
4040
const allEvents = [...rendererEvents, ...flowEvents];
4141

4242
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
43-
const testRunEntryPoint = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
44-
assert.strictEqual(testRunEntryPoint, jsTaskRunEntryPoint);
43+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
44+
assert.strictEqual(testRunEntryPoints?.length, 1);
45+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
4546
});
4647

4748
it('uses the nearest profile call ancestor of a debuggerTaskScheduled as JS task scheduler', async function() {
@@ -62,8 +63,9 @@ describe('AsyncJSCallsHandler', function() {
6263
const allEvents = [...rendererEvents, ...flowEvents];
6364

6465
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
65-
const testRunEntryPoint = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
66-
assert.strictEqual(testRunEntryPoint, jsTaskRunEntryPoint);
66+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
67+
assert.strictEqual(testRunEntryPoints?.length, 1);
68+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
6769
});
6870

6971
it('uses the nearest JS entry point descendant of a debuggerTaskRun as async task run', async function() {
@@ -85,8 +87,75 @@ describe('AsyncJSCallsHandler', function() {
8587
const allEvents = [...rendererEvents, ...flowEvents];
8688

8789
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
88-
const testRunEntryPoint = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
89-
assert.strictEqual(testRunEntryPoint, jsTaskRunEntryPoint);
90+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
91+
assert.strictEqual(testRunEntryPoints?.length, 1);
92+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
93+
});
94+
95+
it('falls back to a JS invocation as task scheduler if no profile call is found before in the debuggerTaskScheduled ancestors',
96+
async function() {
97+
const jsTaskScheduler = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 0, 30, cat, tid, pid);
98+
const asyncTaskScheduled =
99+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
100+
101+
const asyncTaskRun =
102+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
103+
104+
// Two JS entry points belonging to the same subtree are
105+
// descendants to the debuggerTaskRun event. Test the one closest
106+
// to the debuggerTaskRun in the global tree is picked.
107+
const jsTaskRunEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
108+
const secondFakeEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 71, 10, cat, tid, pid);
109+
110+
const flowEvents = makeFlowEvents([asyncTaskScheduled, asyncTaskRun]);
111+
const rendererEvents =
112+
[jsTaskScheduler, asyncTaskScheduled, asyncTaskRun, jsTaskRunEntryPoint, secondFakeEntryPoint];
113+
const allEvents = [...rendererEvents, ...flowEvents];
114+
115+
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
116+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
117+
assert.strictEqual(testRunEntryPoints?.length, 1);
118+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint);
119+
});
120+
121+
it('returns multiple JS entry points when scheduled by the same function', async function() {
122+
const jsTaskScheduler = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 0, 30, cat, tid, pid);
123+
// Two asyncTaskScheduled events right under the function call.
124+
const asyncTaskScheduled1 =
125+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
126+
127+
const asyncTaskScheduled2 =
128+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 10, 0, cat, pid, tid);
129+
130+
const asyncTaskRun1 = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
131+
132+
const asyncTaskRun2 = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 200, 100, cat, tid, pid);
133+
134+
// Two JS entry points,
135+
const firstJSTaskRunEntryPoint = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
136+
const secondJSTaskRunEntryPoint =
137+
makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 210, 10, cat, tid, pid);
138+
139+
const flow1Events = makeFlowEvents([asyncTaskScheduled1, asyncTaskRun1], 1);
140+
const flow2Events = makeFlowEvents([asyncTaskScheduled2, asyncTaskRun2], 2);
141+
const flowEvents = [...flow1Events, ...flow2Events].sort((a, b) => a.ts - b.ts);
142+
const rendererEvents = [
143+
jsTaskScheduler,
144+
asyncTaskScheduled1,
145+
asyncTaskRun1,
146+
asyncTaskScheduled2,
147+
firstJSTaskRunEntryPoint,
148+
asyncTaskRun2,
149+
secondJSTaskRunEntryPoint,
150+
];
151+
const allEvents = [...rendererEvents, ...flowEvents];
152+
153+
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
154+
155+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
156+
assert.strictEqual(testRunEntryPoints?.length, 2);
157+
assert.strictEqual(testRunEntryPoints?.[0], firstJSTaskRunEntryPoint);
158+
assert.strictEqual(testRunEntryPoints?.[1], secondJSTaskRunEntryPoint);
90159
});
91160
});
92161
describe('Resolving async JS tasks to schedulers', function() {

front_end/models/trace/handlers/AsyncJSCallsHandler.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
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 type * as Helpers from '../helpers/helpers.js';
56
import * as Types from '../types/types.js';
67

78
import {data as flowsHandlerData} from './FlowsHandler.js';
89
import {data as rendererHandlerData} from './RendererHandler.js';
910

10-
const schedulerToRunEntryPoints: Map<Types.Events.SyntheticProfileCall, Types.Events.Event> = new Map();
11-
const asyncCallToScheduler:
12-
Map<Types.Events.SyntheticProfileCall, {taskName: string, scheduler: Types.Events.SyntheticProfileCall}> =
13-
new Map();
11+
const schedulerToRunEntryPoints: Map<Types.Events.Event, Types.Events.Event[]> = new Map();
12+
const asyncCallToScheduler: Map<Types.Events.SyntheticProfileCall, {taskName: string, scheduler: Types.Events.Event}> =
13+
new Map();
1414

1515
export function reset(): void {
1616
schedulerToRunEntryPoints.clear();
@@ -35,7 +35,7 @@ export async function finalize(): Promise<void> {
3535
// Unexpected flow shape, ignore.
3636
continue;
3737
}
38-
const asyncCaller = findNearestProfileCallAncestor(asyncTaskScheduled, entryToNode);
38+
const asyncCaller = findNearestJSAncestor(asyncTaskScheduled, entryToNode);
3939
if (!asyncCaller) {
4040
// Unexpected async call trace data shape, ignore.
4141
continue;
@@ -47,7 +47,8 @@ export async function finalize(): Promise<void> {
4747
}
4848
// Set scheduler -> schedulee mapping.
4949
// The schedulee being the JS entrypoint
50-
schedulerToRunEntryPoints.set(asyncCaller, asyncEntryPoint);
50+
const entryPoints = Platform.MapUtilities.getWithDefault(schedulerToRunEntryPoints, asyncCaller, () => []);
51+
entryPoints.push(asyncEntryPoint);
5152

5253
// Set schedulee -> scheduler mapping.
5354
// The schedulees being the JS calls (instead of the entrypoints as
@@ -60,15 +61,15 @@ export async function finalize(): Promise<void> {
6061
}
6162
/**
6263
* Given a DebuggerAsyncTaskScheduled event, returns its closest
63-
* ProfileCall ancestor, which represents the JS call that scheduled
64-
* the async task.
64+
* ProfileCall or JS invocation ancestor, which represents the JS call
65+
* that scheduled the async task.
6566
*/
66-
function findNearestProfileCallAncestor(
67+
function findNearestJSAncestor(
6768
asyncTaskScheduled: Types.Events.DebuggerAsyncTaskScheduled,
68-
entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>): Types.Events.SyntheticProfileCall|null {
69+
entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>): Types.Events.Event|null {
6970
let node = entryToNode.get(asyncTaskScheduled)?.parent;
7071
while (node) {
71-
if (Types.Events.isProfileCall(node.entry)) {
72+
if (Types.Events.isProfileCall(node.entry) || acceptJSInvocationsPredicate(node.entry)) {
7273
return node.entry;
7374
}
7475
node = node.parent;
@@ -82,7 +83,7 @@ function findNearestProfileCallAncestor(
8283
* returns events that end up in the flame chart.
8384
*/
8485
function acceptJSInvocationsPredicate(event: Types.Events.Event): event is Types.Events.Event {
85-
const eventIsConsoleRunTask = event.name === Types.Events.Name.V8_CONSOLE_RUN_TASK;
86+
const eventIsConsoleRunTask = Types.Events.isConsoleRunTask(event);
8687
const eventIsV8EntryPoint = event.name.startsWith('v8') || event.name.startsWith('V8');
8788
return Types.Events.isJSInvocationEvent(event) && (eventIsConsoleRunTask || !eventIsV8EntryPoint);
8889
}

front_end/models/trace/handlers/InitiatorsHandler.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,11 @@ function createRelationshipsFromFlows(): void {
189189
}
190190

191191
function createRelationshipsFromAsyncJSCalls(): void {
192-
const asyncCallPairs = AsyncJSCallsHandlerData().schedulerToRunEntryPoints.entries();
193-
for (const [asyncCaller, asyncCallee] of asyncCallPairs) {
194-
storeInitiator({event: asyncCallee, initiator: asyncCaller});
192+
const asyncCallEntries = AsyncJSCallsHandlerData().schedulerToRunEntryPoints.entries();
193+
for (const [asyncCaller, asyncCallees] of asyncCallEntries) {
194+
for (const asyncCallee of asyncCallees) {
195+
storeInitiator({event: asyncCallee, initiator: asyncCaller});
196+
}
195197
}
196198
}
197199

0 commit comments

Comments
 (0)