Add dedicated LanePriority for Suspense retries (#19287)
A "retry" is a special type of update that attempts to flip a Suspense boundary from its placeholder state back to its primary/resolved state. Currently, retries are given default priority, using the same algorithm as normal updates and occupying range of lanes. This adds a new range of lanes dedicated specifically to retries, and gives them lower priority than normal updates. A couple of existing tests were affected because retries are no longer batched with normal updates; they commit in separate batches. Not totally satisfied with this design, but I think things will snap more into place once the rest of the Lanes changes (like how we handle parallel Suspense transitions) have settled.
Andrew Clark committed
Jul 8, 2020 at 23:09 UTC
965d08cfff50609eed5d3f2317145f7be762423d
6 files changed
+181
-35
packages/react-reconciler/src/ReactFiberLane.js
+62
-26
@@ -26,7 +26,8 @@ export opaque type LanePriority =
26
| 13
27
| 14
28
| 15
29
- | 16;
29
+ | 16
30
+ | 17;
31
export opaque type Lanes = number;
32
export opaque type Lane = number;
33
export opaque type LaneMap<T> = Array<T>;
@@ -42,23 +43,25 @@ import {
43
NoPriority as NoSchedulerPriority,
44
} from './SchedulerWithReactIntegration.new';
45
45
-export const SyncLanePriority: LanePriority = 16;
46
-const SyncBatchedLanePriority: LanePriority = 15;
46
+export const SyncLanePriority: LanePriority = 17;
47
+const SyncBatchedLanePriority: LanePriority = 16;
48
48
-const InputDiscreteHydrationLanePriority: LanePriority = 14;
49
-export const InputDiscreteLanePriority: LanePriority = 13;
49
+const InputDiscreteHydrationLanePriority: LanePriority = 15;
50
+export const InputDiscreteLanePriority: LanePriority = 14;
51
51
-const InputContinuousHydrationLanePriority: LanePriority = 12;
52
-export const InputContinuousLanePriority: LanePriority = 11;
52
+const InputContinuousHydrationLanePriority: LanePriority = 13;
53
+export const InputContinuousLanePriority: LanePriority = 12;
54
54
-const DefaultHydrationLanePriority: LanePriority = 10;
55
-export const DefaultLanePriority: LanePriority = 9;
55
+const DefaultHydrationLanePriority: LanePriority = 11;
56
+export const DefaultLanePriority: LanePriority = 10;
57
57
-const TransitionShortHydrationLanePriority: LanePriority = 8;
58
-export const TransitionShortLanePriority: LanePriority = 7;
58
+const TransitionShortHydrationLanePriority: LanePriority = 9;
59
+export const TransitionShortLanePriority: LanePriority = 8;
60
60
-const TransitionLongHydrationLanePriority: LanePriority = 6;
61
-export const TransitionLongLanePriority: LanePriority = 5;
61
+const TransitionLongHydrationLanePriority: LanePriority = 7;
62
+export const TransitionLongLanePriority: LanePriority = 6;
63
+
64
+const RetryLanePriority: LanePriority = 5;
65
66
const SelectiveHydrationLanePriority: LanePriority = 4;
67
@@ -90,25 +93,30 @@ const InputContinuousUpdateRangeStart = 6;
93
const InputContinuousUpdateRangeEnd = 8;
94
95
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;
93
-const DefaultLanes: Lanes = /* */ 0b0000000000000000011111100000000;
96
+export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111100000000;
97
const DefaultUpdateRangeStart = 9;
95
-const DefaultUpdateRangeEnd = 14;
98
+const DefaultUpdateRangeEnd = 12;
99
+
100
+const TransitionShortHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
101
+const TransitionShortLanes: Lanes = /* */ 0b0000000000000011111000000000000;
102
+const TransitionShortUpdateRangeStart = 13;
103
+const TransitionShortUpdateRangeEnd = 17;
104
97
-const TransitionShortHydrationLane: Lane = /* */ 0b0000000000000000100000000000000;
98
-const TransitionShortLanes: Lanes = /* */ 0b0000000000011111100000000000000;
99
-const TransitionShortUpdateRangeStart = 15;
100
-const TransitionShortUpdateRangeEnd = 20;
105
+const TransitionLongHydrationLane: Lane = /* */ 0b0000000000000100000000000000000;
106
+const TransitionLongLanes: Lanes = /* */ 0b0000000001111100000000000000000;
107
+const TransitionLongUpdateRangeStart = 18;
108
+const TransitionLongUpdateRangeEnd = 22;
109
102
-const TransitionLongHydrationLane: Lane = /* */ 0b0000000000100000000000000000000;
103
-const TransitionLongLanes: Lanes = /* */ 0b0000011111100000000000000000000;
104
-const TransitionLongUpdateRangeStart = 21;
105
-const TransitionLongUpdateRangeEnd = 26;
110
+// Includes all updates. Except Idle updates, which have special semantics.
111
+const UpdateRangeEnd = TransitionLongUpdateRangeEnd;
112
107
-export const SelectiveHydrationLane: Lane = /* */ 0b0000110000000000000000000000000;
113
+const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
114
+const RetryRangeStart = 22;
115
+const RetryRangeEnd = 26;
116
+
117
+export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
118
const SelectiveHydrationRangeEnd = 27;
119
110
-// Includes all non-Idle updates
111
-const UpdateRangeEnd = 27;
120
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
121
122
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
@@ -206,6 +214,12 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
214
return transitionLongLanes;
215
}
216
}
217
+ const retryLanes = RetryLanes & lanes;
218
+ if (retryLanes !== NoLanes) {
219
+ return_highestLanePriority = RetryLanePriority;
220
+ return_updateRangeEnd = RetryRangeEnd;
221
+ return retryLanes;
222
+ }
223
if (lanes & SelectiveHydrationLane) {
224
return_highestLanePriority = SelectiveHydrationLanePriority;
225
return_updateRangeEnd = SelectiveHydrationRangeEnd;
@@ -275,6 +289,7 @@ export function lanePriorityToSchedulerPriority(
289
case TransitionLongHydrationLanePriority:
290
case TransitionLongLanePriority:
291
case SelectiveHydrationLanePriority:
292
+ case RetryLanePriority:
293
return NormalSchedulerPriority;
294
case IdleHydrationLanePriority:
295
case IdleLanePriority:
@@ -537,6 +552,9 @@ export function findUpdateLane(
552
case TransitionLongLanePriority:
553
// Should be handled by findTransitionLane instead
554
break;
555
+ case RetryLanePriority:
556
+ // Should be handled by findRetryLane instead
557
+ break;
558
case IdleLanePriority:
559
let lane = findLane(IdleUpdateRangeStart, IdleUpdateRangeEnd, wipLanes);
560
if (lane === NoLane) {
@@ -604,6 +622,19 @@ export function findTransitionLane(
622
);
623
}
624
625
+// To ensure consistency across multiple updates in the same event, this should
626
+// be pure function, so that it always returns the same lane for given inputs.
627
+export function findRetryLane(wipLanes: Lanes): Lane {
628
+ // This is a fork of `findUpdateLane` designed specifically for Suspense
629
+ // "retries" — a special update that attempts to flip a Suspense boundary
630
+ // from its placeholder state to its primary/resolved state.
631
+ let lane = findLane(RetryRangeStart, RetryRangeEnd, wipLanes);
632
+ if (lane === NoLane) {
633
+ lane = pickArbitraryLane(RetryLanes);
634
+ }
635
+ return lane;
636
+}
637
+
638
function findLane(start, end, skipLanes) {
639
// This finds the first bit between the `start` and `end` positions that isn't
640
// in `skipLanes`.
@@ -808,6 +839,11 @@ export function getBumpedLaneForHydration(
839
case TransitionLongLanePriority:
840
lane = TransitionLongHydrationLane;
841
break;
842
+ case RetryLanePriority:
843
+ // Shouldn't be reachable under normal circumstances, so there's no
844
+ // dedicated lane for retry priority. Use the one for long transitions.
845
+ lane = TransitionLongHydrationLane;
846
+ break;
847
case SelectiveHydrationLanePriority:
848
lane = SelectiveHydrationLane;
849
break;
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+24
-4
@@ -125,6 +125,7 @@ import {
125
NoTimestamp,
126
findUpdateLane,
127
findTransitionLane,
128
+ findRetryLane,
129
includesSomeLane,
130
isSubsetOfLanes,
131
mergeLanes,
@@ -472,6 +473,28 @@ export function requestUpdateLane(
473
return lane;
474
}
475
476
+function requestRetryLane(fiber: Fiber) {
477
+ // This is a fork of `requestUpdateLane` designed specifically for Suspense
478
+ // "retries" — a special update that attempts to flip a Suspense boundary
479
+ // from its placeholder state to its primary/resolved state.
480
+
481
+ // Special cases
482
+ const mode = fiber.mode;
483
+ if ((mode & BlockingMode) === NoMode) {
484
+ return (SyncLane: Lane);
485
+ } else if ((mode & ConcurrentMode) === NoMode) {
486
+ return getCurrentPriorityLevel() === ImmediateSchedulerPriority
487
+ ? (SyncLane: Lane)
488
+ : (SyncBatchedLane: Lane);
489
+ }
490
+
491
+ // See `requestUpdateLane` for explanation of `currentEventWipLanes`
492
+ if (currentEventWipLanes === NoLanes) {
493
+ currentEventWipLanes = workInProgressRootIncludedLanes;
494
+ }
495
+ return findRetryLane(currentEventWipLanes);
496
+}
497
+
498
export function scheduleUpdateOnFiber(
499
fiber: Fiber,
500
lane: Lane,
@@ -2708,10 +2731,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
2731
// suspended it has resolved, which means at least part of the tree was
2732
// likely unblocked. Try rendering again, at a new expiration time.
2733
if (retryLane === NoLane) {
2711
- const suspenseConfig = null; // Retries don't carry over the already committed update.
2712
- // TODO: Should retries get their own lane? Maybe it could share with
2713
- // transitions.
2714
- retryLane = requestUpdateLane(boundaryFiber, suspenseConfig);
2734
+ retryLane = requestRetryLane(boundaryFiber);
2735
}
2736
// TODO: Special case idle priority?
2737
const eventTime = requestEventTime();
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+24
-4
@@ -148,6 +148,7 @@ import {
148
NoTimestamp,
149
findUpdateLane,
150
findTransitionLane,
151
+ findRetryLane,
152
includesSomeLane,
153
isSubsetOfLanes,
154
mergeLanes,
@@ -495,6 +496,28 @@ export function requestUpdateLane(
496
return lane;
497
}
498
499
+function requestRetryLane(fiber: Fiber) {
500
+ // This is a fork of `requestUpdateLane` designed specifically for Suspense
501
+ // "retries" — a special update that attempts to flip a Suspense boundary
502
+ // from its placeholder state to its primary/resolved state.
503
+
504
+ // Special cases
505
+ const mode = fiber.mode;
506
+ if ((mode & BlockingMode) === NoMode) {
507
+ return (SyncLane: Lane);
508
+ } else if ((mode & ConcurrentMode) === NoMode) {
509
+ return getCurrentPriorityLevel() === ImmediateSchedulerPriority
510
+ ? (SyncLane: Lane)
511
+ : (SyncBatchedLane: Lane);
512
+ }
513
+
514
+ // See `requestUpdateLane` for explanation of `currentEventWipLanes`
515
+ if (currentEventWipLanes === NoLanes) {
516
+ currentEventWipLanes = workInProgressRootIncludedLanes;
517
+ }
518
+ return findRetryLane(currentEventWipLanes);
519
+}
520
+
521
export function scheduleUpdateOnFiber(
522
fiber: Fiber,
523
lane: Lane,
@@ -2855,10 +2878,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
2878
// suspended it has resolved, which means at least part of the tree was
2879
// likely unblocked. Try rendering again, at a new expiration time.
2880
if (retryLane === NoLane) {
2858
- const suspenseConfig = null; // Retries don't carry over the already committed update.
2859
- // TODO: Should retries get their own lane? Maybe it could share with
2860
- // transitions.
2861
- retryLane = requestUpdateLane(boundaryFiber, suspenseConfig);
2881
+ retryLane = requestRetryLane(boundaryFiber);
2882
}
2883
// TODO: Special case idle priority?
2884
const eventTime = requestEventTime();
packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.internal.js
+5
@@ -219,6 +219,11 @@ describe('ReactSuspense', () => {
219
await resolve1();
220
ReactNoop.render(element);
221
expect(Scheduler).toFlushWithoutYielding();
222
+
223
+ // Force fallback to commit.
224
+ // TODO: Should be able to use `act` here.
225
+ jest.runAllTimers();
226
+
227
expect(ReactNoop.getChildren()).toEqual([
228
text('Waiting Tier 2'),
229
text('Done'),
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+66
@@ -623,6 +623,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
623
// The new reconciler batches everything together, so it finishes without
624
// suspending again.
625
'Sibling',
626
+
627
+ // NOTE: The final of the update got pushed into a lower priority range of
628
+ // lanes, leading to the extra intermediate render. This is because when
629
+ // we schedule the fourth update, we're already in the middle of rendering
630
+ // the three others. Since there are only three lanes in the default
631
+ // range, the fourth lane is shifted to slightly lower priority. This
632
+ // could easily change when we tweak our batching heuristics. Ideally,
633
+ // they'd all have default priority and render in a single batch.
634
+ 'Suspend! [Step 3]',
635
+ 'Sibling',
636
+
637
'Step 4',
638
]);
639
});
@@ -3896,4 +3907,59 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3907
expect(Scheduler).toHaveYielded(['Promise resolved [B]', 'B']);
3908
expect(root).toMatchRenderedOutput(<span prop="B" />);
3909
});
3910
+
3911
+ it('retries have lower priority than normal updates', async () => {
3912
+ const {useState} = React;
3913
+
3914
+ let setText;
3915
+ function UpdatingText() {
3916
+ const [text, _setText] = useState('A');
3917
+ setText = _setText;
3918
+ return <Text text={text} />;
3919
+ }
3920
+
3921
+ const root = ReactNoop.createRoot();
3922
+ await ReactNoop.act(async () => {
3923
+ root.render(
3924
+ <>
3925
+ <UpdatingText />
3926
+ <Suspense fallback={<Text text="Loading..." />}>
3927
+ <AsyncText text="Async" />
3928
+ </Suspense>
3929
+ </>,
3930
+ );
3931
+ });
3932
+ expect(Scheduler).toHaveYielded(['A', 'Suspend! [Async]', 'Loading...']);
3933
+ expect(root).toMatchRenderedOutput(
3934
+ <>
3935
+ <span prop="A" />
3936
+ <span prop="Loading..." />
3937
+ </>,
3938
+ );
3939
+
3940
+ await ReactNoop.act(async () => {
3941
+ // Resolve the promise. This will trigger a retry.
3942
+ await resolveText('Async');
3943
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
3944
+ // Before the retry happens, schedule a new update.
3945
+ setText('B');
3946
+
3947
+ // The update should be allowed to finish before the retry is attempted.
3948
+ expect(Scheduler).toFlushUntilNextPaint(['B']);
3949
+ expect(root).toMatchRenderedOutput(
3950
+ <>
3951
+ <span prop="B" />
3952
+ <span prop="Loading..." />
3953
+ </>,
3954
+ );
3955
+ });
3956
+ // Then do the retry.
3957
+ expect(Scheduler).toHaveYielded(['Async']);
3958
+ expect(root).toMatchRenderedOutput(
3959
+ <>
3960
+ <span prop="B" />
3961
+ <span prop="Async" />
3962
+ </>,
3963
+ );
3964
+ });
3965
});
packages/react/src/__tests__/ReactProfiler-test.internal.js
-1
@@ -3923,7 +3923,6 @@ describe('Profiler', () => {
3923
3924
expect(onPostCommit).toHaveBeenCalledTimes(1);
3925
expect(onPostCommit.mock.calls[0][4]).toMatchInteractions([
3926
- initialRenderInteraction,
3926
highPriUpdateInteraction,
3927
]);
3928