Skip to content

Commit 6d2a0d0

Browse files
committed
Store interleaved updates on separate queue until end of render
## 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 9a21507 commit 6d2a0d0

9 files changed

+339
-36
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ const classComponentUpdater = {
206206
update.callback = callback;
207207
}
208208

209-
enqueueUpdate(fiber, update);
209+
enqueueUpdate(fiber, update, lane);
210210
scheduleUpdateOnFiber(fiber, lane, eventTime);
211211

212212
if (__DEV__) {
@@ -238,7 +238,7 @@ const classComponentUpdater = {
238238
update.callback = callback;
239239
}
240240

241-
enqueueUpdate(fiber, update);
241+
enqueueUpdate(fiber, update, lane);
242242
scheduleUpdateOnFiber(fiber, lane, eventTime);
243243

244244
if (__DEV__) {
@@ -269,7 +269,7 @@ const classComponentUpdater = {
269269
update.callback = callback;
270270
}
271271

272-
enqueueUpdate(fiber, update);
272+
enqueueUpdate(fiber, update, lane);
273273
scheduleUpdateOnFiber(fiber, lane, eventTime);
274274

275275
if (__DEV__) {

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

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
warnIfNotCurrentlyActingUpdatesInDev,
6868
warnIfNotScopedWithMatchingAct,
6969
markSkippedUpdateLanes,
70+
isInterleavedUpdate,
7071
} from './ReactFiberWorkLoop.new';
7172

7273
import invariant from 'shared/invariant';
@@ -96,6 +97,7 @@ import {logStateUpdateScheduled} from './DebugTracing';
9697
import {markStateUpdateScheduled} from './SchedulingProfiler';
9798
import {CacheContext} from './ReactFiberCacheComponent.new';
9899
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
100+
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
99101

100102
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
101103

@@ -108,8 +110,9 @@ type Update<S, A> = {|
108110
priority?: ReactPriorityLevel,
109111
|};
110112

111-
type UpdateQueue<S, A> = {|
113+
export type UpdateQueue<S, A> = {|
112114
pending: Update<S, A> | null,
115+
interleaved: Update<S, A> | null,
113116
dispatch: (A => mixed) | null,
114117
lastRenderedReducer: ((S, A) => S) | null,
115118
lastRenderedState: S | null,
@@ -627,6 +630,7 @@ function mountReducer<S, I, A>(
627630
hook.memoizedState = hook.baseState = initialState;
628631
const queue = (hook.queue = {
629632
pending: null,
633+
interleaved: null,
630634
dispatch: null,
631635
lastRenderedReducer: reducer,
632636
lastRenderedState: (initialState: any),
@@ -769,6 +773,23 @@ function updateReducer<S, I, A>(
769773
queue.lastRenderedState = newState;
770774
}
771775

776+
// Interleaved updates are stored on a separate queue. We aren't going to
777+
// process them during this render, but we do need to track which lanes
778+
// are remaining.
779+
const lastInterleaved = queue.interleaved;
780+
if (lastInterleaved !== null) {
781+
let interleaved = lastInterleaved;
782+
do {
783+
const interleavedLane = interleaved.lane;
784+
currentlyRenderingFiber.lanes = mergeLanes(
785+
currentlyRenderingFiber.lanes,
786+
interleavedLane,
787+
);
788+
markSkippedUpdateLanes(interleavedLane);
789+
interleaved = ((interleaved: any).next: Update<S, A>);
790+
} while (interleaved !== lastInterleaved);
791+
}
792+
772793
const dispatch: Dispatch<A> = (queue.dispatch: any);
773794
return [hook.memoizedState, dispatch];
774795
}
@@ -1057,6 +1078,7 @@ function useMutableSource<Source, Snapshot>(
10571078
// including any interleaving updates that occur.
10581079
const newQueue = {
10591080
pending: null,
1081+
interleaved: null,
10601082
dispatch: null,
10611083
lastRenderedReducer: basicStateReducer,
10621084
lastRenderedState: snapshot,
@@ -1112,6 +1134,7 @@ function mountState<S>(
11121134
hook.memoizedState = hook.baseState = initialState;
11131135
const queue = (hook.queue = {
11141136
pending: null,
1137+
interleaved: null,
11151138
dispatch: null,
11161139
lastRenderedReducer: basicStateReducer,
11171140
lastRenderedState: (initialState: any),
@@ -1751,7 +1774,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
17511774
cache: seededCache,
17521775
};
17531776
refreshUpdate.payload = payload;
1754-
enqueueUpdate(provider, refreshUpdate);
1777+
enqueueUpdate(provider, refreshUpdate, lane);
17551778
return;
17561779
}
17571780
}
@@ -1786,17 +1809,6 @@ function dispatchAction<S, A>(
17861809
next: (null: any),
17871810
};
17881811

1789-
// Append the update to the end of the list.
1790-
const pending = queue.pending;
1791-
if (pending === null) {
1792-
// This is the first update. Create a circular list.
1793-
update.next = update;
1794-
} else {
1795-
update.next = pending.next;
1796-
pending.next = update;
1797-
}
1798-
queue.pending = update;
1799-
18001812
const alternate = fiber.alternate;
18011813
if (
18021814
fiber === currentlyRenderingFiber ||
@@ -1806,7 +1818,41 @@ function dispatchAction<S, A>(
18061818
// queue -> linked list of updates. After this render pass, we'll restart
18071819
// and apply the stashed updates on top of the work-in-progress hook.
18081820
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
1821+
const pending = queue.pending;
1822+
if (pending === null) {
1823+
// This is the first update. Create a circular list.
1824+
update.next = update;
1825+
} else {
1826+
update.next = pending.next;
1827+
pending.next = update;
1828+
}
1829+
queue.pending = update;
18091830
} else {
1831+
if (isInterleavedUpdate(fiber, lane)) {
1832+
const interleaved = queue.interleaved;
1833+
if (interleaved === null) {
1834+
// This is the first update. Create a circular list.
1835+
update.next = update;
1836+
// At the end of the current render, this queue's interleaved updates will
1837+
// be transfered to the pending queue.
1838+
pushInterleavedQueue(queue);
1839+
} else {
1840+
update.next = interleaved.next;
1841+
interleaved.next = update;
1842+
}
1843+
queue.interleaved = update;
1844+
} else {
1845+
const pending = queue.pending;
1846+
if (pending === null) {
1847+
// This is the first update. Create a circular list.
1848+
update.next = update;
1849+
} else {
1850+
update.next = pending.next;
1851+
pending.next = update;
1852+
}
1853+
queue.pending = update;
1854+
}
1855+
18101856
if (
18111857
fiber.lanes === NoLanes &&
18121858
(alternate === null || alternate.lanes === NoLanes)
Lines changed: 55 additions & 0 deletions
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

Lines changed: 21 additions & 6 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ function throwException(
295295
// prevent a bail out.
296296
const update = createUpdate(NoTimestamp, SyncLane);
297297
update.tag = ForceUpdate;
298-
enqueueUpdate(sourceFiber, update);
298+
enqueueUpdate(sourceFiber, update, SyncLane);
299299
}
300300
}
301301

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ import {
203203
pop as popFromStack,
204204
createCursor,
205205
} from './ReactFiberStack.new';
206+
import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.new';
206207

207208
import {
208209
markNestedUpdateScheduled,
@@ -555,6 +556,7 @@ export function scheduleUpdateOnFiber(
555556
}
556557
}
557558

559+
// TODO: Consolidate with `isInterleavedUpdate` check
558560
if (root === workInProgressRoot) {
559561
// Received an update to a tree that's in the middle of rendering. Mark
560562
// that there was an interleaved update work on this root. Unless the
@@ -692,6 +694,22 @@ function markUpdateLaneFromFiberToRoot(
692694
}
693695
}
694696

697+
export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
698+
return (
699+
// TODO: Optimize slightly by comparing to root that fiber belongs to.
700+
// Requires some refactoring. Not a big deal though since it's rare for
701+
// concurrent apps to have more than a single root.
702+
workInProgressRoot !== null &&
703+
(fiber.mode & BlockingMode) !== NoMode &&
704+
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
705+
// then don't treat this as an interleaved update. This pattern is
706+
// accompanied by a warning but we haven't fully deprecated it yet. We can
707+
// remove once the deferRenderPhaseUpdateToNextBatch flag is enabled.
708+
(deferRenderPhaseUpdateToNextBatch ||
709+
(executionContext & RenderContext) === NoContext)
710+
);
711+
}
712+
695713
// Use this function to schedule a task for a root. There's only one task per
696714
// root; if a task was already scheduled, we'll check to make sure the priority
697715
// of the existing task is the same as the priority of the next level that the
@@ -1373,6 +1391,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13731391
workInProgressRootUpdatedLanes = NoLanes;
13741392
workInProgressRootPingedLanes = NoLanes;
13751393

1394+
enqueueInterleavedUpdates();
1395+
13761396
if (enableSchedulerTracing) {
13771397
spawnedWorkDuringRender = null;
13781398
}
@@ -2438,7 +2458,7 @@ function captureCommitPhaseErrorOnRoot(
24382458
) {
24392459
const errorInfo = createCapturedValue(error, sourceFiber);
24402460
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2441-
enqueueUpdate(rootFiber, update);
2461+
enqueueUpdate(rootFiber, update, (SyncLane: Lane));
24422462
const eventTime = requestEventTime();
24432463
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
24442464
if (root !== null) {
@@ -2475,7 +2495,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
24752495
errorInfo,
24762496
(SyncLane: Lane),
24772497
);
2478-
enqueueUpdate(fiber, update);
2498+
enqueueUpdate(fiber, update, (SyncLane: Lane));
24792499
const eventTime = requestEventTime();
24802500
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
24812501
if (root !== null) {

0 commit comments

Comments
 (0)