diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js
index 66a3a899072c3..8e23a6d5c1615 100644
--- a/packages/react-dom/src/__tests__/ReactUpdates-test.js
+++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js
@@ -1631,6 +1631,7 @@ describe('ReactUpdates', () => {
ReactDOM.render(, container);
while (error === null) {
Scheduler.unstable_flushNumberOfYields(1);
+ Scheduler.unstable_clearYields();
}
expect(error).toContain('Warning: Maximum update depth exceeded.');
expect(stack).toContain('in NonTerminating');
@@ -1653,9 +1654,9 @@ describe('ReactUpdates', () => {
React.useEffect(() => {
if (step < LIMIT) {
setStep(x => x + 1);
- Scheduler.unstable_yieldValue(step);
}
});
+ Scheduler.unstable_yieldValue(step);
return step;
}
@@ -1663,24 +1664,11 @@ describe('ReactUpdates', () => {
act(() => {
ReactDOM.render(, container);
});
-
- // Verify we can flush them asynchronously without warning
- for (let i = 0; i < LIMIT * 2; i++) {
- Scheduler.unstable_flushNumberOfYields(1);
- }
expect(container.textContent).toBe('50');
-
- // Verify restarting from 0 doesn't cross the limit
act(() => {
_setStep(0);
- // flush once to update the dom
- Scheduler.unstable_flushNumberOfYields(1);
- expect(container.textContent).toBe('0');
- for (let i = 0; i < LIMIT * 2; i++) {
- Scheduler.unstable_flushNumberOfYields(1);
- }
- expect(container.textContent).toBe('50');
});
+ expect(container.textContent).toBe('50');
});
it('can have many updates inside useEffect without triggering a warning', () => {
diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js
index cdb9930d4c973..ea0961bec342f 100644
--- a/packages/scheduler/src/Scheduler.js
+++ b/packages/scheduler/src/Scheduler.js
@@ -18,6 +18,7 @@ import {
forceFrameRate,
requestPaint,
} from './SchedulerHostConfig';
+import {push, pop, peek} from './SchedulerMinHeap';
// TODO: Use symbols?
var ImmediatePriority = 1;
@@ -40,9 +41,12 @@ var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
-// Tasks are stored as a circular, doubly linked list.
-var firstTask = null;
-var firstDelayedTask = null;
+// Tasks are stored on a min heap
+var taskQueue = [];
+var timerQueue = [];
+
+// Incrementing id counter. Used to maintain insertion order.
+var taskIdCounter = 0;
// Pausing the scheduler is useful for debugging.
var isSchedulerPaused = false;
@@ -72,35 +76,16 @@ function scheduler_flushTaskAtPriority_Idle(callback, didTimeout) {
return callback(didTimeout);
}
-function flushTask(task, currentTime) {
- // Remove the task from the list before calling the callback. That way the
- // list is in a consistent state even if the callback throws.
- const next = task.next;
- if (next === task) {
- // This is the only scheduled task. Clear the list.
- firstTask = null;
- } else {
- // Remove the task from its position in the list.
- if (task === firstTask) {
- firstTask = next;
- }
- const previous = task.previous;
- previous.next = next;
- next.previous = previous;
- }
- task.next = task.previous = null;
-
- // Now it's safe to execute the task.
- var callback = task.callback;
+function flushTask(task, callback, currentTime) {
var previousPriorityLevel = currentPriorityLevel;
var previousTask = currentTask;
currentPriorityLevel = task.priorityLevel;
currentTask = task;
- var continuationCallback;
try {
var didUserCallbackTimeout = task.expirationTime <= currentTime;
// Add an extra function to the callstack. Profiling tools can use this
// to infer the priority of work that appears higher in the stack.
+ var continuationCallback;
switch (currentPriorityLevel) {
case ImmediatePriority:
continuationCallback = scheduler_flushTaskAtPriority_Immediate(
@@ -133,76 +118,32 @@ function flushTask(task, currentTime) {
);
break;
}
- } catch (error) {
- throw error;
+ return typeof continuationCallback === 'function'
+ ? continuationCallback
+ : null;
} finally {
currentPriorityLevel = previousPriorityLevel;
currentTask = previousTask;
}
-
- // A callback may return a continuation. The continuation should be scheduled
- // with the same priority and expiration as the just-finished callback.
- if (typeof continuationCallback === 'function') {
- var expirationTime = task.expirationTime;
- var continuationTask = task;
- continuationTask.callback = continuationCallback;
-
- // Insert the new callback into the list, sorted by its timeout. This is
- // almost the same as the code in `scheduleCallback`, except the callback
- // is inserted into the list *before* callbacks of equal timeout instead
- // of after.
- if (firstTask === null) {
- // This is the first callback in the list.
- firstTask = continuationTask.next = continuationTask.previous = continuationTask;
- } else {
- var nextAfterContinuation = null;
- var t = firstTask;
- do {
- if (expirationTime <= t.expirationTime) {
- // This task times out at or after the continuation. We will insert
- // the continuation *before* this task.
- nextAfterContinuation = t;
- break;
- }
- t = t.next;
- } while (t !== firstTask);
- if (nextAfterContinuation === null) {
- // No equal or lower priority task was found, which means the new task
- // is the lowest priority task in the list.
- nextAfterContinuation = firstTask;
- } else if (nextAfterContinuation === firstTask) {
- // The new task is the highest priority task in the list.
- firstTask = continuationTask;
- }
-
- const previous = nextAfterContinuation.previous;
- previous.next = nextAfterContinuation.previous = continuationTask;
- continuationTask.next = nextAfterContinuation;
- continuationTask.previous = previous;
- }
- }
}
function advanceTimers(currentTime) {
// Check for tasks that are no longer delayed and add them to the queue.
- if (firstDelayedTask !== null && firstDelayedTask.startTime <= currentTime) {
- do {
- const task = firstDelayedTask;
- const next = task.next;
- if (task === next) {
- firstDelayedTask = null;
- } else {
- firstDelayedTask = next;
- const previous = task.previous;
- previous.next = next;
- next.previous = previous;
- }
- task.next = task.previous = null;
- insertScheduledTask(task, task.expirationTime);
- } while (
- firstDelayedTask !== null &&
- firstDelayedTask.startTime <= currentTime
- );
+ let timer = peek(timerQueue);
+ while (timer !== null) {
+ if (timer.callback === null) {
+ // Timer was cancelled.
+ pop(timerQueue);
+ } else if (timer.startTime <= currentTime) {
+ // Timer fired. Transfer to the task queue.
+ pop(timerQueue);
+ timer.sortIndex = timer.expirationTime;
+ push(taskQueue, timer);
+ } else {
+ // Remaining timers are pending.
+ return;
+ }
+ timer = peek(timerQueue);
}
}
@@ -211,24 +152,19 @@ function handleTimeout(currentTime) {
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
- if (firstTask !== null) {
+ if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
- } else if (firstDelayedTask !== null) {
- requestHostTimeout(
- handleTimeout,
- firstDelayedTask.startTime - currentTime,
- );
+ } else {
+ const firstTimer = peek(timerQueue);
+ if (firstTimer !== null) {
+ requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
+ }
}
}
}
function flushWork(hasTimeRemaining, initialTime) {
- // Exit right away if we're currently paused
- if (enableSchedulerDebugging && isSchedulerPaused) {
- return;
- }
-
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
@@ -237,47 +173,44 @@ function flushWork(hasTimeRemaining, initialTime) {
cancelHostTimeout();
}
- let currentTime = initialTime;
- advanceTimers(currentTime);
-
isPerformingWork = true;
try {
- if (!hasTimeRemaining) {
- // Flush all the expired callbacks without yielding.
- // TODO: Split flushWork into two separate functions instead of using
- // a boolean argument?
- while (
- firstTask !== null &&
- firstTask.expirationTime <= currentTime &&
- !(enableSchedulerDebugging && isSchedulerPaused)
+ let currentTime = initialTime;
+ advanceTimers(currentTime);
+ let task = peek(taskQueue);
+ while (task !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
+ if (
+ task.expirationTime > currentTime &&
+ (!hasTimeRemaining || shouldYieldToHost())
) {
- flushTask(firstTask, currentTime);
+ // This task hasn't expired, and we've reached the deadline.
+ break;
+ }
+ const callback = task.callback;
+ if (callback !== null) {
+ task.callback = null;
+ const continuation = flushTask(task, callback, currentTime);
+ if (continuation !== null) {
+ task.callback = continuation;
+ } else {
+ if (task === peek(taskQueue)) {
+ pop(taskQueue);
+ }
+ }
currentTime = getCurrentTime();
advanceTimers(currentTime);
+ } else {
+ pop(taskQueue);
}
- } else {
- // Keep flushing callbacks until we run out of time in the frame.
- if (firstTask !== null) {
- do {
- flushTask(firstTask, currentTime);
- currentTime = getCurrentTime();
- advanceTimers(currentTime);
- } while (
- firstTask !== null &&
- !shouldYieldToHost() &&
- !(enableSchedulerDebugging && isSchedulerPaused)
- );
- }
+ task = peek(taskQueue);
}
// Return whether there's additional work
- if (firstTask !== null) {
+ if (task !== null) {
return true;
} else {
- if (firstDelayedTask !== null) {
- requestHostTimeout(
- handleTimeout,
- firstDelayedTask.startTime - currentTime,
- );
+ let firstTimer = peek(timerQueue);
+ if (firstTimer !== null) {
+ requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
@@ -388,18 +321,19 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
var expirationTime = startTime + timeout;
var newTask = {
+ id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
- next: null,
- previous: null,
+ sortIndex: -1,
};
if (startTime > currentTime) {
// This is a delayed task.
- insertDelayedTask(newTask, startTime);
- if (firstTask === null && firstDelayedTask === newTask) {
+ newTask.sortIndex = startTime;
+ push(timerQueue, newTask);
+ if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
@@ -411,7 +345,8 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
- insertScheduledTask(newTask, expirationTime);
+ newTask.sortIndex = expirationTime;
+ push(taskQueue, newTask);
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
@@ -423,74 +358,6 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
return newTask;
}
-function insertScheduledTask(newTask, expirationTime) {
- // Insert the new task into the list, ordered first by its timeout, then by
- // insertion. So the new task is inserted after any other task the
- // same timeout
- if (firstTask === null) {
- // This is the first task in the list.
- firstTask = newTask.next = newTask.previous = newTask;
- } else {
- var next = null;
- var task = firstTask;
- do {
- if (expirationTime < task.expirationTime) {
- // The new task times out before this one.
- next = task;
- break;
- }
- task = task.next;
- } while (task !== firstTask);
-
- if (next === null) {
- // No task with a later timeout was found, which means the new task has
- // the latest timeout in the list.
- next = firstTask;
- } else if (next === firstTask) {
- // The new task has the earliest expiration in the entire list.
- firstTask = newTask;
- }
-
- var previous = next.previous;
- previous.next = next.previous = newTask;
- newTask.next = next;
- newTask.previous = previous;
- }
-}
-
-function insertDelayedTask(newTask, startTime) {
- // Insert the new task into the list, ordered by its start time.
- if (firstDelayedTask === null) {
- // This is the first task in the list.
- firstDelayedTask = newTask.next = newTask.previous = newTask;
- } else {
- var next = null;
- var task = firstDelayedTask;
- do {
- if (startTime < task.startTime) {
- // The new task times out before this one.
- next = task;
- break;
- }
- task = task.next;
- } while (task !== firstDelayedTask);
-
- if (next === null) {
- // No task with a later timeout was found, which means the new task has
- // the latest timeout in the list.
- next = firstDelayedTask;
- } else if (next === firstDelayedTask) {
- // The new task has the earliest expiration in the entire list.
- firstDelayedTask = newTask;
- }
-
- var previous = next.previous;
- previous.next = next.previous = newTask;
- newTask.next = next;
- newTask.previous = previous;
- }
-}
-
function unstable_pauseExecution() {
isSchedulerPaused = true;
}
@@ -504,34 +371,14 @@ function unstable_continueExecution() {
}
function unstable_getFirstCallbackNode() {
- return firstTask;
+ return peek(taskQueue);
}
function unstable_cancelCallback(task) {
- var next = task.next;
- if (next === null) {
- // Already cancelled.
- return;
- }
-
- if (task === next) {
- if (task === firstTask) {
- firstTask = null;
- } else if (task === firstDelayedTask) {
- firstDelayedTask = null;
- }
- } else {
- if (task === firstTask) {
- firstTask = next;
- } else if (task === firstDelayedTask) {
- firstDelayedTask = next;
- }
- var previous = task.previous;
- previous.next = next;
- next.previous = previous;
- }
-
- task.next = task.previous = null;
+ // Null out the callback to indicate the task has been canceled. (Can't remove
+ // from the queue because you can't remove arbitrary nodes from an array based
+ // heap, only the first one.)
+ task.callback = null;
}
function unstable_getCurrentPriorityLevel() {
@@ -541,9 +388,12 @@ function unstable_getCurrentPriorityLevel() {
function unstable_shouldYield() {
const currentTime = getCurrentTime();
advanceTimers(currentTime);
+ const firstTask = peek(taskQueue);
return (
- (currentTask !== null &&
+ (firstTask !== currentTask &&
+ currentTask !== null &&
firstTask !== null &&
+ firstTask.callback !== null &&
firstTask.startTime <= currentTime &&
firstTask.expirationTime < currentTask.expirationTime) ||
shouldYieldToHost()
diff --git a/packages/scheduler/src/SchedulerMinHeap.js b/packages/scheduler/src/SchedulerMinHeap.js
new file mode 100644
index 0000000000000..e74d5d6853b14
--- /dev/null
+++ b/packages/scheduler/src/SchedulerMinHeap.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+type Heap = Array;
+type Node = {
+ id: number,
+ sortIndex: number,
+};
+
+export function push(heap: Heap, node: Node): void {
+ const index = heap.length;
+ heap.push(node);
+ siftUp(heap, node, index);
+}
+
+export function peek(heap: Heap): Node | null {
+ const first = heap[0];
+ return first === undefined ? null : first;
+}
+
+export function pop(heap: Heap): Node | null {
+ const first = heap[0];
+ if (first !== undefined) {
+ const last = heap.pop();
+ if (last !== first) {
+ heap[0] = last;
+ siftDown(heap, last, 0);
+ }
+ return first;
+ } else {
+ return null;
+ }
+}
+
+function siftUp(heap, node, index) {
+ while (true) {
+ const parentIndex = Math.floor((index - 1) / 2);
+ const parent = heap[parentIndex];
+ if (parent !== undefined && compare(parent, node) > 0) {
+ // The parent is larger. Swap positions.
+ heap[parentIndex] = node;
+ heap[index] = parent;
+ index = parentIndex;
+ } else {
+ // The parent is smaller. Exit.
+ return;
+ }
+ }
+}
+
+function siftDown(heap, node, index) {
+ const length = heap.length;
+ while (index < length) {
+ const leftIndex = (index + 1) * 2 - 1;
+ const left = heap[leftIndex];
+ const rightIndex = leftIndex + 1;
+ const right = heap[rightIndex];
+
+ // If the left or right node is smaller, swap with the smaller of those.
+ if (left !== undefined && compare(left, node) < 0) {
+ if (right !== undefined && compare(right, left) < 0) {
+ heap[index] = right;
+ heap[rightIndex] = node;
+ index = rightIndex;
+ } else {
+ heap[index] = left;
+ heap[leftIndex] = node;
+ index = leftIndex;
+ }
+ } else if (right !== undefined && compare(right, node) < 0) {
+ heap[index] = right;
+ heap[rightIndex] = node;
+ index = rightIndex;
+ } else {
+ // Neither child is smaller. Exit.
+ return;
+ }
+ }
+}
+
+function compare(a, b) {
+ // Compare sort index first, then task id.
+ const diff = a.sortIndex - b.sortIndex;
+ return diff !== 0 ? diff : a.id - b.id;
+}