Skip to content

Commit 1cd90d2

Browse files
authored
Refactor of interleaved ("concurrent") update queue (#24663)
* Always push updates to interleaved queue first Interleaves updates (updates that are scheduled while another render is already is progress) go into a special queue that isn't applied until the end of the current render. They are transferred to the "real" queue at the beginning of the next render. Currently we check during `setState` whether an update should go directly onto the real queue or onto the special interleaved queue. The logic is subtle and it can lead to bugs if you mess it up, as in #24400. Instead, this changes it to always go onto the interleaved queue. The behavior is the same but the logic is simpler. As a further step, we can also wait to update the `childLanes` until the end of the current render. I'll do this in the next step. * Move setState return path traversal to own call A lot of the logic around scheduling an update needs access to the fiber root. To obtain this reference, we must walk up the fiber return path. We also do this to update `childLanes` on all the parent nodes, so we can use the same traversal for both purposes. The traversal currently happens inside `scheduleUpdateOnFiber`, but sometimes we need to access it beyond that function, too. So I've hoisted the traversal out of `scheduleUpdateOnFiber` into its own function call that happens at the beginning of the `setState` algorithm. * Rename ReactInterleavedUpdates -> ReactFiberConcurrentUpdates The scope of this module is expanding so I've renamed accordingly. No behavioral changes. * Enqueue and update childLanes in same function During a setState, the childLanes are updated immediately, even if a render is already in progress. This can lead to subtle concurrency bugs, so the plan is to wait until the in-progress render has finished before updating the childLanes, to prevent subtle concurrency bugs. As a step toward that change, when scheduling an update, we should not update the childLanes directly, but instead defer to the ReactConcurrentUpdates module to do it at the appropriate time. This makes markUpdateLaneFromFiberToRoot a private function that is only called from the ReactConcurrentUpdates module. * [FORKED] Don't update childLanes until after current render (This is the riskiest commit in the stack. Only affects the "new" reconciler fork.) Updates that occur in a concurrent event while a render is already in progress can't be processed during that render. This is tricky to get right. Previously we solved this by adding concurrent updates to a special `interleaved` queue, then transferring the `interleaved` queue to the `pending` queue after the render phase had completed. However, we would still mutate the `childLanes` along the parent path immediately, which can lead to its own subtle data races. Instead, we can queue the entire operation until after the render phase has completed. This replaces the need for an `interleaved` field on every fiber/hook queue. The main motivation for this change, aside from simplifying the logic a bit, is so we can read information about the current fiber while we're walking up its return path, like whether it's inside a hidden tree. (I haven't done anything like that in this commit, though.) * Add 17691ac to forked revisions
1 parent 4ddd8b4 commit 1cd90d2

28 files changed

+729
-562
lines changed

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {
1818
Fiber,
1919
TransitionTracingCallbacks,
2020
} from 'react-reconciler/src/ReactInternalTypes';
21-
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
21+
import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue.new';
2222
import type {ReactNodeList} from 'shared/ReactTypes';
2323
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
2424

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import type {
3333
CacheComponentState,
3434
SpawnedCachePool,
3535
} from './ReactFiberCacheComponent.new';
36-
import type {UpdateQueue} from './ReactUpdateQueue.new';
36+
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
3737
import type {RootState} from './ReactFiberRoot.new';
3838
import {
3939
enableSuspenseAvoidThisFallback,
@@ -131,7 +131,7 @@ import {
131131
cloneUpdateQueue,
132132
initializeUpdateQueue,
133133
enqueueCapturedUpdate,
134-
} from './ReactUpdateQueue.new';
134+
} from './ReactFiberClassUpdateQueue.new';
135135
import {
136136
NoLane,
137137
NoLanes,
@@ -234,6 +234,7 @@ import {
234234
getWorkInProgressRoot,
235235
pushRenderLanes,
236236
} from './ReactFiberWorkLoop.new';
237+
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new';
237238
import {setWorkInProgressVersion} from './ReactMutableSource.new';
238239
import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.new';
239240
import {createCapturedValue} from './ReactCapturedValue';
@@ -2626,7 +2627,13 @@ function updateDehydratedSuspenseComponent(
26262627
suspenseState.retryLane = attemptHydrationAtLane;
26272628
// TODO: Ideally this would inherit the event time of the current render
26282629
const eventTime = NoTimestamp;
2629-
scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
2630+
enqueueConcurrentRenderForLane(current, attemptHydrationAtLane);
2631+
scheduleUpdateOnFiber(
2632+
root,
2633+
current,
2634+
attemptHydrationAtLane,
2635+
eventTime,
2636+
);
26302637
} else {
26312638
// We have already tried to ping at a higher priority than we're rendering with
26322639
// so if we got here, we must have failed to hydrate at those levels. We must

packages/react-reconciler/src/ReactFiberBeginWork.old.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import type {
3333
CacheComponentState,
3434
SpawnedCachePool,
3535
} from './ReactFiberCacheComponent.old';
36-
import type {UpdateQueue} from './ReactUpdateQueue.old';
36+
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
3737
import type {RootState} from './ReactFiberRoot.old';
3838
import {
3939
enableSuspenseAvoidThisFallback,
@@ -131,7 +131,7 @@ import {
131131
cloneUpdateQueue,
132132
initializeUpdateQueue,
133133
enqueueCapturedUpdate,
134-
} from './ReactUpdateQueue.old';
134+
} from './ReactFiberClassUpdateQueue.old';
135135
import {
136136
NoLane,
137137
NoLanes,
@@ -234,6 +234,7 @@ import {
234234
getWorkInProgressRoot,
235235
pushRenderLanes,
236236
} from './ReactFiberWorkLoop.old';
237+
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old';
237238
import {setWorkInProgressVersion} from './ReactMutableSource.old';
238239
import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.old';
239240
import {createCapturedValue} from './ReactCapturedValue';
@@ -2626,7 +2627,13 @@ function updateDehydratedSuspenseComponent(
26262627
suspenseState.retryLane = attemptHydrationAtLane;
26272628
// TODO: Ideally this would inherit the event time of the current render
26282629
const eventTime = NoTimestamp;
2629-
scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
2630+
enqueueConcurrentRenderForLane(current, attemptHydrationAtLane);
2631+
scheduleUpdateOnFiber(
2632+
root,
2633+
current,
2634+
attemptHydrationAtLane,
2635+
eventTime,
2636+
);
26302637
} else {
26312638
// We have already tried to ping at a higher priority than we're rendering with
26322639
// so if we got here, we must have failed to hydrate at those levels. We must

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import type {Fiber} from './ReactInternalTypes';
1111
import type {Lanes} from './ReactFiberLane.new';
12-
import type {UpdateQueue} from './ReactUpdateQueue.new';
12+
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
1313
import type {Flags} from './ReactFiberFlags';
1414

1515
import * as React from 'react';
@@ -58,7 +58,7 @@ import {
5858
ForceUpdate,
5959
initializeUpdateQueue,
6060
cloneUpdateQueue,
61-
} from './ReactUpdateQueue.new';
61+
} from './ReactFiberClassUpdateQueue.new';
6262
import {NoLanes} from './ReactFiberLane.new';
6363
import {
6464
cacheContext,
@@ -215,9 +215,9 @@ const classComponentUpdater = {
215215
update.callback = callback;
216216
}
217217

218-
enqueueUpdate(fiber, update, lane);
219-
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
218+
const root = enqueueUpdate(fiber, update, lane);
220219
if (root !== null) {
220+
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
221221
entangleTransitions(root, fiber, lane);
222222
}
223223

@@ -250,9 +250,9 @@ const classComponentUpdater = {
250250
update.callback = callback;
251251
}
252252

253-
enqueueUpdate(fiber, update, lane);
254-
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
253+
const root = enqueueUpdate(fiber, update, lane);
255254
if (root !== null) {
255+
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
256256
entangleTransitions(root, fiber, lane);
257257
}
258258

@@ -284,9 +284,9 @@ const classComponentUpdater = {
284284
update.callback = callback;
285285
}
286286

287-
enqueueUpdate(fiber, update, lane);
288-
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
287+
const root = enqueueUpdate(fiber, update, lane);
289288
if (root !== null) {
289+
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
290290
entangleTransitions(root, fiber, lane);
291291
}
292292

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import type {Fiber} from './ReactInternalTypes';
1111
import type {Lanes} from './ReactFiberLane.old';
12-
import type {UpdateQueue} from './ReactUpdateQueue.old';
12+
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
1313
import type {Flags} from './ReactFiberFlags';
1414

1515
import * as React from 'react';
@@ -58,7 +58,7 @@ import {
5858
ForceUpdate,
5959
initializeUpdateQueue,
6060
cloneUpdateQueue,
61-
} from './ReactUpdateQueue.old';
61+
} from './ReactFiberClassUpdateQueue.old';
6262
import {NoLanes} from './ReactFiberLane.old';
6363
import {
6464
cacheContext,
@@ -215,9 +215,9 @@ const classComponentUpdater = {
215215
update.callback = callback;
216216
}
217217

218-
enqueueUpdate(fiber, update, lane);
219-
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
218+
const root = enqueueUpdate(fiber, update, lane);
220219
if (root !== null) {
220+
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
221221
entangleTransitions(root, fiber, lane);
222222
}
223223

@@ -250,9 +250,9 @@ const classComponentUpdater = {
250250
update.callback = callback;
251251
}
252252

253-
enqueueUpdate(fiber, update, lane);
254-
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
253+
const root = enqueueUpdate(fiber, update, lane);
255254
if (root !== null) {
255+
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
256256
entangleTransitions(root, fiber, lane);
257257
}
258258

@@ -284,9 +284,9 @@ const classComponentUpdater = {
284284
update.callback = callback;
285285
}
286286

287-
enqueueUpdate(fiber, update, lane);
288-
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
287+
const root = enqueueUpdate(fiber, update, lane);
289288
if (root !== null) {
289+
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
290290
entangleTransitions(root, fiber, lane);
291291
}
292292

packages/react-reconciler/src/ReactUpdateQueue.new.js renamed to packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,12 @@ import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags
107107
import {StrictLegacyMode} from './ReactTypeOfMode';
108108
import {
109109
markSkippedUpdateLanes,
110-
isInterleavedUpdate,
110+
isUnsafeClassRenderPhaseUpdate,
111111
} from './ReactFiberWorkLoop.new';
112-
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
112+
import {
113+
enqueueConcurrentClassUpdate,
114+
unsafe_markUpdateLaneFromFiberToRoot,
115+
} from './ReactFiberConcurrentUpdates.new';
113116
import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
114117

115118
import assign from 'shared/assign';
@@ -129,7 +132,6 @@ export type Update<State> = {|
129132

130133
export type SharedQueue<State> = {|
131134
pending: Update<State> | null,
132-
interleaved: Update<State> | null,
133135
lanes: Lanes,
134136
|};
135137

@@ -169,7 +171,6 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
169171
lastBaseUpdate: null,
170172
shared: {
171173
pending: null,
172-
interleaved: null,
173174
lanes: NoLanes,
174175
},
175176
effects: null,
@@ -214,40 +215,15 @@ export function enqueueUpdate<State>(
214215
fiber: Fiber,
215216
update: Update<State>,
216217
lane: Lane,
217-
) {
218+
): FiberRoot | null {
218219
const updateQueue = fiber.updateQueue;
219220
if (updateQueue === null) {
220221
// Only occurs if the fiber has been unmounted.
221-
return;
222+
return null;
222223
}
223224

224225
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
225226

226-
if (isInterleavedUpdate(fiber, lane)) {
227-
const interleaved = sharedQueue.interleaved;
228-
if (interleaved === null) {
229-
// This is the first update. Create a circular list.
230-
update.next = update;
231-
// At the end of the current render, this queue's interleaved updates will
232-
// be transferred to the pending queue.
233-
pushInterleavedQueue(sharedQueue);
234-
} else {
235-
update.next = interleaved.next;
236-
interleaved.next = update;
237-
}
238-
sharedQueue.interleaved = update;
239-
} else {
240-
const pending = sharedQueue.pending;
241-
if (pending === null) {
242-
// This is the first update. Create a circular list.
243-
update.next = update;
244-
} else {
245-
update.next = pending.next;
246-
pending.next = update;
247-
}
248-
sharedQueue.pending = update;
249-
}
250-
251227
if (__DEV__) {
252228
if (
253229
currentlyProcessingQueue === sharedQueue &&
@@ -262,6 +238,28 @@ export function enqueueUpdate<State>(
262238
didWarnUpdateInsideUpdate = true;
263239
}
264240
}
241+
242+
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
243+
// This is an unsafe render phase update. Add directly to the update
244+
// queue so we can process it immediately during the current render.
245+
const pending = sharedQueue.pending;
246+
if (pending === null) {
247+
// This is the first update. Create a circular list.
248+
update.next = update;
249+
} else {
250+
update.next = pending.next;
251+
pending.next = update;
252+
}
253+
sharedQueue.pending = update;
254+
255+
// Update the childLanes even though we're most likely already rendering
256+
// this fiber. This is for backwards compatibility in the case where you
257+
// update a different component during render phase than the one that is
258+
// currently renderings (a pattern that is accompanied by a warning).
259+
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
260+
} else {
261+
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
262+
}
265263
}
266264

267265
export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
@@ -622,17 +620,7 @@ export function processUpdateQueue<State>(
622620
queue.firstBaseUpdate = newFirstBaseUpdate;
623621
queue.lastBaseUpdate = newLastBaseUpdate;
624622

625-
// Interleaved updates are stored on a separate queue. We aren't going to
626-
// process them during this render, but we do need to track which lanes
627-
// are remaining.
628-
const lastInterleaved = queue.shared.interleaved;
629-
if (lastInterleaved !== null) {
630-
let interleaved = lastInterleaved;
631-
do {
632-
newLanes = mergeLanes(newLanes, interleaved.lane);
633-
interleaved = ((interleaved: any).next: Update<State>);
634-
} while (interleaved !== lastInterleaved);
635-
} else if (firstBaseUpdate === null) {
623+
if (firstBaseUpdate === null) {
636624
// `queue.lanes` is used for entangling transitions. We can set it back to
637625
// zero once the queue is empty.
638626
queue.shared.lanes = NoLanes;

0 commit comments

Comments
 (0)