Skip to content

Commit 2384933

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 741dcbd commit 2384933

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,
@@ -629,6 +632,7 @@ function mountReducer<S, I, A>(
629632
hook.memoizedState = hook.baseState = initialState;
630633
const queue = (hook.queue = {
631634
pending: null,
635+
interleaved: null,
632636
dispatch: null,
633637
lastRenderedReducer: reducer,
634638
lastRenderedState: (initialState: any),
@@ -771,6 +775,23 @@ function updateReducer<S, I, A>(
771775
queue.lastRenderedState = newState;
772776
}
773777

778+
// Interleaved updates are stored on a separate queue. We aren't going to
779+
// process them during this render, but we do need to track which lanes
780+
// are remaining.
781+
const lastInterleaved = queue.interleaved;
782+
if (lastInterleaved !== null) {
783+
let interleaved = lastInterleaved;
784+
do {
785+
const interleavedLane = interleaved.lane;
786+
currentlyRenderingFiber.lanes = mergeLanes(
787+
currentlyRenderingFiber.lanes,
788+
interleavedLane,
789+
);
790+
markSkippedUpdateLanes(interleavedLane);
791+
interleaved = ((interleaved: any).next: Update<S, A>);
792+
} while (interleaved !== lastInterleaved);
793+
}
794+
774795
const dispatch: Dispatch<A> = (queue.dispatch: any);
775796
return [hook.memoizedState, dispatch];
776797
}
@@ -1059,6 +1080,7 @@ function useMutableSource<Source, Snapshot>(
10591080
// including any interleaving updates that occur.
10601081
const newQueue = {
10611082
pending: null,
1083+
interleaved: null,
10621084
dispatch: null,
10631085
lastRenderedReducer: basicStateReducer,
10641086
lastRenderedState: snapshot,
@@ -1114,6 +1136,7 @@ function mountState<S>(
11141136
hook.memoizedState = hook.baseState = initialState;
11151137
const queue = (hook.queue = {
11161138
pending: null,
1139+
interleaved: null,
11171140
dispatch: null,
11181141
lastRenderedReducer: basicStateReducer,
11191142
lastRenderedState: (initialState: any),
@@ -1748,7 +1771,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
17481771
cache: seededCache,
17491772
};
17501773
refreshUpdate.payload = payload;
1751-
enqueueUpdate(provider, refreshUpdate);
1774+
enqueueUpdate(provider, refreshUpdate, lane);
17521775
return;
17531776
}
17541777
}
@@ -1783,17 +1806,6 @@ function dispatchAction<S, A>(
17831806
next: (null: any),
17841807
};
17851808

1786-
// Append the update to the end of the list.
1787-
const pending = queue.pending;
1788-
if (pending === null) {
1789-
// This is the first update. Create a circular list.
1790-
update.next = update;
1791-
} else {
1792-
update.next = pending.next;
1793-
pending.next = update;
1794-
}
1795-
queue.pending = update;
1796-
17971809
const alternate = fiber.alternate;
17981810
if (
17991811
fiber === currentlyRenderingFiber ||
@@ -1803,7 +1815,41 @@ function dispatchAction<S, A>(
18031815
// queue -> linked list of updates. After this render pass, we'll restart
18041816
// and apply the stashed updates on top of the work-in-progress hook.
18051817
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
1818+
const pending = queue.pending;
1819+
if (pending === null) {
1820+
// This is the first update. Create a circular list.
1821+
update.next = update;
1822+
} else {
1823+
update.next = pending.next;
1824+
pending.next = update;
1825+
}
1826+
queue.pending = update;
18061827
} else {
1828+
if (isInterleavedUpdate(fiber, lane)) {
1829+
const interleaved = queue.interleaved;
1830+
if (interleaved === null) {
1831+
// This is the first update. Create a circular list.
1832+
update.next = update;
1833+
// At the end of the current render, this queue's interleaved updates will
1834+
// be transfered to the pending queue.
1835+
pushInterleavedQueue(queue);
1836+
} else {
1837+
update.next = interleaved.next;
1838+
interleaved.next = update;
1839+
}
1840+
queue.interleaved = update;
1841+
} else {
1842+
const pending = queue.pending;
1843+
if (pending === null) {
1844+
// This is the first update. Create a circular list.
1845+
update.next = update;
1846+
} else {
1847+
update.next = pending.next;
1848+
pending.next = update;
1849+
}
1850+
queue.pending = update;
1851+
}
1852+
18071853
if (
18081854
fiber.lanes === NoLanes &&
18091855
(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
@@ -195,6 +195,7 @@ import {
195195
pop as popFromStack,
196196
createCursor,
197197
} from './ReactFiberStack.new';
198+
import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.new';
198199

199200
import {
200201
markNestedUpdateScheduled,
@@ -525,6 +526,7 @@ export function scheduleUpdateOnFiber(
525526
}
526527
}
527528

529+
// TODO: Consolidate with `isInterleavedUpdate` check
528530
if (root === workInProgressRoot) {
529531
// Received an update to a tree that's in the middle of rendering. Mark
530532
// that there was an interleaved update work on this root. Unless the
@@ -662,6 +664,22 @@ function markUpdateLaneFromFiberToRoot(
662664
}
663665
}
664666

667+
export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
668+
return (
669+
// TODO: Optimize slightly by comparing to root that fiber belongs to.
670+
// Requires some refactoring. Not a big deal though since it's rare for
671+
// concurrent apps to have more than a single root.
672+
workInProgressRoot !== null &&
673+
(fiber.mode & BlockingMode) !== NoMode &&
674+
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
675+
// then don't treat this as an interleaved update. This pattern is
676+
// accompanied by a warning but we haven't fully deprecated it yet. We can
677+
// remove once the deferRenderPhaseUpdateToNextBatch flag is enabled.
678+
(deferRenderPhaseUpdateToNextBatch ||
679+
(executionContext & RenderContext) === NoContext)
680+
);
681+
}
682+
665683
// Use this function to schedule a task for a root. There's only one task per
666684
// root; if a task was already scheduled, we'll check to make sure the priority
667685
// of the existing task is the same as the priority of the next level that the
@@ -1343,6 +1361,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13431361
workInProgressRootUpdatedLanes = NoLanes;
13441362
workInProgressRootPingedLanes = NoLanes;
13451363

1364+
enqueueInterleavedUpdates();
1365+
13461366
if (enableSchedulerTracing) {
13471367
spawnedWorkDuringRender = null;
13481368
}
@@ -2307,7 +2327,7 @@ function captureCommitPhaseErrorOnRoot(
23072327
) {
23082328
const errorInfo = createCapturedValue(error, sourceFiber);
23092329
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2310-
enqueueUpdate(rootFiber, update);
2330+
enqueueUpdate(rootFiber, update, (SyncLane: Lane));
23112331
const eventTime = requestEventTime();
23122332
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
23132333
if (root !== null) {
@@ -2344,7 +2364,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
23442364
errorInfo,
23452365
(SyncLane: Lane),
23462366
);
2347-
enqueueUpdate(fiber, update);
2367+
enqueueUpdate(fiber, update, (SyncLane: Lane));
23482368
const eventTime = requestEventTime();
23492369
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
23502370
if (root !== null) {

0 commit comments

Comments
 (0)