Skip to content

Commit 383697b

Browse files
committed
[FORKED] Bugfix: Revealing a hidden update
This fixes a bug I discovered related to revealing a hidden Offscreen tree. When this happens, we include in that render all the updates that had previously been deferred — that is, all the updates that would have already committed if the tree weren't hidden. This is necessary to avoid tearing with the surrounding contents. (This was the "flickering" Suspense bug we found a few years ago: facebook#18411.) The way we do this is by tracking the lanes of the updates that were deferred by a hidden tree. These are the "base" lanes. Then, in order to reveal the hidden tree, we process any update that matches one of those base lanes. The bug I discovered is that some of these base lanes may include updates that were not present at the time the tree was hidden. We cannot flush those updates earlier that the surrounding contents — that, too, could cause tearing. The crux of the problem is that we sometimes reuse the same lane for base updates and for non-base updates. So the lane alone isn't sufficient to distinguish between these cases. We must track this in some other way. The solution I landed upon was to add an extra OffscreenLane bit to any update that is made to a hidden tree. Then later when we reveal the tree, we'll know not to treat them as base updates. The extra OffscreenLane bit is removed as soon as that lane is committed by the root (markRootFinished) — at that point, it gets "upgraded" to a base update. The trickiest part of this algorithm is reliably detecting when an update is made to a hidden tree. What makes this challenging is when the update is received during a concurrent event, while a render is already in progress — it's possible the work-in-progress render is about to flip the visibility of the tree that's being updated, leading to a race condition. To avoid a race condition, we will wait to read the visibility of the tree until the current render has finished. In other words, this makes it an atomic operation. Most of this logic was already implemented in facebook#24663. Because this bugfix depends on a moderately risky refactor to the update queue (facebook#24663), it only works in the "new" reconciler fork. We will roll it out gradually to www before landing in the main fork.
1 parent 4428cc7 commit 383697b

9 files changed

+488
-15
lines changed

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ import type {Lanes, Lane} from './ReactFiberLane.new';
9090
import {
9191
NoLane,
9292
NoLanes,
93+
OffscreenLane,
9394
isSubsetOfLanes,
9495
mergeLanes,
96+
removeLanes,
9597
isTransitionLane,
9698
intersectLanes,
9799
markRootEntangled,
@@ -108,6 +110,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
108110
import {
109111
markSkippedUpdateLanes,
110112
isUnsafeClassRenderPhaseUpdate,
113+
getWorkInProgressRootRenderLanes,
111114
} from './ReactFiberWorkLoop.new';
112115
import {
113116
enqueueConcurrentClassUpdate,
@@ -523,9 +526,23 @@ export function processUpdateQueue<State>(
523526

524527
let update = firstBaseUpdate;
525528
do {
526-
const updateLane = update.lane;
529+
// TODO: Don't need this field anymore
527530
const updateEventTime = update.eventTime;
528-
if (!isSubsetOfLanes(renderLanes, updateLane)) {
531+
532+
// An extra OffscreenLane bit is added to updates that were made to
533+
// a hidden tree, so that we can distinguish them from updates that were
534+
// already there when the tree was hidden.
535+
const updateLane = removeLanes(update.lane, OffscreenLane);
536+
const isHiddenUpdate = updateLane !== update.lane;
537+
538+
// Check if this update was made while the tree was hidden. If so, then
539+
// it's not a "base" update and we should disregard the extra base lanes
540+
// that were added to renderLanes when we entered the Offscreen tree.
541+
const shouldSkipUpdate = isHiddenUpdate
542+
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
543+
: !isSubsetOfLanes(renderLanes, updateLane);
544+
545+
if (shouldSkipUpdate) {
529546
// Priority is insufficient. Skip this update. If this is the first
530547
// skipped update, the previous update/state is the new base
531548
// update/state.

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

+42-10
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@ import type {
1717
Update as ClassUpdate,
1818
} from './ReactFiberClassUpdateQueue.new';
1919
import type {Lane, Lanes} from './ReactFiberLane.new';
20+
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
2021

2122
import {warnAboutUpdateOnNotYetMountedFiberInDEV} from './ReactFiberWorkLoop.new';
22-
import {NoLane, NoLanes, mergeLanes} from './ReactFiberLane.new';
23+
import {
24+
NoLane,
25+
NoLanes,
26+
mergeLanes,
27+
markHiddenUpdate,
28+
} from './ReactFiberLane.new';
2329
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
24-
import {HostRoot} from './ReactWorkTags';
30+
import {HostRoot, OffscreenComponent} from './ReactWorkTags';
2531

26-
type ConcurrentUpdate = {
32+
export type ConcurrentUpdate = {
2733
next: ConcurrentUpdate,
34+
lane: Lane,
2835
};
2936

3037
type ConcurrentQueue = {
@@ -38,11 +45,13 @@ type ConcurrentQueue = {
3845
const concurrentQueues: Array<any> = [];
3946
let concurrentQueuesIndex = 0;
4047

41-
export function finishQueueingConcurrentUpdates(): Lanes {
48+
let concurrentlyUpdatedLanes: Lanes = NoLanes;
49+
50+
export function finishQueueingConcurrentUpdates(): void {
4251
const endIndex = concurrentQueuesIndex;
4352
concurrentQueuesIndex = 0;
4453

45-
let lanes = NoLanes;
54+
concurrentlyUpdatedLanes = NoLanes;
4655

4756
let i = 0;
4857
while (i < endIndex) {
@@ -68,12 +77,13 @@ export function finishQueueingConcurrentUpdates(): Lanes {
6877
}
6978

7079
if (lane !== NoLane) {
71-
lanes = mergeLanes(lanes, lane);
72-
markUpdateLaneFromFiberToRoot(fiber, lane);
80+
markUpdateLaneFromFiberToRoot(fiber, update, lane);
7381
}
7482
}
83+
}
7584

76-
return lanes;
85+
export function getConcurrentlyUpdatedLanes(): Lanes {
86+
return concurrentlyUpdatedLanes;
7787
}
7888

7989
function enqueueUpdate(
@@ -89,6 +99,8 @@ function enqueueUpdate(
8999
concurrentQueues[concurrentQueuesIndex++] = update;
90100
concurrentQueues[concurrentQueuesIndex++] = lane;
91101

102+
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
103+
92104
// The fiber's `lane` field is used in some places to check if any work is
93105
// scheduled, to perform an eager bailout, so we need to update it immediately.
94106
// TODO: We should probably move this to the "shared" queue instead.
@@ -151,27 +163,47 @@ export function unsafe_markUpdateLaneFromFiberToRoot(
151163
sourceFiber: Fiber,
152164
lane: Lane,
153165
): FiberRoot | null {
154-
markUpdateLaneFromFiberToRoot(sourceFiber, lane);
166+
markUpdateLaneFromFiberToRoot(sourceFiber, null, lane);
155167
return getRootForUpdatedFiber(sourceFiber);
156168
}
157169

158-
function markUpdateLaneFromFiberToRoot(sourceFiber: Fiber, lane: Lane): void {
170+
function markUpdateLaneFromFiberToRoot(
171+
sourceFiber: Fiber,
172+
update: ConcurrentUpdate | null,
173+
lane: Lane,
174+
): void {
159175
// Update the source fiber's lanes
160176
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
161177
let alternate = sourceFiber.alternate;
162178
if (alternate !== null) {
163179
alternate.lanes = mergeLanes(alternate.lanes, lane);
164180
}
165181
// Walk the parent path to the root and update the child lanes.
182+
let isHidden = false;
166183
let parent = sourceFiber.return;
184+
let node = sourceFiber;
167185
while (parent !== null) {
168186
parent.childLanes = mergeLanes(parent.childLanes, lane);
169187
alternate = parent.alternate;
170188
if (alternate !== null) {
171189
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
172190
}
191+
192+
if (parent.tag === OffscreenComponent) {
193+
const offscreenInstance: OffscreenInstance = parent.stateNode;
194+
if (offscreenInstance.isHidden) {
195+
isHidden = true;
196+
}
197+
}
198+
199+
node = parent;
173200
parent = parent.return;
174201
}
202+
203+
if (isHidden && update !== null && node.tag === HostRoot) {
204+
const root: FiberRoot = node.stateNode;
205+
markHiddenUpdate(root, update, lane);
206+
}
175207
}
176208

177209
function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
import {
4545
NoLane,
4646
SyncLane,
47+
OffscreenLane,
4748
NoLanes,
4849
isSubsetOfLanes,
4950
includesBlockingLane,
@@ -83,6 +84,7 @@ import {
8384
} from './ReactHookEffectTags';
8485
import {
8586
getWorkInProgressRoot,
87+
getWorkInProgressRootRenderLanes,
8688
scheduleUpdateOnFiber,
8789
requestUpdateLane,
8890
requestEventTime,
@@ -811,8 +813,20 @@ function updateReducer<S, I, A>(
811813
let newBaseQueueLast = null;
812814
let update = first;
813815
do {
814-
const updateLane = update.lane;
815-
if (!isSubsetOfLanes(renderLanes, updateLane)) {
816+
// An extra OffscreenLane bit is added to updates that were made to
817+
// a hidden tree, so that we can distinguish them from updates that were
818+
// already there when the tree was hidden.
819+
const updateLane = removeLanes(update.lane, OffscreenLane);
820+
const isHiddenUpdate = updateLane !== update.lane;
821+
822+
// Check if this update was made while the tree was hidden. If so, then
823+
// it's not a "base" update and we should disregard the extra base lanes
824+
// that were added to renderLanes when we entered the Offscreen tree.
825+
const shouldSkipUpdate = isHiddenUpdate
826+
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
827+
: !isSubsetOfLanes(renderLanes, updateLane);
828+
829+
if (shouldSkipUpdate) {
816830
// Priority is insufficient. Skip this update. If this is the first
817831
// skipped update, the previous update/state is the new base
818832
// update/state.

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

+33
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import type {FiberRoot} from './ReactInternalTypes';
1111
import type {Transition} from './ReactFiberTracingMarkerComponent.new';
12+
import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates.new';
1213

1314
// TODO: Ideally these types would be opaque but that doesn't work well with
1415
// our reconciler fork infra, since these leak into non-reconciler packages.
@@ -648,6 +649,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
648649
const entanglements = root.entanglements;
649650
const eventTimes = root.eventTimes;
650651
const expirationTimes = root.expirationTimes;
652+
const hiddenUpdates = root.hiddenUpdates;
651653

652654
// Clear the lanes that no longer have pending work
653655
let lanes = noLongerPendingLanes;
@@ -659,6 +661,21 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
659661
eventTimes[index] = NoTimestamp;
660662
expirationTimes[index] = NoTimestamp;
661663

664+
const hiddenUpdatesForLane = hiddenUpdates[index];
665+
if (hiddenUpdatesForLane !== null) {
666+
hiddenUpdates[index] = null;
667+
// "Hidden" updates are updates that were made to a hidden component. They
668+
// have special logic associated with them because they may be entangled
669+
// with updates that occur outside that tree. But once the outer tree
670+
// commits, they behave like regular updates.
671+
for (let i = 0; i < hiddenUpdatesForLane.length; i++) {
672+
const update = hiddenUpdatesForLane[i];
673+
if (update !== null) {
674+
update.lane &= ~OffscreenLane;
675+
}
676+
}
677+
}
678+
662679
lanes &= ~lane;
663680
}
664681
}
@@ -694,6 +711,22 @@ export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
694711
}
695712
}
696713

714+
export function markHiddenUpdate(
715+
root: FiberRoot,
716+
update: ConcurrentUpdate,
717+
lane: Lane,
718+
) {
719+
const index = laneToIndex(lane);
720+
const hiddenUpdates = root.hiddenUpdates;
721+
const hiddenUpdatesForLane = hiddenUpdates[index];
722+
if (hiddenUpdatesForLane === null) {
723+
hiddenUpdates[index] = [update];
724+
} else {
725+
hiddenUpdatesForLane.push(update);
726+
}
727+
update.lane = lane | OffscreenLane;
728+
}
729+
697730
export function getBumpedLaneForHydration(
698731
root: FiberRoot,
699732
renderLanes: Lanes,

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

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ function FiberRootNode(
8080
this.entangledLanes = NoLanes;
8181
this.entanglements = createLaneMap(NoLanes);
8282

83+
this.hiddenUpdates = createLaneMap(null);
84+
8385
this.identifierPrefix = identifierPrefix;
8486
this.onRecoverableError = onRecoverableError;
8587

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ import {
199199
import {
200200
enqueueConcurrentRenderForLane,
201201
finishQueueingConcurrentUpdates,
202+
getConcurrentlyUpdatedLanes,
202203
} from './ReactFiberConcurrentUpdates.new';
203204

204205
import {
@@ -425,6 +426,10 @@ export function getWorkInProgressRoot(): FiberRoot | null {
425426
return workInProgressRoot;
426427
}
427428

429+
export function getWorkInProgressRootRenderLanes(): Lanes {
430+
return workInProgressRootRenderLanes;
431+
}
432+
428433
export function requestEventTime() {
429434
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
430435
// We're inside React, so it's fine to read the actual time.
@@ -2059,7 +2064,7 @@ function commitRootImpl(
20592064

20602065
// Make sure to account for lanes that were updated by a concurrent event
20612066
// during the render phase; don't mark them as finished.
2062-
const concurrentlyUpdatedLanes = finishQueueingConcurrentUpdates();
2067+
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
20632068
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
20642069

20652070
markRootFinished(root, remainingLanes);

packages/react-reconciler/src/ReactInternalTypes.js

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {RootTag} from './ReactRootTags';
2727
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
2828
import type {Cache} from './ReactFiberCacheComponent.old';
2929
import type {Transition} from './ReactFiberTracingMarkerComponent.new';
30+
import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates.new';
3031

3132
// Unwind Circular: moved from ReactFiberHooks.old
3233
export type HookType =
@@ -225,6 +226,7 @@ type BaseFiberRootProperties = {|
225226
callbackPriority: Lane,
226227
eventTimes: LaneMap<number>,
227228
expirationTimes: LaneMap<number>,
229+
hiddenUpdates: LaneMap<Array<ConcurrentUpdate> | null>,
228230

229231
pendingLanes: Lanes,
230232
suspendedLanes: Lanes,

0 commit comments

Comments
 (0)