Skip to content

Commit 161aba9

Browse files
committed
[Scheduler] Mark user-timing events
Marks when Scheduler starts and stops running a task. Also marks when a task is initially scheduled, and when Scheduler is waiting for a callback, which can't be inferred from a sample-based JavaScript CPU profile alone. The plan is to use the user-timing events to build a Scheduler profiler that shows how the lifetime of tasks interact with each other and with unscheduled main thread work. The test suite works by printing an text representation of a Scheduler flamegraph.
1 parent 62b04cf commit 161aba9

File tree

8 files changed

+651
-16
lines changed

8 files changed

+651
-16
lines changed

packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ describe('ReactDebugFiberPerf', () => {
127127
require('shared/ReactFeatureFlags').enableProfilerTimer = false;
128128
require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
129129
require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
130+
require('scheduler/src/SchedulerFeatureFlags').enableProfiling = false;
130131

131132
// Import after the polyfill is set up:
132133
React = require('react');

packages/scheduler/src/Scheduler.js

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88

99
/* eslint-disable no-var */
1010

11-
import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
1211
import {
13-
requestHostCallback,
12+
enableSchedulerDebugging,
13+
enableProfiling,
14+
} from './SchedulerFeatureFlags';
15+
import {
16+
requestHostCallback as requestHostCallbackWithoutProfiling,
1417
requestHostTimeout,
18+
cancelHostCallback,
1519
cancelHostTimeout,
1620
shouldYieldToHost,
1721
getCurrentTime,
@@ -21,11 +25,23 @@ import {
2125
import {push, pop, peek} from './SchedulerMinHeap';
2226

2327
// TODO: Use symbols?
24-
var ImmediatePriority = 1;
25-
var UserBlockingPriority = 2;
26-
var NormalPriority = 3;
27-
var LowPriority = 4;
28-
var IdlePriority = 5;
28+
import {
29+
ImmediatePriority,
30+
UserBlockingPriority,
31+
NormalPriority,
32+
LowPriority,
33+
IdlePriority,
34+
} from './SchedulerPriorities';
35+
import {
36+
markTaskRun,
37+
markTaskYield,
38+
markTaskCompleted,
39+
markTaskCanceled,
40+
markTaskErrored,
41+
markSchedulerSuspended,
42+
markSchedulerUnsuspended,
43+
markTaskStart,
44+
} from './SchedulerProfiling';
2945

3046
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
3147
// Math.pow(2, 30) - 1
@@ -60,13 +76,29 @@ var isPerformingWork = false;
6076
var isHostCallbackScheduled = false;
6177
var isHostTimeoutScheduled = false;
6278

79+
function requestHostCallbackWithProfiling(cb) {
80+
if (enableProfiling) {
81+
markSchedulerSuspended();
82+
requestHostCallbackWithoutProfiling(cb);
83+
}
84+
}
85+
86+
const requestHostCallback = enableProfiling
87+
? requestHostCallbackWithProfiling
88+
: requestHostCallbackWithoutProfiling;
89+
6390
function flushTask(task, callback, currentTime) {
6491
currentPriorityLevel = task.priorityLevel;
6592
var didUserCallbackTimeout = task.expirationTime <= currentTime;
93+
markTaskRun(task);
6694
var continuationCallback = callback(didUserCallbackTimeout);
67-
return typeof continuationCallback === 'function'
68-
? continuationCallback
69-
: null;
95+
if (typeof continuationCallback === 'function') {
96+
markTaskYield(task);
97+
return continuationCallback;
98+
} else {
99+
markTaskCompleted(task);
100+
return null;
101+
}
70102
}
71103

72104
function advanceTimers(currentTime) {
@@ -81,6 +113,7 @@ function advanceTimers(currentTime) {
81113
pop(timerQueue);
82114
timer.sortIndex = timer.expirationTime;
83115
push(taskQueue, timer);
116+
markTaskStart(timer);
84117
} else {
85118
// Remaining timers are pending.
86119
return;
@@ -107,6 +140,10 @@ function handleTimeout(currentTime) {
107140
}
108141

109142
function flushWork(hasTimeRemaining, initialTime) {
143+
if (isHostCallbackScheduled) {
144+
markSchedulerUnsuspended();
145+
}
146+
110147
// We'll need a host callback the next time work is scheduled.
111148
isHostCallbackScheduled = false;
112149
if (isHostTimeoutScheduled) {
@@ -152,6 +189,8 @@ function flushWork(hasTimeRemaining, initialTime) {
152189
}
153190
// Return whether there's additional work
154191
if (currentTask !== null) {
192+
markSchedulerSuspended();
193+
isHostCallbackScheduled = true;
155194
return true;
156195
} else {
157196
let firstTimer = peek(timerQueue);
@@ -160,6 +199,14 @@ function flushWork(hasTimeRemaining, initialTime) {
160199
}
161200
return false;
162201
}
202+
} catch (error) {
203+
if (currentTask !== null) {
204+
markTaskErrored(currentTask);
205+
if (currentTask === peek(taskQueue)) {
206+
pop(taskQueue);
207+
}
208+
}
209+
throw error;
163210
} finally {
164211
currentTask = null;
165212
currentPriorityLevel = previousPriorityLevel;
@@ -250,6 +297,7 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
250297

251298
var startTime;
252299
var timeout;
300+
var label;
253301
if (typeof options === 'object' && options !== null) {
254302
var delay = options.delay;
255303
if (typeof delay === 'number' && delay > 0) {
@@ -261,6 +309,12 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
261309
typeof options.timeout === 'number'
262310
? options.timeout
263311
: timeoutForPriorityLevel(priorityLevel);
312+
if (enableProfiling) {
313+
var _label = options.label;
314+
if (typeof _label === 'string') {
315+
label = _label;
316+
}
317+
}
264318
} else {
265319
timeout = timeoutForPriorityLevel(priorityLevel);
266320
startTime = currentTime;
@@ -269,14 +323,20 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
269323
var expirationTime = startTime + timeout;
270324

271325
var newTask = {
272-
id: taskIdCounter++,
326+
id: ++taskIdCounter,
273327
callback,
274328
priorityLevel,
275329
startTime,
276330
expirationTime,
277331
sortIndex: -1,
278332
};
279333

334+
if (enableProfiling) {
335+
if (typeof options === 'object' && options !== null) {
336+
newTask.label = label;
337+
}
338+
}
339+
280340
if (startTime > currentTime) {
281341
// This is a delayed task.
282342
newTask.sortIndex = startTime;
@@ -295,6 +355,7 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
295355
} else {
296356
newTask.sortIndex = expirationTime;
297357
push(taskQueue, newTask);
358+
markTaskStart(newTask);
298359
// Schedule a host callback, if needed. If we're already performing work,
299360
// wait until the next time we yield.
300361
if (!isHostCallbackScheduled && !isPerformingWork) {
@@ -323,10 +384,23 @@ function unstable_getFirstCallbackNode() {
323384
}
324385

325386
function unstable_cancelCallback(task) {
326-
// Null out the callback to indicate the task has been canceled. (Can't remove
327-
// from the queue because you can't remove arbitrary nodes from an array based
328-
// heap, only the first one.)
329-
task.callback = null;
387+
if (enableProfiling && task.callback !== null) {
388+
markTaskCanceled(task);
389+
}
390+
if (task !== null && task === peek(taskQueue)) {
391+
pop(taskQueue);
392+
if (enableProfiling && !isPerformingWork && taskQueue.length === 0) {
393+
// The queue is now empty.
394+
markSchedulerUnsuspended();
395+
isHostCallbackScheduled = false;
396+
cancelHostCallback();
397+
}
398+
} else {
399+
// Null out the callback to indicate the task has been canceled. (Can't
400+
// remove from the queue because you can't remove arbitrary nodes from an
401+
// array based heap, only the first one.)
402+
task.callback = null;
403+
}
330404
}
331405

332406
function unstable_getCurrentPriorityLevel() {

packages/scheduler/src/SchedulerFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export const enableIsInputPending = false;
1111
export const requestIdleCallbackBeforeFirstFrame = false;
1212
export const requestTimerEventBeforeFirstFrame = false;
1313
export const enableMessageLoopImplementation = false;
14+
export const enableProfiling = __PROFILE__;
15+
export const enableUserTimingAPI = false;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export type PriorityLevel = 1 | 2 | 3 | 4 | 5;
11+
12+
// TODO: Use symbols?
13+
export const ImmediatePriority = 1;
14+
export const UserBlockingPriority = 2;
15+
export const NormalPriority = 3;
16+
export const LowPriority = 4;
17+
export const IdlePriority = 5;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {PriorityLevel} from './SchedulerPriorities';
11+
import {
12+
enableProfiling,
13+
enableUserTimingAPI as enableUserTimingAPIFeatureFlag,
14+
} from './SchedulerFeatureFlags';
15+
16+
const enableUserTimingAPI =
17+
enableUserTimingAPIFeatureFlag &&
18+
typeof performance !== 'undefined' &&
19+
typeof performance.mark === 'function' &&
20+
typeof performance.clearMarks === 'function';
21+
22+
let runIdCounter: number = 0;
23+
let mainThreadIdCounter: number = 0;
24+
25+
export function markTaskStart(task: {id: number}) {
26+
if (enableProfiling) {
27+
if (enableUserTimingAPI) {
28+
// Use extra field to track if delayed task starts.
29+
const taskStartMark = `SchedulerTask-${task.id}-Start`;
30+
performance.mark(taskStartMark);
31+
performance.clearMarks(taskStartMark);
32+
}
33+
}
34+
}
35+
36+
export function markTaskCompleted(task: {
37+
id: number,
38+
priorityLevel: PriorityLevel,
39+
label?: string,
40+
}) {
41+
if (enableProfiling) {
42+
if (enableUserTimingAPI) {
43+
const info = JSON.stringify({
44+
priorityLevel: task.priorityLevel,
45+
label: task.label,
46+
exitStatus: 'completed',
47+
});
48+
const taskEndMark = `SchedulerTask-${task.id}-End-${info}`;
49+
performance.mark(taskEndMark);
50+
performance.clearMarks(taskEndMark);
51+
}
52+
}
53+
}
54+
55+
export function markTaskCanceled(task: {
56+
id: number,
57+
priorityLevel: PriorityLevel,
58+
label?: string,
59+
}) {
60+
if (enableProfiling) {
61+
if (enableUserTimingAPI) {
62+
const info = JSON.stringify({
63+
priorityLevel: task.priorityLevel,
64+
label: task.label,
65+
exitStatus: 'canceled',
66+
});
67+
const taskEndMark = `SchedulerTask-${task.id}-End-${info}`;
68+
performance.mark(taskEndMark);
69+
performance.clearMarks(taskEndMark);
70+
}
71+
}
72+
}
73+
74+
export function markTaskErrored(task: {
75+
id: number,
76+
priorityLevel: PriorityLevel,
77+
label?: string,
78+
}) {
79+
if (enableProfiling) {
80+
if (enableUserTimingAPI) {
81+
const info = JSON.stringify({
82+
priorityLevel: task.priorityLevel,
83+
label: task.label,
84+
exitStatus: 'errored',
85+
});
86+
const taskEndMark = `SchedulerTask-${task.id}-End-${info}`;
87+
performance.mark(taskEndMark);
88+
performance.clearMarks(taskEndMark);
89+
}
90+
}
91+
}
92+
93+
export function markTaskRun(task: {id: number}) {
94+
if (enableProfiling) {
95+
if (enableUserTimingAPI) {
96+
runIdCounter++;
97+
const runMark = `SchedulerTask-${task.id}-Run-${runIdCounter}`;
98+
performance.mark(runMark);
99+
performance.clearMarks(runMark);
100+
}
101+
}
102+
}
103+
104+
export function markTaskYield(task: {id: number}) {
105+
if (enableProfiling) {
106+
if (enableUserTimingAPI) {
107+
const yieldMark = `SchedulerTask-${task.id}-Yield-${runIdCounter}`;
108+
performance.mark(yieldMark);
109+
performance.clearMarks(yieldMark);
110+
}
111+
}
112+
}
113+
114+
export function markSchedulerSuspended() {
115+
if (enableProfiling) {
116+
if (enableUserTimingAPI) {
117+
mainThreadIdCounter++;
118+
const suspendStartMark =
119+
'SchedulerSuspended-Start-' + mainThreadIdCounter;
120+
performance.mark(suspendStartMark);
121+
performance.clearMarks(suspendStartMark);
122+
}
123+
}
124+
}
125+
126+
export function markSchedulerUnsuspended() {
127+
if (enableProfiling) {
128+
if (enableUserTimingAPI) {
129+
const suspendedEndMark = 'SchedulerSuspended-End-' + mainThreadIdCounter;
130+
performance.mark(suspendedEndMark);
131+
performance.clearMarks(suspendedEndMark);
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)