@samitouri / QOS-React-2 / commits / 0f6e3cd61c

[Scheduler] Profiler Features (second try) (#16542)

* Revert "Revert "[Scheduler] Profiling features (#16145)" (#16392)" This reverts commit 4ba1412305be3c77b625a70e3c0b9a8638626bd0. * Fix copy paste mistake * Remove init path dependency on ArrayBuffer * Add a regression test for cancelling multiple tasks * Prevent deopt from adding isQueued later * Remove pop() calls that were added for profiling * Verify that Suspend/Unsuspend events match up in tests This currently breaks tests. * Treat Suspend and Resume as exiting and entering work loop Their definitions used to be more fuzzy. For example, Suspend didn't always fire on exit, and sometimes fired when we did _not_ exit (such as at task enqueue). I chatted to Boone, and he's saying treating Suspend and Resume as strictly exiting and entering the loop is fine for their use case. * Revert "Prevent deopt from adding isQueued later" This reverts commit 9c30b0b695d81e9c43b296ab93d895e4416ef713. Unnecessary because GCC * Start counter with 1 * Group exports into unstable_Profiling namespace * No catch in PROD codepath * No label TODO * No null checks

Dan Abramov committed Aug 22, 2019 at 21:58 UTC 0f6e3cd61cf4a5a1491bb3c92780936aebc2a146
18 files changed +919 -66
.eslintrc.js
+2
@@ -142,6 +142,8 @@ module.exports = {
142 ],
143
144 globals: {
145 + SharedArrayBuffer: true,
146 +
147 spyOnDev: true,
148 spyOnDevAndProd: true,
149 spyOnProd: true,
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js
+1
@@ -136,6 +136,7 @@ describe('ReactDebugFiberPerf', () => {
136 require('shared/ReactFeatureFlags').enableProfilerTimer = false;
137 require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
138 require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
139 + require('scheduler/src/SchedulerFeatureFlags').enableProfiling = false;
140
141 // Import after the polyfill is set up:
142 React = require('react');
packages/scheduler/npm/umd/scheduler.development.js
+4
@@ -144,5 +144,9 @@
144 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
145 .Scheduler.unstable_UserBlockingPriority;
146 },
147 + get unstable_Profiling() {
148 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
149 + .Scheduler.unstable_Profiling;
150 + },
151 });
152 });
packages/scheduler/npm/umd/scheduler.production.min.js
+4
@@ -138,5 +138,9 @@
138 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
139 .Scheduler.unstable_UserBlockingPriority;
140 },
141 + get unstable_Profiling() {
142 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
143 + .Scheduler.unstable_Profiling;
144 + },
145 });
146 });
packages/scheduler/npm/umd/scheduler.profiling.min.js
+4
@@ -138,5 +138,9 @@
138 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
139 .Scheduler.unstable_UserBlockingPriority;
140 },
141 + get unstable_Profiling() {
142 + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
143 + .Scheduler.unstable_Profiling;
144 + },
145 });
146 });
packages/scheduler/src/Scheduler.js
+127 -57
@@ -8,7 +8,10 @@
8
9 /* eslint-disable no-var */
10
11 -import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
11 +import {
12 + enableSchedulerDebugging,
13 + enableProfiling,
14 +} from './SchedulerFeatureFlags';
15 import {
16 requestHostCallback,
17 requestHostTimeout,
@@ -21,11 +24,26 @@ import {
24 import {push, pop, peek} from './SchedulerMinHeap';
25
26 // TODO: Use symbols?
24 -var ImmediatePriority = 1;
25 -var UserBlockingPriority = 2;
26 -var NormalPriority = 3;
27 -var LowPriority = 4;
28 -var IdlePriority = 5;
27 +import {
28 + ImmediatePriority,
29 + UserBlockingPriority,
30 + NormalPriority,
31 + LowPriority,
32 + IdlePriority,
33 +} from './SchedulerPriorities';
34 +import {
35 + sharedProfilingBuffer,
36 + markTaskRun,
37 + markTaskYield,
38 + markTaskCompleted,
39 + markTaskCanceled,
40 + markTaskErrored,
41 + markSchedulerSuspended,
42 + markSchedulerUnsuspended,
43 + markTaskStart,
44 + stopLoggingProfilingEvents,
45 + startLoggingProfilingEvents,
46 +} from './SchedulerProfiling';
47
48 // Max 31 bit integer. The max integer size in V8 for 32-bit systems.
49 // Math.pow(2, 30) - 1
@@ -46,7 +64,7 @@ var taskQueue = [];
64 var timerQueue = [];
65
66 // Incrementing id counter. Used to maintain insertion order.
49 -var taskIdCounter = 0;
67 +var taskIdCounter = 1;
68
69 // Pausing the scheduler is useful for debugging.
70 var isSchedulerPaused = false;
@@ -60,15 +78,6 @@ var isPerformingWork = false;
78 var isHostCallbackScheduled = false;
79 var isHostTimeoutScheduled = false;
80
63 -function flushTask(task, callback, currentTime) {
64 - currentPriorityLevel = task.priorityLevel;
65 - var didUserCallbackTimeout = task.expirationTime <= currentTime;
66 - var continuationCallback = callback(didUserCallbackTimeout);
67 - return typeof continuationCallback === 'function'
68 - ? continuationCallback
69 - : null;
70 -}
71 -
81 function advanceTimers(currentTime) {
82 // Check for tasks that are no longer delayed and add them to the queue.
83 let timer = peek(timerQueue);
@@ -81,6 +90,10 @@ function advanceTimers(currentTime) {
90 pop(timerQueue);
91 timer.sortIndex = timer.expirationTime;
92 push(taskQueue, timer);
93 + if (enableProfiling) {
94 + markTaskStart(timer);
95 + timer.isQueued = true;
96 + }
97 } else {
98 // Remaining timers are pending.
99 return;
@@ -107,6 +120,10 @@ function handleTimeout(currentTime) {
120 }
121
122 function flushWork(hasTimeRemaining, initialTime) {
123 + if (enableProfiling) {
124 + markSchedulerUnsuspended(initialTime);
125 + }
126 +
127 // We'll need a host callback the next time work is scheduled.
128 isHostCallbackScheduled = false;
129 if (isHostTimeoutScheduled) {
@@ -118,52 +135,82 @@ function flushWork(hasTimeRemaining, initialTime) {
135 isPerformingWork = true;
136 const previousPriorityLevel = currentPriorityLevel;
137 try {
121 - let currentTime = initialTime;
122 - advanceTimers(currentTime);
123 - currentTask = peek(taskQueue);
124 - while (
125 - currentTask !== null &&
126 - !(enableSchedulerDebugging && isSchedulerPaused)
127 - ) {
128 - if (
129 - currentTask.expirationTime > currentTime &&
130 - (!hasTimeRemaining || shouldYieldToHost())
131 - ) {
132 - // This currentTask hasn't expired, and we've reached the deadline.
133 - break;
134 - }
135 - const callback = currentTask.callback;
136 - if (callback !== null) {
137 - currentTask.callback = null;
138 - const continuation = flushTask(currentTask, callback, currentTime);
139 - if (continuation !== null) {
140 - currentTask.callback = continuation;
141 - } else {
142 - if (currentTask === peek(taskQueue)) {
143 - pop(taskQueue);
144 - }
138 + if (enableProfiling) {
139 + try {
140 + return workLoop(hasTimeRemaining, initialTime);
141 + } catch (error) {
142 + if (currentTask !== null) {
143 + const currentTime = getCurrentTime();
144 + markTaskErrored(currentTask, currentTime);
145 + currentTask.isQueued = false;
146 }
146 - currentTime = getCurrentTime();
147 - advanceTimers(currentTime);
148 - } else {
149 - pop(taskQueue);
147 + throw error;
148 }
151 - currentTask = peek(taskQueue);
152 - }
153 - // Return whether there's additional work
154 - if (currentTask !== null) {
155 - return true;
149 } else {
157 - let firstTimer = peek(timerQueue);
158 - if (firstTimer !== null) {
159 - requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
160 - }
161 - return false;
150 + // No catch in prod codepath.
151 + return workLoop(hasTimeRemaining, initialTime);
152 }
153 } finally {
154 currentTask = null;
155 currentPriorityLevel = previousPriorityLevel;
156 isPerformingWork = false;
157 + if (enableProfiling) {
158 + const currentTime = getCurrentTime();
159 + markSchedulerSuspended(currentTime);
160 + }
161 + }
162 +}
163 +
164 +function workLoop(hasTimeRemaining, initialTime) {
165 + let currentTime = initialTime;
166 + advanceTimers(currentTime);
167 + currentTask = peek(taskQueue);
168 + while (
169 + currentTask !== null &&
170 + !(enableSchedulerDebugging && isSchedulerPaused)
171 + ) {
172 + if (
173 + currentTask.expirationTime > currentTime &&
174 + (!hasTimeRemaining || shouldYieldToHost())
175 + ) {
176 + // This currentTask hasn't expired, and we've reached the deadline.
177 + break;
178 + }
179 + const callback = currentTask.callback;
180 + if (callback !== null) {
181 + currentTask.callback = null;
182 + currentPriorityLevel = currentTask.priorityLevel;
183 + const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
184 + markTaskRun(currentTask, currentTime);
185 + const continuationCallback = callback(didUserCallbackTimeout);
186 + currentTime = getCurrentTime();
187 + if (typeof continuationCallback === 'function') {
188 + currentTask.callback = continuationCallback;
189 + markTaskYield(currentTask, currentTime);
190 + } else {
191 + if (enableProfiling) {
192 + markTaskCompleted(currentTask, currentTime);
193 + currentTask.isQueued = false;
194 + }
195 + if (currentTask === peek(taskQueue)) {
196 + pop(taskQueue);
197 + }
198 + }
199 + advanceTimers(currentTime);
200 + } else {
201 + pop(taskQueue);
202 + }
203 + currentTask = peek(taskQueue);
204 + }
205 + // Return whether there's additional work
206 + if (currentTask !== null) {
207 + return true;
208 + } else {
209 + let firstTimer = peek(timerQueue);
210 + if (firstTimer !== null) {
211 + requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
212 + }
213 + return false;
214 }
215 }
216
@@ -276,6 +323,9 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
323 expirationTime,
324 sortIndex: -1,
325 };
326 + if (enableProfiling) {
327 + newTask.isQueued = false;
328 + }
329
330 if (startTime > currentTime) {
331 // This is a delayed task.
@@ -295,6 +345,10 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
345 } else {
346 newTask.sortIndex = expirationTime;
347 push(taskQueue, newTask);
348 + if (enableProfiling) {
349 + markTaskStart(newTask, currentTime);
350 + newTask.isQueued = true;
351 + }
352 // Schedule a host callback, if needed. If we're already performing work,
353 // wait until the next time we yield.
354 if (!isHostCallbackScheduled && !isPerformingWork) {
@@ -323,9 +377,17 @@ function unstable_getFirstCallbackNode() {
377 }
378
379 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.)
380 + if (enableProfiling) {
381 + if (task.isQueued) {
382 + const currentTime = getCurrentTime();
383 + markTaskCanceled(task, currentTime);
384 + task.isQueued = false;
385 + }
386 + }
387 +
388 + // Null out the callback to indicate the task has been canceled. (Can't
389 + // remove from the queue because you can't remove arbitrary nodes from an
390 + // array based heap, only the first one.)
391 task.callback = null;
392 }
393
@@ -370,3 +432,11 @@ export {
432 getCurrentTime as unstable_now,
433 forceFrameRate as unstable_forceFrameRate,
434 };
435 +
436 +export const unstable_Profiling = enableProfiling
437 + ? {
438 + startLoggingProfilingEvents,
439 + stopLoggingProfilingEvents,
440 + sharedProfilingBuffer,
441 + }
442 + : null;
packages/scheduler/src/SchedulerFeatureFlags.js
+1
@@ -11,3 +11,4 @@ export const enableIsInputPending = false;
11 export const requestIdleCallbackBeforeFirstFrame = false;
12 export const requestTimerEventBeforeFirstFrame = false;
13 export const enableMessageLoopImplementation = false;
14 +export const enableProfiling = __PROFILE__;
packages/scheduler/src/SchedulerPriorities.js new
+18
@@ -0,0 +1,18 @@
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 = 0 | 1 | 2 | 3 | 4 | 5;
11 +
12 +// TODO: Use symbols?
13 +export const NoPriority = 0;
14 +export const ImmediatePriority = 1;
15 +export const UserBlockingPriority = 2;
16 +export const NormalPriority = 3;
17 +export const LowPriority = 4;
18 +export const IdlePriority = 5;
packages/scheduler/src/SchedulerProfiling.js new
+203
@@ -0,0 +1,203 @@
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 {enableProfiling} from './SchedulerFeatureFlags';
12 +
13 +import {NoPriority} from './SchedulerPriorities';
14 +
15 +let runIdCounter: number = 0;
16 +let mainThreadIdCounter: number = 0;
17 +
18 +const profilingStateSize = 4;
19 +export const sharedProfilingBuffer = enableProfiling
20 + ? // $FlowFixMe Flow doesn't know about SharedArrayBuffer
21 + typeof SharedArrayBuffer === 'function'
22 + ? new SharedArrayBuffer(profilingStateSize * Int32Array.BYTES_PER_ELEMENT)
23 + : // $FlowFixMe Flow doesn't know about ArrayBuffer
24 + typeof ArrayBuffer === 'function'
25 + ? new ArrayBuffer(profilingStateSize * Int32Array.BYTES_PER_ELEMENT)
26 + : null // Don't crash the init path on IE9
27 + : null;
28 +
29 +const profilingState =
30 + enableProfiling && sharedProfilingBuffer !== null
31 + ? new Int32Array(sharedProfilingBuffer)
32 + : []; // We can't read this but it helps save bytes for null checks
33 +
34 +const PRIORITY = 0;
35 +const CURRENT_TASK_ID = 1;
36 +const CURRENT_RUN_ID = 2;
37 +const QUEUE_SIZE = 3;
38 +
39 +if (enableProfiling) {
40 + profilingState[PRIORITY] = NoPriority;
41 + // This is maintained with a counter, because the size of the priority queue
42 + // array might include canceled tasks.
43 + profilingState[QUEUE_SIZE] = 0;
44 + profilingState[CURRENT_TASK_ID] = 0;
45 +}
46 +
47 +const INITIAL_EVENT_LOG_SIZE = 1000;
48 +
49 +let eventLogSize = 0;
50 +let eventLogBuffer = null;
51 +let eventLog = null;
52 +let eventLogIndex = 0;
53 +
54 +const TaskStartEvent = 1;
55 +const TaskCompleteEvent = 2;
56 +const TaskErrorEvent = 3;
57 +const TaskCancelEvent = 4;
58 +const TaskRunEvent = 5;
59 +const TaskYieldEvent = 6;
60 +const SchedulerSuspendEvent = 7;
61 +const SchedulerResumeEvent = 8;
62 +
63 +function logEvent(entries) {
64 + if (eventLog !== null) {
65 + const offset = eventLogIndex;
66 + eventLogIndex += entries.length;
67 + if (eventLogIndex + 1 > eventLogSize) {
68 + eventLogSize = eventLogIndex + 1;
69 + const newEventLog = new Int32Array(
70 + eventLogSize * Int32Array.BYTES_PER_ELEMENT,
71 + );
72 + newEventLog.set(eventLog);
73 + eventLogBuffer = newEventLog.buffer;
74 + eventLog = newEventLog;
75 + }
76 + eventLog.set(entries, offset);
77 + }
78 +}
79 +
80 +export function startLoggingProfilingEvents(): void {
81 + eventLogSize = INITIAL_EVENT_LOG_SIZE;
82 + eventLogBuffer = new ArrayBuffer(eventLogSize * Int32Array.BYTES_PER_ELEMENT);
83 + eventLog = new Int32Array(eventLogBuffer);
84 + eventLogIndex = 0;
85 +}
86 +
87 +export function stopLoggingProfilingEvents(): ArrayBuffer | null {
88 + const buffer = eventLogBuffer;
89 + eventLogBuffer = eventLog = null;
90 + return buffer;
91 +}
92 +
93 +export function markTaskStart(
94 + task: {id: number, priorityLevel: PriorityLevel},
95 + time: number,
96 +) {
97 + if (enableProfiling) {
98 + profilingState[QUEUE_SIZE]++;
99 +
100 + if (eventLog !== null) {
101 + logEvent([TaskStartEvent, time, task.id, task.priorityLevel]);
102 + }
103 + }
104 +}
105 +
106 +export function markTaskCompleted(
107 + task: {
108 + id: number,
109 + priorityLevel: PriorityLevel,
110 + },
111 + time: number,
112 +) {
113 + if (enableProfiling) {
114 + profilingState[PRIORITY] = NoPriority;
115 + profilingState[CURRENT_TASK_ID] = 0;
116 + profilingState[QUEUE_SIZE]--;
117 +
118 + if (eventLog !== null) {
119 + logEvent([TaskCompleteEvent, time, task.id]);
120 + }
121 + }
122 +}
123 +
124 +export function markTaskCanceled(
125 + task: {
126 + id: number,
127 + priorityLevel: PriorityLevel,
128 + },
129 + time: number,
130 +) {
131 + if (enableProfiling) {
132 + profilingState[QUEUE_SIZE]--;
133 +
134 + if (eventLog !== null) {
135 + logEvent([TaskCancelEvent, time, task.id]);
136 + }
137 + }
138 +}
139 +
140 +export function markTaskErrored(
141 + task: {
142 + id: number,
143 + priorityLevel: PriorityLevel,
144 + },
145 + time: number,
146 +) {
147 + if (enableProfiling) {
148 + profilingState[PRIORITY] = NoPriority;
149 + profilingState[CURRENT_TASK_ID] = 0;
150 + profilingState[QUEUE_SIZE]--;
151 +
152 + if (eventLog !== null) {
153 + logEvent([TaskErrorEvent, time, task.id]);
154 + }
155 + }
156 +}
157 +
158 +export function markTaskRun(
159 + task: {id: number, priorityLevel: PriorityLevel},
160 + time: number,
161 +) {
162 + if (enableProfiling) {
163 + runIdCounter++;
164 +
165 + profilingState[PRIORITY] = task.priorityLevel;
166 + profilingState[CURRENT_TASK_ID] = task.id;
167 + profilingState[CURRENT_RUN_ID] = runIdCounter;
168 +
169 + if (eventLog !== null) {
170 + logEvent([TaskRunEvent, time, task.id, runIdCounter]);
171 + }
172 + }
173 +}
174 +
175 +export function markTaskYield(task: {id: number}, time: number) {
176 + if (enableProfiling) {
177 + profilingState[PRIORITY] = NoPriority;
178 + profilingState[CURRENT_TASK_ID] = 0;
179 + profilingState[CURRENT_RUN_ID] = 0;
180 +
181 + if (eventLog !== null) {
182 + logEvent([TaskYieldEvent, time, task.id, runIdCounter]);
183 + }
184 + }
185 +}
186 +
187 +export function markSchedulerSuspended(time: number) {
188 + if (enableProfiling) {
189 + mainThreadIdCounter++;
190 +
191 + if (eventLog !== null) {
192 + logEvent([SchedulerSuspendEvent, time, mainThreadIdCounter]);
193 + }
194 + }
195 +}
196 +
197 +export function markSchedulerUnsuspended(time: number) {
198 + if (enableProfiling) {
199 + if (eventLog !== null) {
200 + logEvent([SchedulerResumeEvent, time, mainThreadIdCounter]);
201 + }
202 + }
203 +}
packages/scheduler/src/__tests__/SchedulerDOM-test.js
+7 -3
@@ -59,11 +59,15 @@ describe('SchedulerDOM', () => {
59 runPostMessageCallbacks(config);
60 }
61
62 - let frameSize = 33;
63 - let startOfLatestFrame = 0;
64 - let currentTime = 0;
62 + let frameSize;
63 + let startOfLatestFrame;
64 + let currentTime;
65
66 beforeEach(() => {
67 + frameSize = 33;
68 + startOfLatestFrame = 0;
69 + currentTime = 0;
70 +
71 delete global.performance;
72 global.requestAnimationFrame = function(cb) {
73 return rAFCallbacks.push(() => {
packages/scheduler/src/__tests__/SchedulerProfiling-test.js new
+508
@@ -0,0 +1,508 @@
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 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +
11 +/* eslint-disable no-for-of-loops/no-for-of-loops */
12 +
13 +'use strict';
14 +
15 +let Scheduler;
16 +let sharedProfilingArray;
17 +// let runWithPriority;
18 +let ImmediatePriority;
19 +let UserBlockingPriority;
20 +let NormalPriority;
21 +let LowPriority;
22 +let IdlePriority;
23 +let scheduleCallback;
24 +let cancelCallback;
25 +// let wrapCallback;
26 +// let getCurrentPriorityLevel;
27 +// let shouldYield;
28 +
29 +function priorityLevelToString(priorityLevel) {
30 + switch (priorityLevel) {
31 + case ImmediatePriority:
32 + return 'Immediate';
33 + case UserBlockingPriority:
34 + return 'User-blocking';
35 + case NormalPriority:
36 + return 'Normal';
37 + case LowPriority:
38 + return 'Low';
39 + case IdlePriority:
40 + return 'Idle';
41 + default:
42 + return null;
43 + }
44 +}
45 +
46 +describe('Scheduler', () => {
47 + if (!__PROFILE__) {
48 + // The tests in this suite only apply when profiling is on
49 + it('profiling APIs are not available', () => {
50 + Scheduler = require('scheduler');
51 + expect(Scheduler.unstable_Profiling).toBe(null);
52 + });
53 + return;
54 + }
55 +
56 + beforeEach(() => {
57 + jest.resetModules();
58 + jest.mock('scheduler', () => require('scheduler/unstable_mock'));
59 + Scheduler = require('scheduler');
60 +
61 + sharedProfilingArray = new Int32Array(
62 + Scheduler.unstable_Profiling.sharedProfilingBuffer,
63 + );
64 +
65 + // runWithPriority = Scheduler.unstable_runWithPriority;
66 + ImmediatePriority = Scheduler.unstable_ImmediatePriority;
67 + UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
68 + NormalPriority = Scheduler.unstable_NormalPriority;
69 + LowPriority = Scheduler.unstable_LowPriority;
70 + IdlePriority = Scheduler.unstable_IdlePriority;
71 + scheduleCallback = Scheduler.unstable_scheduleCallback;
72 + cancelCallback = Scheduler.unstable_cancelCallback;
73 + // wrapCallback = Scheduler.unstable_wrapCallback;
74 + // getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
75 + // shouldYield = Scheduler.unstable_shouldYield;
76 + });
77 +
78 + const PRIORITY = 0;
79 + const CURRENT_TASK_ID = 1;
80 + const CURRENT_RUN_ID = 2;
81 + const QUEUE_SIZE = 3;
82 +
83 + afterEach(() => {
84 + if (sharedProfilingArray[QUEUE_SIZE] !== 0) {
85 + throw Error(
86 + 'Test exited, but the shared profiling buffer indicates that a task ' +
87 + 'is still running',
88 + );
89 + }
90 + });
91 +
92 + const TaskStartEvent = 1;
93 + const TaskCompleteEvent = 2;
94 + const TaskErrorEvent = 3;
95 + const TaskCancelEvent = 4;
96 + const TaskRunEvent = 5;
97 + const TaskYieldEvent = 6;
98 + const SchedulerSuspendEvent = 7;
99 + const SchedulerResumeEvent = 8;
100 +
101 + function stopProfilingAndPrintFlamegraph() {
102 + const eventLog = new Int32Array(
103 + Scheduler.unstable_Profiling.stopLoggingProfilingEvents(),
104 + );
105 +
106 + const tasks = new Map();
107 + const mainThreadRuns = [];
108 +
109 + let isSuspended = true;
110 + let i = 0;
111 + processLog: while (i < eventLog.length) {
112 + const instruction = eventLog[i];
113 + const time = eventLog[i + 1];
114 + switch (instruction) {
115 + case 0: {
116 + break processLog;
117 + }
118 + case TaskStartEvent: {
119 + const taskId = eventLog[i + 2];
120 + const priorityLevel = eventLog[i + 3];
121 + const task = {
122 + id: taskId,
123 + priorityLevel,
124 + label: null,
125 + start: time,
126 + end: -1,
127 + exitStatus: null,
128 + runs: [],
129 + };
130 + tasks.set(taskId, task);
131 + i += 4;
132 + break;
133 + }
134 + case TaskCompleteEvent: {
135 + if (isSuspended) {
136 + throw Error('Task cannot Complete outside the work loop.');
137 + }
138 + const taskId = eventLog[i + 2];
139 + const task = tasks.get(taskId);
140 + if (task === undefined) {
141 + throw Error('Task does not exist.');
142 + }
143 + task.end = time;
144 + task.exitStatus = 'completed';
145 + i += 3;
146 + break;
147 + }
148 + case TaskErrorEvent: {
149 + if (isSuspended) {
150 + throw Error('Task cannot Error outside the work loop.');
151 + }
152 + const taskId = eventLog[i + 2];
153 + const task = tasks.get(taskId);
154 + if (task === undefined) {
155 + throw Error('Task does not exist.');
156 + }
157 + task.end = time;
158 + task.exitStatus = 'errored';
159 + i += 3;
160 + break;
161 + }
162 + case TaskCancelEvent: {
163 + const taskId = eventLog[i + 2];
164 + const task = tasks.get(taskId);
165 + if (task === undefined) {
166 + throw Error('Task does not exist.');
167 + }
168 + task.end = time;
169 + task.exitStatus = 'canceled';
170 + i += 3;
171 + break;
172 + }
173 + case TaskRunEvent:
174 + case TaskYieldEvent: {
175 + if (isSuspended) {
176 + throw Error('Task cannot Run or Yield outside the work loop.');
177 + }
178 + const taskId = eventLog[i + 2];
179 + const task = tasks.get(taskId);
180 + if (task === undefined) {
181 + throw Error('Task does not exist.');
182 + }
183 + task.runs.push(time);
184 + i += 4;
185 + break;
186 + }
187 + case SchedulerSuspendEvent: {
188 + if (isSuspended) {
189 + throw Error('Scheduler cannot Suspend outside the work loop.');
190 + }
191 + isSuspended = true;
192 + mainThreadRuns.push(time);
193 + i += 3;
194 + break;
195 + }
196 + case SchedulerResumeEvent: {
197 + if (!isSuspended) {
198 + throw Error('Scheduler cannot Resume inside the work loop.');
199 + }
200 + isSuspended = false;
201 + mainThreadRuns.push(time);
202 + i += 3;
203 + break;
204 + }
205 + default: {
206 + throw Error('Unknown instruction type: ' + instruction);
207 + }
208 + }
209 + }
210 +
211 + // Now we can render the tasks as a flamegraph.
212 + const labelColumnWidth = 30;
213 + const msPerChar = 50;
214 +
215 + let result = '';
216 +
217 + const mainThreadLabelColumn = '!!! Main thread ';
218 + let mainThreadTimelineColumn = '';
219 + let isMainThreadBusy = true;
220 + for (const time of mainThreadRuns) {
221 + const index = time / msPerChar;
222 + mainThreadTimelineColumn += (isMainThreadBusy ? '█' : '░').repeat(
223 + index - mainThreadTimelineColumn.length,
224 + );
225 + isMainThreadBusy = !isMainThreadBusy;
226 + }
227 + result += `${mainThreadLabelColumn}│${mainThreadTimelineColumn}\n`;
228 +
229 + const tasksByPriority = Array.from(tasks.values()).sort(
230 + (t1, t2) => t1.priorityLevel - t2.priorityLevel,
231 + );
232 +
233 + for (const task of tasksByPriority) {
234 + let label = task.label;
235 + if (label === undefined) {
236 + label = 'Task';
237 + }
238 + let labelColumn = `Task ${task.id} [${priorityLevelToString(
239 + task.priorityLevel,
240 + )}]`;
241 + labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1);
242 +
243 + // Add empty space up until the start mark
244 + let timelineColumn = ' '.repeat(task.start / msPerChar);
245 +
246 + let isRunning = false;
247 + for (const time of task.runs) {
248 + const index = time / msPerChar;
249 + timelineColumn += (isRunning ? '█' : '░').repeat(
250 + index - timelineColumn.length,
251 + );
252 + isRunning = !isRunning;
253 + }
254 +
255 + const endIndex = task.end / msPerChar;
256 + timelineColumn += (isRunning ? '█' : '░').repeat(
257 + endIndex - timelineColumn.length,
258 + );
259 +
260 + if (task.exitStatus !== 'completed') {
261 + timelineColumn += `🡐 ${task.exitStatus}`;
262 + }
263 +
264 + result += `${labelColumn}│${timelineColumn}\n`;
265 + }
266 +
267 + return '\n' + result;
268 + }
269 +
270 + function getProfilingInfo() {
271 + const queueSize = sharedProfilingArray[QUEUE_SIZE];
272 + if (queueSize === 0) {
273 + return 'Empty Queue';
274 + }
275 + const priorityLevel = sharedProfilingArray[PRIORITY];
276 + if (priorityLevel === 0) {
277 + return 'Suspended, Queue Size: ' + queueSize;
278 + }
279 + return (
280 + `Task: ${sharedProfilingArray[CURRENT_TASK_ID]}, ` +
281 + `Run: ${sharedProfilingArray[CURRENT_RUN_ID]}, ` +
282 + `Priority: ${priorityLevelToString(priorityLevel)}, ` +
283 + `Queue Size: ${sharedProfilingArray[QUEUE_SIZE]}`
284 + );
285 + }
286 +
287 + it('creates a basic flamegraph', () => {
288 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
289 +
290 + Scheduler.unstable_advanceTime(100);
291 + scheduleCallback(
292 + NormalPriority,
293 + () => {
294 + Scheduler.unstable_advanceTime(300);
295 + Scheduler.unstable_yieldValue(getProfilingInfo());
296 + scheduleCallback(
297 + UserBlockingPriority,
298 + () => {
299 + Scheduler.unstable_yieldValue(getProfilingInfo());
300 + Scheduler.unstable_advanceTime(300);
301 + },
302 + {label: 'Bar'},
303 + );
304 + Scheduler.unstable_advanceTime(100);
305 + Scheduler.unstable_yieldValue('Yield');
306 + return () => {
307 + Scheduler.unstable_yieldValue(getProfilingInfo());
308 + Scheduler.unstable_advanceTime(300);
309 + };
310 + },
311 + {label: 'Foo'},
312 + );
313 + expect(Scheduler).toFlushAndYieldThrough([
314 + 'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
315 + 'Yield',
316 + ]);
317 + Scheduler.unstable_advanceTime(100);
318 + expect(Scheduler).toFlushAndYield([
319 + 'Task: 2, Run: 2, Priority: User-blocking, Queue Size: 2',
320 + 'Task: 1, Run: 3, Priority: Normal, Queue Size: 1',
321 + ]);
322 +
323 + expect(getProfilingInfo()).toEqual('Empty Queue');
324 +
325 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
326 + `
327 +!!! Main thread │██░░░░░░░░██░░░░░░░░░░░░
328 +Task 2 [User-blocking] │ ░░░░██████
329 +Task 1 [Normal] │ ████████░░░░░░░░██████
330 +`,
331 + );
332 + });
333 +
334 + it('marks when a task is canceled', () => {
335 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
336 +
337 + const task = scheduleCallback(NormalPriority, () => {
338 + Scheduler.unstable_yieldValue(getProfilingInfo());
339 + Scheduler.unstable_advanceTime(300);
340 + Scheduler.unstable_yieldValue('Yield');
341 + return () => {
342 + Scheduler.unstable_yieldValue('Continuation');
343 + Scheduler.unstable_advanceTime(200);
344 + };
345 + });
346 +
347 + expect(Scheduler).toFlushAndYieldThrough([
348 + 'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
349 + 'Yield',
350 + ]);
351 + Scheduler.unstable_advanceTime(100);
352 +
353 + cancelCallback(task);
354 +
355 + Scheduler.unstable_advanceTime(1000);
356 + expect(Scheduler).toFlushWithoutYielding();
357 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
358 + `
359 +!!! Main thread │░░░░░░██████████████████████
360 +Task 1 [Normal] │██████░░🡐 canceled
361 +`,
362 + );
363 + });
364 +
365 + it('marks when a task errors', () => {
366 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
367 +
368 + scheduleCallback(NormalPriority, () => {
369 + Scheduler.unstable_advanceTime(300);
370 + throw Error('Oops');
371 + });
372 +
373 + expect(Scheduler).toFlushAndThrow('Oops');
374 + Scheduler.unstable_advanceTime(100);
375 +
376 + Scheduler.unstable_advanceTime(1000);
377 + expect(Scheduler).toFlushWithoutYielding();
378 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
379 + `
380 +!!! Main thread │░░░░░░██████████████████████
381 +Task 1 [Normal] │██████🡐 errored
382 +`,
383 + );
384 + });
385 +
386 + it('marks when multiple tasks are canceled', () => {
387 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
388 +
389 + const task1 = scheduleCallback(NormalPriority, () => {
390 + Scheduler.unstable_yieldValue(getProfilingInfo());
391 + Scheduler.unstable_advanceTime(300);
392 + Scheduler.unstable_yieldValue('Yield');
393 + return () => {
394 + Scheduler.unstable_yieldValue('Continuation');
395 + Scheduler.unstable_advanceTime(200);
396 + };
397 + });
398 + const task2 = scheduleCallback(NormalPriority, () => {
399 + Scheduler.unstable_yieldValue(getProfilingInfo());
400 + Scheduler.unstable_advanceTime(300);
401 + Scheduler.unstable_yieldValue('Yield');
402 + return () => {
403 + Scheduler.unstable_yieldValue('Continuation');
404 + Scheduler.unstable_advanceTime(200);
405 + };
406 + });
407 +
408 + expect(Scheduler).toFlushAndYieldThrough([
409 + 'Task: 1, Run: 1, Priority: Normal, Queue Size: 2',
410 + 'Yield',
411 + ]);
412 + Scheduler.unstable_advanceTime(100);
413 +
414 + cancelCallback(task1);
415 + cancelCallback(task2);
416 +
417 + // Advance more time. This should not affect the size of the main
418 + // thread row, since the Scheduler queue is empty.
419 + Scheduler.unstable_advanceTime(1000);
420 + expect(Scheduler).toFlushWithoutYielding();
421 +
422 + // The main thread row should end when the callback is cancelled.
423 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
424 + `
425 +!!! Main thread │░░░░░░██████████████████████
426 +Task 1 [Normal] │██████░░🡐 canceled
427 +Task 2 [Normal] │░░░░░░░░🡐 canceled
428 +`,
429 + );
430 + });
431 +
432 + it('handles cancelling a task that already finished', () => {
433 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
434 +
435 + const task = scheduleCallback(NormalPriority, () => {
436 + Scheduler.unstable_yieldValue('A');
437 + Scheduler.unstable_advanceTime(1000);
438 + });
439 + expect(Scheduler).toFlushAndYield(['A']);
440 + cancelCallback(task);
441 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
442 + `
443 +!!! Main thread │░░░░░░░░░░░░░░░░░░░░
444 +Task 1 [Normal] │████████████████████
445 +`,
446 + );
447 + });
448 +
449 + it('handles cancelling a task multiple times', () => {
450 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
451 +
452 + scheduleCallback(
453 + NormalPriority,
454 + () => {
455 + Scheduler.unstable_yieldValue('A');
456 + Scheduler.unstable_advanceTime(1000);
457 + },
458 + {label: 'A'},
459 + );
460 + Scheduler.unstable_advanceTime(200);
461 + const task = scheduleCallback(
462 + NormalPriority,
463 + () => {
464 + Scheduler.unstable_yieldValue('B');
465 + Scheduler.unstable_advanceTime(1000);
466 + },
467 + {label: 'B'},
468 + );
469 + Scheduler.unstable_advanceTime(400);
470 + cancelCallback(task);
471 + cancelCallback(task);
472 + cancelCallback(task);
473 + expect(Scheduler).toFlushAndYield(['A']);
474 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
475 + `
476 +!!! Main thread │████████████░░░░░░░░░░░░░░░░░░░░
477 +Task 1 [Normal] │░░░░░░░░░░░░████████████████████
478 +Task 2 [Normal] │ ░░░░░░░░🡐 canceled
479 +`,
480 + );
481 + });
482 +
483 + it('handles cancelling a delayed task', () => {
484 + Scheduler.unstable_Profiling.startLoggingProfilingEvents();
485 + const task = scheduleCallback(
486 + NormalPriority,
487 + () => Scheduler.unstable_yieldValue('A'),
488 + {delay: 1000},
489 + );
490 + cancelCallback(task);
491 + expect(Scheduler).toFlushWithoutYielding();
492 + expect(stopProfilingAndPrintFlamegraph()).toEqual(
493 + `
494 +!!! Main thread │
495 +`,
496 + );
497 + });
498 +
499 + it('resizes event log buffer if there are many events', () => {
500 + const tasks = [];
501 + for (let i = 0; i < 5000; i++) {
502 + tasks.push(scheduleCallback(NormalPriority, () => {}));
503 + }
504 + expect(getProfilingInfo()).toEqual('Suspended, Queue Size: 5000');
505 + tasks.forEach(task => cancelCallback(task));
506 + expect(getProfilingInfo()).toEqual('Empty Queue');
507 + });
508 +});
packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
+2
@@ -13,3 +13,5 @@ export const {
13 requestTimerEventBeforeFirstFrame,
14 enableMessageLoopImplementation,
15 } = require('SchedulerFeatureFlags');
16 +
17 +export const enableProfiling = __PROFILE__;
packages/scheduler/src/forks/SchedulerHostConfig.default.js
+11 -5
@@ -53,8 +53,9 @@ if (
53 }
54 }
55 };
56 + const initialTime = Date.now();
57 getCurrentTime = function() {
57 - return Date.now();
58 + return Date.now() - initialTime;
59 };
60 requestHostCallback = function(cb) {
61 if (_callback !== null) {
@@ -111,10 +112,15 @@ if (
112 typeof requestIdleCallback === 'function' &&
113 typeof cancelIdleCallback === 'function';
114
114 - getCurrentTime =
115 - typeof performance === 'object' && typeof performance.now === 'function'
116 - ? () => performance.now()
117 - : () => Date.now();
115 + if (
116 + typeof performance === 'object' &&
117 + typeof performance.now === 'function'
118 + ) {
119 + getCurrentTime = () => performance.now();
120 + } else {
121 + const initialTime = Date.now();
122 + getCurrentTime = () => Date.now() - initialTime;
123 + }
124
125 let isRAFLoopRunning = false;
126 let isMessageLoopRunning = false;
scripts/rollup/bundles.js
+7 -1
@@ -411,7 +411,13 @@ const bundles = [
411
412 /******* React Scheduler (experimental) *******/
413 {
414 - bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
414 + bundleTypes: [
415 + NODE_DEV,
416 + NODE_PROD,
417 + FB_WWW_DEV,
418 + FB_WWW_PROD,
419 + FB_WWW_PROFILING,
420 + ],
421 moduleType: ISOMORPHIC,
422 entry: 'scheduler',
423 global: 'Scheduler',
scripts/rollup/validate/eslintrc.cjs.js
+5
@@ -21,6 +21,11 @@ module.exports = {
21 process: true,
22 setImmediate: true,
23 Buffer: true,
24 +
25 + // Scheduler profiling
26 + SharedArrayBuffer: true,
27 + Int32Array: true,
28 + ArrayBuffer: true,
29 },
30 parserOptions: {
31 ecmaVersion: 5,
scripts/rollup/validate/eslintrc.fb.js
+5
@@ -22,6 +22,11 @@ module.exports = {
22 // Node.js Server Rendering
23 setImmediate: true,
24 Buffer: true,
25 +
26 + // Scheduler profiling
27 + SharedArrayBuffer: true,
28 + Int32Array: true,
29 + ArrayBuffer: true,
30 },
31 parserOptions: {
32 ecmaVersion: 5,
scripts/rollup/validate/eslintrc.rn.js
+5
@@ -21,6 +21,11 @@ module.exports = {
21 // Fabric. See https://github.com/facebook/react/pull/15490
22 // for more information
23 nativeFabricUIManager: true,
24 +
25 + // Scheduler profiling
26 + SharedArrayBuffer: true,
27 + Int32Array: true,
28 + ArrayBuffer: true,
29 },
30 parserOptions: {
31 ecmaVersion: 5,
scripts/rollup/validate/eslintrc.umd.js
+5
@@ -24,6 +24,11 @@ module.exports = {
24 define: true,
25 require: true,
26 global: true,
27 +
28 + // Scheduler profiling
29 + SharedArrayBuffer: true,
30 + Int32Array: true,
31 + ArrayBuffer: true,
32 },
33 parserOptions: {
34 ecmaVersion: 5,