main
js 613 lines 17.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 /* eslint-disable no-var */
11
12 import type {PriorityLevel} from '../SchedulerPriorities';
13
14 import {
15 enableProfiling,
16 frameYieldMs,
17 userBlockingPriorityTimeout,
18 lowPriorityTimeout,
19 normalPriorityTimeout,
20 enableRequestPaint,
21 enableAlwaysYieldScheduler,
22 } from '../SchedulerFeatureFlags';
23
24 import {push, pop, peek} from '../SchedulerMinHeap';
25
26 // TODO: Use symbols?
27 import {
28 ImmediatePriority,
29 UserBlockingPriority,
30 NormalPriority,
31 LowPriority,
32 IdlePriority,
33 } from '../SchedulerPriorities';
34 import {
35 markTaskRun,
36 markTaskYield,
37 markTaskCompleted,
38 markTaskCanceled,
39 markTaskErrored,
40 markSchedulerSuspended,
41 markSchedulerUnsuspended,
42 markTaskStart,
43 stopLoggingProfilingEvents,
44 startLoggingProfilingEvents,
45 } from '../SchedulerProfiling';
46
47 export type Callback = boolean => ?Callback;
48
49 export opaque type Task = {
50 id: number,
51 callback: Callback | null,
52 priorityLevel: PriorityLevel,
53 startTime: number,
54 expirationTime: number,
55 sortIndex: number,
56 isQueued?: boolean,
57 };
58
59 let getCurrentTime: () => number | DOMHighResTimeStamp;
60 const hasPerformanceNow =
61 // $FlowFixMe[method-unbinding]
62 typeof performance === 'object' && typeof performance.now === 'function';
63
64 if (hasPerformanceNow) {
65 const localPerformance = performance;
66 getCurrentTime = () => localPerformance.now();
67 } else {
68 const localDate = Date;
69 const initialTime = localDate.now();
70 getCurrentTime = () => localDate.now() - initialTime;
71 }
72
73 // Max 31 bit integer. The max integer size in V8 for 32-bit systems.
74 // Math.pow(2, 30) - 1
75 // 0b111111111111111111111111111111
76 var maxSigned31BitInt = 1073741823;
77
78 // Tasks are stored on a min heap
79 var taskQueue: Array<Task> = [];
80 var timerQueue: Array<Task> = [];
81
82 // Incrementing id counter. Used to maintain insertion order.
83 var taskIdCounter = 1;
84
85 var currentTask = null;
86 var currentPriorityLevel: PriorityLevel = NormalPriority;
87
88 // This is set while performing work, to prevent re-entrance.
89 var isPerformingWork = false;
90
91 var isHostCallbackScheduled = false;
92 var isHostTimeoutScheduled = false;
93
94 var needsPaint = false;
95
96 // Capture local references to native APIs, in case a polyfill overrides them.
97 const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
98 const localClearTimeout =
99 typeof clearTimeout === 'function' ? clearTimeout : null;
100 const localSetImmediate =
101 typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
102
103 function advanceTimers(currentTime: number) {
104 // Check for tasks that are no longer delayed and add them to the queue.
105 let timer = peek(timerQueue);
106 while (timer !== null) {
107 if (timer.callback === null) {
108 // Timer was cancelled.
109 pop(timerQueue);
110 } else if (timer.startTime <= currentTime) {
111 // Timer fired. Transfer to the task queue.
112 pop(timerQueue);
113 timer.sortIndex = timer.expirationTime;
114 push(taskQueue, timer);
115 // $FlowFixMe[constant-condition]
116 if (enableProfiling) {
117 markTaskStart(timer, currentTime);
118 timer.isQueued = true;
119 }
120 } else {
121 // Remaining timers are pending.
122 return;
123 }
124 timer = peek(timerQueue);
125 }
126 }
127
128 function handleTimeout(currentTime: number) {
129 isHostTimeoutScheduled = false;
130 advanceTimers(currentTime);
131
132 if (!isHostCallbackScheduled) {
133 if (peek(taskQueue) !== null) {
134 isHostCallbackScheduled = true;
135 requestHostCallback();
136 } else {
137 const firstTimer = peek(timerQueue);
138 if (firstTimer !== null) {
139 requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
140 }
141 }
142 }
143 }
144
145 function flushWork(initialTime: number) {
146 // $FlowFixMe[constant-condition]
147 if (enableProfiling) {
148 markSchedulerUnsuspended(initialTime);
149 }
150
151 // We'll need a host callback the next time work is scheduled.
152 isHostCallbackScheduled = false;
153 if (isHostTimeoutScheduled) {
154 // We scheduled a timeout but it's no longer needed. Cancel it.
155 isHostTimeoutScheduled = false;
156 cancelHostTimeout();
157 }
158
159 isPerformingWork = true;
160 const previousPriorityLevel = currentPriorityLevel;
161 try {
162 // $FlowFixMe[constant-condition]
163 if (enableProfiling) {
164 try {
165 return workLoop(initialTime);
166 } catch (error) {
167 if (currentTask !== null) {
168 const currentTime = getCurrentTime();
169 // $FlowFixMe[incompatible-call] found when upgrading Flow
170 // $FlowFixMe[incompatible-type]
171 markTaskErrored(currentTask, currentTime);
172 // $FlowFixMe[incompatible-use] found when upgrading Flow
173 currentTask.isQueued = false;
174 }
175 throw error;
176 }
177 } else {
178 // No catch in prod code path.
179 return workLoop(initialTime);
180 }
181 } finally {
182 currentTask = null;
183 currentPriorityLevel = previousPriorityLevel;
184 isPerformingWork = false;
185 // $FlowFixMe[constant-condition]
186 if (enableProfiling) {
187 const currentTime = getCurrentTime();
188 markSchedulerSuspended(currentTime);
189 }
190 }
191 }
192
193 function workLoop(initialTime: number) {
194 let currentTime = initialTime;
195 advanceTimers(currentTime);
196 currentTask = peek(taskQueue);
197 while (currentTask !== null) {
198 if (!enableAlwaysYieldScheduler) {
199 if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
200 // This currentTask hasn't expired, and we've reached the deadline.
201 break;
202 }
203 }
204 // $FlowFixMe[incompatible-use] found when upgrading Flow
205 const callback = currentTask.callback;
206 if (typeof callback === 'function') {
207 // $FlowFixMe[incompatible-use] found when upgrading Flow
208 currentTask.callback = null;
209 // $FlowFixMe[incompatible-use] found when upgrading Flow
210 currentPriorityLevel = currentTask.priorityLevel;
211 // $FlowFixMe[incompatible-use] found when upgrading Flow
212 const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
213 // $FlowFixMe[constant-condition]
214 if (enableProfiling) {
215 // $FlowFixMe[incompatible-type] found when upgrading Flow
216 markTaskRun(currentTask, currentTime);
217 }
218 const continuationCallback = callback(didUserCallbackTimeout);
219 currentTime = getCurrentTime();
220 if (typeof continuationCallback === 'function') {
221 // If a continuation is returned, immediately yield to the main thread
222 // regardless of how much time is left in the current time slice.
223 // $FlowFixMe[incompatible-use] found when upgrading Flow
224 currentTask.callback = continuationCallback;
225 // $FlowFixMe[constant-condition]
226 if (enableProfiling) {
227 // $FlowFixMe[incompatible-type] found when upgrading Flow
228 markTaskYield(currentTask, currentTime);
229 }
230 advanceTimers(currentTime);
231 return true;
232 } else {
233 // $FlowFixMe[constant-condition]
234 if (enableProfiling) {
235 // $FlowFixMe[incompatible-type] found when upgrading Flow
236 markTaskCompleted(currentTask, currentTime);
237 // $FlowFixMe[incompatible-use] found when upgrading Flow
238 currentTask.isQueued = false;
239 }
240 if (currentTask === peek(taskQueue)) {
241 pop(taskQueue);
242 }
243 advanceTimers(currentTime);
244 }
245 } else {
246 pop(taskQueue);
247 }
248 currentTask = peek(taskQueue);
249 if (enableAlwaysYieldScheduler) {
250 if (currentTask === null || currentTask.expirationTime > currentTime) {
251 // This currentTask hasn't expired we yield to the browser task.
252 break;
253 }
254 }
255 }
256 // Return whether there's additional work
257 if (currentTask !== null) {
258 return true;
259 } else {
260 const firstTimer = peek(timerQueue);
261 if (firstTimer !== null) {
262 requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
263 }
264 return false;
265 }
266 }
267
268 function unstable_runWithPriority<T>(
269 priorityLevel: PriorityLevel,
270 eventHandler: () => T,
271 ): T {
272 switch (priorityLevel) {
273 case ImmediatePriority:
274 case UserBlockingPriority:
275 case NormalPriority:
276 case LowPriority:
277 case IdlePriority:
278 break;
279 default:
280 priorityLevel = NormalPriority;
281 }
282
283 var previousPriorityLevel = currentPriorityLevel;
284 currentPriorityLevel = priorityLevel;
285
286 try {
287 return eventHandler();
288 } finally {
289 currentPriorityLevel = previousPriorityLevel;
290 }
291 }
292
293 function unstable_next<T>(eventHandler: () => T): T {
294 var priorityLevel: PriorityLevel;
295 switch (currentPriorityLevel) {
296 case ImmediatePriority:
297 case UserBlockingPriority:
298 case NormalPriority:
299 // Shift down to normal priority
300 priorityLevel = NormalPriority;
301 break;
302 default:
303 // Anything lower than normal priority should remain at the current level.
304 priorityLevel = currentPriorityLevel;
305 break;
306 }
307
308 var previousPriorityLevel = currentPriorityLevel;
309 currentPriorityLevel = priorityLevel;
310
311 try {
312 return eventHandler();
313 } finally {
314 currentPriorityLevel = previousPriorityLevel;
315 }
316 }
317
318 function unstable_wrapCallback<T: (...Array<mixed>) => mixed>(callback: T): T {
319 var parentPriorityLevel = currentPriorityLevel;
320 // $FlowFixMe[incompatible-type]
321 // $FlowFixMe[missing-this-annot]
322 return function () {
323 // This is a fork of runWithPriority, inlined for performance.
324 var previousPriorityLevel = currentPriorityLevel;
325 currentPriorityLevel = parentPriorityLevel;
326
327 try {
328 return callback.apply(this, arguments);
329 } finally {
330 currentPriorityLevel = previousPriorityLevel;
331 }
332 };
333 }
334
335 function unstable_scheduleCallback(
336 priorityLevel: PriorityLevel,
337 callback: Callback,
338 options?: {delay: number},
339 ): Task {
340 var currentTime = getCurrentTime();
341
342 var startTime;
343 // $FlowFixMe[invalid-compare]
344 if (typeof options === 'object' && options !== null) {
345 var delay = options.delay;
346 if (typeof delay === 'number' && delay > 0) {
347 startTime = currentTime + delay;
348 } else {
349 startTime = currentTime;
350 }
351 } else {
352 startTime = currentTime;
353 }
354
355 var timeout;
356 switch (priorityLevel) {
357 case ImmediatePriority:
358 // Times out immediately
359 timeout = -1;
360 break;
361 case UserBlockingPriority:
362 // Eventually times out
363 timeout = userBlockingPriorityTimeout;
364 break;
365 case IdlePriority:
366 // Never times out
367 timeout = maxSigned31BitInt;
368 break;
369 case LowPriority:
370 // Eventually times out
371 timeout = lowPriorityTimeout;
372 break;
373 case NormalPriority:
374 default:
375 // Eventually times out
376 timeout = normalPriorityTimeout;
377 break;
378 }
379
380 var expirationTime = startTime + timeout;
381
382 var newTask: Task = {
383 id: taskIdCounter++,
384 callback,
385 priorityLevel,
386 startTime,
387 expirationTime,
388 sortIndex: -1,
389 };
390 // $FlowFixMe[constant-condition]
391 if (enableProfiling) {
392 newTask.isQueued = false;
393 }
394
395 if (startTime > currentTime) {
396 // This is a delayed task.
397 newTask.sortIndex = startTime;
398 push(timerQueue, newTask);
399 if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
400 // All tasks are delayed, and this is the task with the earliest delay.
401 if (isHostTimeoutScheduled) {
402 // Cancel an existing timeout.
403 cancelHostTimeout();
404 } else {
405 isHostTimeoutScheduled = true;
406 }
407 // Schedule a timeout.
408 requestHostTimeout(handleTimeout, startTime - currentTime);
409 }
410 } else {
411 newTask.sortIndex = expirationTime;
412 push(taskQueue, newTask);
413 // $FlowFixMe[constant-condition]
414 if (enableProfiling) {
415 markTaskStart(newTask, currentTime);
416 newTask.isQueued = true;
417 }
418 // Schedule a host callback, if needed. If we're already performing work,
419 // wait until the next time we yield.
420 if (!isHostCallbackScheduled && !isPerformingWork) {
421 isHostCallbackScheduled = true;
422 requestHostCallback();
423 }
424 }
425
426 return newTask;
427 }
428
429 function unstable_cancelCallback(task: Task) {
430 // $FlowFixMe[constant-condition]
431 if (enableProfiling) {
432 if (task.isQueued) {
433 const currentTime = getCurrentTime();
434 markTaskCanceled(task, currentTime);
435 task.isQueued = false;
436 }
437 }
438
439 // Null out the callback to indicate the task has been canceled. (Can't
440 // remove from the queue because you can't remove arbitrary nodes from an
441 // array based heap, only the first one.)
442 task.callback = null;
443 }
444
445 function unstable_getCurrentPriorityLevel(): PriorityLevel {
446 return currentPriorityLevel;
447 }
448
449 let isMessageLoopRunning = false;
450 let taskTimeoutID: TimeoutID = -1 as any;
451
452 // Scheduler periodically yields in case there is other work on the main
453 // thread, like user events. By default, it yields multiple times per frame.
454 // It does not attempt to align with frame boundaries, since most tasks don't
455 // need to be frame aligned; for those that do, use requestAnimationFrame.
456 let frameInterval: number = frameYieldMs;
457 let startTime = -1;
458
459 function shouldYieldToHost(): boolean {
460 if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
461 // Yield now.
462 return true;
463 }
464 const timeElapsed = getCurrentTime() - startTime;
465 if (timeElapsed < frameInterval) {
466 // The main thread has only been blocked for a really short amount of time;
467 // smaller than a single frame. Don't yield yet.
468 return false;
469 }
470 // Yield now.
471 return true;
472 }
473
474 function requestPaint() {
475 // $FlowFixMe[constant-condition]
476 if (enableRequestPaint) {
477 needsPaint = true;
478 }
479 }
480
481 function forceFrameRate(fps: number) {
482 if (fps < 0 || fps > 125) {
483 // Using console['error'] to evade Babel and ESLint
484 console['error'](
485 'forceFrameRate takes a positive int between 0 and 125, ' +
486 'forcing frame rates higher than 125 fps is not supported',
487 );
488 return;
489 }
490 if (fps > 0) {
491 frameInterval = Math.floor(1000 / fps);
492 } else {
493 // reset the framerate
494 frameInterval = frameYieldMs;
495 }
496 }
497
498 const performWorkUntilDeadline = () => {
499 // $FlowFixMe[constant-condition]
500 if (enableRequestPaint) {
501 needsPaint = false;
502 }
503 if (isMessageLoopRunning) {
504 const currentTime = getCurrentTime();
505 // Keep track of the start time so we can measure how long the main thread
506 // has been blocked.
507 startTime = currentTime;
508
509 // If a scheduler task throws, exit the current browser task so the
510 // error can be observed.
511 //
512 // Intentionally not using a try-catch, since that makes some debugging
513 // techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
514 // remain true, and we'll continue the work loop.
515 let hasMoreWork = true;
516 try {
517 hasMoreWork = flushWork(currentTime);
518 } finally {
519 if (hasMoreWork) {
520 // If there's more work, schedule the next message event at the end
521 // of the preceding one.
522 schedulePerformWorkUntilDeadline();
523 } else {
524 isMessageLoopRunning = false;
525 }
526 }
527 }
528 };
529
530 let schedulePerformWorkUntilDeadline;
531 if (typeof localSetImmediate === 'function') {
532 // Node.js and old IE.
533 // There's a few reasons for why we prefer setImmediate.
534 //
535 // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
536 // (Even though this is a DOM fork of the Scheduler, you could get here
537 // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
538 // https://github.com/facebook/react/issues/20756
539 //
540 // But also, it runs earlier which is the semantic we want.
541 // If other browsers ever implement it, it's better to use it.
542 // Although both of these would be inferior to native scheduling.
543 schedulePerformWorkUntilDeadline = () => {
544 localSetImmediate(performWorkUntilDeadline);
545 };
546 } else if (typeof MessageChannel !== 'undefined') {
547 // DOM and Worker environments.
548 // We prefer MessageChannel because of the 4ms setTimeout clamping.
549 const channel = new MessageChannel();
550 const port = channel.port2;
551 channel.port1.onmessage = performWorkUntilDeadline;
552 schedulePerformWorkUntilDeadline = () => {
553 port.postMessage(null);
554 };
555 } else {
556 // We should only fallback here in non-browser environments.
557 schedulePerformWorkUntilDeadline = () => {
558 // $FlowFixMe[not-a-function] nullable value
559 localSetTimeout(performWorkUntilDeadline, 0);
560 };
561 }
562
563 function requestHostCallback() {
564 if (!isMessageLoopRunning) {
565 isMessageLoopRunning = true;
566 schedulePerformWorkUntilDeadline();
567 }
568 }
569
570 function requestHostTimeout(
571 callback: (currentTime: number) => void,
572 ms: number,
573 ) {
574 // $FlowFixMe[not-a-function] nullable value
575 taskTimeoutID = localSetTimeout(() => {
576 callback(getCurrentTime());
577 }, ms);
578 }
579
580 function cancelHostTimeout() {
581 // $FlowFixMe[not-a-function] nullable value
582 localClearTimeout(taskTimeoutID);
583 taskTimeoutID = -1 as any as TimeoutID;
584 }
585
586 export {
587 ImmediatePriority as unstable_ImmediatePriority,
588 UserBlockingPriority as unstable_UserBlockingPriority,
589 NormalPriority as unstable_NormalPriority,
590 IdlePriority as unstable_IdlePriority,
591 LowPriority as unstable_LowPriority,
592 unstable_runWithPriority,
593 unstable_next,
594 unstable_scheduleCallback,
595 unstable_cancelCallback,
596 unstable_wrapCallback,
597 unstable_getCurrentPriorityLevel,
598 shouldYieldToHost as unstable_shouldYield,
599 requestPaint as unstable_requestPaint,
600 getCurrentTime as unstable_now,
601 forceFrameRate as unstable_forceFrameRate,
602 };
603
604 export const unstable_Profiling: {
605 startLoggingProfilingEvents(): void,
606 stopLoggingProfilingEvents(): ArrayBuffer | null,
607 // $FlowFixMe[constant-condition]
608 } | null = enableProfiling
609 ? {
610 startLoggingProfilingEvents,
611 stopLoggingProfilingEvents,
612 }
613 : null;