@samitouri / QOS-React / commits / 9bda7b28f3

Suspended high pri work forces lower priority work to expire early (#12965)

* onFatal, onComplete, onSuspend, onYield For every call to renderRoot, one of onFatal, onComplete, onSuspend, and onYield is called upon exiting. We use these in lieu of returning a tuple. I've also chosen not to inline them into renderRoot because these will eventually be lifted into the renderer. * Suspended high pri work forces lower priority work to expire early If an error is thrown, and there is lower priority pending work, we retry at the lower priority. The lower priority work should expire at the same time at which the high priority work would have expired. Effectively, this increases the priority of the low priority work. Simple example: If an error is thrown during a synchronous render, and there's an async update, the async update should flush synchronously in case it's able to fix the error. I've added a unit test for this scenario. User provided timeouts should have the same behavior, but I'll leave that for a future PR.

Andrew Clark committed Jun 14, 2018 at 15:29 UTC 9bda7b28f3b149c8f8ca826f0c395a81ed2d3bec
6 files changed +179 -121
packages/react-reconciler/src/ReactFiberExpirationTime.js
-1
@@ -9,7 +9,6 @@
9
10 import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
11
12 -// TODO: Use an opaque type once ESLint et al support the syntax
12 export type ExpirationTime = number;
13
14 export const NoWork = 0;
packages/react-reconciler/src/ReactFiberPendingPriority.js
+25 -11
@@ -35,6 +35,7 @@ export function markPendingPriorityLevel(
35 }
36 }
37 }
38 + findNextPendingPriorityLevel(root);
39 }
40
41 export function markCommittedPriorityLevels(
@@ -49,6 +50,7 @@ export function markCommittedPriorityLevels(
50 root.earliestSuspendedTime = NoWork;
51 root.latestSuspendedTime = NoWork;
52 root.latestPingedTime = NoWork;
53 + findNextPendingPriorityLevel(root);
54 return;
55 }
56
@@ -77,6 +79,7 @@ export function markCommittedPriorityLevels(
79 // There's no suspended work. Treat the earliest remaining level as a
80 // pending level.
81 markPendingPriorityLevel(root, earliestRemainingTime);
82 + findNextPendingPriorityLevel(root);
83 return;
84 }
85
@@ -91,6 +94,7 @@ export function markCommittedPriorityLevels(
94 // There's no suspended work. Treat the earliest remaining level as a
95 // pending level.
96 markPendingPriorityLevel(root, earliestRemainingTime);
97 + findNextPendingPriorityLevel(root);
98 return;
99 }
100
@@ -98,11 +102,13 @@ export function markCommittedPriorityLevels(
102 // The earliest remaining time is earlier than all the suspended work.
103 // Treat it as a pending update.
104 markPendingPriorityLevel(root, earliestRemainingTime);
105 + findNextPendingPriorityLevel(root);
106 return;
107 }
108
109 // The earliest remaining time falls within the range of known suspended
110 // levels. We should treat this as suspended work.
111 + findNextPendingPriorityLevel(root);
112 }
113
114 export function markSuspendedPriorityLevel(
@@ -148,6 +154,7 @@ export function markSuspendedPriorityLevel(
154 root.latestSuspendedTime = suspendedTime;
155 }
156 }
157 + findNextPendingPriorityLevel(root);
158 }
159
160 export function markPingedPriorityLevel(
@@ -161,22 +168,29 @@ export function markPingedPriorityLevel(
168 root.latestPingedTime = pingedTime;
169 }
170 }
171 + findNextPendingPriorityLevel(root);
172 }
173
166 -export function findNextPendingPriorityLevel(root: FiberRoot): ExpirationTime {
174 +function findNextPendingPriorityLevel(root) {
175 const earliestSuspendedTime = root.earliestSuspendedTime;
176 const earliestPendingTime = root.earliestPendingTime;
177 + let nextExpirationTimeToWorkOn;
178 + let expirationTime;
179 if (earliestSuspendedTime === NoWork) {
180 // Fast path. There's no suspended work.
171 - return earliestPendingTime;
172 - }
173 -
174 - // First, check if there's known pending work.
175 - if (earliestPendingTime !== NoWork) {
176 - return earliestPendingTime;
181 + nextExpirationTimeToWorkOn = expirationTime = earliestPendingTime;
182 + } else if (earliestPendingTime !== NoWork) {
183 + // Check if there's known pending work.
184 + nextExpirationTimeToWorkOn = earliestPendingTime;
185 + expirationTime =
186 + earliestSuspendedTime < earliestPendingTime
187 + ? earliestSuspendedTime
188 + : earliestPendingTime;
189 + } else {
190 + // Finally, if a suspended level was pinged, work on that. Otherwise there's
191 + // nothing to work on.
192 + nextExpirationTimeToWorkOn = expirationTime = root.latestPingedTime;
193 }
178 -
179 - // Finally, if a suspended level was pinged, work on that. Otherwise there's
180 - // nothing to work on.
181 - return root.latestPingedTime;
194 + root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn;
195 + root.expirationTime = expirationTime;
196 }
packages/react-reconciler/src/ReactFiberRoot.js
+4 -2
@@ -57,7 +57,8 @@ export type FiberRoot = {
57 +hydrate: boolean,
58 // Remaining expiration time on this root.
59 // TODO: Lift this into the renderer
60 - remainingExpirationTime: ExpirationTime,
60 + nextExpirationTimeToWorkOn: ExpirationTime,
61 + expirationTime: ExpirationTime,
62 // List of top-level batches. This list indicates whether a commit should be
63 // deferred. Also contains completion callbacks.
64 // TODO: Lift this into the renderer
@@ -90,7 +91,8 @@ export function createFiberRoot(
91 context: null,
92 pendingContext: null,
93 hydrate,
93 - remainingExpirationTime: NoWork,
94 + nextExpirationTimeToWorkOn: NoWork,
95 + expirationTime: NoWork,
96 firstBatch: null,
97 nextScheduledRoot: null,
98 };
packages/react-reconciler/src/ReactFiberScheduler.js
+99 -105
@@ -60,7 +60,6 @@ import {
60 import {
61 markPendingPriorityLevel,
62 markCommittedPriorityLevels,
63 - findNextPendingPriorityLevel,
63 markSuspendedPriorityLevel,
64 markPingedPriorityLevel,
65 } from './ReactFiberPendingPriority';
@@ -230,7 +229,6 @@ let nextRoot: FiberRoot | null = null;
229 // The time at which we're currently rendering work.
230 let nextRenderExpirationTime: ExpirationTime = NoWork;
231 let nextLatestTimeoutMs: number = -1;
233 -let nextRenderIsExpired: boolean = false;
232
233 // The next fiber with an effect that we're currently committing.
234 let nextEffect: Fiber | null = null;
@@ -254,7 +252,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
252 replayUnitOfWork = (
253 failedUnitOfWork: Fiber,
254 thrownValue: mixed,
257 - isAsync: boolean,
255 + isYieldy: boolean,
256 ) => {
257 if (
258 thrownValue !== null &&
@@ -303,7 +301,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
301 // Replay the begin phase.
302 isReplayingFailedUnitOfWork = true;
303 originalReplayError = thrownValue;
306 - invokeGuardedCallback(null, workLoop, null, isAsync);
304 + invokeGuardedCallback(null, workLoop, null, isYieldy);
305 isReplayingFailedUnitOfWork = false;
306 originalReplayError = null;
307 if (hasCaughtError()) {
@@ -345,7 +343,6 @@ function resetStack() {
343 nextRoot = null;
344 nextRenderExpirationTime = NoWork;
345 nextLatestTimeoutMs = -1;
348 - nextRenderIsExpired = false;
346 nextUnitOfWork = null;
347 }
348
@@ -494,12 +491,11 @@ function markLegacyErrorBoundaryAsFailed(instance: mixed) {
491 }
492 }
493
497 -function commitRoot(finishedWork: Fiber): ExpirationTime {
494 +function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
495 isWorking = true;
496 isCommitting = true;
497 startCommitTimer();
498
502 - const root: FiberRoot = finishedWork.stateNode;
499 invariant(
500 root.current !== finishedWork,
501 'Cannot commit the same tree as before. This is probably a bug ' +
@@ -690,13 +686,13 @@ function commitRoot(finishedWork: Fiber): ExpirationTime {
686 ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
687 }
688
693 - const nextPendingPriorityLevel = findNextPendingPriorityLevel(root);
694 - if (nextPendingPriorityLevel === NoWork) {
689 + const expirationTime = root.expirationTime;
690 + if (expirationTime === NoWork) {
691 // If there's no remaining work, we can clear the set of already failed
692 // error boundaries.
693 legacyErrorBoundariesThatAlreadyFailed = null;
694 }
699 - return nextPendingPriorityLevel;
695 + onCommit(root, expirationTime);
696 }
697
698 function resetExpirationTime(
@@ -853,11 +849,7 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
849 // This fiber did not complete because something threw. Pop values off
850 // the stack without entering the complete phase. If this is a boundary,
851 // capture values if possible.
856 - const next = unwindWork(
857 - workInProgress,
858 - nextRenderIsExpired,
859 - nextRenderExpirationTime,
860 - );
852 + const next = unwindWork(workInProgress, nextRenderExpirationTime);
853 // Because this fiber did not complete, don't reset its expiration time.
854 if (workInProgress.effectTag & DidCapture) {
855 // Restarting an error boundary
@@ -973,9 +965,9 @@ function performUnitOfWork(workInProgress: Fiber): Fiber | null {
965 return next;
966 }
967
976 -function workLoop(isAsync) {
977 - if (!isAsync) {
978 - // Flush all expired work.
968 +function workLoop(isYieldy) {
969 + if (!isYieldy) {
970 + // Flush work without yielding
971 while (nextUnitOfWork !== null) {
972 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
973 }
@@ -993,11 +985,7 @@ function workLoop(isAsync) {
985 }
986 }
987
996 -function renderRoot(
997 - root: FiberRoot,
998 - expirationTime: ExpirationTime,
999 - isAsync: boolean,
1000 -): Fiber | null {
988 +function renderRoot(root: FiberRoot, isYieldy: boolean): void {
989 invariant(
990 !isWorking,
991 'renderRoot was called recursively. This error is likely caused ' +
@@ -1005,6 +993,8 @@ function renderRoot(
993 );
994 isWorking = true;
995
996 + const expirationTime = root.nextExpirationTimeToWorkOn;
997 +
998 // Check if we're starting from a fresh stack, or if we're resuming from
999 // previously yielded work.
1000 if (
@@ -1027,14 +1017,11 @@ function renderRoot(
1017
1018 let didFatal = false;
1019
1030 - nextRenderIsExpired =
1031 - !isAsync || nextRenderExpirationTime <= mostRecentCurrentTime;
1032 -
1020 startWorkLoopTimer(nextUnitOfWork);
1021
1022 do {
1023 try {
1037 - workLoop(isAsync);
1024 + workLoop(isYieldy);
1025 } catch (thrownValue) {
1026 if (enableProfilerTimer) {
1027 // Stop "base" render timer in the event of an error.
@@ -1054,7 +1041,7 @@ function renderRoot(
1041
1042 const failedUnitOfWork: Fiber = nextUnitOfWork;
1043 if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
1057 - replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync);
1044 + replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
1045 }
1046
1047 // TODO: we already know this isn't true in some cases.
@@ -1085,7 +1072,6 @@ function renderRoot(
1072 returnFiber,
1073 sourceFiber,
1074 thrownValue,
1088 - nextRenderIsExpired,
1075 nextRenderExpirationTime,
1076 mostRecentCurrentTimeMs,
1077 );
@@ -1107,7 +1093,7 @@ function renderRoot(
1093 if (__DEV__) {
1094 resetStackAfterFatalErrorInDev();
1095 }
1110 - return null;
1096 + onFatal(root);
1097 } else if (nextUnitOfWork === null) {
1098 // We reached the root.
1099 const rootWorkInProgress = root.current.alternate;
@@ -1121,32 +1107,27 @@ function renderRoot(
1107 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1108 interruptedBy = null;
1109 // The root successfully completed.
1124 - root.pendingCommitExpirationTime = expirationTime;
1125 - // Return the completed work-in-progress.
1126 - return rootWorkInProgress;
1110 + onComplete(root, rootWorkInProgress, expirationTime);
1111 } else {
1112 // The root did not complete.
1113 const didCompleteRoot = false;
1114 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1115 interruptedBy = null;
1132 - if (nextLatestTimeoutMs >= 0) {
1133 - setTimeout(() => {
1134 - retrySuspendedRoot(root, expirationTime);
1135 - }, nextLatestTimeoutMs);
1136 - }
1116 markSuspendedPriorityLevel(root, expirationTime);
1138 - const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root);
1139 - onBlock(firstUnblockedExpirationTime);
1140 - // Return null to indicate that the root did not complete.
1141 - return null;
1117 + const suspendedExpirationTime = expirationTime;
1118 + const newExpirationTime = root.expirationTime;
1119 + onSuspend(
1120 + root,
1121 + suspendedExpirationTime,
1122 + newExpirationTime,
1123 + nextLatestTimeoutMs,
1124 + );
1125 }
1126 } else {
1127 const didCompleteRoot = false;
1128 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1129 interruptedBy = null;
1147 - // There's more work to do, but we ran out of time. Yield back to
1148 - // the renderer.
1149 - return null;
1130 + onYield(root);
1131 }
1132 }
1133
@@ -1311,9 +1292,9 @@ function markTimeout(
1292
1293 function retrySuspendedRoot(root: FiberRoot, suspendedTime: ExpirationTime) {
1294 markPingedPriorityLevel(root, suspendedTime);
1314 - const retryTime = findNextPendingPriorityLevel(root);
1295 + const retryTime = root.expirationTime;
1296 if (retryTime !== NoWork) {
1316 - requestRetry(root, retryTime);
1297 + requestWork(root, retryTime);
1298 }
1299 }
1300
@@ -1358,7 +1339,6 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1339 resetStack();
1340 }
1341 markPendingPriorityLevel(root, expirationTime);
1361 - const nextExpirationTimeToWorkOn = findNextPendingPriorityLevel(root);
1342 if (
1343 // If we're in the render phase, we don't need to schedule this root
1344 // for an update, because we'll do it before we exit...
@@ -1367,7 +1347,8 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1347 // ...unless this is a different root than the one we're rendering.
1348 nextRoot !== root
1349 ) {
1370 - requestWork(root, nextExpirationTimeToWorkOn);
1350 + const rootExpirationTime = root.expirationTime;
1351 + requestWork(root, rootExpirationTime);
1352 }
1353 if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1354 invariant(
@@ -1454,7 +1435,7 @@ let nestedUpdateCount: number = 0;
1435
1436 const timeHeuristicForUnitOfWork = 1;
1437
1457 -function scheduleCallbackWithExpiration(expirationTime) {
1438 +function scheduleCallbackWithExpirationTime(expirationTime) {
1439 if (callbackExpirationTime !== NoWork) {
1440 // A callback is already scheduled. Check its expiration time (timeout).
1441 if (expirationTime > callbackExpirationTime) {
@@ -1472,25 +1453,52 @@ function scheduleCallbackWithExpiration(expirationTime) {
1453 startRequestCallbackTimer();
1454 }
1455
1475 - // Compute a timeout for the given expiration time.
1476 - const currentMs = now() - originalStartTimeMs;
1477 - const expirationMs = expirationTimeToMs(expirationTime);
1478 - const timeout = expirationMs - currentMs;
1479 -
1456 callbackExpirationTime = expirationTime;
1457 + const currentMs = now() - originalStartTimeMs;
1458 + const expirationTimeMs = expirationTimeToMs(expirationTime);
1459 + const timeout = expirationTimeMs - currentMs;
1460 callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
1461 }
1462
1484 -function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) {
1485 - if (
1486 - root.remainingExpirationTime === NoWork ||
1487 - root.remainingExpirationTime < expirationTime
1488 - ) {
1489 - // For a retry, only update the remaining expiration time if it has a
1490 - // *lower priority* than the existing value. This is because, on a retry,
1491 - // we should attempt to coalesce as much as possible.
1492 - requestWork(root, expirationTime);
1463 +// For every call to renderRoot, one of onFatal, onComplete, onSuspend, and
1464 +// onYield is called upon exiting. We use these in lieu of returning a tuple.
1465 +// I've also chosen not to inline them into renderRoot because these will
1466 +// eventually be lifted into the renderer.
1467 +function onFatal(root) {
1468 + root.finishedWork = null;
1469 +}
1470 +
1471 +function onComplete(
1472 + root: FiberRoot,
1473 + finishedWork: Fiber,
1474 + expirationTime: ExpirationTime,
1475 +) {
1476 + root.pendingCommitExpirationTime = expirationTime;
1477 + root.finishedWork = finishedWork;
1478 +}
1479 +
1480 +function onSuspend(
1481 + root: FiberRoot,
1482 + suspendedExpirationTime: ExpirationTime,
1483 + expirationTime: ExpirationTime,
1484 + timeoutMs: number,
1485 +) {
1486 + if (timeoutMs >= 0) {
1487 + const suspendedRoot = root;
1488 + setTimeout(() => {
1489 + retrySuspendedRoot(suspendedRoot, suspendedExpirationTime);
1490 + }, timeoutMs);
1491 }
1492 + root.expirationTime = expirationTime;
1493 +}
1494 +
1495 +function onYield(root) {
1496 + root.finishedWork = null;
1497 +}
1498 +
1499 +function onCommit(root, expirationTime) {
1500 + root.expirationTime = expirationTime;
1501 + root.finishedWork = null;
1502 }
1503
1504 // requestWork is called by the scheduler whenever a root receives an update.
@@ -1520,7 +1528,7 @@ function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
1528 if (expirationTime === Sync) {
1529 performSyncWork();
1530 } else {
1523 - scheduleCallbackWithExpiration(expirationTime);
1531 + scheduleCallbackWithExpirationTime(expirationTime);
1532 }
1533 }
1534
@@ -1529,7 +1537,7 @@ function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
1537 // Check if this root is already part of the schedule.
1538 if (root.nextScheduledRoot === null) {
1539 // This root is not already scheduled. Add it.
1532 - root.remainingExpirationTime = expirationTime;
1540 + root.expirationTime = expirationTime;
1541 if (lastScheduledRoot === null) {
1542 firstScheduledRoot = lastScheduledRoot = root;
1543 root.nextScheduledRoot = root;
@@ -1540,13 +1548,13 @@ function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
1548 }
1549 } else {
1550 // This root is already scheduled, but its priority may have increased.
1543 - const remainingExpirationTime = root.remainingExpirationTime;
1551 + const remainingExpirationTime = root.expirationTime;
1552 if (
1553 remainingExpirationTime === NoWork ||
1554 expirationTime < remainingExpirationTime
1555 ) {
1556 // Update the priority.
1549 - root.remainingExpirationTime = expirationTime;
1557 + root.expirationTime = expirationTime;
1558 }
1559 }
1560 }
@@ -1558,7 +1566,7 @@ function findHighestPriorityRoot() {
1566 let previousScheduledRoot = lastScheduledRoot;
1567 let root = firstScheduledRoot;
1568 while (root !== null) {
1561 - const remainingExpirationTime = root.remainingExpirationTime;
1569 + const remainingExpirationTime = root.expirationTime;
1570 if (remainingExpirationTime === NoWork) {
1571 // This root no longer has work. Remove it from the scheduler.
1572
@@ -1628,18 +1636,14 @@ function findHighestPriorityRoot() {
1636 }
1637
1638 function performAsyncWork(dl) {
1631 - performWork(NoWork, true, dl);
1639 + performWork(NoWork, dl);
1640 }
1641
1642 function performSyncWork() {
1635 - performWork(Sync, false, null);
1643 + performWork(Sync, null);
1644 }
1645
1638 -function performWork(
1639 - minExpirationTime: ExpirationTime,
1640 - isAsync: boolean,
1641 - dl: Deadline | null,
1642 -) {
1646 +function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1647 deadline = dl;
1648
1649 // Keep working on roots until there's no more work, or until the we reach
@@ -1656,7 +1660,7 @@ function performWork(
1660 stopRequestCallbackTimer(didExpire, timeout);
1661 }
1662
1659 - if (isAsync) {
1663 + if (deadline !== null) {
1664 while (
1665 nextFlushedRoot !== null &&
1666 nextFlushedExpirationTime !== NoWork &&
@@ -1666,11 +1670,7 @@ function performWork(
1670 recalculateCurrentTime() >= nextFlushedExpirationTime)
1671 ) {
1672 recalculateCurrentTime();
1669 - performWorkOnRoot(
1670 - nextFlushedRoot,
1671 - nextFlushedExpirationTime,
1672 - !deadlineDidExpire,
1673 - );
1673 + performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
1674 findHighestPriorityRoot();
1675 }
1676 } else {
@@ -1695,7 +1695,7 @@ function performWork(
1695 }
1696 // If there's work left over, schedule a new callback.
1697 if (nextFlushedExpirationTime !== NoWork) {
1698 - scheduleCallbackWithExpiration(nextFlushedExpirationTime);
1698 + scheduleCallbackWithExpirationTime(nextFlushedExpirationTime);
1699 }
1700
1701 // Clean-up.
@@ -1752,7 +1752,7 @@ function finishRendering() {
1752 function performWorkOnRoot(
1753 root: FiberRoot,
1754 expirationTime: ExpirationTime,
1755 - isAsync: boolean,
1755 + isYieldy: boolean,
1756 ) {
1757 invariant(
1758 !isRendering,
@@ -1763,14 +1763,16 @@ function performWorkOnRoot(
1763 isRendering = true;
1764
1765 // Check if this is async work or sync/expired work.
1766 - if (!isAsync) {
1767 - // Flush sync work.
1766 + if (!isYieldy) {
1767 + // Flush work without yielding.
1768 let finishedWork = root.finishedWork;
1769 if (finishedWork !== null) {
1770 // This root is already complete. We can commit it.
1771 completeRoot(root, finishedWork, expirationTime);
1772 } else {
1773 - finishedWork = renderRoot(root, expirationTime, false);
1773 + root.finishedWork = null;
1774 + renderRoot(root, false);
1775 + finishedWork = root.finishedWork;
1776 if (finishedWork !== null) {
1777 // We've completed the root. Commit it.
1778 completeRoot(root, finishedWork, expirationTime);
@@ -1783,7 +1785,9 @@ function performWorkOnRoot(
1785 // This root is already complete. We can commit it.
1786 completeRoot(root, finishedWork, expirationTime);
1787 } else {
1786 - finishedWork = renderRoot(root, expirationTime, true);
1788 + root.finishedWork = null;
1789 + renderRoot(root, true);
1790 + finishedWork = root.finishedWork;
1791 if (finishedWork !== null) {
1792 // We've completed the root. Check the deadline one more time
1793 // before committing.
@@ -1825,20 +1829,20 @@ function completeRoot(
1829 // This root is blocked from committing by a batch. Unschedule it until
1830 // we receive another update.
1831 root.finishedWork = finishedWork;
1828 - root.remainingExpirationTime = NoWork;
1832 + root.expirationTime = NoWork;
1833 return;
1834 }
1835 }
1836
1837 // Commit the root.
1838 root.finishedWork = null;
1835 - root.remainingExpirationTime = commitRoot(finishedWork);
1839 + commitRoot(root, finishedWork);
1840 }
1841
1842 // When working on async work, the reconciler asks the renderer if it should
1843 // yield execution. For DOM, we implement this with requestIdleCallback.
1844 function shouldYield() {
1841 - if (deadline === null) {
1845 + if (deadline === null || deadlineDidExpire) {
1846 return false;
1847 }
1848 if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
@@ -1858,23 +1862,13 @@ function onUncaughtError(error: mixed) {
1862 );
1863 // Unschedule this root so we don't work on it again until there's
1864 // another update.
1861 - nextFlushedRoot.remainingExpirationTime = NoWork;
1865 + nextFlushedRoot.expirationTime = NoWork;
1866 if (!hasUnhandledError) {
1867 hasUnhandledError = true;
1868 unhandledError = error;
1869 }
1870 }
1871
1868 -function onBlock(remainingExpirationTime: ExpirationTime) {
1869 - invariant(
1870 - nextFlushedRoot !== null,
1871 - 'Should be working on a root. This error is likely caused by a bug in ' +
1872 - 'React. Please file an issue.',
1873 - );
1874 - // This root was blocked. Unschedule it until there's another update.
1875 - nextFlushedRoot.remainingExpirationTime = remainingExpirationTime;
1876 -}
1877 -
1872 // TODO: Batching should be implemented at the renderer level, not inside
1873 // the reconciler.
1874 function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
@@ -1936,7 +1930,7 @@ function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
1930 lowestPendingInteractiveExpirationTime !== NoWork
1931 ) {
1932 // Synchronously flush pending interactive updates.
1939 - performWork(lowestPendingInteractiveExpirationTime, false, null);
1933 + performWork(lowestPendingInteractiveExpirationTime, null);
1934 lowestPendingInteractiveExpirationTime = NoWork;
1935 }
1936 const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
@@ -1957,7 +1951,7 @@ function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
1951 function flushInteractiveUpdates() {
1952 if (!isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
1953 // Synchronously flush pending interactive updates.
1960 - performWork(lowestPendingInteractiveExpirationTime, false, null);
1954 + performWork(lowestPendingInteractiveExpirationTime, null);
1955 lowestPendingInteractiveExpirationTime = NoWork;
1956 }
1957 }
@@ -1970,7 +1964,7 @@ function flushControlled(fn: () => mixed): void {
1964 } finally {
1965 isBatchingUpdates = previousIsBatchingUpdates;
1966 if (!isBatchingUpdates && !isRendering) {
1973 - performWork(Sync, false, null);
1967 + performWork(Sync, null);
1968 }
1969 }
1970 }
packages/react-reconciler/src/ReactFiberUnwindWork.js
+1 -2
@@ -142,7 +142,6 @@ function throwException(
142 returnFiber: Fiber,
143 sourceFiber: Fiber,
144 value: mixed,
145 - renderIsExpired: boolean,
145 renderExpirationTime: ExpirationTime,
146 currentTimeMs: number,
147 ) {
@@ -174,6 +173,7 @@ function throwException(
173 // This is a thenable.
174 const thenable: Thenable = (value: any);
175
176 + // TODO: Should use the earliest known expiration time
177 const expirationTimeMs = expirationTimeToMs(renderExpirationTime);
178 const startTimeMs = expirationTimeMs - 5000;
179 let elapsedMs = currentTimeMs - startTimeMs;
@@ -312,7 +312,6 @@ function throwException(
312
313 function unwindWork(
314 workInProgress: Fiber,
315 - renderIsExpired: boolean,
315 renderExpirationTime: ExpirationTime,
316 ) {
317 if (enableProfilerTimer) {
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+50
@@ -229,6 +229,56 @@ describe('ReactIncrementalErrorHandling', () => {
229 expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
230 });
231
232 + it('on error, retries at a lower priority using the expiration of higher priority', () => {
233 + class Parent extends React.Component {
234 + state = {hideChild: false};
235 + componentDidUpdate() {
236 + ReactNoop.yield('commit: ' + this.state.hideChild);
237 + }
238 + render() {
239 + if (this.state.hideChild) {
240 + ReactNoop.yield('(empty)');
241 + return <span prop="(empty)" />;
242 + }
243 + return <Child isBroken={this.props.childIsBroken} />;
244 + }
245 + }
246 +
247 + function Child(props) {
248 + if (props.isBroken) {
249 + ReactNoop.yield('Error!');
250 + throw new Error('Error!');
251 + }
252 + ReactNoop.yield('Child');
253 + return <span prop="Child" />;
254 + }
255 +
256 + // Initial mount
257 + const parent = React.createRef(null);
258 + ReactNoop.render(<Parent ref={parent} childIsBroken={false} />);
259 + expect(ReactNoop.flush()).toEqual(['Child']);
260 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
261 +
262 + // Schedule a low priority update to hide the child
263 + parent.current.setState({hideChild: true});
264 +
265 + // Before the low priority update is flushed, synchronsouly trigger an
266 + // error in the child.
267 + ReactNoop.flushSync(() => {
268 + ReactNoop.render(<Parent ref={parent} childIsBroken={true} />);
269 + });
270 + expect(ReactNoop.clearYields()).toEqual([
271 + // First the sync update triggers an error
272 + 'Error!',
273 + // Because there's a pending low priority update, we restart at the
274 + // lower priority. This hides the children, suppressing the error.
275 + '(empty)',
276 + // Now the tree can commit.
277 + 'commit: true',
278 + ]);
279 + expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
280 + });
281 +
282 it('calls componentDidCatch multiple times for multiple errors', () => {
283 let id = 0;
284 class BadMount extends React.Component {