@samitouri / QOS-React / commits / e0c78344e2

Retry on error if there's lower priority pending work (#12957)

* Remove enableSuspense flag from PendingPriority module We're going to use this for suspending on error, too. * Retry on error if there's lower priority pending work If an error is thrown, and there's lower priority work, it's possible the lower priority work will fix the error. Retry at the lower priority. If an error is thrown and there's no more work to try, handle the error like we normally do (trigger the nearest error boundary).

Andrew Clark committed Jun 13, 2018 at 10:47 UTC e0c78344e2f49fb91a75607ccdb170b5649bb1e9
4 files changed +190 -157
packages/react-reconciler/src/ReactFiberPendingPriority.js
+117 -131
@@ -12,30 +12,26 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
12
13 import {NoWork} from './ReactFiberExpirationTime';
14
15 -import {enableSuspense} from 'shared/ReactFeatureFlags';
16 -
15 // TODO: Offscreen updates
16
17 export function markPendingPriorityLevel(
18 root: FiberRoot,
19 expirationTime: ExpirationTime,
20 ): void {
23 - if (enableSuspense) {
24 - // Update the latest and earliest pending times
25 - const earliestPendingTime = root.earliestPendingTime;
26 - if (earliestPendingTime === NoWork) {
27 - // No other pending updates.
28 - root.earliestPendingTime = root.latestPendingTime = expirationTime;
21 + // Update the latest and earliest pending times
22 + const earliestPendingTime = root.earliestPendingTime;
23 + if (earliestPendingTime === NoWork) {
24 + // No other pending updates.
25 + root.earliestPendingTime = root.latestPendingTime = expirationTime;
26 + } else {
27 + if (earliestPendingTime > expirationTime) {
28 + // This is the earliest pending update.
29 + root.earliestPendingTime = expirationTime;
30 } else {
30 - if (earliestPendingTime > expirationTime) {
31 - // This is the earliest pending update.
32 - root.earliestPendingTime = expirationTime;
33 - } else {
34 - const latestPendingTime = root.latestPendingTime;
35 - if (latestPendingTime < expirationTime) {
36 - // This is the latest pending update
37 - root.latestPendingTime = expirationTime;
38 - }
31 + const latestPendingTime = root.latestPendingTime;
32 + if (latestPendingTime < expirationTime) {
33 + // This is the latest pending update
34 + root.latestPendingTime = expirationTime;
35 }
36 }
37 }
@@ -46,114 +42,110 @@ export function markCommittedPriorityLevels(
42 currentTime: ExpirationTime,
43 earliestRemainingTime: ExpirationTime,
44 ): void {
49 - if (enableSuspense) {
50 - if (earliestRemainingTime === NoWork) {
51 - // Fast path. There's no remaining work. Clear everything.
52 - root.earliestPendingTime = NoWork;
53 - root.latestPendingTime = NoWork;
54 - root.earliestSuspendedTime = NoWork;
55 - root.latestSuspendedTime = NoWork;
56 - root.latestPingedTime = NoWork;
57 - return;
58 - }
45 + if (earliestRemainingTime === NoWork) {
46 + // Fast path. There's no remaining work. Clear everything.
47 + root.earliestPendingTime = NoWork;
48 + root.latestPendingTime = NoWork;
49 + root.earliestSuspendedTime = NoWork;
50 + root.latestSuspendedTime = NoWork;
51 + root.latestPingedTime = NoWork;
52 + return;
53 + }
54
60 - // Let's see if the previous latest known pending level was just flushed.
61 - const latestPendingTime = root.latestPendingTime;
62 - if (latestPendingTime !== NoWork) {
63 - if (latestPendingTime < earliestRemainingTime) {
64 - // We've flushed all the known pending levels.
65 - root.earliestPendingTime = root.latestPendingTime = NoWork;
66 - } else {
67 - const earliestPendingTime = root.earliestPendingTime;
68 - if (earliestPendingTime < earliestRemainingTime) {
69 - // We've flushed the earliest known pending level. Set this to the
70 - // latest pending time.
71 - root.earliestPendingTime = root.latestPendingTime;
72 - }
55 + // Let's see if the previous latest known pending level was just flushed.
56 + const latestPendingTime = root.latestPendingTime;
57 + if (latestPendingTime !== NoWork) {
58 + if (latestPendingTime < earliestRemainingTime) {
59 + // We've flushed all the known pending levels.
60 + root.earliestPendingTime = root.latestPendingTime = NoWork;
61 + } else {
62 + const earliestPendingTime = root.earliestPendingTime;
63 + if (earliestPendingTime < earliestRemainingTime) {
64 + // We've flushed the earliest known pending level. Set this to the
65 + // latest pending time.
66 + root.earliestPendingTime = root.latestPendingTime;
67 }
68 }
69 + }
70
76 - // Now let's handle the earliest remaining level in the whole tree. We need to
77 - // decide whether to treat it as a pending level or as suspended. Check
78 - // it falls within the range of known suspended levels.
79 -
80 - const earliestSuspendedTime = root.earliestSuspendedTime;
81 - if (earliestSuspendedTime === NoWork) {
82 - // There's no suspended work. Treat the earliest remaining level as a
83 - // pending level.
84 - markPendingPriorityLevel(root, earliestRemainingTime);
85 - return;
86 - }
71 + // Now let's handle the earliest remaining level in the whole tree. We need to
72 + // decide whether to treat it as a pending level or as suspended. Check
73 + // it falls within the range of known suspended levels.
74
88 - const latestSuspendedTime = root.latestSuspendedTime;
89 - if (earliestRemainingTime > latestSuspendedTime) {
90 - // The earliest remaining level is later than all the suspended work. That
91 - // means we've flushed all the suspended work.
92 - root.earliestSuspendedTime = NoWork;
93 - root.latestSuspendedTime = NoWork;
94 - root.latestPingedTime = NoWork;
95 -
96 - // There's no suspended work. Treat the earliest remaining level as a
97 - // pending level.
98 - markPendingPriorityLevel(root, earliestRemainingTime);
99 - return;
100 - }
75 + const earliestSuspendedTime = root.earliestSuspendedTime;
76 + if (earliestSuspendedTime === NoWork) {
77 + // There's no suspended work. Treat the earliest remaining level as a
78 + // pending level.
79 + markPendingPriorityLevel(root, earliestRemainingTime);
80 + return;
81 + }
82
102 - if (earliestRemainingTime < earliestSuspendedTime) {
103 - // The earliest remaining time is earlier than all the suspended work.
104 - // Treat it as a pending update.
105 - markPendingPriorityLevel(root, earliestRemainingTime);
106 - return;
107 - }
83 + const latestSuspendedTime = root.latestSuspendedTime;
84 + if (earliestRemainingTime > latestSuspendedTime) {
85 + // The earliest remaining level is later than all the suspended work. That
86 + // means we've flushed all the suspended work.
87 + root.earliestSuspendedTime = NoWork;
88 + root.latestSuspendedTime = NoWork;
89 + root.latestPingedTime = NoWork;
90 +
91 + // There's no suspended work. Treat the earliest remaining level as a
92 + // pending level.
93 + markPendingPriorityLevel(root, earliestRemainingTime);
94 + return;
95 + }
96
109 - // The earliest remaining time falls within the range of known suspended
110 - // levels. We should treat this as suspended work.
97 + if (earliestRemainingTime < earliestSuspendedTime) {
98 + // The earliest remaining time is earlier than all the suspended work.
99 + // Treat it as a pending update.
100 + markPendingPriorityLevel(root, earliestRemainingTime);
101 + return;
102 }
103 +
104 + // The earliest remaining time falls within the range of known suspended
105 + // levels. We should treat this as suspended work.
106 }
107
108 export function markSuspendedPriorityLevel(
109 root: FiberRoot,
110 suspendedTime: ExpirationTime,
111 ): void {
118 - if (enableSuspense) {
119 - // First, check the known pending levels and update them if needed.
120 - const earliestPendingTime = root.earliestPendingTime;
121 - const latestPendingTime = root.latestPendingTime;
122 - if (earliestPendingTime === suspendedTime) {
123 - if (latestPendingTime === suspendedTime) {
124 - // Both known pending levels were suspended. Clear them.
125 - root.earliestPendingTime = root.latestPendingTime = NoWork;
126 - } else {
127 - // The earliest pending level was suspended. Clear by setting it to the
128 - // latest pending level.
129 - root.earliestPendingTime = latestPendingTime;
130 - }
131 - } else if (latestPendingTime === suspendedTime) {
132 - // The latest pending level was suspended. Clear by setting it to the
112 + // First, check the known pending levels and update them if needed.
113 + const earliestPendingTime = root.earliestPendingTime;
114 + const latestPendingTime = root.latestPendingTime;
115 + if (earliestPendingTime === suspendedTime) {
116 + if (latestPendingTime === suspendedTime) {
117 + // Both known pending levels were suspended. Clear them.
118 + root.earliestPendingTime = root.latestPendingTime = NoWork;
119 + } else {
120 + // The earliest pending level was suspended. Clear by setting it to the
121 // latest pending level.
134 - root.latestPendingTime = earliestPendingTime;
122 + root.earliestPendingTime = latestPendingTime;
123 }
124 + } else if (latestPendingTime === suspendedTime) {
125 + // The latest pending level was suspended. Clear by setting it to the
126 + // latest pending level.
127 + root.latestPendingTime = earliestPendingTime;
128 + }
129
137 - // Next, if we're working on the lowest known suspended level, clear the ping.
138 - // TODO: What if a promise suspends and pings before the root completes?
139 - const latestSuspendedTime = root.latestSuspendedTime;
140 - if (latestSuspendedTime === suspendedTime) {
141 - root.latestPingedTime = NoWork;
142 - }
130 + // Next, if we're working on the lowest known suspended level, clear the ping.
131 + // TODO: What if a promise suspends and pings before the root completes?
132 + const latestSuspendedTime = root.latestSuspendedTime;
133 + if (latestSuspendedTime === suspendedTime) {
134 + root.latestPingedTime = NoWork;
135 + }
136
144 - // Finally, update the known suspended levels.
145 - const earliestSuspendedTime = root.earliestSuspendedTime;
146 - if (earliestSuspendedTime === NoWork) {
147 - // No other suspended levels.
148 - root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;
149 - } else {
150 - if (earliestSuspendedTime > suspendedTime) {
151 - // This is the earliest suspended level.
152 - root.earliestSuspendedTime = suspendedTime;
153 - } else if (latestSuspendedTime < suspendedTime) {
154 - // This is the latest suspended level
155 - root.latestSuspendedTime = suspendedTime;
156 - }
137 + // Finally, update the known suspended levels.
138 + const earliestSuspendedTime = root.earliestSuspendedTime;
139 + if (earliestSuspendedTime === NoWork) {
140 + // No other suspended levels.
141 + root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;
142 + } else {
143 + if (earliestSuspendedTime > suspendedTime) {
144 + // This is the earliest suspended level.
145 + root.earliestSuspendedTime = suspendedTime;
146 + } else if (latestSuspendedTime < suspendedTime) {
147 + // This is the latest suspended level
148 + root.latestSuspendedTime = suspendedTime;
149 }
150 }
151 }
@@ -162,35 +154,29 @@ export function markPingedPriorityLevel(
154 root: FiberRoot,
155 pingedTime: ExpirationTime,
156 ): void {
165 - if (enableSuspense) {
166 - const latestSuspendedTime = root.latestSuspendedTime;
167 - if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) {
168 - const latestPingedTime = root.latestPingedTime;
169 - if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
170 - root.latestPingedTime = pingedTime;
171 - }
157 + const latestSuspendedTime = root.latestSuspendedTime;
158 + if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) {
159 + const latestPingedTime = root.latestPingedTime;
160 + if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
161 + root.latestPingedTime = pingedTime;
162 }
163 }
164 }
165
166 export function findNextPendingPriorityLevel(root: FiberRoot): ExpirationTime {
177 - if (enableSuspense) {
178 - const earliestSuspendedTime = root.earliestSuspendedTime;
179 - const earliestPendingTime = root.earliestPendingTime;
180 - if (earliestSuspendedTime === NoWork) {
181 - // Fast path. There's no suspended work.
182 - return earliestPendingTime;
183 - }
184 -
185 - // First, check if there's known pending work.
186 - if (earliestPendingTime !== NoWork) {
187 - return earliestPendingTime;
188 - }
167 + const earliestSuspendedTime = root.earliestSuspendedTime;
168 + const earliestPendingTime = root.earliestPendingTime;
169 + if (earliestSuspendedTime === NoWork) {
170 + // Fast path. There's no suspended work.
171 + return earliestPendingTime;
172 + }
173
190 - // Finally, if a suspended level was pinged, work on that. Otherwise there's
191 - // nothing to work on.
192 - return root.latestPingedTime;
193 - } else {
194 - return root.current.expirationTime;
174 + // First, check if there's known pending work.
175 + if (earliestPendingTime !== NoWork) {
176 + return earliestPendingTime;
177 }
178 +
179 + // Finally, if a suspended level was pinged, work on that. Otherwise there's
180 + // nothing to work on.
181 + return root.latestPingedTime;
182 }
packages/react-reconciler/src/ReactFiberScheduler.js
+26 -24
@@ -237,8 +237,6 @@ let nextEffect: Fiber | null = null;
237
238 let isCommitting: boolean = false;
239
240 -let isRootReadyForCommit: boolean = false;
241 -
240 let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
241
242 // Used for performance tracking.
@@ -349,8 +347,6 @@ function resetStack() {
347 nextLatestTimeoutMs = -1;
348 nextRenderIsExpired = false;
349 nextUnitOfWork = null;
352 -
353 - isRootReadyForCommit = false;
350 }
351
352 function commitAllHostEffects() {
@@ -518,7 +514,12 @@ function commitRoot(finishedWork: Fiber): ExpirationTime {
514 );
515 root.pendingCommitExpirationTime = NoWork;
516
517 + // Update the pending priority levels to account for the work that we are
518 + // about to commit. This needs to happen before calling the lifecycles, since
519 + // they may schedule additional updates.
520 + const earliestRemainingTime = finishedWork.expirationTime;
521 const currentTime = recalculateCurrentTime();
522 + markCommittedPriorityLevels(root, currentTime, earliestRemainingTime);
523
524 // Reset this to null before calling lifecycles
525 ReactCurrentOwner.current = null;
@@ -689,14 +690,13 @@ function commitRoot(finishedWork: Fiber): ExpirationTime {
690 ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
691 }
692
692 - markCommittedPriorityLevels(root, currentTime, root.current.expirationTime);
693 - const remainingTime = findNextPendingPriorityLevel(root);
694 - if (remainingTime === NoWork) {
693 + const nextPendingPriorityLevel = findNextPendingPriorityLevel(root);
694 + if (nextPendingPriorityLevel === NoWork) {
695 // If there's no remaining work, we can clear the set of already failed
696 // error boundaries.
697 legacyErrorBoundariesThatAlreadyFailed = null;
698 }
699 - return remainingTime;
699 + return nextPendingPriorityLevel;
700 }
701
702 function resetExpirationTime(
@@ -847,7 +847,6 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
847 continue;
848 } else {
849 // We've reached the root.
850 - isRootReadyForCommit = true;
850 return null;
851 }
852 } else {
@@ -1097,11 +1096,11 @@ function renderRoot(
1096 } while (true);
1097
1098 // We're done performing work. Time to clean up.
1100 - let didCompleteRoot = false;
1099 isWorking = false;
1100
1101 // Yield back to main thread.
1102 if (didFatal) {
1103 + const didCompleteRoot = false;
1104 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1105 interruptedBy = null;
1106 // There was a fatal error.
@@ -1111,34 +1110,38 @@ function renderRoot(
1110 return null;
1111 } else if (nextUnitOfWork === null) {
1112 // We reached the root.
1114 - if (isRootReadyForCommit) {
1115 - didCompleteRoot = true;
1113 + const rootWorkInProgress = root.current.alternate;
1114 + invariant(
1115 + rootWorkInProgress !== null,
1116 + 'Finished root should have a work-in-progress. This error is likely ' +
1117 + 'caused by a bug in React. Please file an issue.',
1118 + );
1119 + if ((rootWorkInProgress.effectTag & Incomplete) === NoEffect) {
1120 + const didCompleteRoot = true;
1121 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1122 interruptedBy = null;
1118 - // The root successfully completed. It's ready for commit.
1123 + // The root successfully completed.
1124 root.pendingCommitExpirationTime = expirationTime;
1120 - const finishedWork = root.current.alternate;
1121 - return finishedWork;
1125 + // Return the completed work-in-progress.
1126 + return rootWorkInProgress;
1127 } else {
1128 // The root did not complete.
1129 + const didCompleteRoot = false;
1130 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1131 interruptedBy = null;
1126 - invariant(
1127 - !nextRenderIsExpired,
1128 - 'Expired work should have completed. This error is likely caused ' +
1129 - 'by a bug in React. Please file an issue.',
1130 - );
1131 - markSuspendedPriorityLevel(root, expirationTime);
1132 if (nextLatestTimeoutMs >= 0) {
1133 setTimeout(() => {
1134 retrySuspendedRoot(root, expirationTime);
1135 }, nextLatestTimeoutMs);
1136 }
1137 + 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;
1142 }
1143 } else {
1144 + const didCompleteRoot = false;
1145 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1146 interruptedBy = null;
1147 // There's more work to do, but we ran out of time. Yield back to
@@ -1294,8 +1297,7 @@ function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
1297 return expirationTime;
1298 }
1299
1297 -// TODO: Rename this to scheduleTimeout or something
1298 -function suspendRoot(
1300 +function markTimeout(
1301 root: FiberRoot,
1302 thenable: Thenable,
1303 timeoutMs: number,
@@ -1978,7 +1980,7 @@ export {
1980 computeExpirationForFiber,
1981 captureCommitPhaseError,
1982 onUncaughtError,
1981 - suspendRoot,
1983 + markTimeout,
1984 retrySuspendedRoot,
1985 markLegacyErrorBoundaryAsFailed,
1986 isAlreadyFailedLegacyErrorBoundary,
packages/react-reconciler/src/ReactFiberUnwindWork.js
+16 -2
@@ -55,7 +55,7 @@ import {
55 recordElapsedActualRenderTime,
56 } from './ReactProfilerTimer';
57 import {
58 - suspendRoot,
58 + markTimeout,
59 onUncaughtError,
60 markLegacyErrorBoundaryAsFailed,
61 isAlreadyFailedLegacyErrorBoundary,
@@ -151,6 +151,20 @@ function throwException(
151 // Its effect list is no longer valid.
152 sourceFiber.firstEffect = sourceFiber.lastEffect = null;
153
154 + // Check if there is lower priority work, regardless of whether it's pending.
155 + // If so, it may have the effect of fixing the exception that was just thrown.
156 + const latestPendingTime = root.latestPendingTime;
157 + const latestSuspendedTime = root.latestSuspendedTime;
158 + if (
159 + renderExpirationTime !== latestPendingTime &&
160 + renderExpirationTime !== latestSuspendedTime &&
161 + renderExpirationTime !== Never
162 + ) {
163 + // There's lower priority work. Exit to suspend this render and retry at
164 + // the lower priority.
165 + return;
166 + }
167 +
168 if (
169 enableSuspense &&
170 value !== null &&
@@ -206,7 +220,7 @@ function throwException(
220
221 if (renderExpirationTime === Never || msUntilTimeout > 0) {
222 // There's still time remaining.
209 - suspendRoot(root, thenable, msUntilTimeout, renderExpirationTime);
223 + markTimeout(root, thenable, msUntilTimeout, renderExpirationTime);
224 const onResolveOrReject = () => {
225 retrySuspendedRoot(root, renderExpirationTime);
226 };
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+31
@@ -198,6 +198,37 @@ describe('ReactIncrementalErrorHandling', () => {
198 expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
199 });
200
201 + it("retries at a lower priority if there's additional pending work", () => {
202 + function App(props) {
203 + if (props.isBroken) {
204 + ReactNoop.yield('error');
205 + throw new Error('Oops!');
206 + }
207 + ReactNoop.yield('success');
208 + return <span prop="Everything is fine." />;
209 + }
210 +
211 + function onCommit() {
212 + ReactNoop.yield('commit');
213 + }
214 +
215 + ReactNoop.render(<App isBroken={true} />, onCommit);
216 + ReactNoop.expire(2000);
217 + ReactNoop.render(<App isBroken={false} />, onCommit);
218 +
219 + expect(ReactNoop.flush()).toEqual([
220 + // The first render fails. But because there's a lower priority pending
221 + // update, it doesn't throw.
222 + 'error',
223 + // Now we retry at the lower priority. This time it succeeds.
224 + 'success',
225 + // Nothing commits until the second update completes.
226 + 'commit',
227 + 'commit',
228 + ]);
229 + expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
230 + });
231 +
232 it('calls componentDidCatch multiple times for multiple errors', () => {
233 let id = 0;
234 class BadMount extends React.Component {