@samitouri / QOS-React-2 / commits / a7dabcb60a

Revert "Re-arrange slightly to prevent refactor hazard (#16743)" (#16769)

This reverts commit ab4951fc03750a726e412b90126e0737fbd04014. * Track "pending" and "suspended" ranges A FiberRoot can have pending work at many distinct priorities. (Note: we refer to these levels as "expiration times" to distinguish the concept from Scheduler's notion of priority levels, which represent broad categories of work. React expiration times are more granualar. They're more like a concurrent thread ID, which also happens to correspond to a moment on a timeline. It's an overloaded concept and I'm handwaving over some of the details.) Given a root, there's no convenient way to read all the pending levels in the entire tree, i.e. there's no single queue-like structure that tracks all the levels, because that granularity of information is not needed by our algorithms. Instead we track the subset of information that we actually need — most importantly, the highest priority level that exists in the entire tree. Aside from that, the other information we track includes the range of pending levels that are known to be suspended, and therefore should not be worked on. This is a refactor of how that information is tracked, and what each field represents: - A *pending* level is work that is unfinished, or not yet committed. This includes work that is suspended from committing. `firstPendingTime` and `lastPendingTime` represent the range of pending work. (Previously, "pending" was the same as "not suspended.") - A *suspended* level is work that did not complete because data was missing. `firstSuspendedTime` and `lastSuspendedTime` represent the range of suspended work. It is a subset of the pending range. (These fields are new to this commit.) - `nextAfterSuspendedTime` represents the next known level that comes after the suspended range. This commit doesn't change much in terms of observable behavior. The one change is that, when a level is suspended, React will continue working on the next known level instead of jumping straight to the last pending level. Subsequent commits will use this new structure for a more substantial refactor for how tasks are scheduled per root. * Get next expiration time from FiberRoot Given a FiberRoot, we should be able to determine the next expiration time that needs to be worked on, taking into account the levels that are pending, suspended, pinged, and so on. This removes the `expirationTime` argument from `scheduleCallbackForRoot`, and renames it to `ensureRootIsScheduled` to reflect the new signature. The expiration time is instead read from the root using a new function, `getNextExpirationTimeToWorkOn`. The next step will be to remove the `expirationTime` argument from `renderRoot`, too. * Don't bind expiration time to render callback This is a fragile pattern because there's only meant to be a single task per root, running at a single expiration time. Instead of binding the expiration time to the render task, or closing over it, we should determine the correct expiration time to work on using fields we store on the root object itself. This removes the "return a continuation" pattern from the `renderRoot` function. Continuation handling is now handled by the wrapper function, which I've renamed from `runRootCallback` to `performWorkOnRoot`. That function is merely an entry point to `renderRoot`, so I've also removed the callback argument. So to sum up, at at the beginning of each task, `performWorkOnRoot` determines which expiration time to work on, then calls `renderRoot`. And before exiting, it checks if it needs to schedule another task. * Update error recovery test to match new semantics * Remove `lastPendingTime` field It's no longer used anywhere * Restart on update to already suspended root If the work-in-progress root already suspended with a delay, then the current render definitely won't finish. We should interrupt the render and switch to the incoming update. * Restart on suspend if return path has an update Similar to the previous commit, if we suspend with a delay, and something in the return path has a pending update, we should abort the current render and switch to the update instead. * Track the next unprocessed level globally Instead of backtracking the return path. The main advantage over the backtracking approach is that we don't have to backtrack from the source fiber. (The main disadvantages are that it requires another module-level variable, and that it could include updates from unrelated sibling paths.) * Re-arrange slightly to prevent refactor hazard It should not be possible to perform any work on a root without calling `ensureRootIsScheduled` before exiting. Otherwise, we could fail to schedule a callback for pending work and the app could freeze. To help prevent a future refactor from introducing such a bug, this change makes it so that `renderRoot` is always wrapped in try-finally, and the `finally` block calls `ensureRootIsScheduled`. * Remove recursive calls to `renderRoot`. There are a few leftover cases where `renderRoot` is called recursively. All of them are related to synchronously flushing work before its expiration time. We can remove these calls by tracking the last expired level on the root, similar to what we do for other types of pending work, like pings. * Remove argument from performSyncWorkOnRoot Read the expiration time from the root, like we do in performConcurrentWorkOnRoot.

Andrew Clark committed Sep 12, 2019 at 14:21 UTC a7dabcb60afa1ec4de29501f0b298565af47f9e3
10 files changed +282 -900
packages/react-reconciler/src/ReactFiberBeginWork.js
-6
@@ -177,7 +177,6 @@ import {
177 retryDehydratedSuspenseBoundary,
178 scheduleWork,
179 renderDidSuspendDelayIfPossible,
180 - markUnprocessedUpdateTime,
180 } from './ReactFiberWorkLoop';
181
182 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -2710,11 +2709,6 @@ function bailoutOnAlreadyFinishedWork(
2709 stopProfilerTimerIfRunning(workInProgress);
2710 }
2711
2713 - const updateExpirationTime = workInProgress.expirationTime;
2714 - if (updateExpirationTime !== NoWork) {
2715 - markUnprocessedUpdateTime(updateExpirationTime);
2716 - }
2717 -
2712 // Check if the children have any pending work.
2713 const childExpirationTime = workInProgress.childExpirationTime;
2714 if (childExpirationTime < renderExpirationTime) {
packages/react-reconciler/src/ReactFiberExpirationTime.js
-3
@@ -21,10 +21,7 @@ import {
21 export type ExpirationTime = number;
22
23 export const NoWork = 0;
24 -// TODO: Think of a better name for Never.
24 export const Never = 1;
26 -// TODO: Use the Idle expiration time for idle state updates
27 -export const Idle = 2;
25 export const Sync = MAX_SIGNED_31_BIT_INT;
26 export const Batched = Sync - 1;
27
packages/react-reconciler/src/ReactFiberHooks.js
-3
@@ -43,7 +43,6 @@ import {
43 warnIfNotCurrentlyActingUpdatesInDev,
44 warnIfNotScopedWithMatchingAct,
45 markRenderEventTimeAndConfig,
46 - markUnprocessedUpdateTime,
46 } from './ReactFiberWorkLoop';
47
48 import invariant from 'shared/invariant';
@@ -532,7 +531,6 @@ export function resetHooks(): void {
531 // This is used to reset the state of this module when a component throws.
532 // It's also called inside mountIndeterminateComponent if we determine the
533 // component is a module-style component.
535 -
534 renderExpirationTime = NoWork;
535 currentlyRenderingFiber = null;
536
@@ -757,7 +755,6 @@ function updateReducer<S, I, A>(
755 // Update the remaining priority in the queue.
756 if (updateExpirationTime > remainingExpirationTime) {
757 remainingExpirationTime = updateExpirationTime;
760 - markUnprocessedUpdateTime(remainingExpirationTime);
758 }
759 } else {
760 // This update does have sufficient priority.
packages/react-reconciler/src/ReactFiberRoot.js
+7 -125
@@ -14,7 +14,6 @@ import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
14 import type {Thenable} from './ReactFiberWorkLoop';
15 import type {Interaction} from 'scheduler/src/Tracing';
16 import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
17 -import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
17
18 import {noTimeout} from './ReactFiberHostConfig';
19 import {createHostRootFiber} from './ReactFiber';
@@ -24,7 +23,6 @@ import {
23 enableSuspenseCallback,
24 } from 'shared/ReactFeatureFlags';
25 import {unstable_getThreadID} from 'scheduler/tracing';
27 -import {NoPriority} from './SchedulerWithReactIntegration';
26
27 // TODO: This should be lifted into the renderer.
28 export type Batch = {
@@ -71,20 +69,12 @@ type BaseFiberRootProperties = {|
69 callbackNode: *,
70 // Expiration of the callback associated with this root
71 callbackExpirationTime: ExpirationTime,
74 - // Priority of the callback associated with this root
75 - callbackPriority: ReactPriorityLevel,
72 // The earliest pending expiration time that exists in the tree
73 firstPendingTime: ExpirationTime,
78 - // The earliest suspended expiration time that exists in the tree
79 - firstSuspendedTime: ExpirationTime,
80 - // The latest suspended expiration time that exists in the tree
81 - lastSuspendedTime: ExpirationTime,
82 - // The next known expiration time after the suspended range
83 - nextKnownPendingLevel: ExpirationTime,
84 - // The latest time at which a suspended component pinged the root to
85 - // render again
86 - lastPingedTime: ExpirationTime,
87 - lastExpiredTime: ExpirationTime,
74 + // The latest pending expiration time that exists in the tree
75 + lastPendingTime: ExpirationTime,
76 + // The time at which a suspended component pinged the root to render again
77 + pingTime: ExpirationTime,
78 |};
79
80 // The following attributes are only used by interaction tracing builds.
@@ -127,13 +117,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
117 this.hydrate = hydrate;
118 this.firstBatch = null;
119 this.callbackNode = null;
130 - this.callbackPriority = NoPriority;
120 + this.callbackExpirationTime = NoWork;
121 this.firstPendingTime = NoWork;
132 - this.firstSuspendedTime = NoWork;
133 - this.lastSuspendedTime = NoWork;
134 - this.nextKnownPendingLevel = NoWork;
135 - this.lastPingedTime = NoWork;
136 - this.lastExpiredTime = NoWork;
122 + this.lastPendingTime = NoWork;
123 + this.pingTime = NoWork;
124
125 if (enableSchedulerTracing) {
126 this.interactionThreadID = unstable_getThreadID();
@@ -164,108 +151,3 @@ export function createFiberRoot(
151
152 return root;
153 }
167 -
168 -export function isRootSuspendedAtTime(
169 - root: FiberRoot,
170 - expirationTime: ExpirationTime,
171 -): boolean {
172 - const firstSuspendedTime = root.firstSuspendedTime;
173 - const lastSuspendedTime = root.lastSuspendedTime;
174 - return (
175 - firstSuspendedTime !== NoWork &&
176 - (firstSuspendedTime >= expirationTime &&
177 - lastSuspendedTime <= expirationTime)
178 - );
179 -}
180 -
181 -export function markRootSuspendedAtTime(
182 - root: FiberRoot,
183 - expirationTime: ExpirationTime,
184 -): void {
185 - const firstSuspendedTime = root.firstSuspendedTime;
186 - const lastSuspendedTime = root.lastSuspendedTime;
187 - if (firstSuspendedTime < expirationTime) {
188 - root.firstSuspendedTime = expirationTime;
189 - }
190 - if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) {
191 - root.lastSuspendedTime = expirationTime;
192 - }
193 -
194 - if (expirationTime <= root.lastPingedTime) {
195 - root.lastPingedTime = NoWork;
196 - }
197 -
198 - if (expirationTime <= root.lastExpiredTime) {
199 - root.lastExpiredTime = NoWork;
200 - }
201 -}
202 -
203 -export function markRootUpdatedAtTime(
204 - root: FiberRoot,
205 - expirationTime: ExpirationTime,
206 -): void {
207 - // Update the range of pending times
208 - const firstPendingTime = root.firstPendingTime;
209 - if (expirationTime > firstPendingTime) {
210 - root.firstPendingTime = expirationTime;
211 - }
212 -
213 - // Update the range of suspended times. Treat everything lower priority or
214 - // equal to this update as unsuspended.
215 - const firstSuspendedTime = root.firstSuspendedTime;
216 - if (firstSuspendedTime !== NoWork) {
217 - if (expirationTime >= firstSuspendedTime) {
218 - // The entire suspended range is now unsuspended.
219 - root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
220 - } else if (expirationTime >= root.lastSuspendedTime) {
221 - root.lastSuspendedTime = expirationTime + 1;
222 - }
223 -
224 - // This is a pending level. Check if it's higher priority than the next
225 - // known pending level.
226 - if (expirationTime > root.nextKnownPendingLevel) {
227 - root.nextKnownPendingLevel = expirationTime;
228 - }
229 - }
230 -}
231 -
232 -export function markRootFinishedAtTime(
233 - root: FiberRoot,
234 - finishedExpirationTime: ExpirationTime,
235 - remainingExpirationTime: ExpirationTime,
236 -): void {
237 - // Update the range of pending times
238 - root.firstPendingTime = remainingExpirationTime;
239 -
240 - // Update the range of suspended times. Treat everything higher priority or
241 - // equal to this update as unsuspended.
242 - if (finishedExpirationTime <= root.lastSuspendedTime) {
243 - // The entire suspended range is now unsuspended.
244 - root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
245 - } else if (finishedExpirationTime <= root.firstSuspendedTime) {
246 - // Part of the suspended range is now unsuspended. Narrow the range to
247 - // include everything between the unsuspended time (non-inclusive) and the
248 - // last suspended time.
249 - root.firstSuspendedTime = finishedExpirationTime - 1;
250 - }
251 -
252 - if (finishedExpirationTime <= root.lastPingedTime) {
253 - // Clear the pinged time
254 - root.lastPingedTime = NoWork;
255 - }
256 -
257 - if (finishedExpirationTime <= root.lastExpiredTime) {
258 - // Clear the expired time
259 - root.lastExpiredTime = NoWork;
260 - }
261 -}
262 -
263 -export function markRootExpiredAtTime(
264 - root: FiberRoot,
265 - expirationTime: ExpirationTime,
266 -): void {
267 - const lastExpiredTime = root.lastExpiredTime;
268 - if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) {
269 - root.lastExpiredTime = expirationTime;
270 - }
271 -}
packages/react-reconciler/src/ReactFiberWorkLoop.js
+227 -344
@@ -10,7 +10,10 @@
10 import type {Fiber} from './ReactFiber';
11 import type {FiberRoot} from './ReactFiberRoot';
12 import type {ExpirationTime} from './ReactFiberExpirationTime';
13 -import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
13 +import type {
14 + ReactPriorityLevel,
15 + SchedulerCallback,
16 +} from './SchedulerWithReactIntegration';
17 import type {Interaction} from 'scheduler/src/Tracing';
18 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
19 import type {SuspenseState} from './ReactFiberSuspenseComponent';
@@ -63,13 +66,6 @@ import {
66 } from './ReactFiberHostConfig';
67
68 import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
66 -import {
67 - isRootSuspendedAtTime,
68 - markRootSuspendedAtTime,
69 - markRootFinishedAtTime,
70 - markRootUpdatedAtTime,
71 - markRootExpiredAtTime,
72 -} from './ReactFiberRoot';
69 import {
70 NoMode,
71 StrictMode,
@@ -116,7 +112,6 @@ import {
112 inferPriorityFromExpirationTime,
113 LOW_PRIORITY_EXPIRATION,
114 Batched,
119 - Idle,
115 } from './ReactFiberExpirationTime';
116 import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
117 import {completeWork} from './ReactFiberCompleteWork';
@@ -228,10 +223,6 @@ let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
223 let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
224 let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
225 let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
231 -// The work left over by components that were visited during this render. Only
232 -// includes unprocessed updates, not work in bailed out children.
233 -let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
234 -
226 // If we're pinged while rendering we don't always restart immediately.
227 // This flag determines if it might be worthwhile to restart if an opportunity
228 // happens latere.
@@ -387,6 +378,8 @@ export function scheduleUpdateOnFiber(
378 return;
379 }
380
381 + root.pingTime = NoWork;
382 +
383 checkForInterruption(fiber, expirationTime);
384 recordScheduleUpdate();
385
@@ -407,10 +400,12 @@ export function scheduleUpdateOnFiber(
400 // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
401 // root inside of batchedUpdates should be synchronous, but layout updates
402 // should be deferred until the end of the batch.
410 - performSyncWorkOnRoot(root);
403 + let callback = renderRoot(root, Sync, true);
404 + while (callback !== null) {
405 + callback = callback(true);
406 + }
407 } else {
412 - ensureRootIsScheduled(root);
413 - schedulePendingInteractions(root, expirationTime);
408 + scheduleCallbackForRoot(root, ImmediatePriority, Sync);
409 if (executionContext === NoContext) {
410 // Flush the synchronous work now, unless we're already working or inside
411 // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
@@ -421,8 +416,7 @@ export function scheduleUpdateOnFiber(
416 }
417 }
418 } else {
424 - ensureRootIsScheduled(root);
425 - schedulePendingInteractions(root, expirationTime);
419 + scheduleCallbackForRoot(root, priorityLevel, expirationTime);
420 }
421
422 if (
@@ -490,193 +484,97 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
484 }
485
486 if (root !== null) {
493 - if (workInProgressRoot === root) {
494 - // Received an update to a tree that's in the middle of rendering. Mark
495 - // that's unprocessed work on this root.
496 - markUnprocessedUpdateTime(expirationTime);
497 -
498 - if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
499 - // The root already suspended with a delay, which means this render
500 - // definitely won't finish. Since we have a new update, let's mark it as
501 - // suspended now, right before marking the incoming update. This has the
502 - // effect of interrupting the current render and switching to the update.
503 - // TODO: This happens to work when receiving an update during the render
504 - // phase, because of the trick inside computeExpirationForFiber to
505 - // subtract 1 from `renderExpirationTime` to move it into a
506 - // separate bucket. But we should probably model it with an exception,
507 - // using the same mechanism we use to force hydration of a subtree.
508 - // TODO: This does not account for low pri updates that were already
509 - // scheduled before the root started rendering. Need to track the next
510 - // pending expiration time (perhaps by backtracking the return path) and
511 - // then trigger a restart in the `renderDidSuspendDelayIfPossible` path.
512 - markRootSuspendedAtTime(root, renderExpirationTime);
513 - }
487 + // Update the first and last pending expiration times in this root
488 + const firstPendingTime = root.firstPendingTime;
489 + if (expirationTime > firstPendingTime) {
490 + root.firstPendingTime = expirationTime;
491 + }
492 + const lastPendingTime = root.lastPendingTime;
493 + if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
494 + root.lastPendingTime = expirationTime;
495 }
515 - // Mark that the root has a pending update.
516 - markRootUpdatedAtTime(root, expirationTime);
496 }
497
498 return root;
499 }
500
522 -function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
523 - // Determines the next expiration time that the root should render, taking
524 - // into account levels that may be suspended, or levels that may have
525 - // received a ping.
526 -
527 - const lastExpiredTime = root.lastExpiredTime;
528 - if (lastExpiredTime !== NoWork) {
529 - return lastExpiredTime;
530 - }
531 -
532 - // "Pending" refers to any update that hasn't committed yet, including if it
533 - // suspended. The "suspended" range is therefore a subset.
534 - const firstPendingTime = root.firstPendingTime;
535 - if (!isRootSuspendedAtTime(root, firstPendingTime)) {
536 - // The highest priority pending time is not suspended. Let's work on that.
537 - return firstPendingTime;
538 - }
539 -
540 - // If the first pending time is suspended, check if there's a lower priority
541 - // pending level that we know about. Or check if we received a ping. Work
542 - // on whichever is higher priority.
543 - const lastPingedTime = root.lastPingedTime;
544 - const nextKnownPendingLevel = root.nextKnownPendingLevel;
545 - return lastPingedTime > nextKnownPendingLevel
546 - ? lastPingedTime
547 - : nextKnownPendingLevel;
548 -}
549 -
550 -// Use this function to schedule a task for a root. There's only one task per
551 -// root; if a task was already scheduled, we'll check to make sure the
552 -// expiration time of the existing task is the same as the expiration time of
553 -// the next level that the root has work on. This function is called on every
554 -// update, and right before exiting a task.
555 -function ensureRootIsScheduled(root: FiberRoot) {
556 - const lastExpiredTime = root.lastExpiredTime;
557 - if (lastExpiredTime !== NoWork) {
558 - // Special case: Expired work should flush synchronously.
559 - root.callbackExpirationTime = Sync;
560 - root.callbackPriority = ImmediatePriority;
561 - root.callbackNode = scheduleSyncCallback(
562 - performSyncWorkOnRoot.bind(null, root),
563 - );
564 - return;
565 - }
566 -
567 - const expirationTime = getNextRootExpirationTimeToWorkOn(root);
568 - const existingCallbackNode = root.callbackNode;
569 - if (expirationTime === NoWork) {
570 - // There's nothing to work on.
501 +// Use this function, along with runRootCallback, to ensure that only a single
502 +// callback per root is scheduled. It's still possible to call renderRoot
503 +// directly, but scheduling via this function helps avoid excessive callbacks.
504 +// It works by storing the callback node and expiration time on the root. When a
505 +// new callback comes in, it compares the expiration time to determine if it
506 +// should cancel the previous one. It also relies on commitRoot scheduling a
507 +// callback to render the next level, because that means we don't need a
508 +// separate callback per expiration time.
509 +function scheduleCallbackForRoot(
510 + root: FiberRoot,
511 + priorityLevel: ReactPriorityLevel,
512 + expirationTime: ExpirationTime,
513 +) {
514 + const existingCallbackExpirationTime = root.callbackExpirationTime;
515 + if (existingCallbackExpirationTime < expirationTime) {
516 + // New callback has higher priority than the existing one.
517 + const existingCallbackNode = root.callbackNode;
518 if (existingCallbackNode !== null) {
572 - root.callbackNode = null;
573 - root.callbackExpirationTime = NoWork;
574 - root.callbackPriority = NoPriority;
519 + cancelCallback(existingCallbackNode);
520 }
576 - return;
577 - }
521 + root.callbackExpirationTime = expirationTime;
522
579 - // TODO: If this is an update, we already read the current time. Pass the
580 - // time as an argument.
581 - const currentTime = requestCurrentTime();
582 - const priorityLevel = inferPriorityFromExpirationTime(
583 - currentTime,
584 - expirationTime,
585 - );
523 + if (expirationTime === Sync) {
524 + // Sync React callbacks are scheduled on a special internal queue
525 + root.callbackNode = scheduleSyncCallback(
526 + runRootCallback.bind(
527 + null,
528 + root,
529 + renderRoot.bind(null, root, expirationTime),
530 + ),
531 + );
532 + } else {
533 + let options = null;
534 + if (
535 + !disableSchedulerTimeoutBasedOnReactExpirationTime &&
536 + expirationTime !== Never
537 + ) {
538 + let timeout = expirationTimeToMs(expirationTime) - now();
539 + options = {timeout};
540 + }
541
587 - // If there's an existing render task, confirm it has the correct priority and
588 - // expiration time. Otherwise, we'll cancel it and schedule a new one.
589 - if (existingCallbackNode !== null) {
590 - const existingCallbackPriority = root.callbackPriority;
591 - const existingCallbackExpirationTime = root.callbackExpirationTime;
592 - if (
593 - // Callback must have the exact same expiration time.
594 - existingCallbackExpirationTime === expirationTime &&
595 - // Callback must have greater or equal priority.
596 - existingCallbackPriority >= priorityLevel
597 - ) {
598 - // Existing callback is sufficient.
599 - return;
542 + root.callbackNode = scheduleCallback(
543 + priorityLevel,
544 + runRootCallback.bind(
545 + null,
546 + root,
547 + renderRoot.bind(null, root, expirationTime),
548 + ),
549 + options,
550 + );
551 }
601 - // Need to schedule a new task.
602 - // TODO: Instead of scheduling a new task, we should be able to change the
603 - // priority of the existing one.
604 - cancelCallback(existingCallbackNode);
605 - }
606 -
607 - root.callbackExpirationTime = expirationTime;
608 - root.callbackPriority = priorityLevel;
609 -
610 - let callbackNode;
611 - if (expirationTime === Sync) {
612 - // Sync React callbacks are scheduled on a special internal queue
613 - callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
614 - } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
615 - callbackNode = scheduleCallback(
616 - priorityLevel,
617 - performConcurrentWorkOnRoot.bind(null, root),
618 - );
619 - } else {
620 - callbackNode = scheduleCallback(
621 - priorityLevel,
622 - performConcurrentWorkOnRoot.bind(null, root),
623 - // Compute a task timeout based on the expiration time. This also affects
624 - // ordering because tasks are processed in timeout order.
625 - {timeout: expirationTimeToMs(expirationTime) - now()},
626 - );
552 }
553
629 - root.callbackNode = callbackNode;
630 -}
631 -
632 -// This is the entry point for every concurrent task, i.e. anything that
633 -// goes through Scheduler.
634 -function performConcurrentWorkOnRoot(root, didTimeout) {
635 - // Since we know we're in a React event, we can clear the current
636 - // event time. The next update will compute a new event time.
637 - currentEventTime = NoWork;
638 -
639 - if (didTimeout) {
640 - // An async update expired.
641 - const currentTime = requestCurrentTime();
642 - markRootExpiredAtTime(root, currentTime);
643 - }
644 -
645 - // Determine the next expiration time to work on, using the fields stored
646 - // on the root.
647 - const expirationTime = getNextRootExpirationTimeToWorkOn(root);
648 - if (expirationTime !== NoWork) {
649 - const originalCallbackNode = root.callbackNode;
650 - try {
651 - renderRoot(root, expirationTime, didTimeout);
652 - if (root.callbackNode === originalCallbackNode) {
653 - // The task node scheduled for this root is the same one that's
654 - // currently executed. Need to return a continuation.
655 - return performConcurrentWorkOnRoot.bind(null, root);
656 - }
657 - } finally {
658 - // Before exiting, make sure there's a callback scheduled for the
659 - // pending level.
660 - ensureRootIsScheduled(root);
661 - }
662 - }
663 - return null;
554 + // Associate the current interactions with this new root+priority.
555 + schedulePendingInteractions(root, expirationTime);
556 }
557
666 -// This is the entry point for synchronous tasks that don't go
667 -// through Scheduler
668 -function performSyncWorkOnRoot(root) {
669 - // Check if there's expired work on this root. Otherwise, render at Sync.
670 - const lastExpiredTime = root.lastExpiredTime;
671 - const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
558 +function runRootCallback(root, callback, isSync) {
559 + const prevCallbackNode = root.callbackNode;
560 + let continuation = null;
561 try {
673 - renderRoot(root, expirationTime, true);
562 + continuation = callback(isSync);
563 + if (continuation !== null) {
564 + return runRootCallback.bind(null, root, continuation);
565 + } else {
566 + return null;
567 + }
568 } finally {
675 - // Before exiting, make sure there's a callback scheduled for the
676 - // pending level.
677 - ensureRootIsScheduled(root);
569 + // If the callback exits without returning a continuation, remove the
570 + // corresponding callback node from the root. Unless the callback node
571 + // has changed, which implies that it was already cancelled by a high
572 + // priority update.
573 + if (continuation === null && prevCallbackNode === root.callbackNode) {
574 + root.callbackNode = null;
575 + root.callbackExpirationTime = NoWork;
576 + }
577 }
679 - return null;
578 }
579
580 export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -687,8 +585,7 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
585 'means you attempted to commit from inside a lifecycle method.',
586 );
587 }
690 - markRootExpiredAtTime(root, expirationTime);
691 - ensureRootIsScheduled(root);
588 + scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
589 flushSyncCallbackQueue();
590 }
591
@@ -757,8 +654,7 @@ function flushPendingDiscreteUpdates() {
654 const roots = rootsWithPendingDiscreteUpdates;
655 rootsWithPendingDiscreteUpdates = null;
656 roots.forEach((expirationTime, root) => {
760 - markRootExpiredAtTime(root, expirationTime);
761 - ensureRootIsScheduled(root);
657 + scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
658 });
659 // Now flush the immediate queue.
660 flushSyncCallbackQueue();
@@ -890,7 +786,6 @@ function prepareFreshStack(root, expirationTime) {
786 workInProgressRootLatestProcessedExpirationTime = Sync;
787 workInProgressRootLatestSuspenseTimeout = Sync;
788 workInProgressRootCanSuspendUsingConfig = null;
893 - workInProgressRootNextUnprocessedUpdateTime = NoWork;
789 workInProgressRootHasPendingPing = false;
790
791 if (enableSchedulerTracing) {
@@ -903,24 +798,28 @@ function prepareFreshStack(root, expirationTime) {
798 }
799 }
800
906 -// renderRoot should only be called from inside either
907 -// `performConcurrentWorkOnRoot` or `performSyncWorkOnRoot`.
801 function renderRoot(
802 root: FiberRoot,
803 expirationTime: ExpirationTime,
804 isSync: boolean,
912 -): void {
805 +): SchedulerCallback | null {
806 invariant(
807 (executionContext & (RenderContext | CommitContext)) === NoContext,
808 'Should not already be working.',
809 );
810
811 + if (root.firstPendingTime < expirationTime) {
812 + // If there's no work left at this expiration time, exit immediately. This
813 + // happens when multiple callbacks are scheduled for a single root, but an
814 + // earlier callback flushes the work of a later one.
815 + return null;
816 + }
817 +
818 if (isSync && root.finishedExpirationTime === expirationTime) {
819 // There's already a pending commit at this expiration time.
820 // TODO: This is poorly factored. This case only exists for the
821 // batch.commit() API.
922 - commitRoot(root);
923 - return;
822 + return commitRoot.bind(null, root);
823 }
824
825 flushPassiveEffects();
@@ -930,6 +829,26 @@ function renderRoot(
829 if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
830 prepareFreshStack(root, expirationTime);
831 startWorkOnPendingInteractions(root, expirationTime);
832 + } else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
833 + // We could've received an update at a lower priority while we yielded.
834 + // We're suspended in a delayed state. Once we complete this render we're
835 + // just going to try to recover at the last pending time anyway so we might
836 + // as well start doing that eagerly.
837 + // Ideally we should be able to do this even for retries but we don't yet
838 + // know if we're going to process an update which wants to commit earlier,
839 + // and this path happens very early so it would happen too often. Instead,
840 + // for that case, we'll wait until we complete.
841 + if (workInProgressRootHasPendingPing) {
842 + // We have a ping at this expiration. Let's restart to see if we get unblocked.
843 + prepareFreshStack(root, expirationTime);
844 + } else {
845 + const lastPendingTime = root.lastPendingTime;
846 + if (lastPendingTime < expirationTime) {
847 + // There's lower priority work. It might be unsuspended. Try rendering
848 + // at that level immediately, while preserving the position in the queue.
849 + return renderRoot.bind(null, root, lastPendingTime);
850 + }
851 + }
852 }
853
854 // If we have a work-in-progress fiber, it means there's still work to do
@@ -953,6 +872,32 @@ function renderRoot(
872
873 startWorkLoopTimer(workInProgress);
874
875 + // TODO: Fork renderRoot into renderRootSync and renderRootAsync
876 + if (isSync) {
877 + if (expirationTime !== Sync) {
878 + // An async update expired. There may be other expired updates on
879 + // this root. We should render all the expired work in a
880 + // single batch.
881 + const currentTime = requestCurrentTime();
882 + if (currentTime < expirationTime) {
883 + // Restart at the current time.
884 + executionContext = prevExecutionContext;
885 + resetContextDependencies();
886 + ReactCurrentDispatcher.current = prevDispatcher;
887 + if (enableSchedulerTracing) {
888 + __interactionsRef.current = ((prevInteractions: any): Set<
889 + Interaction,
890 + >);
891 + }
892 + return renderRoot.bind(null, root, currentTime);
893 + }
894 + }
895 + } else {
896 + // Since we know we're in a React event, we can clear the current
897 + // event time. The next update will compute a new event time.
898 + currentEventTime = NoWork;
899 + }
900 +
901 do {
902 try {
903 if (isSync) {
@@ -974,7 +919,6 @@ function renderRoot(
919 // boundary.
920 prepareFreshStack(root, expirationTime);
921 executionContext = prevExecutionContext;
977 - markRootSuspendedAtTime(root, expirationTime);
922 throw thrownValue;
923 }
924
@@ -993,8 +937,6 @@ function renderRoot(
937 thrownValue,
938 renderExpirationTime,
939 );
996 - // TODO: This is not wrapped in a try-catch, so if the complete phase
997 - // throws, we won't capture it.
940 workInProgress = completeUnitOfWork(sourceFiber);
941 }
942 } while (true);
@@ -1007,9 +949,9 @@ function renderRoot(
949 }
950
951 if (workInProgress !== null) {
1010 - // There's still work left over. Exit without committing.
952 + // There's still work left over. Return a continuation.
953 stopInterruptedWorkLoopTimer();
1012 - return;
954 + return renderRoot.bind(null, root, expirationTime);
955 }
956 }
957
@@ -1017,8 +959,7 @@ function renderRoot(
959 // something suspended, wait to commit it after a timeout.
960 stopFinishedWorkLoopTimer();
961
1020 - const finishedWork: Fiber = ((root.finishedWork =
1021 - root.current.alternate): any);
962 + root.finishedWork = root.current.alternate;
963 root.finishedExpirationTime = expirationTime;
964
965 const isLocked = resolveLocksOnRoot(root, expirationTime);
@@ -1026,8 +967,7 @@ function renderRoot(
967 // This root has a lock that prevents it from committing. Exit. If we begin
968 // work on the root again, without any intervening updates, it will finish
969 // without doing additional work.
1029 - markRootSuspendedAtTime(root, expirationTime);
1030 - return;
970 + return null;
971 }
972
973 // Set this to null to indicate there's no in-progress render.
@@ -1041,25 +981,28 @@ function renderRoot(
981 // but eslint doesn't know about invariant, so it complains if I do.
982 // eslint-disable-next-line no-fallthrough
983 case RootErrored: {
1044 - if (!isSync && expirationTime !== Idle) {
1045 - // If this was an async render, the error may have happened due to
1046 - // a mutation in a concurrent event. Try rendering one more time,
1047 - // synchronously, to see if the error goes away. If there are lower
1048 - // priority updates, let's include those, too, in case they fix the
1049 - // inconsistency. Render at Idle to include all updates.
1050 - markRootExpiredAtTime(root, Idle);
1051 - return;
984 + // An error was thrown. First check if there is lower priority work
985 + // scheduled on this root.
986 + const lastPendingTime = root.lastPendingTime;
987 + if (lastPendingTime < expirationTime) {
988 + // There's lower priority work. Before raising the error, try rendering
989 + // at the lower priority to see if it fixes it. Use a continuation to
990 + // maintain the existing priority and position in the queue.
991 + return renderRoot.bind(null, root, lastPendingTime);
992 }
1053 - // Commit the root in its errored state.
1054 - commitRoot(root);
1055 - return;
993 + if (!isSync) {
994 + // If we're rendering asynchronously, it's possible the error was
995 + // caused by tearing due to a mutation during an event. Try rendering
996 + // one more time without yiedling to events.
997 + prepareFreshStack(root, expirationTime);
998 + scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
999 + return null;
1000 + }
1001 + // If we're already rendering synchronously, commit the root in its
1002 + // errored state.
1003 + return commitRoot.bind(null, root);
1004 }
1005 case RootSuspended: {
1058 - markRootSuspendedAtTime(root, expirationTime);
1059 - const lastSuspendedTime = root.lastSuspendedTime;
1060 - if (expirationTime === lastSuspendedTime) {
1061 - root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1062 - }
1006 flushSuspensePriorityWarningInDEV();
1007
1008 // We have an acceptable loading state. We need to figure out if we should
@@ -1091,31 +1034,17 @@ function renderRoot(
1034 // Don't bother with a very short suspense time.
1035 if (msUntilTimeout > 10) {
1036 if (workInProgressRootHasPendingPing) {
1094 - const lastPingedTime = root.lastPingedTime;
1095 - if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
1096 - // This render was pinged but we didn't get to restart earlier so
1097 - // try restarting now instead.
1098 - root.lastPingedTime = expirationTime;
1099 - prepareFreshStack(root, expirationTime);
1100 - return;
1101 - }
1102 - }
1103 -
1104 - const nextTime = getNextRootExpirationTimeToWorkOn(root);
1105 - if (nextTime !== NoWork && nextTime !== expirationTime) {
1106 - // There's additional work on this root.
1107 - return;
1037 + // This render was pinged but we didn't get to restart earlier so try
1038 + // restarting now instead.
1039 + prepareFreshStack(root, expirationTime);
1040 + return renderRoot.bind(null, root, expirationTime);
1041 }
1109 - if (
1110 - lastSuspendedTime !== NoWork &&
1111 - lastSuspendedTime !== expirationTime
1112 - ) {
1113 - // We should prefer to render the fallback of at the last suspended
1114 - // level. Ping the last suspended level to try rendering it again.
1115 - root.lastPingedTime = lastSuspendedTime;
1116 - return;
1042 + const lastPendingTime = root.lastPendingTime;
1043 + if (lastPendingTime < expirationTime) {
1044 + // There's lower priority work. It might be unsuspended. Try rendering
1045 + // at that level.
1046 + return renderRoot.bind(null, root, lastPendingTime);
1047 }
1118 -
1048 // The render is suspended, it hasn't timed out, and there's no lower
1049 // priority work to do. Instead of committing the fallback
1050 // immediately, wait for more data to arrive.
@@ -1123,19 +1052,13 @@ function renderRoot(
1052 commitRoot.bind(null, root),
1053 msUntilTimeout,
1054 );
1126 - return;
1055 + return null;
1056 }
1057 }
1058 // The work expired. Commit immediately.
1130 - commitRoot(root);
1131 - return;
1059 + return commitRoot.bind(null, root);
1060 }
1061 case RootSuspendedWithDelay: {
1134 - markRootSuspendedAtTime(root, expirationTime);
1135 - const lastSuspendedTime = root.lastSuspendedTime;
1136 - if (expirationTime === lastSuspendedTime) {
1137 - root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1138 - }
1062 flushSuspensePriorityWarningInDEV();
1063
1064 if (
@@ -1150,29 +1073,16 @@ function renderRoot(
1073 // We're suspended in a state that should be avoided. We'll try to avoid committing
1074 // it for as long as the timeouts let us.
1075 if (workInProgressRootHasPendingPing) {
1153 - const lastPingedTime = root.lastPingedTime;
1154 - if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
1155 - // This render was pinged but we didn't get to restart earlier so
1156 - // try restarting now instead.
1157 - root.lastPingedTime = expirationTime;
1158 - prepareFreshStack(root, expirationTime);
1159 - return;
1160 - }
1161 - }
1162 -
1163 - const nextTime = getNextRootExpirationTimeToWorkOn(root);
1164 - if (nextTime !== NoWork && nextTime !== expirationTime) {
1165 - // There's additional work on this root.
1166 - return;
1076 + // This render was pinged but we didn't get to restart earlier so try
1077 + // restarting now instead.
1078 + prepareFreshStack(root, expirationTime);
1079 + return renderRoot.bind(null, root, expirationTime);
1080 }
1168 - if (
1169 - lastSuspendedTime !== NoWork &&
1170 - lastSuspendedTime !== expirationTime
1171 - ) {
1172 - // We should prefer to render the fallback of at the last suspended
1173 - // level. Ping the last suspended level to try rendering it again.
1174 - root.lastPingedTime = lastSuspendedTime;
1175 - return;
1081 + const lastPendingTime = root.lastPendingTime;
1082 + if (lastPendingTime < expirationTime) {
1083 + // There's lower priority work. It might be unsuspended. Try rendering
1084 + // at that level immediately.
1085 + return renderRoot.bind(null, root, lastPendingTime);
1086 }
1087
1088 let msUntilTimeout;
@@ -1220,12 +1130,11 @@ function renderRoot(
1130 commitRoot.bind(null, root),
1131 msUntilTimeout,
1132 );
1223 - return;
1133 + return null;
1134 }
1135 }
1136 // The work expired. Commit immediately.
1227 - commitRoot(root);
1228 - return;
1137 + return commitRoot.bind(null, root);
1138 }
1139 case RootCompleted: {
1140 // The work completed. Ready to commit.
@@ -1249,16 +1158,14 @@ function renderRoot(
1158 workInProgressRootCanSuspendUsingConfig,
1159 );
1160 if (msUntilTimeout > 10) {
1252 - markRootSuspendedAtTime(root, expirationTime);
1161 root.timeoutHandle = scheduleTimeout(
1162 commitRoot.bind(null, root),
1163 msUntilTimeout,
1164 );
1257 - return;
1165 + return null;
1166 }
1167 }
1260 - commitRoot(root);
1261 - return;
1168 + return commitRoot.bind(null, root);
1169 }
1170 default: {
1171 invariant(false, 'Unknown root exit status.');
@@ -1292,14 +1199,6 @@ export function markRenderEventTimeAndConfig(
1199 }
1200 }
1201
1295 -export function markUnprocessedUpdateTime(
1296 - expirationTime: ExpirationTime,
1297 -): void {
1298 - if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) {
1299 - workInProgressRootNextUnprocessedUpdateTime = expirationTime;
1300 - }
1301 -}
1302 -
1202 export function renderDidSuspend(): void {
1203 if (workInProgressRootExitStatus === RootIncomplete) {
1204 workInProgressRootExitStatus = RootSuspended;
@@ -1313,22 +1212,6 @@ export function renderDidSuspendDelayIfPossible(): void {
1212 ) {
1213 workInProgressRootExitStatus = RootSuspendedWithDelay;
1214 }
1316 -
1317 - // Check if there's a lower priority update somewhere else in the tree.
1318 - if (
1319 - workInProgressRootNextUnprocessedUpdateTime !== NoWork &&
1320 - workInProgressRoot !== null
1321 - ) {
1322 - // Mark the current render as suspended, and then mark that there's a
1323 - // pending update.
1324 - // TODO: This should immediately interrupt the current render, instead
1325 - // of waiting until the next time we yield.
1326 - markRootSuspendedAtTime(workInProgressRoot, renderExpirationTime);
1327 - markRootUpdatedAtTime(
1328 - workInProgressRoot,
1329 - workInProgressRootNextUnprocessedUpdateTime,
1330 - );
1331 - }
1215 }
1216
1217 export function renderDidError() {
@@ -1543,14 +1426,6 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
1426 return null;
1427 }
1428
1546 -function getRemainingExpirationTime(fiber: Fiber) {
1547 - const updateExpirationTime = fiber.expirationTime;
1548 - const childExpirationTime = fiber.childExpirationTime;
1549 - return updateExpirationTime > childExpirationTime
1550 - ? updateExpirationTime
1551 - : childExpirationTime;
1552 -}
1553 -
1429 function resetChildExpirationTime(completedWork: Fiber) {
1430 if (
1431 renderExpirationTime !== Never &&
@@ -1653,21 +1528,23 @@ function commitRootImpl(root, renderPriorityLevel) {
1528 // So we can clear these now to allow a new callback to be scheduled.
1529 root.callbackNode = null;
1530 root.callbackExpirationTime = NoWork;
1656 - root.callbackPriority = NoPriority;
1657 - root.nextKnownPendingLevel = NoWork;
1531
1532 startCommitTimer();
1533
1534 // Update the first and last pending times on this root. The new first
1535 // pending time is whatever is left on the root fiber.
1663 - const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
1664 - finishedWork,
1665 - );
1666 - markRootFinishedAtTime(
1667 - root,
1668 - expirationTime,
1669 - remainingExpirationTimeBeforeCommit,
1670 - );
1536 + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
1537 + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
1538 + const firstPendingTimeBeforeCommit =
1539 + childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
1540 + ? childExpirationTimeBeforeCommit
1541 + : updateExpirationTimeBeforeCommit;
1542 + root.firstPendingTime = firstPendingTimeBeforeCommit;
1543 + if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
1544 + // This usually means we've finished all the work, but it can also happen
1545 + // when something gets downprioritized during render, like a hidden tree.
1546 + root.lastPendingTime = firstPendingTimeBeforeCommit;
1547 + }
1548
1549 if (root === workInProgressRoot) {
1550 // We can reset these now that they are finished.
@@ -1869,6 +1746,12 @@ function commitRootImpl(root, renderPriorityLevel) {
1746 // Check if there's remaining work on this root
1747 const remainingExpirationTime = root.firstPendingTime;
1748 if (remainingExpirationTime !== NoWork) {
1749 + const currentTime = requestCurrentTime();
1750 + const priorityLevel = inferPriorityFromExpirationTime(
1751 + currentTime,
1752 + remainingExpirationTime,
1753 + );
1754 +
1755 if (enableSchedulerTracing) {
1756 if (spawnedWorkDuringRender !== null) {
1757 const expirationTimes = spawnedWorkDuringRender;
@@ -1881,8 +1764,9 @@ function commitRootImpl(root, renderPriorityLevel) {
1764 );
1765 }
1766 }
1884 - schedulePendingInteractions(root, remainingExpirationTime);
1767 }
1768 +
1769 + scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
1770 } else {
1771 // If there's no remaining work, we can clear the set of already failed
1772 // error boundaries.
@@ -1899,6 +1783,8 @@ function commitRootImpl(root, renderPriorityLevel) {
1783 }
1784 }
1785
1786 + onCommitRoot(finishedWork.stateNode, expirationTime);
1787 +
1788 if (remainingExpirationTime === Sync) {
1789 // Count the number of times the root synchronously re-renders without
1790 // finishing. If there are too many, it indicates an infinite update loop.
@@ -1912,12 +1798,6 @@ function commitRootImpl(root, renderPriorityLevel) {
1798 nestedUpdateCount = 0;
1799 }
1800
1915 - onCommitRoot(finishedWork.stateNode, expirationTime);
1916 -
1917 - // Always call this before exiting `commitRoot`, to ensure that any
1918 - // additional work on this root is scheduled.
1919 - ensureRootIsScheduled(root);
1920 -
1801 if (hasUncaughtError) {
1802 hasUncaughtError = false;
1803 const error = firstUncaughtError;
@@ -2182,8 +2062,7 @@ function captureCommitPhaseErrorOnRoot(
2062 enqueueUpdate(rootFiber, update);
2063 const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
2064 if (root !== null) {
2185 - ensureRootIsScheduled(root);
2186 - schedulePendingInteractions(root, Sync);
2065 + scheduleCallbackForRoot(root, ImmediatePriority, Sync);
2066 }
2067 }
2068
@@ -2218,8 +2097,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2097 enqueueUpdate(fiber, update);
2098 const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
2099 if (root !== null) {
2221 - ensureRootIsScheduled(root);
2222 - schedulePendingInteractions(root, Sync);
2100 + scheduleCallbackForRoot(root, ImmediatePriority, Sync);
2101 }
2102 return;
2103 }
@@ -2271,19 +2149,20 @@ export function pingSuspendedRoot(
2149 return;
2150 }
2151
2274 - if (!isRootSuspendedAtTime(root, suspendedTime)) {
2152 + const lastPendingTime = root.lastPendingTime;
2153 + if (lastPendingTime < suspendedTime) {
2154 // The root is no longer suspended at this time.
2155 return;
2156 }
2157
2279 - const lastPingedTime = root.lastPingedTime;
2280 - if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2158 + const pingTime = root.pingTime;
2159 + if (pingTime !== NoWork && pingTime < suspendedTime) {
2160 // There's already a lower priority ping scheduled.
2161 return;
2162 }
2163
2164 // Mark the time at which this ping was scheduled.
2286 - root.lastPingedTime = suspendedTime;
2165 + root.pingTime = suspendedTime;
2166
2167 if (root.finishedExpirationTime === suspendedTime) {
2168 // If there's a pending fallback waiting to commit, throw it away.
@@ -2291,8 +2170,12 @@ export function pingSuspendedRoot(
2170 root.finishedWork = null;
2171 }
2172
2294 - ensureRootIsScheduled(root);
2295 - schedulePendingInteractions(root, suspendedTime);
2173 + const currentTime = requestCurrentTime();
2174 + const priorityLevel = inferPriorityFromExpirationTime(
2175 + currentTime,
2176 + suspendedTime,
2177 + );
2178 + scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
2179 }
2180
2181 function retryTimedOutBoundary(
@@ -2303,9 +2186,9 @@ function retryTimedOutBoundary(
2186 // previously was rendered in its fallback state. One of the promises that
2187 // suspended it has resolved, which means at least part of the tree was
2188 // likely unblocked. Try rendering again, at a new expiration time.
2189 + const currentTime = requestCurrentTime();
2190 if (retryTime === Never) {
2191 const suspenseConfig = null; // Retries don't carry over the already committed update.
2308 - const currentTime = requestCurrentTime();
2192 retryTime = computeExpirationForFiber(
2193 currentTime,
2194 boundaryFiber,
@@ -2313,10 +2196,10 @@ function retryTimedOutBoundary(
2196 );
2197 }
2198 // TODO: Special case idle priority?
2199 + const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
2200 const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
2201 if (root !== null) {
2318 - ensureRootIsScheduled(root);
2319 - schedulePendingInteractions(root, retryTime);
2202 + scheduleCallbackForRoot(root, priorityLevel, retryTime);
2203 }
2204 }
2205
packages/react-reconciler/src/ReactUpdateQueue.js
+1 -5
@@ -103,10 +103,7 @@ import {
103 } from 'shared/ReactFeatureFlags';
104
105 import {StrictMode} from './ReactTypeOfMode';
106 -import {
107 - markRenderEventTimeAndConfig,
108 - markUnprocessedUpdateTime,
109 -} from './ReactFiberWorkLoop';
106 +import {markRenderEventTimeAndConfig} from './ReactFiberWorkLoop';
107
108 import invariant from 'shared/invariant';
109 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -583,7 +580,6 @@ export function processUpdateQueue<State>(
580 // dealt with the props. Context in components that specify
581 // shouldComponentUpdate is tricky; but we'll have to account for
582 // that regardless.
586 - markUnprocessedUpdateTime(newExpirationTime);
583 workInProgress.expirationTime = newExpirationTime;
584 workInProgress.memoizedState = resultState;
585
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+45 -76
@@ -243,20 +243,11 @@ describe('ReactIncrementalErrorHandling', () => {
243 // This update is in a separate batch
244 ReactNoop.render(<App isBroken={false} />, onCommit);
245
246 - expect(Scheduler).toFlushAndYieldThrough([
246 + expect(Scheduler).toFlushAndYield([
247 // The first render fails. But because there's a lower priority pending
248 // update, it doesn't throw.
249 'error',
250 - ]);
251 -
252 - // React will try to recover by rendering all the pending updates in a
253 - // single batch, synchronously. This time it succeeds.
254 - //
255 - // This tells Scheduler to render a single unit of work. Because the render
256 - // to recover from the error is synchronous, this should be enough to
257 - // finish the rest of the work.
258 - Scheduler.unstable_flushNumberOfYields(1);
259 - expect(Scheduler).toHaveYielded([
250 + // Now we retry at the lower priority. This time it succeeds.
251 'success',
252 // Nothing commits until the second update completes.
253 'commit',
@@ -265,80 +256,54 @@ describe('ReactIncrementalErrorHandling', () => {
256 expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
257 });
258
268 - it('does not include offscreen work when retrying after an error', () => {
269 - function App(props) {
270 - if (props.isBroken) {
271 - Scheduler.unstable_yieldValue('error');
272 - throw new Error('Oops!');
259 + it('on error, retries at a lower priority using the expiration of higher priority', () => {
260 + class Parent extends React.Component {
261 + state = {hideChild: false};
262 + componentDidUpdate() {
263 + Scheduler.unstable_yieldValue('commit: ' + this.state.hideChild);
264 + }
265 + render() {
266 + if (this.state.hideChild) {
267 + Scheduler.unstable_yieldValue('(empty)');
268 + return <span prop="(empty)" />;
269 + }
270 + return <Child isBroken={this.props.childIsBroken} />;
271 }
274 - Scheduler.unstable_yieldValue('success');
275 - return (
276 - <>
277 - Everything is fine
278 - <div hidden={true}>
279 - <div>Offscreen content</div>
280 - </div>
281 - </>
282 - );
283 - }
284 -
285 - function onCommit() {
286 - Scheduler.unstable_yieldValue('commit');
272 }
273
289 - function interrupt() {
290 - ReactNoop.flushSync(() => {
291 - ReactNoop.renderToRootWithID(null, 'other-root');
292 - });
274 + function Child(props) {
275 + if (props.isBroken) {
276 + Scheduler.unstable_yieldValue('Error!');
277 + throw new Error('Error!');
278 + }
279 + Scheduler.unstable_yieldValue('Child');
280 + return <span prop="Child" />;
281 }
282
295 - ReactNoop.render(<App isBroken={true} />, onCommit);
296 - Scheduler.unstable_advanceTime(1000);
297 - expect(Scheduler).toFlushAndYieldThrough(['error']);
298 - interrupt();
283 + // Initial mount
284 + const parent = React.createRef(null);
285 + ReactNoop.render(<Parent ref={parent} childIsBroken={false} />);
286 + expect(Scheduler).toFlushAndYield(['Child']);
287 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
288
300 - expect(ReactNoop).toMatchRenderedOutput(null);
301 -
302 - // This update is in a separate batch
303 - ReactNoop.render(<App isBroken={false} />, onCommit);
304 -
305 - expect(Scheduler).toFlushAndYieldThrough([
306 - // The first render fails. But because there's a lower priority pending
307 - // update, it doesn't throw.
308 - 'error',
309 - ]);
289 + // Schedule a low priority update to hide the child
290 + parent.current.setState({hideChild: true});
291
311 - // React will try to recover by rendering all the pending updates in a
312 - // single batch, synchronously. This time it succeeds.
313 - //
314 - // This tells Scheduler to render a single unit of work. Because the render
315 - // to recover from the error is synchronous, this should be enough to
316 - // finish the rest of the work.
317 - Scheduler.unstable_flushNumberOfYields(1);
292 + // Before the low priority update is flushed, synchronously trigger an
293 + // error in the child.
294 + ReactNoop.flushSync(() => {
295 + ReactNoop.render(<Parent ref={parent} childIsBroken={true} />);
296 + });
297 expect(Scheduler).toHaveYielded([
319 - 'success',
320 - // Nothing commits until the second update completes.
321 - 'commit',
322 - 'commit',
298 + // First the sync update triggers an error
299 + 'Error!',
300 + // Because there's a pending low priority update, we restart at the
301 + // lower priority. This hides the children, suppressing the error.
302 + '(empty)',
303 + // Now the tree can commit.
304 + 'commit: true',
305 ]);
324 - // This should not include the offscreen content
325 - expect(ReactNoop).toMatchRenderedOutput(
326 - <>
327 - Everything is fine
328 - <div hidden={true} />
329 - </>,
330 - );
331 -
332 - // The offscreen content finishes in a subsequent render
333 - expect(Scheduler).toFlushAndYield([]);
334 - expect(ReactNoop).toMatchRenderedOutput(
335 - <>
336 - Everything is fine
337 - <div hidden={true}>
338 - <div>Offscreen content</div>
339 - </div>
340 - </>,
341 - );
306 + expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
307 });
308
309 it('retries one more time before handling error', () => {
@@ -381,6 +346,10 @@ describe('ReactIncrementalErrorHandling', () => {
346 expect(ReactNoop.getChildren()).toEqual([]);
347 });
348
349 + // TODO: This is currently unobservable, but will be once we lift renderRoot
350 + // and commitRoot into the renderer.
351 + // it("does not retry synchronously if there's an update between complete and commit");
352 +
353 it('calls componentDidCatch multiple times for multiple errors', () => {
354 let id = 0;
355 class BadMount extends React.Component {
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
-268
@@ -263,274 +263,6 @@ describe('ReactSuspense', () => {
263 expect(root).toMatchRenderedOutput('AsyncAfter SuspenseSibling');
264 });
265
266 - it(
267 - 'interrupts current render if something already suspended with a ' +
268 - "delay, and then subsequently there's a lower priority update",
269 - () => {
270 - const root = ReactTestRenderer.create(
271 - <>
272 - <Suspense fallback={<Text text="Loading..." />} />
273 - <Text text="Initial" />
274 - </>,
275 - {
276 - unstable_isConcurrent: true,
277 - },
278 - );
279 - expect(Scheduler).toFlushAndYield(['Initial']);
280 - expect(root).toMatchRenderedOutput('Initial');
281 -
282 - // The update will suspend.
283 - root.update(
284 - <>
285 - <Suspense fallback={<Text text="Loading..." />}>
286 - <AsyncText text="Async" ms={2000} />
287 - </Suspense>
288 - <Text text="After Suspense" />
289 - <Text text="Sibling" />
290 - </>,
291 - );
292 -
293 - // Yield past the Suspense boundary but don't complete the last sibling.
294 - expect(Scheduler).toFlushAndYieldThrough([
295 - 'Suspend! [Async]',
296 - 'Loading...',
297 - 'After Suspense',
298 - ]);
299 -
300 - // Receives a lower priority update before the current render phase
301 - // has completed.
302 - Scheduler.unstable_advanceTime(1000);
303 - root.update(
304 - <>
305 - <Suspense fallback={<Text text="Loading..." />} />
306 - <Text text="Updated" />
307 - </>,
308 - );
309 - expect(Scheduler).toHaveYielded([]);
310 - expect(root).toMatchRenderedOutput('Initial');
311 -
312 - // Render the update, instead of continuing
313 - expect(Scheduler).toFlushAndYield(['Updated']);
314 - expect(root).toMatchRenderedOutput('Updated');
315 - },
316 - );
317 -
318 - it(
319 - 'interrupts current render when something suspends with a ' +
320 - "delay and we've already skipped over a lower priority update in " +
321 - 'a parent',
322 - () => {
323 - function interrupt() {
324 - // React has a heuristic to batch all updates that occur within the same
325 - // event. This is a trick to circumvent that heuristic.
326 - ReactTestRenderer.create('whatever');
327 - }
328 -
329 - function App({shouldSuspend, step}) {
330 - return (
331 - <>
332 - <Text text={`A${step}`} />
333 - <Suspense fallback={<Text text="Loading..." />}>
334 - {shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
335 - </Suspense>
336 - <Text text={`B${step}`} />
337 - <Text text={`C${step}`} />
338 - </>
339 - );
340 - }
341 -
342 - const root = ReactTestRenderer.create(null, {
343 - unstable_isConcurrent: true,
344 - });
345 -
346 - root.update(<App shouldSuspend={false} step={0} />);
347 - expect(Scheduler).toFlushAndYield(['A0', 'B0', 'C0']);
348 - expect(root).toMatchRenderedOutput('A0B0C0');
349 -
350 - // This update will suspend.
351 - root.update(<App shouldSuspend={true} step={1} />);
352 -
353 - // Need to move into the next async bucket.
354 - Scheduler.unstable_advanceTime(1000);
355 - // Do a bit of work, then interrupt to trigger a restart.
356 - expect(Scheduler).toFlushAndYieldThrough(['A1']);
357 - interrupt();
358 -
359 - // Schedule another update. This will have lower priority because of
360 - // the interrupt trick above.
361 - root.update(<App shouldSuspend={false} step={2} />);
362 -
363 - expect(Scheduler).toFlushAndYieldThrough([
364 - // Should have restarted the first update, because of the interruption
365 - 'A1',
366 - 'Suspend! [Async]',
367 - 'Loading...',
368 - 'B1',
369 - ]);
370 -
371 - // Should not have committed loading state
372 - expect(root).toMatchRenderedOutput('A0B0C0');
373 -
374 - // After suspending, should abort the first update and switch to the
375 - // second update. So, C1 should not appear in the log.
376 - // TODO: This should work even if React does not yield to the main
377 - // thread. Should use same mechanism as selective hydration to interrupt
378 - // the render before the end of the current slice of work.
379 - expect(Scheduler).toFlushAndYield(['A2', 'B2', 'C2']);
380 -
381 - expect(root).toMatchRenderedOutput('A2B2C2');
382 - },
383 - );
384 -
385 - it(
386 - 'interrupts current render when something suspends with a ' +
387 - "delay and we've already bailed out lower priority update in " +
388 - 'a parent',
389 - async () => {
390 - // This is similar to the previous test case, except this covers when
391 - // React completely bails out on the parent component, without processing
392 - // the update queue.
393 -
394 - const {useState} = React;
395 -
396 - function interrupt() {
397 - // React has a heuristic to batch all updates that occur within the same
398 - // event. This is a trick to circumvent that heuristic.
399 - ReactTestRenderer.create('whatever');
400 - }
401 -
402 - let setShouldSuspend;
403 - function Async() {
404 - const [shouldSuspend, _setShouldSuspend] = useState(false);
405 - setShouldSuspend = _setShouldSuspend;
406 - return (
407 - <>
408 - <Text text="A" />
409 - <Suspense fallback={<Text text="Loading..." />}>
410 - {shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
411 - </Suspense>
412 - <Text text="B" />
413 - <Text text="C" />
414 - </>
415 - );
416 - }
417 -
418 - let setShouldHideInParent;
419 - function App() {
420 - const [shouldHideInParent, _setShouldHideInParent] = useState(false);
421 - setShouldHideInParent = _setShouldHideInParent;
422 - Scheduler.unstable_yieldValue(
423 - 'shouldHideInParent: ' + shouldHideInParent,
424 - );
425 - return shouldHideInParent ? <Text text="(empty)" /> : <Async />;
426 - }
427 -
428 - const root = ReactTestRenderer.create(null, {
429 - unstable_isConcurrent: true,
430 - });
431 -
432 - await ReactTestRenderer.act(async () => {
433 - root.update(<App />);
434 - expect(Scheduler).toFlushAndYield([
435 - 'shouldHideInParent: false',
436 - 'A',
437 - 'B',
438 - 'C',
439 - ]);
440 - expect(root).toMatchRenderedOutput('ABC');
441 -
442 - // This update will suspend.
443 - setShouldSuspend(true);
444 -
445 - // Need to move into the next async bucket.
446 - Scheduler.unstable_advanceTime(1000);
447 - // Do a bit of work, then interrupt to trigger a restart.
448 - expect(Scheduler).toFlushAndYieldThrough(['A']);
449 - interrupt();
450 - // Should not have committed loading state
451 - expect(root).toMatchRenderedOutput('ABC');
452 -
453 - // Schedule another update. This will have lower priority because of
454 - // the interrupt trick above.
455 - setShouldHideInParent(true);
456 -
457 - expect(Scheduler).toFlushAndYieldThrough([
458 - // Should have restarted the first update, because of the interruption
459 - 'A',
460 - 'Suspend! [Async]',
461 - 'Loading...',
462 - 'B',
463 - ]);
464 -
465 - // Should not have committed loading state
466 - expect(root).toMatchRenderedOutput('ABC');
467 -
468 - // After suspending, should abort the first update and switch to the
469 - // second update.
470 - expect(Scheduler).toFlushAndYield([
471 - 'shouldHideInParent: true',
472 - '(empty)',
473 - ]);
474 -
475 - expect(root).toMatchRenderedOutput('(empty)');
476 - });
477 - },
478 - );
479 -
480 - it(
481 - 'interrupts current render when something suspends with a ' +
482 - 'delay, and a parent received an update after it completed',
483 - () => {
484 - function App({shouldSuspend, step}) {
485 - return (
486 - <>
487 - <Text text={`A${step}`} />
488 - <Suspense fallback={<Text text="Loading..." />}>
489 - {shouldSuspend ? <AsyncText text="Async" ms={2000} /> : null}
490 - </Suspense>
491 - <Text text={`B${step}`} />
492 - <Text text={`C${step}`} />
493 - </>
494 - );
495 - }
496 -
497 - const root = ReactTestRenderer.create(null, {
498 - unstable_isConcurrent: true,
499 - });
500 -
501 - root.update(<App shouldSuspend={false} step={0} />);
502 - expect(Scheduler).toFlushAndYield(['A0', 'B0', 'C0']);
503 - expect(root).toMatchRenderedOutput('A0B0C0');
504 -
505 - // This update will suspend.
506 - root.update(<App shouldSuspend={true} step={1} />);
507 - // Flush past the root, but stop before the async component.
508 - expect(Scheduler).toFlushAndYieldThrough(['A1']);
509 -
510 - // Schedule an update on the root, which already completed.
511 - root.update(<App shouldSuspend={false} step={2} />);
512 - // We'll keep working on the existing update.
513 - expect(Scheduler).toFlushAndYieldThrough([
514 - // Now the async component suspends
515 - 'Suspend! [Async]',
516 - 'Loading...',
517 - 'B1',
518 - ]);
519 -
520 - // Should not have committed loading state
521 - expect(root).toMatchRenderedOutput('A0B0C0');
522 -
523 - // After suspending, should abort the first update and switch to the
524 - // second update. So, C1 should not appear in the log.
525 - // TODO: This should work even if React does not yield to the main
526 - // thread. Should use same mechanism as selective hydration to interrupt
527 - // the render before the end of the current slice of work.
528 - expect(Scheduler).toFlushAndYield(['A2', 'B2', 'C2']);
529 -
530 - expect(root).toMatchRenderedOutput('A2B2C2');
531 - },
532 - );
533 -
266 it('mounts a lazy class component in non-concurrent mode', async () => {
267 class Class extends React.Component {
268 componentDidMount() {
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+1 -66
@@ -494,7 +494,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
494 return <Text text="(empty)" />;
495 }
496 return (
497 - <Suspense fallback="Loading...">
497 + <Suspense>
498 <AsyncText ms={2000} text="Async" />
499 </Suspense>
500 );
@@ -518,71 +518,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
518 expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
519 });
520
521 - it('tries each subsequent level after suspending', async () => {
522 - const root = ReactNoop.createRoot();
523 -
524 - function App({step, shouldSuspend}) {
525 - return (
526 - <Suspense fallback="Loading...">
527 - <Text text="Sibling" />
528 - {shouldSuspend ? (
529 - <AsyncText ms={10000} text={'Step ' + step} />
530 - ) : (
531 - <Text text={'Step ' + step} />
532 - )}
533 - </Suspense>
534 - );
535 - }
536 -
537 - function interrupt() {
538 - // React has a heuristic to batch all updates that occur within the same
539 - // event. This is a trick to circumvent that heuristic.
540 - ReactNoop.flushSync(() => {
541 - ReactNoop.renderToRootWithID(null, 'other-root');
542 - });
543 - }
544 -
545 - // Mount the Suspense boundary without suspending, so that the subsequent
546 - // updates suspend with a delay.
547 - await ReactNoop.act(async () => {
548 - root.render(<App step={0} shouldSuspend={false} />);
549 - });
550 - await advanceTimers(1000);
551 - expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
552 -
553 - // Schedule an update at several distinct expiration times
554 - await ReactNoop.act(async () => {
555 - root.render(<App step={1} shouldSuspend={true} />);
556 - Scheduler.unstable_advanceTime(1000);
557 - expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
558 - interrupt();
559 -
560 - root.render(<App step={2} shouldSuspend={true} />);
561 - Scheduler.unstable_advanceTime(1000);
562 - expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
563 - interrupt();
564 -
565 - root.render(<App step={3} shouldSuspend={true} />);
566 - Scheduler.unstable_advanceTime(1000);
567 - expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
568 - interrupt();
569 -
570 - root.render(<App step={4} shouldSuspend={false} />);
571 - });
572 -
573 - // Should suspend at each distinct level
574 - expect(Scheduler).toHaveYielded([
575 - 'Sibling',
576 - 'Suspend! [Step 1]',
577 - 'Sibling',
578 - 'Suspend! [Step 2]',
579 - 'Sibling',
580 - 'Suspend! [Step 3]',
581 - 'Sibling',
582 - 'Step 4',
583 - ]);
584 - });
585 -
521 it('forces an expiration after an update times out', async () => {
522 ReactNoop.render(
523 <Fragment>
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+1 -4
@@ -422,10 +422,7 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
422 `;
423
424 exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
425 -"⚛ (React Tree Reconciliation: Completed Root)
426 - ⚛ Foo [mount]
427 -
428 -⚛ (Committing Changes)
425 +"⚛ (Committing Changes)
426 ⚛ (Committing Snapshot Effects: 0 Total)
427 ⚛ (Committing Host Effects: 1 Total)
428 ⚛ (Calling Lifecycle Methods: 0 Total)