Skip to content

Commit 22c7cf5

Browse files
and-oliDevtools-frontend LUCI CQ
authored and
Devtools-frontend LUCI CQ
committed
[PP] Handle multiple async task runs caused by the same schedule event
Before this CL we assumed that every async task run event was connected via Perfetto's flow API to its corresponding scheduling event. However, because for async tasks the flow id is created based on its memory address, if a single scheduling task causes multiple "run" events, each of them will appear as parent of the subsequent one (since they will all have the same flow id). To fix this, when we detect a schedule event to be the async parent of a run event, we cache it. This way, when we detect the same run event to be the async parent of another run, we can assign the original schedule event as its effective async parent event This fixes a bug in itself and will also be used to improve how we build async stack traces for trace entries (that will happen in a follow up). Bug: 389056697 Change-Id: I8234dce05709f6e6c355613065d4240676eb4496 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6448277 Commit-Queue: Andres Olivares <andoli@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
1 parent aa0af54 commit 22c7cf5

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,35 @@ describe('AsyncJSCallsHandler', function() {
157157
assert.strictEqual(testRunEntryPoints?.[0], firstJSTaskRunEntryPoint);
158158
assert.strictEqual(testRunEntryPoints?.[1], secondJSTaskRunEntryPoint);
159159
});
160+
161+
it('assigns the right scheduled events to an event that scheduled multiple tasks', async function() {
162+
const jsTaskScheduler = makeProfileCall('setInterval', 0, 30, pid, tid);
163+
const asyncTaskScheduled =
164+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid);
165+
166+
// Simulate two async task runs caused by the same scheduled event.
167+
const asyncTaskRun1 = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
168+
const jsTaskRunEntryPoint1 = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
169+
170+
const asyncTaskRun2 = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 260, 100, cat, tid, pid);
171+
const jsTaskRunEntryPoint2 = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 270, 20, cat, tid, pid);
172+
173+
// Create flow events in the same way perfetto does for traces (as separate pairs).
174+
// schedule -> run 1, run 1 -> run 2.
175+
const flowEvents = [
176+
...makeFlowEvents([asyncTaskScheduled, asyncTaskRun1], 0), ...makeFlowEvents([asyncTaskRun1, asyncTaskRun2], 1)
177+
];
178+
const rendererEvents = [
179+
jsTaskScheduler, asyncTaskScheduled, asyncTaskRun1, jsTaskRunEntryPoint1, asyncTaskRun2, jsTaskRunEntryPoint2
180+
];
181+
const allEvents = [...rendererEvents, ...flowEvents];
182+
183+
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
184+
const testRunEntryPoints = asyncCallStacksData.schedulerToRunEntryPoints.get(jsTaskScheduler);
185+
assert.strictEqual(testRunEntryPoints?.length, 2);
186+
assert.strictEqual(testRunEntryPoints?.[0], jsTaskRunEntryPoint1);
187+
assert.strictEqual(testRunEntryPoints?.[1], jsTaskRunEntryPoint2);
188+
});
160189
});
161190
describe('Resolving async JS tasks to schedulers', function() {
162191
it('associates an async JS task to its scheduler', async function() {
@@ -207,5 +236,42 @@ describe('AsyncJSCallsHandler', function() {
207236
testScheduler = asyncCallStacksData.asyncCallToScheduler.get(asyncJSTask2)?.scheduler;
208237
assert.isUndefined(testScheduler);
209238
});
239+
it('returns the right scheduler for a task with multiple run events', async function() {
240+
const jsTaskScheduler = makeProfileCall('setInterval', 0, 50, pid, tid);
241+
const asyncTaskScheduled =
242+
makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_SCHEDULED, 0, 0, cat, pid, tid) as
243+
Trace.Types.Events.DebuggerAsyncTaskScheduled;
244+
asyncTaskScheduled.args.taskName = 'interval';
245+
246+
// Simulate two async task runs caused by the same scheduled event.
247+
const asyncTaskRun1 = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 60, 100, cat, tid, pid);
248+
const jsTaskRunEntryPoint1 = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 70, 20, cat, tid, pid);
249+
const asyncJSTask1 = makeProfileCall('scheduledFunction1', 71, 10, pid, tid);
250+
251+
const asyncTaskRun2 = makeCompleteEvent(Trace.Types.Events.Name.DEBUGGER_ASYNC_TASK_RUN, 160, 100, cat, tid, pid);
252+
const jsTaskRunEntryPoint2 = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, 170, 20, cat, tid, pid);
253+
const asyncJSTask2 = makeProfileCall('scheduledFunction2', 171, 10, pid, tid);
254+
255+
// Create flow events in the same way perfetto does for traces (as separate pairs).
256+
// schedule -> run 1, run 1 -> run 2.
257+
const flowEvents = [
258+
...makeFlowEvents([asyncTaskScheduled, asyncTaskRun1], 0), ...makeFlowEvents([asyncTaskRun1, asyncTaskRun2], 1)
259+
];
260+
const rendererEvents = [
261+
jsTaskScheduler, asyncTaskScheduled, asyncTaskRun1, jsTaskRunEntryPoint1, asyncJSTask1, asyncTaskRun2,
262+
jsTaskRunEntryPoint2, asyncJSTask2
263+
];
264+
const allEvents = [...rendererEvents, ...flowEvents];
265+
266+
const asyncCallStacksData = await buildAsyncJSCallsHandlerData(allEvents);
267+
let testScheduler = asyncCallStacksData.asyncCallToScheduler.get(asyncJSTask1);
268+
assert.strictEqual(testScheduler?.scheduler, jsTaskScheduler);
269+
assert.strictEqual(testScheduler?.taskName, asyncTaskScheduled.args.taskName);
270+
271+
testScheduler = asyncCallStacksData.asyncCallToScheduler.get(asyncJSTask2);
272+
assert.strictEqual(testScheduler?.scheduler, jsTaskScheduler);
273+
assert.strictEqual(testScheduler?.taskName, asyncTaskScheduled.args.taskName);
274+
});
275+
210276
});
211277
});

front_end/models/trace/handlers/AsyncJSCallsHandler.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ import {data as flowsHandlerData} from './FlowsHandler.js';
99
import {data as rendererHandlerData} from './RendererHandler.js';
1010

1111
const schedulerToRunEntryPoints = new Map<Types.Events.Event, Types.Events.Event[]>();
12+
13+
const taskScheduleForTaskRunEvent =
14+
new Map<Types.Events.DebuggerAsyncTaskRun, Types.Events.DebuggerAsyncTaskScheduled>();
1215
const asyncCallToScheduler =
1316
new Map<Types.Events.SyntheticProfileCall, {taskName: string, scheduler: Types.Events.Event}>();
1417

1518
export function reset(): void {
1619
schedulerToRunEntryPoints.clear();
1720
asyncCallToScheduler.clear();
21+
taskScheduleForTaskRunEvent.clear();
1822
}
1923

2024
export function handleEvent(_: Types.Events.Event): void {
@@ -25,17 +29,48 @@ export async function finalize(): Promise<void> {
2529
const {entryToNode} = rendererHandlerData();
2630
// Process async task flows
2731
for (const flow of flows) {
28-
const asyncTaskScheduled = flow.at(0);
29-
if (!asyncTaskScheduled || !Types.Events.isDebuggerAsyncTaskScheduled(asyncTaskScheduled)) {
32+
let maybeAsyncTaskScheduled = flow.at(0);
33+
if (!maybeAsyncTaskScheduled) {
3034
continue;
3135
}
32-
const taskName = asyncTaskScheduled.args.taskName;
36+
if (Types.Events.isDebuggerAsyncTaskRun(maybeAsyncTaskScheduled)) {
37+
// Sometimes a AsyncTaskRun event run can incorrectly appear as
38+
// initiated by another AsyncTaskRun from Perfetto's flows
39+
// perspective.
40+
// For example, in this snippet:
41+
//
42+
// const myTask = console.createTask('hola'); // creates an AsyncTaskSchedule
43+
// myTask.run(something); // creates an AsyncTaskRun
44+
// myTask.run(somethingElse); // creates an AsyncTaskRun
45+
//
46+
// or also in this one
47+
//
48+
// setInterval(something); // creates multiple connected AsyncTaskRun.
49+
//
50+
// Because the flow id is created based on the task's memory address,
51+
// the three events will end up belonging to the same flow (even if
52+
// in the frontend we receive it as pairs), and elements in a flow
53+
// are connected to their immediately consecutive neighbor.
54+
//
55+
// To ensure we use the right Schedule event, if the "initiating"
56+
// portion of the flow is a Run event, we look for any corresponding
57+
// Schedule event that we might have found before.
58+
maybeAsyncTaskScheduled = taskScheduleForTaskRunEvent.get(maybeAsyncTaskScheduled);
59+
}
60+
if (!maybeAsyncTaskScheduled || !Types.Events.isDebuggerAsyncTaskScheduled(maybeAsyncTaskScheduled)) {
61+
continue;
62+
}
63+
const taskName = maybeAsyncTaskScheduled.args.taskName;
3364
const asyncTaskRun = flow.at(1);
3465
if (!asyncTaskRun || !Types.Events.isDebuggerAsyncTaskRun(asyncTaskRun)) {
3566
// Unexpected flow shape, ignore.
3667
continue;
3768
}
38-
const asyncCaller = findNearestJSAncestor(asyncTaskScheduled, entryToNode);
69+
// Cache the Schedule event for this Run for future reference.
70+
taskScheduleForTaskRunEvent.set(asyncTaskRun, maybeAsyncTaskScheduled);
71+
72+
// Get the JS call scheduled the task.
73+
const asyncCaller = findNearestJSAncestor(maybeAsyncTaskScheduled, entryToNode);
3974
if (!asyncCaller) {
4075
// Unexpected async call trace data shape, ignore.
4176
continue;

front_end/testing/TraceHelpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,12 @@ export function makeFlowPhaseEvent(
301301
* flow. `events` must be ordered.
302302
*/
303303
export function makeFlowEvents(events: Trace.Types.Events.Event[], flowId = 0): Trace.Types.Events.FlowEvent[] {
304-
const lastEvent = events.at(-1);
305304
const firstEvent = events.at(0);
305+
const lastEvent = events.at(-1);
306306
if (!lastEvent || !firstEvent) {
307307
return [];
308308
}
309-
const flowName = events[0].name;
309+
const flowName = firstEvent.name;
310310
const flowStart = makeFlowPhaseEvent(
311311
flowName, firstEvent.ts, firstEvent.cat, Trace.Types.Events.Phase.FLOW_START, flowId, firstEvent.pid,
312312
firstEvent.tid);

0 commit comments

Comments
 (0)