main
js 698 lines 18.8 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 /* eslint-disable react-internal/prod-error-codes */
12
13 import type {PriorityLevel} from '../SchedulerPriorities';
14
15 import {enableProfiling} from '../SchedulerFeatureFlags';
16 import {push, pop, peek} from '../SchedulerMinHeap';
17
18 // TODO: Use symbols?
19 import {
20 ImmediatePriority,
21 UserBlockingPriority,
22 NormalPriority,
23 LowPriority,
24 IdlePriority,
25 } from '../SchedulerPriorities';
26 import {
27 markTaskRun,
28 markTaskYield,
29 markTaskCompleted,
30 markTaskCanceled,
31 markTaskErrored,
32 markSchedulerSuspended,
33 markSchedulerUnsuspended,
34 markTaskStart,
35 stopLoggingProfilingEvents,
36 startLoggingProfilingEvents,
37 } from '../SchedulerProfiling';
38
39 type Callback = boolean => ?Callback;
40
41 type Task = {
42 id: number,
43 callback: Callback | null,
44 priorityLevel: PriorityLevel,
45 startTime: number,
46 expirationTime: number,
47 sortIndex: number,
48 isQueued?: boolean,
49 };
50
51 // Max 31 bit integer. The max integer size in V8 for 32-bit systems.
52 // Math.pow(2, 30) - 1
53 // 0b111111111111111111111111111111
54 var maxSigned31BitInt = 1073741823;
55
56 // Times out immediately
57 var IMMEDIATE_PRIORITY_TIMEOUT = -1;
58 // Eventually times out
59 var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
60 var NORMAL_PRIORITY_TIMEOUT = 5000;
61 var LOW_PRIORITY_TIMEOUT = 10000;
62 // Never times out
63 var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
64
65 // Tasks are stored on a min heap
66 var taskQueue: Array<Task> = [];
67 var timerQueue: Array<Task> = [];
68
69 // Incrementing id counter. Used to maintain insertion order.
70 var taskIdCounter = 1;
71
72 var currentTask = null;
73 var currentPriorityLevel: PriorityLevel = NormalPriority;
74
75 // This is set while performing work, to prevent re-entrance.
76 var isPerformingWork = false;
77
78 var isHostCallbackScheduled = false;
79 var isHostTimeoutScheduled = false;
80
81 let currentMockTime: number = 0;
82 let scheduledCallback:
83 | null
84 | ((
85 hasTimeRemaining: boolean,
86 initialTime: DOMHighResTimeStamp | number,
87 ) => boolean) = null;
88 let scheduledTimeout: (number => void) | null = null;
89 let timeoutTime: number = -1;
90 let yieldedValues: Array<mixed> | null = null;
91 let expectedNumberOfYields: number = -1;
92 let didStop: boolean = false;
93 let isFlushing: boolean = false;
94 let needsPaint: boolean = false;
95 let shouldYieldForPaint: boolean = false;
96
97 var disableYieldValue = false;
98
99 function setDisableYieldValue(newValue: boolean) {
100 disableYieldValue = newValue;
101 }
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(flushWork);
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(hasTimeRemaining: boolean, 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(hasTimeRemaining, 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(hasTimeRemaining, 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(hasTimeRemaining: boolean, initialTime: number): boolean {
194 let currentTime = initialTime;
195 advanceTimers(currentTime);
196 currentTask = peek(taskQueue);
197 while (currentTask !== null) {
198 if (
199 currentTask.expirationTime > currentTime &&
200 (!hasTimeRemaining || shouldYieldToHost())
201 ) {
202 // This currentTask hasn't expired, and we've reached the deadline.
203 break;
204 }
205 // $FlowFixMe[incompatible-use] found when upgrading Flow
206 const callback = currentTask.callback;
207 if (typeof callback === 'function') {
208 // $FlowFixMe[incompatible-use] found when upgrading Flow
209 currentTask.callback = null;
210 // $FlowFixMe[incompatible-use] found when upgrading Flow
211 currentPriorityLevel = currentTask.priorityLevel;
212 // $FlowFixMe[incompatible-use] found when upgrading Flow
213 const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
214 // $FlowFixMe[constant-condition]
215 if (enableProfiling) {
216 // $FlowFixMe[incompatible-call] found when upgrading Flow
217 // $FlowFixMe[incompatible-type]
218 markTaskRun(currentTask, currentTime);
219 }
220 const continuationCallback = callback(didUserCallbackTimeout);
221 currentTime = getCurrentTime();
222 if (typeof continuationCallback === 'function') {
223 // If a continuation is returned, immediately yield to the main thread
224 // regardless of how much time is left in the current time slice.
225 // $FlowFixMe[incompatible-use] found when upgrading Flow
226 currentTask.callback = continuationCallback;
227 // $FlowFixMe[constant-condition]
228 if (enableProfiling) {
229 // $FlowFixMe[incompatible-call] found when upgrading Flow
230 // $FlowFixMe[incompatible-type]
231 markTaskYield(currentTask, currentTime);
232 }
233 advanceTimers(currentTime);
234
235 if (shouldYieldForPaint) {
236 needsPaint = true;
237 return true;
238 } else {
239 // If `shouldYieldForPaint` is false, we keep flushing synchronously
240 // without yielding to the main thread. This is the behavior of the
241 // `toFlushAndYield` and `toFlushAndYieldThrough` testing helpers .
242 }
243 } else {
244 // $FlowFixMe[constant-condition]
245 if (enableProfiling) {
246 // $FlowFixMe[incompatible-call] found when upgrading Flow
247 // $FlowFixMe[incompatible-type]
248 markTaskCompleted(currentTask, currentTime);
249 // $FlowFixMe[incompatible-use] found when upgrading Flow
250 currentTask.isQueued = false;
251 }
252 if (currentTask === peek(taskQueue)) {
253 pop(taskQueue);
254 }
255 advanceTimers(currentTime);
256 }
257 } else {
258 pop(taskQueue);
259 }
260 currentTask = peek(taskQueue);
261 }
262 // Return whether there's additional work
263 if (currentTask !== null) {
264 return true;
265 } else {
266 const firstTimer = peek(timerQueue);
267 if (firstTimer !== null) {
268 requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
269 }
270 return false;
271 }
272 }
273
274 function unstable_runWithPriority<T>(
275 priorityLevel: PriorityLevel,
276 eventHandler: () => T,
277 ): T {
278 switch (priorityLevel) {
279 case ImmediatePriority:
280 case UserBlockingPriority:
281 case NormalPriority:
282 case LowPriority:
283 case IdlePriority:
284 break;
285 default:
286 priorityLevel = NormalPriority;
287 }
288
289 var previousPriorityLevel = currentPriorityLevel;
290 currentPriorityLevel = priorityLevel;
291
292 try {
293 return eventHandler();
294 } finally {
295 currentPriorityLevel = previousPriorityLevel;
296 }
297 }
298
299 function unstable_next<T>(eventHandler: () => T): T {
300 var priorityLevel: PriorityLevel;
301 switch (currentPriorityLevel) {
302 case ImmediatePriority:
303 case UserBlockingPriority:
304 case NormalPriority:
305 // Shift down to normal priority
306 priorityLevel = NormalPriority;
307 break;
308 default:
309 // Anything lower than normal priority should remain at the current level.
310 priorityLevel = currentPriorityLevel;
311 break;
312 }
313
314 var previousPriorityLevel = currentPriorityLevel;
315 currentPriorityLevel = priorityLevel;
316
317 try {
318 return eventHandler();
319 } finally {
320 currentPriorityLevel = previousPriorityLevel;
321 }
322 }
323
324 function unstable_wrapCallback<T: (...Array<mixed>) => mixed>(callback: T): T {
325 var parentPriorityLevel = currentPriorityLevel;
326 // $FlowFixMe[incompatible-return]
327 // $FlowFixMe[missing-this-annot]
328 // $FlowFixMe[incompatible-type]
329 return function () {
330 // This is a fork of runWithPriority, inlined for performance.
331 var previousPriorityLevel = currentPriorityLevel;
332 currentPriorityLevel = parentPriorityLevel;
333
334 try {
335 return callback.apply(this, arguments);
336 } finally {
337 currentPriorityLevel = previousPriorityLevel;
338 }
339 };
340 }
341
342 function unstable_scheduleCallback(
343 priorityLevel: PriorityLevel,
344 callback: Callback,
345 options?: {delay: number},
346 ): Task {
347 var currentTime = getCurrentTime();
348
349 var startTime;
350 // $FlowFixMe[invalid-compare]
351 if (typeof options === 'object' && options !== null) {
352 var delay = options.delay;
353 if (typeof delay === 'number' && delay > 0) {
354 startTime = currentTime + delay;
355 } else {
356 startTime = currentTime;
357 }
358 } else {
359 startTime = currentTime;
360 }
361
362 var timeout;
363 switch (priorityLevel) {
364 case ImmediatePriority:
365 timeout = IMMEDIATE_PRIORITY_TIMEOUT;
366 break;
367 case UserBlockingPriority:
368 timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
369 break;
370 case IdlePriority:
371 timeout = IDLE_PRIORITY_TIMEOUT;
372 break;
373 case LowPriority:
374 timeout = LOW_PRIORITY_TIMEOUT;
375 break;
376 case NormalPriority:
377 default:
378 timeout = NORMAL_PRIORITY_TIMEOUT;
379 break;
380 }
381
382 var expirationTime = startTime + timeout;
383
384 var newTask: Task = {
385 id: taskIdCounter++,
386 callback,
387 priorityLevel,
388 startTime,
389 expirationTime,
390 sortIndex: -1,
391 };
392 // $FlowFixMe[constant-condition]
393 if (enableProfiling) {
394 newTask.isQueued = false;
395 }
396
397 if (startTime > currentTime) {
398 // This is a delayed task.
399 newTask.sortIndex = startTime;
400 push(timerQueue, newTask);
401 if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
402 // All tasks are delayed, and this is the task with the earliest delay.
403 if (isHostTimeoutScheduled) {
404 // Cancel an existing timeout.
405 cancelHostTimeout();
406 } else {
407 isHostTimeoutScheduled = true;
408 }
409 // Schedule a timeout.
410 requestHostTimeout(handleTimeout, startTime - currentTime);
411 }
412 } else {
413 newTask.sortIndex = expirationTime;
414 push(taskQueue, newTask);
415 // $FlowFixMe[constant-condition]
416 if (enableProfiling) {
417 markTaskStart(newTask, currentTime);
418 newTask.isQueued = true;
419 }
420 // Schedule a host callback, if needed. If we're already performing work,
421 // wait until the next time we yield.
422 if (!isHostCallbackScheduled && !isPerformingWork) {
423 isHostCallbackScheduled = true;
424 requestHostCallback(flushWork);
425 }
426 }
427
428 return newTask;
429 }
430
431 function unstable_cancelCallback(task: Task) {
432 // $FlowFixMe[constant-condition]
433 if (enableProfiling) {
434 if (task.isQueued) {
435 const currentTime = getCurrentTime();
436 markTaskCanceled(task, currentTime);
437 task.isQueued = false;
438 }
439 }
440
441 // Null out the callback to indicate the task has been canceled. (Can't
442 // remove from the queue because you can't remove arbitrary nodes from an
443 // array based heap, only the first one.)
444 task.callback = null;
445 }
446
447 function unstable_getCurrentPriorityLevel(): PriorityLevel {
448 return currentPriorityLevel;
449 }
450
451 function requestHostCallback(callback: (boolean, number) => boolean) {
452 scheduledCallback = callback;
453 }
454
455 function requestHostTimeout(callback: number => void, ms: number) {
456 scheduledTimeout = callback;
457 timeoutTime = currentMockTime + ms;
458 }
459
460 function cancelHostTimeout(): void {
461 scheduledTimeout = null;
462 timeoutTime = -1;
463 }
464
465 function shouldYieldToHost(): boolean {
466 if (
467 (expectedNumberOfYields === 0 && yieldedValues === null) ||
468 (expectedNumberOfYields !== -1 &&
469 yieldedValues !== null &&
470 yieldedValues.length >= expectedNumberOfYields) ||
471 (shouldYieldForPaint && needsPaint)
472 ) {
473 // We yielded at least as many values as expected. Stop flushing.
474 didStop = true;
475 return true;
476 }
477 return false;
478 }
479
480 function getCurrentTime(): number {
481 return currentMockTime;
482 }
483
484 function forceFrameRate() {
485 // No-op
486 }
487
488 function reset() {
489 if (isFlushing) {
490 throw new Error('Cannot reset while already flushing work.');
491 }
492 currentMockTime = 0;
493 scheduledCallback = null;
494 scheduledTimeout = null;
495 timeoutTime = -1;
496 yieldedValues = null;
497 expectedNumberOfYields = -1;
498 didStop = false;
499 isFlushing = false;
500 needsPaint = false;
501 }
502
503 // Should only be used via an assertion helper that inspects the yielded values.
504 function unstable_flushNumberOfYields(count: number): void {
505 if (isFlushing) {
506 throw new Error('Already flushing work.');
507 }
508 if (scheduledCallback !== null) {
509 const cb = scheduledCallback;
510 expectedNumberOfYields = count;
511 isFlushing = true;
512 try {
513 let hasMoreWork = true;
514 do {
515 hasMoreWork = cb(true, currentMockTime);
516 } while (hasMoreWork && !didStop);
517 if (!hasMoreWork) {
518 scheduledCallback = null;
519 }
520 } finally {
521 expectedNumberOfYields = -1;
522 didStop = false;
523 isFlushing = false;
524 }
525 }
526 }
527
528 function unstable_flushUntilNextPaint(): false {
529 if (isFlushing) {
530 throw new Error('Already flushing work.');
531 }
532 if (scheduledCallback !== null) {
533 const cb = scheduledCallback;
534 shouldYieldForPaint = true;
535 needsPaint = false;
536 isFlushing = true;
537 try {
538 let hasMoreWork = true;
539 do {
540 hasMoreWork = cb(true, currentMockTime);
541 } while (hasMoreWork && !didStop);
542 if (!hasMoreWork) {
543 scheduledCallback = null;
544 }
545 } finally {
546 shouldYieldForPaint = false;
547 didStop = false;
548 isFlushing = false;
549 }
550 }
551 return false;
552 }
553
554 function unstable_hasPendingWork(): boolean {
555 return scheduledCallback !== null;
556 }
557
558 function unstable_flushExpired() {
559 if (isFlushing) {
560 throw new Error('Already flushing work.');
561 }
562 if (scheduledCallback !== null) {
563 isFlushing = true;
564 try {
565 const hasMoreWork = scheduledCallback(false, currentMockTime);
566 if (!hasMoreWork) {
567 scheduledCallback = null;
568 }
569 } finally {
570 isFlushing = false;
571 }
572 }
573 }
574
575 function unstable_flushAllWithoutAsserting(): boolean {
576 // Returns false if no work was flushed.
577 if (isFlushing) {
578 throw new Error('Already flushing work.');
579 }
580 if (scheduledCallback !== null) {
581 const cb = scheduledCallback;
582 isFlushing = true;
583 try {
584 let hasMoreWork = true;
585 do {
586 hasMoreWork = cb(true, currentMockTime);
587 } while (hasMoreWork);
588 // $FlowFixMe[constant-condition]
589 if (!hasMoreWork) {
590 scheduledCallback = null;
591 }
592 return true;
593 } finally {
594 isFlushing = false;
595 }
596 } else {
597 return false;
598 }
599 }
600
601 function unstable_clearLog(): Array<mixed> {
602 if (yieldedValues === null) {
603 return [];
604 }
605 const values = yieldedValues;
606 yieldedValues = null;
607 return values;
608 }
609
610 function unstable_flushAll(): void {
611 if (yieldedValues !== null) {
612 throw new Error(
613 'Log is not empty. Assert on the log of yielded values before ' +
614 'flushing additional work.',
615 );
616 }
617 unstable_flushAllWithoutAsserting();
618 if (yieldedValues !== null) {
619 throw new Error(
620 'While flushing work, something yielded a value. Use an ' +
621 'assertion helper to assert on the log of yielded values, e.g. ' +
622 'expect(Scheduler).toFlushAndYield([...])',
623 );
624 }
625 }
626
627 function log(value: mixed): void {
628 // eslint-disable-next-line react-internal/no-production-logging
629 if (console.log.name === 'disabledLog' || disableYieldValue) {
630 // If console.log has been patched, we assume we're in render
631 // replaying and we ignore any values yielding in the second pass.
632 return;
633 }
634 if (yieldedValues === null) {
635 yieldedValues = [value];
636 } else {
637 yieldedValues.push(value);
638 }
639 }
640
641 function unstable_advanceTime(ms: number) {
642 // eslint-disable-next-line react-internal/no-production-logging
643 if (console.log.name === 'disabledLog' || disableYieldValue) {
644 // If console.log has been patched, we assume we're in render
645 // replaying and we ignore any time advancing in the second pass.
646 return;
647 }
648 currentMockTime += ms;
649 if (scheduledTimeout !== null && timeoutTime <= currentMockTime) {
650 scheduledTimeout(currentMockTime);
651 timeoutTime = -1;
652 scheduledTimeout = null;
653 }
654 }
655
656 function requestPaint() {
657 needsPaint = true;
658 }
659
660 export {
661 ImmediatePriority as unstable_ImmediatePriority,
662 UserBlockingPriority as unstable_UserBlockingPriority,
663 NormalPriority as unstable_NormalPriority,
664 IdlePriority as unstable_IdlePriority,
665 LowPriority as unstable_LowPriority,
666 unstable_runWithPriority,
667 unstable_next,
668 unstable_scheduleCallback,
669 unstable_cancelCallback,
670 unstable_wrapCallback,
671 unstable_getCurrentPriorityLevel,
672 shouldYieldToHost as unstable_shouldYield,
673 requestPaint as unstable_requestPaint,
674 getCurrentTime as unstable_now,
675 forceFrameRate as unstable_forceFrameRate,
676 unstable_flushAllWithoutAsserting,
677 unstable_flushNumberOfYields,
678 unstable_flushExpired,
679 unstable_clearLog,
680 unstable_flushUntilNextPaint,
681 unstable_hasPendingWork,
682 unstable_flushAll,
683 log,
684 unstable_advanceTime,
685 reset,
686 setDisableYieldValue as unstable_setDisableYieldValue,
687 };
688
689 export const unstable_Profiling: {
690 startLoggingProfilingEvents(): void,
691 stopLoggingProfilingEvents(): ArrayBuffer | null,
692 // $FlowFixMe[constant-condition]
693 } | null = enableProfiling
694 ? {
695 startLoggingProfilingEvents,
696 stopLoggingProfilingEvents,
697 }
698 : null;