@samitouri / QOS-React / commits / 71f591501b

Fix scheduler control flow (#10015)

* Use performFailedUnitOfWork instead of beginFailedWork and completeUnitOfWork separately. Also, we unwind the context stack *before* beginning work again. * Only use error loop directly after a commit We have a special, forked version of work loop that checks if a fiber is in a failed state (needs to be unmounted). This is only relevant right after a commit -- begin phase errors are handled differently, by unwinding the stack. Also renamed findNextUnitOfWork to resetNextUnitOfWork and made it a void function to signal that it's impure. * Reset nextUnitOfWork after every commit * Include the error boundary when unwinding a failed subtree Also added a warning to the error boundary to show that it failed. * Push context providers in beginFailedWork to avoid push/pop mismatch Added a test that demonstrates how an error boundary that is also a context provider could pop its context too many times if you neglect to push it in beginFailedWork. This happens because we've already popped the context once in unwindContext. The solution is a code smell. I don't like how we push/pop context in so many places. Shouldn't they all happen in the same location? * Refactor work loop - Optimizes the normal, non-error path by reducing the number of checks needed to begin performing work. - Prevents control flow from oscillating between fast normal loop and slower error loop. * Improve context unwinding test Tests that we correctly unwind an error boundary that is also a context provider. * Triangle tester should assert the tree is consistent after every action ...not just at the end. * Better implementation of infinite loop error Infinite loops should only be possible if a while loop never terminates. Now that we've refactored to avoid oscillation between different work loops, we can count updates local to each loop. The two loops that could infinite loop are the sync work loop and the loop that surrounds the body of the render phase catch block. The async loop could also fall into an infinite loop if the deadline never terminates, but we'll assume that it always eventually does. This change also creates better error stack traces because the error is thrown from inside the first setState that exceeds the limit. Added a test case for an error boundary whose parent remounts it on recovery. * Use invokeGuardedCallback in DEV

Andrew Clark committed Jul 18, 2017 at 16:31 UTC 71f591501b639c4adf329e1f586c7e04875dde7f
7 files changed +367 -243
scripts/fiber/tests-passing.txt
+1
@@ -597,6 +597,7 @@ src/renderers/__tests__/ReactUpdates-test.js
597 * does not re-render if state update is null
598 * synchronously renders hidden subtrees
599 * does not fall into an infinite update loop
600 +* does not fall into an infinite error loop
601
602 src/renderers/__tests__/multiple-copies-of-react-test.js
603 * throws the "Refs must have owner" warning
src/renderers/__tests__/ReactUpdates-test.js
+32
@@ -1158,4 +1158,36 @@ describe('ReactUpdates', () => {
1158 ReactDOM.render(<NonTerminating />, container);
1159 }).toThrow('Maximum');
1160 });
1161 +
1162 + if (ReactDOMFeatureFlags.useFiber) {
1163 + it('does not fall into an infinite error loop', () => {
1164 + function BadRender() {
1165 + throw new Error('error');
1166 + }
1167 +
1168 + class ErrorBoundary extends React.Component {
1169 + componentDidCatch() {
1170 + this.props.parent.remount();
1171 + }
1172 + render() {
1173 + return <BadRender />;
1174 + }
1175 + }
1176 +
1177 + class NonTerminating extends React.Component {
1178 + state = {step: 0};
1179 + remount() {
1180 + this.setState(state => ({step: state.step + 1}));
1181 + }
1182 + render() {
1183 + return <ErrorBoundary key={this.state.step} parent={this} />;
1184 + }
1185 + }
1186 +
1187 + const container = document.createElement('div');
1188 + expect(() => {
1189 + ReactDOM.render(<NonTerminating />, container);
1190 + }).toThrow('Maximum');
1191 + });
1192 + }
1193 });
src/renderers/shared/fiber/ReactDebugFiberPerf.js
+14
@@ -270,6 +270,20 @@ if (__DEV__) {
270 endFiberMark(fiber, null, null);
271 },
272
273 + stopFailedWorkTimer(fiber: Fiber): void {
274 + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
275 + return;
276 + }
277 + // If we pause, its parent is the fiber to unwind from.
278 + currentFiber = fiber.return;
279 + if (!fiber._debugIsCurrentlyTiming) {
280 + return;
281 + }
282 + fiber._debugIsCurrentlyTiming = false;
283 + const warning = 'An error was thrown inside this error boundary';
284 + endFiberMark(fiber, null, warning);
285 + },
286 +
287 startPhaseTimer(fiber: Fiber, phase: MeasurementPhase): void {
288 if (!supportsUserTiming) {
289 return;
src/renderers/shared/fiber/ReactFiberBeginWork.js
+16 -5
@@ -773,11 +773,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
773 workInProgress: Fiber,
774 priorityLevel: PriorityLevel,
775 ) {
776 - invariant(
777 - workInProgress.tag === ClassComponent || workInProgress.tag === HostRoot,
778 - 'Invalid type of work. This error is likely caused by a bug in React. ' +
779 - 'Please file an issue.',
780 - );
776 + // Push context providers here to avoid a push/pop context mismatch.
777 + switch (workInProgress.tag) {
778 + case ClassComponent:
779 + pushContextProvider(workInProgress);
780 + break;
781 + case HostRoot:
782 + const root: FiberRoot = workInProgress.stateNode;
783 + pushHostContainer(workInProgress, root.containerInfo);
784 + break;
785 + default:
786 + invariant(
787 + false,
788 + 'Invalid type of work. This error is likely caused by a bug in React. ' +
789 + 'Please file an issue.',
790 + );
791 + }
792
793 // Add an error effect so we can handle the error during the commit phase
794 workInProgress.effectTag |= Err;
src/renderers/shared/fiber/ReactFiberScheduler.js
+296 -232
@@ -96,6 +96,7 @@ if (__DEV__) {
96 recordScheduleUpdate,
97 startWorkTimer,
98 stopWorkTimer,
99 + stopFailedWorkTimer,
100 startWorkLoopTimer,
101 stopWorkLoopTimer,
102 startCommitTimer,
@@ -182,10 +183,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
183 // Might be less confusing.
184 let priorityContext: PriorityLevel = NoWork;
185
185 - // Keep track of this so we can reset the priority context if an error
186 - // is thrown during reconciliation.
187 - let priorityContextBeforeReconciliation: PriorityLevel = NoWork;
188 -
186 // Keeps track of whether we're currently in a work loop.
187 let isPerformingWork: boolean = false;
188
@@ -226,14 +223,14 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
223 // Error boundaries that captured an error during the current commit.
224 let commitPhaseBoundaries: Set<Fiber> | null = null;
225 let firstUncaughtError: Error | null = null;
229 - let fatalError: Error | null = null;
226 + let didFatal: boolean = false;
227
228 let isCommitting: boolean = false;
229 let isUnmounting: boolean = false;
230
231 // Use these to prevent an infinite loop of nested updates
235 - let nestedSyncUpdates = 0;
236 - let NESTED_SYNC_UPDATE_LIMIT = 1000;
232 + const NESTED_UPDATE_LIMIT = 1000;
233 + let nestedUpdateCount = 0;
234
235 function resetContextStack() {
236 // Reset the stack
@@ -243,10 +240,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
240 resetHostContainer();
241 }
242
246 - // findNextUnitOfWork mutates the current priority context. It is reset after
247 - // after the workLoop exits, so never call findNextUnitOfWork from outside
243 + // resetNextUnitOfWork mutates the current priority context. It is reset after
244 + // after the workLoop exits, so never call resetNextUnitOfWork from outside
245 // the work loop.
249 - function findNextUnitOfWork() {
246 + function resetNextUnitOfWork() {
247 // Clear out roots with no more work on them, or if they have uncaught errors
248 while (
249 nextScheduledRoot !== null &&
@@ -287,21 +284,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
284 }
285 if (highestPriorityRoot !== null) {
286 nextPriorityLevel = highestPriorityLevel;
290 - priorityContext = nextPriorityLevel;
291 -
292 - if (
293 - nextPriorityLevel === TaskPriority ||
294 - nextPriorityLevel === SynchronousPriority
295 - ) {
296 - invariant(
297 - nestedSyncUpdates++ <= NESTED_SYNC_UPDATE_LIMIT,
298 - 'Maximum update depth exceeded. This can happen when a ' +
299 - 'component repeatedly calls setState inside componentWillUpdate or ' +
300 - 'componentDidUpdate. React limits the number of nested updates to ' +
301 - 'prevent infinite loops.',
302 - );
303 - }
304 -
287 // Before we start any new work, let's make sure that we have a fresh
288 // stack to work from.
289 // TODO: This call is buried a bit too deep. It would be nice to have
@@ -309,14 +291,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
291 // unfortunately this is it.
292 resetContextStack();
293
312 - return createWorkInProgress(
294 + nextUnitOfWork = createWorkInProgress(
295 highestPriorityRoot.current,
296 highestPriorityLevel,
297 );
298 + return;
299 }
300
301 nextPriorityLevel = NoWork;
319 - return null;
302 + nextUnitOfWork = null;
303 + return;
304 }
305
306 function commitAllHostEffects() {
@@ -445,13 +429,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
429 'in React. Please file an issue.',
430 );
431
432 + if (
433 + nextPriorityLevel === SynchronousPriority ||
434 + nextPriorityLevel === TaskPriority
435 + ) {
436 + // Keep track of the number of iterations to prevent an infinite
437 + // update loop.
438 + nestedUpdateCount++;
439 + }
440 +
441 // Reset this to null before calling lifecycles
442 ReactCurrentOwner.current = null;
443
451 - // Updates that occur during the commit phase should have Task priority
452 - const previousPriorityContext = priorityContext;
453 - priorityContext = TaskPriority;
454 -
444 let firstEffect;
445 if (finishedWork.effectTag > PerformedWork) {
446 // A fiber's effect list consists only of its children, not itself. So if
@@ -565,7 +554,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
554 commitPhaseBoundaries = null;
555 }
556
568 - priorityContext = previousPriorityContext;
557 + // This tree is done. Reset the unit of work pointer to the next highest
558 + // priority root. If there's no more work left, the pointer is set to null.
559 + resetNextUnitOfWork();
560 }
561
562 function resetWorkPriority(
@@ -668,16 +659,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
659 workInProgress = returnFiber;
660 continue;
661 } else {
671 - // We've reached the root. Unless we're current performing deferred
672 - // work, we should commit the completed work immediately. If we are
673 - // performing deferred work, returning null indicates to the caller
674 - // that we just completed the root so they can handle that case correctly.
675 - if (nextPriorityLevel < HighPriority) {
676 - // Otherwise, we should commit immediately.
677 - commitAllWork(workInProgress);
678 - } else {
679 - pendingCommit = workInProgress;
680 - }
662 + // We've reached the root. Mark the root as pending commit. Depending
663 + // on how much time we have left, we'll either commit it now or in
664 + // the next frame.
665 + pendingCommit = workInProgress;
666 return null;
667 }
668 }
@@ -746,114 +731,203 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
731 return next;
732 }
733
749 - function clearErrors() {
750 - if (nextUnitOfWork === null) {
751 - nextUnitOfWork = findNextUnitOfWork();
752 - }
753 - // Keep performing work until there are no more errors
754 - while (
755 - capturedErrors !== null &&
756 - capturedErrors.size &&
757 - nextUnitOfWork !== null &&
758 - nextPriorityLevel !== NoWork &&
759 - nextPriorityLevel <= TaskPriority
760 - ) {
761 - if (hasCapturedError(nextUnitOfWork)) {
762 - // Use a forked version of performUnitOfWork
763 - nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
764 - } else {
765 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
766 - }
767 - if (nextUnitOfWork === null) {
768 - // If performUnitOfWork returns null, that means we just committed
769 - // a root. Normally we'd need to clear any errors that were scheduled
770 - // during the commit phase. But we're already clearing errors, so
771 - // we can continue.
772 - nextUnitOfWork = findNextUnitOfWork();
734 + function performDeferredWork(deadline: Deadline) {
735 + performWork(OffscreenPriority, deadline);
736 + }
737 +
738 + function handleCommitPhaseErrors() {
739 + // This is a special work loop for handling commit phase errors. It's
740 + // similar to the syncrhonous work loop, but does an additional check on
741 + // each fiber to see if it's an error boundary with an unhandled error. If
742 + // so, it uses a forked version of performUnitOfWork that unmounts the
743 + // failed subtree.
744 + //
745 + // The loop stops once the children have unmounted and error lifecycles are
746 + // called. Then we return to the regular flow.
747 +
748 + if (capturedErrors !== null && capturedErrors.size > 0) {
749 + while (nextUnitOfWork !== null) {
750 + if (hasCapturedError(nextUnitOfWork)) {
751 + // Use a forked version of performUnitOfWork
752 + nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
753 + } else {
754 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
755 + }
756 + if (nextUnitOfWork === null) {
757 + invariant(
758 + pendingCommit !== null,
759 + 'Should have a pending commit. This error is likely caused by ' +
760 + 'a bug in React. Please file an issue.',
761 + );
762 + // We just completed a root. Commit it now.
763 + priorityContext = TaskPriority;
764 + commitAllWork(pendingCommit);
765 + priorityContext = nextPriorityLevel;
766 +
767 + if (capturedErrors === null || capturedErrors.size === 0) {
768 + // There are no more unhandled errors. We can exit this special
769 + // work loop. If there's still additional work, we'll perform it
770 + // using one of the normal work loops.
771 + break;
772 + }
773 + // The commit phase produced additional errors. Continue working.
774 + invariant(
775 + nextPriorityLevel === TaskPriority,
776 + 'Commit phase errors should be scheduled to recover with task ' +
777 + 'priority. This error is likely caused by a bug in React. ' +
778 + 'Please file an issue.',
779 + );
780 + }
781 }
782 }
783 }
784
777 - function workLoopAsync(minPriorityLevel: PriorityLevel, deadline: Deadline) {
778 - // Flush asynchronous work until the deadline expires.
779 - while (nextUnitOfWork !== null && !deadlineHasExpired) {
780 - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
781 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
782 - // In a deferred work batch, iff nextUnitOfWork returns null, we just
783 - // completed a root and a pendingCommit exists. Logically, we could
784 - // omit either of the checks in the following condition, but we need
785 - // both to satisfy Flow.
786 - if (nextUnitOfWork === null && pendingCommit !== null) {
787 - // If we have time, we should commit the work now.
788 - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
785 + function workLoop(
786 + minPriorityLevel: PriorityLevel,
787 + deadline: Deadline | null,
788 + ) {
789 + if (pendingCommit !== null) {
790 + priorityContext = TaskPriority;
791 + commitAllWork(pendingCommit);
792 + handleCommitPhaseErrors();
793 + } else if (nextUnitOfWork === null) {
794 + resetNextUnitOfWork();
795 + }
796 +
797 + if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) {
798 + return;
799 + }
800 +
801 + // During the render phase, updates should have the same priority at which
802 + // we're rendering.
803 + priorityContext = nextPriorityLevel;
804 +
805 + loop: do {
806 + if (nextPriorityLevel <= TaskPriority) {
807 + // Flush all synchronous and task work.
808 + while (nextUnitOfWork !== null) {
809 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
810 + if (nextUnitOfWork === null) {
811 + invariant(
812 + pendingCommit !== null,
813 + 'Should have a pending commit. This error is likely caused by ' +
814 + 'a bug in React. Please file an issue.',
815 + );
816 + // We just completed a root. Commit it now.
817 + priorityContext = TaskPriority;
818 commitAllWork(pendingCommit);
790 - nextUnitOfWork = findNextUnitOfWork();
819 + priorityContext = nextPriorityLevel;
820 // Clear any errors that were scheduled during the commit phase.
792 - clearErrors();
821 + handleCommitPhaseErrors();
822 // The priority level may have changed. Check again.
823 if (
824 nextPriorityLevel === NoWork ||
825 nextPriorityLevel > minPriorityLevel ||
797 - nextPriorityLevel < HighPriority
826 + nextPriorityLevel > TaskPriority
827 ) {
828 // The priority level does not match.
829 break;
830 }
831 + }
832 + }
833 + } else if (deadline !== null) {
834 + // Flush asynchronous work until the deadline expires.
835 + while (nextUnitOfWork !== null && !deadlineHasExpired) {
836 + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
837 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
838 + // In a deferred work batch, iff nextUnitOfWork returns null, we just
839 + // completed a root and a pendingCommit exists. Logically, we could
840 + // omit either of the checks in the following condition, but we need
841 + // both to satisfy Flow.
842 + if (nextUnitOfWork === null) {
843 + invariant(
844 + pendingCommit !== null,
845 + 'Should have a pending commit. This error is likely caused by ' +
846 + 'a bug in React. Please file an issue.',
847 + );
848 + // We just completed a root. If we have time, commit it now.
849 + // Otherwise, we'll commit it in the next frame.
850 + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
851 + priorityContext = TaskPriority;
852 + commitAllWork(pendingCommit);
853 + priorityContext = nextPriorityLevel;
854 + // Clear any errors that were scheduled during the commit phase.
855 + handleCommitPhaseErrors();
856 + // The priority level may have changed. Check again.
857 + if (
858 + nextPriorityLevel === NoWork ||
859 + nextPriorityLevel > minPriorityLevel ||
860 + nextPriorityLevel < HighPriority
861 + ) {
862 + // The priority level does not match.
863 + break;
864 + }
865 + } else {
866 + deadlineHasExpired = true;
867 + }
868 + }
869 } else {
870 deadlineHasExpired = true;
871 }
805 - // Otherwise the root will committed in the next frame.
872 }
807 - } else {
808 - deadlineHasExpired = true;
873 }
810 - }
811 - }
874
813 - function workLoopSync(minPriorityLevel: PriorityLevel) {
814 - // Flush all synchronous and task work.
815 - while (nextUnitOfWork !== null) {
816 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
817 - if (nextUnitOfWork === null) {
818 - nextUnitOfWork = findNextUnitOfWork();
819 - // performUnitOfWork returned null, which means we just committed a
820 - // root. Clear any errors that were scheduled during the commit phase.
821 - clearErrors();
822 - // The priority level may have changed. Check again.
823 - if (
824 - nextPriorityLevel === NoWork ||
825 - nextPriorityLevel > minPriorityLevel ||
826 - nextPriorityLevel > TaskPriority
827 - ) {
828 - // The priority level does not match.
829 - break;
830 - }
875 + // There might be work left. Depending on the priority, we should
876 + // either perform it now or schedule a callback to perform it later.
877 + switch (nextPriorityLevel) {
878 + case SynchronousPriority:
879 + case TaskPriority:
880 + // We have remaining synchronous or task work. Keep performing it,
881 + // regardless of whether we're inside a callback.
882 + if (nextPriorityLevel <= minPriorityLevel) {
883 + continue loop;
884 + }
885 + break loop;
886 + case HighPriority:
887 + case LowPriority:
888 + case OffscreenPriority:
889 + // We have remaining async work.
890 + if (deadline === null) {
891 + // We're not inside a callback. Exit and perform the work during
892 + // the next callback.
893 + break loop;
894 + }
895 + // We are inside a callback.
896 + if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) {
897 + // We still have time. Keep working.
898 + continue loop;
899 + }
900 + // We've run out of time. Exit.
901 + break loop;
902 + case NoWork:
903 + // No work left. We can exit.
904 + break loop;
905 + default:
906 + invariant(false, 'Switch statement should be exhuastive.');
907 }
832 - }
908 + } while (true);
909 }
910
835 - function workLoop(
911 + function performWorkCatchBlock(
912 + failedWork: Fiber,
913 + boundary: Fiber,
914 minPriorityLevel: PriorityLevel,
915 deadline: Deadline | null,
916 ) {
839 - // Clear any errors.
840 - clearErrors();
841 -
842 - if (nextUnitOfWork === null) {
843 - nextUnitOfWork = findNextUnitOfWork();
844 - }
845 -
846 - if (nextPriorityLevel !== NoWork && nextPriorityLevel <= minPriorityLevel) {
847 - if (nextPriorityLevel <= TaskPriority) {
848 - workLoopSync(minPriorityLevel);
849 - } else if (deadline !== null) {
850 - workLoopAsync(minPriorityLevel, deadline);
851 - }
852 - }
853 - }
854 -
855 - function performDeferredWork(deadline: Deadline) {
856 - performWork(OffscreenPriority, deadline);
917 + // We're going to restart the error boundary that captured the error.
918 + // Conceptually, we're unwinding the stack. We need to unwind the
919 + // context stack, too.
920 + unwindContexts(failedWork, boundary);
921 +
922 + // Restart the error boundary using a forked version of
923 + // performUnitOfWork that deletes the boundary's children. The entire
924 + // failed subree will be unmounted. During the commit phase, a special
925 + // lifecycle method is called on the error boundary, which triggers
926 + // a re-render.
927 + nextUnitOfWork = performFailedUnitOfWork(boundary);
928 +
929 + // Continue working.
930 + workLoop(minPriorityLevel, deadline);
931 }
932
933 function performWork(
@@ -871,144 +945,117 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
945 );
946 isPerformingWork = true;
947
874 - let hasRemainingAsyncWork = false;
875 -
876 - // This outer loop exists so that we can restart the work loop after
877 - // catching an error. It also lets us flush Task work at the end of a
878 - // deferred batch.
879 - while (fatalError === null) {
880 - // Before starting any work, check to see if there are any pending
881 - // commits from the previous frame.
882 - // TODO: Only commit asynchronous priority at beginning or end of a frame.
883 - // Task work can be committed whenever.
884 - if (pendingCommit !== null && !deadlineHasExpired) {
885 - // Safe to call this outside the work loop because the commit phase has
886 - // its own try-catch.
887 - commitAllWork(pendingCommit);
948 + nestedUpdateCount = 0;
949 +
950 + // The priority context changes during the render phase. We'll need to
951 + // reset it at the end.
952 + const previousPriorityContext = priorityContext;
953 +
954 + let error;
955 + if (__DEV__) {
956 + error = invokeGuardedCallback(
957 + null,
958 + workLoop,
959 + null,
960 + minPriorityLevel,
961 + deadline,
962 + );
963 + } else {
964 + try {
965 + workLoop(minPriorityLevel, deadline);
966 + error = null;
967 + } catch (e) {
968 + error = e;
969 + }
970 + }
971 +
972 + // An error was thrown during the render phase.
973 + while (error !== null) {
974 + if (didFatal) {
975 + // This was a fatal error. Don't attempt to recover from it.
976 + firstUncaughtError = error;
977 + break;
978 + }
979 +
980 + const failedWork = nextUnitOfWork;
981 + if (failedWork === null) {
982 + // An error was thrown but there's no current unit of work. This can
983 + // happen during the commit phase if there's a bug in the renderer.
984 + didFatal = true;
985 + continue;
986 + }
987 +
988 + // "Capture" the error by finding the nearest boundary. If there is no
989 + // error boundary, we use the root.
990 + const boundary = captureError(failedWork, error);
991 + invariant(
992 + boundary !== null,
993 + 'Should have found an error boundary. This error is likely ' +
994 + 'caused by a bug in React. Please file an issue.',
995 + );
996 +
997 + if (didFatal) {
998 + // The error we just captured was a fatal error. This happens
999 + // when the error propagates to the root more than once.
1000 + continue;
1001 }
1002
890 - // Nothing in performWork should be allowed to throw. All unsafe
891 - // operations must happen within workLoop, which is extracted to a
892 - // separate function so that it can be optimized by the JS engine.
893 - priorityContextBeforeReconciliation = priorityContext;
894 - let error = null;
1003 if (__DEV__) {
1004 error = invokeGuardedCallback(
1005 null,
898 - workLoop,
1006 + performWorkCatchBlock,
1007 null,
1008 + failedWork,
1009 + boundary,
1010 minPriorityLevel,
1011 deadline,
1012 );
1013 } else {
1014 try {
905 - workLoop(minPriorityLevel, deadline);
1015 + performWorkCatchBlock(
1016 + failedWork,
1017 + boundary,
1018 + minPriorityLevel,
1019 + deadline,
1020 + );
1021 + error = null;
1022 } catch (e) {
1023 error = e;
1024 }
1025 }
910 - // Reset the priority context to its value before reconcilation.
911 - priorityContext = priorityContextBeforeReconciliation;
1026
1027 if (error !== null) {
914 - // We caught an error during either the begin or complete phases.
915 - const failedWork = nextUnitOfWork;
916 -
917 - if (failedWork !== null) {
918 - // "Capture" the error by finding the nearest boundary. If there is no
919 - // error boundary, the nearest host container acts as one. If
920 - // captureError returns null, the error was intentionally ignored.
921 - const maybeBoundary = captureError(failedWork, error);
922 - if (maybeBoundary !== null) {
923 - const boundary = maybeBoundary;
924 -
925 - // Complete the boundary as if it rendered null. This will unmount
926 - // the failed tree.
927 - beginFailedWork(boundary.alternate, boundary, nextPriorityLevel);
928 -
929 - // The next unit of work is now the boundary that captured the error.
930 - // Conceptually, we're unwinding the stack. We need to unwind the
931 - // context stack, too, from the failed work to the boundary that
932 - // captured the error.
933 - // TODO: If we set the memoized props in beginWork instead of
934 - // completeWork, rather than unwind the stack, we can just restart
935 - // from the root. Can't do that until then because without memoized
936 - // props, the nodes higher up in the tree will rerender unnecessarily.
937 - unwindContexts(failedWork, boundary);
938 - nextUnitOfWork = completeUnitOfWork(boundary);
939 - }
940 - // Continue performing work
941 - continue;
942 - } else if (fatalError === null) {
943 - // There is no current unit of work. This is a worst-case scenario
944 - // and should only be possible if there's a bug in the renderer, e.g.
945 - // inside resetAfterCommit.
946 - fatalError = error;
947 - }
948 - } else {
949 - // There might be work left. Depending on the priority, we should
950 - // either perform it now or schedule a callback to perform it later.
951 - switch (nextPriorityLevel) {
952 - case SynchronousPriority:
953 - case TaskPriority:
954 - // We have remaining synchronous or task work. Keep performing it,
955 - // regardless of whether we're inside a callback.
956 - if (nextPriorityLevel <= minPriorityLevel) {
957 - continue;
958 - }
959 - break;
960 - case HighPriority:
961 - case LowPriority:
962 - case OffscreenPriority:
963 - // We have remaining async work.
964 - if (deadline === null) {
965 - // We're not inside a callback. Exit and perform the work during
966 - // the next callback.
967 - hasRemainingAsyncWork = true;
968 - } else {
969 - // We are inside a callback.
970 - if (
971 - !deadlineHasExpired &&
972 - nextPriorityLevel <= minPriorityLevel
973 - ) {
974 - // We still have time. Keep working.
975 - continue;
976 - }
977 - // We've run out of time. Exit.
978 - hasRemainingAsyncWork = true;
979 - }
980 - break;
981 - case NoWork:
982 - // No work left. We can exit.
983 - break;
984 - default:
985 - invariant(false, 'Switch statement should be exhuastive.');
986 - }
987 - // Exit the loop.
988 - break;
1028 + // Another error was thrown during the render phase. Continue the
1029 + // loop to handle the new error.
1030 + continue;
1031 }
1032 +
1033 + // We're finished working. Exit the error loop.
1034 + break;
1035 }
1036
1037 + // Reset the priority context to its previous value.
1038 + priorityContext = previousPriorityContext;
1039 +
1040 // If we're inside a callback, set this to false, since we just flushed it.
1041 if (deadline !== null) {
1042 isCallbackScheduled = false;
1043 }
1044 // If there's remaining async work, make sure we schedule another callback.
997 - if (hasRemainingAsyncWork && !isCallbackScheduled) {
1045 + if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) {
1046 scheduleDeferredCallback(performDeferredWork);
1047 isCallbackScheduled = true;
1048 }
1049
1002 - const errorToThrow = fatalError !== null ? fatalError : firstUncaughtError;
1050 + const errorToThrow = firstUncaughtError;
1051
1052 // We're done performing work. Time to clean up.
1053 isPerformingWork = false;
1054 deadlineHasExpired = false;
1007 - fatalError = null;
1055 + didFatal = false;
1056 firstUncaughtError = null;
1057 capturedErrors = null;
1058 failedBoundaries = null;
1011 - nestedSyncUpdates = 0;
1059 if (__DEV__) {
1060 stopWorkLoopTimer();
1061 }
@@ -1026,8 +1073,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1073 if (__DEV__) {
1074 ReactDebugCurrentFiber.resetCurrentFiber();
1075 }
1029 - // It is no longer valid because this unit of work failed.
1030 - nextUnitOfWork = null;
1076
1077 // Search for the nearest error boundary.
1078 let boundary: Fiber | null = null;
@@ -1047,7 +1092,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1092 // If this root already failed, there must have been an error when
1093 // attempting to unmount it. This is a worst-case scenario and
1094 // should only be possible if there's a bug in the renderer.
1050 - fatalError = error;
1095 + didFatal = true;
1096 }
1097 } else {
1098 let node = failedWork.return;
@@ -1141,6 +1186,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1186 commitPhaseBoundaries.add(boundary);
1187 } else {
1188 // Otherwise, schedule an update now.
1189 + // TODO: Is this actually necessary during the render phase? Is it
1190 + // possible to unwind and continue rendering at the same priority,
1191 + // without corrupting internal state?
1192 scheduleErrorRecovery(boundary);
1193 }
1194 return boundary;
@@ -1231,7 +1279,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1279
1280 function unwindContexts(from: Fiber, to: Fiber) {
1281 let node = from;
1234 - while (node !== null && node !== to && node.alternate !== to) {
1282 + while (node !== null) {
1283 switch (node.tag) {
1284 case ClassComponent:
1285 popContextProvider(node);
@@ -1246,7 +1294,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1294 popHostContainer(node);
1295 break;
1296 }
1249 - if (__DEV__) {
1297 + if (node === to || node.alternate === to) {
1298 + if (__DEV__) {
1299 + stopFailedWorkTimer(node);
1300 + }
1301 + break;
1302 + } else if (__DEV__) {
1303 stopWorkTimer(node);
1304 }
1305 node = node.return;
@@ -1277,7 +1330,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1330 recordScheduleUpdate();
1331 }
1332
1280 - if (priorityLevel <= nextPriorityLevel) {
1333 + if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1334 + didFatal = true;
1335 + invariant(
1336 + false,
1337 + 'Maximum update depth exceeded. This can happen when a ' +
1338 + 'component repeatedly calls setState inside componentWillUpdate or ' +
1339 + 'componentDidUpdate. React limits the number of nested updates to ' +
1340 + 'prevent infinite loops.',
1341 + );
1342 + }
1343 +
1344 + if (!isPerformingWork && priorityLevel <= nextPriorityLevel) {
1345 // We must reset the current unit of work pointer so that we restart the
1346 // search from the root during the next tick, in case there is now higher
1347 // priority work somewhere earlier than before.
src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+6 -5
@@ -165,7 +165,7 @@ describe('ReactIncrementalTriangle', () => {
165 // first child.
166 if (counter === undefined) {
167 if (typeof num === 'string') {
168 - counter = num.substr(1, num.length - 2);
168 + counter = parseInt(num.substr(1, num.length - 2), 10);
169 } else {
170 counter = num;
171 }
@@ -192,8 +192,8 @@ describe('ReactIncrementalTriangle', () => {
192 let expectedCounterAtEnd = app.state.counter;
193
194 let activeTriangle = null;
195 - ReactNoop.batchedUpdates(() => {
196 - for (let i = 0; i < actions.length; i++) {
195 + for (var i = 0; i < actions.length; i++) {
196 + ReactNoop.batchedUpdates(() => {
197 const action = actions[i];
198 switch (action.type) {
199 case FLUSH:
@@ -225,8 +225,9 @@ describe('ReactIncrementalTriangle', () => {
225 default:
226 break;
227 }
228 - }
229 - });
228 + });
229 + assertConsistentTree(activeTriangle);
230 + }
231 // Flush remaining work
232 ReactNoop.flush();
233 assertConsistentTree(activeTriangle, expectedCounterAtEnd);
src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
+2 -1
@@ -171,9 +171,10 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
171 "// Stop on Baddie and restart from Boundary
172 ⛔ (React Tree Reconciliation) Warning: There were cascading updates
173 ⚛ Parent [mount]
174 - ⚛ Boundary [mount]
174 + ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
175 ⚛ Parent [mount]
176 ⚛ Baddie [mount]
177 + ⚛ Boundary [mount]
178 ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
179 ⚛ (Committing Host Effects: 2 Total)
180 ⚛ (Calling Lifecycle Methods: 1 Total)