Skip to content

Commit f15f8f6

Browse files
authored
Store interleaved updates on separate queue until end of render (#20615)
## Motivation An *interleaved* update is one that is scheduled while a render is already in progress, typically from a concurrent user input event. We have to take care not to process these updates during the current render, because a multiple interleaved updates may have been scheduled across many components; to avoid tearing, we cannot render some of those updates without rendering all of them. ## Old approach What we currently do when we detect an interleaved update is assign a lane that is not part of the current render. This has some unfortunate drawbacks. For example, we will eventually run out of lanes at a given priority level. When this happens, our last resort is to interrupt the current render and start over from scratch. If this happens enough, it can lead to starvation. More concerning, there are a suprising number of places that must separately account for this case, often in subtle ways. The maintenance complexity has led to a number of tearing bugs. ## New approach I added a new field to the update queue, `interleaved`. It's a linked list, just like the `pending` field. When an interleaved update is scheduled, we add it to the `interleaved` list instead of `pending`. Then we push the entire queue object onto a global array. When the current render exits, we iterate through the array of interleaved queues and transfer the `interleaved` list to the `pending` list. So, until the current render has exited (whether due to a commit or an interruption), it's impossible to process an interleaved update, because they have not yet been enqueued. In this new approach, we don't need to resort to clever lanes tricks to avoid inconsistencies. This should allow us to simplify a lot of the logic that's currently in ReactFiberWorkLoop and ReactFiberLane, especially `findUpdateLane` and `getNextLanes`. All the logic for interleaved updates is isolated to one place.
1 parent 0fd6805 commit f15f8f6

9 files changed

+339
-36
lines changed

packages/react-reconciler/src/ReactFiberClassComponent.new.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ const classComponentUpdater = {
213213
update.callback = callback;
214214
}
215215

216-
enqueueUpdate(fiber, update);
216+
enqueueUpdate(fiber, update, lane);
217217
scheduleUpdateOnFiber(fiber, lane, eventTime);
218218

219219
if (__DEV__) {
@@ -245,7 +245,7 @@ const classComponentUpdater = {
245245
update.callback = callback;
246246
}
247247

248-
enqueueUpdate(fiber, update);
248+
enqueueUpdate(fiber, update, lane);
249249
scheduleUpdateOnFiber(fiber, lane, eventTime);
250250

251251
if (__DEV__) {
@@ -276,7 +276,7 @@ const classComponentUpdater = {
276276
update.callback = callback;
277277
}
278278

279-
enqueueUpdate(fiber, update);
279+
enqueueUpdate(fiber, update, lane);
280280
scheduleUpdateOnFiber(fiber, lane, eventTime);
281281

282282
if (__DEV__) {

packages/react-reconciler/src/ReactFiberHooks.new.js

+59-13
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
warnIfNotCurrentlyActingUpdatesInDev,
7676
warnIfNotScopedWithMatchingAct,
7777
markSkippedUpdateLanes,
78+
isInterleavedUpdate,
7879
} from './ReactFiberWorkLoop.new';
7980

8081
import invariant from 'shared/invariant';
@@ -104,6 +105,7 @@ import {logStateUpdateScheduled} from './DebugTracing';
104105
import {markStateUpdateScheduled} from './SchedulingProfiler';
105106
import {CacheContext} from './ReactFiberCacheComponent.new';
106107
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
108+
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
107109

108110
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
109111

@@ -116,8 +118,9 @@ type Update<S, A> = {|
116118
priority?: ReactPriorityLevel,
117119
|};
118120

119-
type UpdateQueue<S, A> = {|
121+
export type UpdateQueue<S, A> = {|
120122
pending: Update<S, A> | null,
123+
interleaved: Update<S, A> | null,
121124
dispatch: (A => mixed) | null,
122125
lastRenderedReducer: ((S, A) => S) | null,
123126
lastRenderedState: S | null,
@@ -650,6 +653,7 @@ function mountReducer<S, I, A>(
650653
hook.memoizedState = hook.baseState = initialState;
651654
const queue = (hook.queue = {
652655
pending: null,
656+
interleaved: null,
653657
dispatch: null,
654658
lastRenderedReducer: reducer,
655659
lastRenderedState: (initialState: any),
@@ -792,6 +796,23 @@ function updateReducer<S, I, A>(
792796
queue.lastRenderedState = newState;
793797
}
794798

799+
// Interleaved updates are stored on a separate queue. We aren't going to
800+
// process them during this render, but we do need to track which lanes
801+
// are remaining.
802+
const lastInterleaved = queue.interleaved;
803+
if (lastInterleaved !== null) {
804+
let interleaved = lastInterleaved;
805+
do {
806+
const interleavedLane = interleaved.lane;
807+
currentlyRenderingFiber.lanes = mergeLanes(
808+
currentlyRenderingFiber.lanes,
809+
interleavedLane,
810+
);
811+
markSkippedUpdateLanes(interleavedLane);
812+
interleaved = ((interleaved: any).next: Update<S, A>);
813+
} while (interleaved !== lastInterleaved);
814+
}
815+
795816
const dispatch: Dispatch<A> = (queue.dispatch: any);
796817
return [hook.memoizedState, dispatch];
797818
}
@@ -1080,6 +1101,7 @@ function useMutableSource<Source, Snapshot>(
10801101
// including any interleaving updates that occur.
10811102
const newQueue = {
10821103
pending: null,
1104+
interleaved: null,
10831105
dispatch: null,
10841106
lastRenderedReducer: basicStateReducer,
10851107
lastRenderedState: snapshot,
@@ -1135,6 +1157,7 @@ function mountState<S>(
11351157
hook.memoizedState = hook.baseState = initialState;
11361158
const queue = (hook.queue = {
11371159
pending: null,
1160+
interleaved: null,
11381161
dispatch: null,
11391162
lastRenderedReducer: basicStateReducer,
11401163
lastRenderedState: (initialState: any),
@@ -1812,7 +1835,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
18121835
cache: seededCache,
18131836
};
18141837
refreshUpdate.payload = payload;
1815-
enqueueUpdate(provider, refreshUpdate);
1838+
enqueueUpdate(provider, refreshUpdate, lane);
18161839
return;
18171840
}
18181841
}
@@ -1847,17 +1870,6 @@ function dispatchAction<S, A>(
18471870
next: (null: any),
18481871
};
18491872

1850-
// Append the update to the end of the list.
1851-
const pending = queue.pending;
1852-
if (pending === null) {
1853-
// This is the first update. Create a circular list.
1854-
update.next = update;
1855-
} else {
1856-
update.next = pending.next;
1857-
pending.next = update;
1858-
}
1859-
queue.pending = update;
1860-
18611873
const alternate = fiber.alternate;
18621874
if (
18631875
fiber === currentlyRenderingFiber ||
@@ -1867,7 +1879,41 @@ function dispatchAction<S, A>(
18671879
// queue -> linked list of updates. After this render pass, we'll restart
18681880
// and apply the stashed updates on top of the work-in-progress hook.
18691881
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
1882+
const pending = queue.pending;
1883+
if (pending === null) {
1884+
// This is the first update. Create a circular list.
1885+
update.next = update;
1886+
} else {
1887+
update.next = pending.next;
1888+
pending.next = update;
1889+
}
1890+
queue.pending = update;
18701891
} else {
1892+
if (isInterleavedUpdate(fiber, lane)) {
1893+
const interleaved = queue.interleaved;
1894+
if (interleaved === null) {
1895+
// This is the first update. Create a circular list.
1896+
update.next = update;
1897+
// At the end of the current render, this queue's interleaved updates will
1898+
// be transfered to the pending queue.
1899+
pushInterleavedQueue(queue);
1900+
} else {
1901+
update.next = interleaved.next;
1902+
interleaved.next = update;
1903+
}
1904+
queue.interleaved = update;
1905+
} else {
1906+
const pending = queue.pending;
1907+
if (pending === null) {
1908+
// This is the first update. Create a circular list.
1909+
update.next = update;
1910+
} else {
1911+
update.next = pending.next;
1912+
pending.next = update;
1913+
}
1914+
queue.pending = update;
1915+
}
1916+
18711917
if (
18721918
fiber.lanes === NoLanes &&
18731919
(alternate === null || alternate.lanes === NoLanes)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {UpdateQueue as HookQueue} from './ReactFiberHooks.new';
11+
import type {SharedQueue as ClassQueue} from './ReactUpdateQueue.new';
12+
13+
// An array of all update queues that received updates during the current
14+
// render. When this render exits, either because it finishes or because it is
15+
// interrupted, the interleaved updates will be transfered onto the main part
16+
// of the queue.
17+
let interleavedQueues: Array<
18+
HookQueue<any, any> | ClassQueue<any>,
19+
> | null = null;
20+
21+
export function pushInterleavedQueue(
22+
queue: HookQueue<any, any> | ClassQueue<any>,
23+
) {
24+
if (interleavedQueues === null) {
25+
interleavedQueues = [queue];
26+
} else {
27+
interleavedQueues.push(queue);
28+
}
29+
}
30+
31+
export function enqueueInterleavedUpdates() {
32+
// Transfer the interleaved updates onto the main queue. Each queue has a
33+
// `pending` field and an `interleaved` field. When they are not null, they
34+
// point to the last node in a circular linked list. We need to append the
35+
// interleaved list to the end of the pending list by joining them into a
36+
// single, circular list.
37+
if (interleavedQueues !== null) {
38+
for (let i = 0; i < interleavedQueues.length; i++) {
39+
const queue = interleavedQueues[i];
40+
const lastInterleavedUpdate = queue.interleaved;
41+
if (lastInterleavedUpdate !== null) {
42+
queue.interleaved = null;
43+
const firstInterleavedUpdate = lastInterleavedUpdate.next;
44+
const lastPendingUpdate = queue.pending;
45+
if (lastPendingUpdate !== null) {
46+
const firstPendingUpdate = lastPendingUpdate.next;
47+
lastPendingUpdate.next = (firstInterleavedUpdate: any);
48+
lastInterleavedUpdate.next = (firstPendingUpdate: any);
49+
}
50+
queue.pending = (lastInterleavedUpdate: any);
51+
}
52+
}
53+
interleavedQueues = null;
54+
}
55+
}

packages/react-reconciler/src/ReactFiberNewContext.new.js

+21-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {ReactContext} from 'shared/ReactTypes';
1111
import type {Fiber, ContextDependency} from './ReactInternalTypes';
1212
import type {StackCursor} from './ReactFiberStack.new';
1313
import type {Lanes} from './ReactFiberLane.new';
14+
import type {SharedQueue} from './ReactUpdateQueue.new';
1415

1516
import {isPrimaryRenderer} from './ReactFiberHostConfig';
1617
import {createCursor, push, pop} from './ReactFiberStack.new';
@@ -31,7 +32,7 @@ import {
3132

3233
import invariant from 'shared/invariant';
3334
import is from 'shared/objectIs';
34-
import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.new';
35+
import {createUpdate, ForceUpdate} from './ReactUpdateQueue.new';
3536
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new';
3637
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
3738

@@ -211,16 +212,30 @@ export function propagateContextChange<T>(
211212

212213
if (fiber.tag === ClassComponent) {
213214
// Schedule a force update on the work-in-progress.
214-
const update = createUpdate(
215-
NoTimestamp,
216-
pickArbitraryLane(renderLanes),
217-
);
215+
const lane = pickArbitraryLane(renderLanes);
216+
const update = createUpdate(NoTimestamp, lane);
218217
update.tag = ForceUpdate;
219218
// TODO: Because we don't have a work-in-progress, this will add the
220219
// update to the current fiber, too, which means it will persist even if
221220
// this render is thrown away. Since it's a race condition, not sure it's
222221
// worth fixing.
223-
enqueueUpdate(fiber, update);
222+
223+
// Inlined `enqueueUpdate` to remove interleaved update check
224+
const updateQueue = fiber.updateQueue;
225+
if (updateQueue === null) {
226+
// Only occurs if the fiber has been unmounted.
227+
} else {
228+
const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
229+
const pending = sharedQueue.pending;
230+
if (pending === null) {
231+
// This is the first update. Create a circular list.
232+
update.next = update;
233+
} else {
234+
update.next = pending.next;
235+
pending.next = update;
236+
}
237+
sharedQueue.pending = update;
238+
}
224239
}
225240
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
226241
const alternate = fiber.alternate;

packages/react-reconciler/src/ReactFiberReconciler.new.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export function updateContainer(
314314
update.callback = callback;
315315
}
316316

317-
enqueueUpdate(current, update);
317+
enqueueUpdate(current, update, lane);
318318
scheduleUpdateOnFiber(current, lane, eventTime);
319319

320320
return lane;

packages/react-reconciler/src/ReactFiberThrow.new.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ function throwException(
293293
// prevent a bail out.
294294
const update = createUpdate(NoTimestamp, SyncLane);
295295
update.tag = ForceUpdate;
296-
enqueueUpdate(sourceFiber, update);
296+
enqueueUpdate(sourceFiber, update, SyncLane);
297297
}
298298
}
299299

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ import {
204204
pop as popFromStack,
205205
createCursor,
206206
} from './ReactFiberStack.new';
207+
import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.new';
207208

208209
import {
209210
markNestedUpdateScheduled,
@@ -534,6 +535,7 @@ export function scheduleUpdateOnFiber(
534535
}
535536
}
536537

538+
// TODO: Consolidate with `isInterleavedUpdate` check
537539
if (root === workInProgressRoot) {
538540
// Received an update to a tree that's in the middle of rendering. Mark
539541
// that there was an interleaved update work on this root. Unless the
@@ -671,6 +673,22 @@ function markUpdateLaneFromFiberToRoot(
671673
}
672674
}
673675

676+
export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
677+
return (
678+
// TODO: Optimize slightly by comparing to root that fiber belongs to.
679+
// Requires some refactoring. Not a big deal though since it's rare for
680+
// concurrent apps to have more than a single root.
681+
workInProgressRoot !== null &&
682+
(fiber.mode & BlockingMode) !== NoMode &&
683+
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
684+
// then don't treat this as an interleaved update. This pattern is
685+
// accompanied by a warning but we haven't fully deprecated it yet. We can
686+
// remove once the deferRenderPhaseUpdateToNextBatch flag is enabled.
687+
(deferRenderPhaseUpdateToNextBatch ||
688+
(executionContext & RenderContext) === NoContext)
689+
);
690+
}
691+
674692
// Use this function to schedule a task for a root. There's only one task per
675693
// root; if a task was already scheduled, we'll check to make sure the priority
676694
// of the existing task is the same as the priority of the next level that the
@@ -1352,6 +1370,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13521370
workInProgressRootUpdatedLanes = NoLanes;
13531371
workInProgressRootPingedLanes = NoLanes;
13541372

1373+
enqueueInterleavedUpdates();
1374+
13551375
if (enableSchedulerTracing) {
13561376
spawnedWorkDuringRender = null;
13571377
}
@@ -2282,7 +2302,7 @@ function captureCommitPhaseErrorOnRoot(
22822302
) {
22832303
const errorInfo = createCapturedValue(error, sourceFiber);
22842304
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2285-
enqueueUpdate(rootFiber, update);
2305+
enqueueUpdate(rootFiber, update, (SyncLane: Lane));
22862306
const eventTime = requestEventTime();
22872307
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
22882308
if (root !== null) {
@@ -2319,7 +2339,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
23192339
errorInfo,
23202340
(SyncLane: Lane),
23212341
);
2322-
enqueueUpdate(fiber, update);
2342+
enqueueUpdate(fiber, update, (SyncLane: Lane));
23232343
const eventTime = requestEventTime();
23242344
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
23252345
if (root !== null) {

0 commit comments

Comments
 (0)