Skip to content

Commit 261cf29

Browse files
committed
Assign different lanes to consecutive transitions
Currently we always assign the same lane to all transitions. This means if there are two pending transitions at the same time, neither transition can finish until both can finish, even if they affect completely separate parts of the UI. The new approach is to assign a different lane to each consecutive transition, by shifting the bit to the right each time. When we reach the end of the bit range, we cycle back to the first bit. In practice, this should mean that all transitions get their own dedicated lane, unless we have more pending transitions than lanes, which should be rare. We retain our existing behavior of assigning the same lane to all transitions within the same event. This is achieved by caching the first lane assigned to a transition, then re-using that one until the next React task, by which point the event must have finished. This preserves the guarantee that all transition updates that result from a single event will be consistent.
1 parent cb328f5 commit 261cf29

File tree

5 files changed

+589
-125
lines changed

5 files changed

+589
-125
lines changed

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

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const DefaultLanes: Lanes = /* */ 0b0000000000000000000
9292

9393
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
9494
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
95+
const SomeTransitionLane: Lane = /* */ 0b0000000000000000010000000000000;
9596

9697
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
9798

@@ -110,6 +111,9 @@ export const NoTimestamp = -1;
110111

111112
let currentUpdateLanePriority: LanePriority = NoLanePriority;
112113

114+
let nextTransitionLane: Lane = SomeTransitionLane;
115+
let nextRetryLane: Lane = SomeRetryLane;
116+
113117
export function getCurrentUpdateLanePriority(): LanePriority {
114118
return currentUpdateLanePriority;
115119
}
@@ -338,6 +342,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
338342
// entanglement is usually "best effort": we'll try our best to render the
339343
// lanes in the same batch, but it's not worth throwing out partially
340344
// completed work in order to do it.
345+
// TODO: Reconsider this. The counter-argument is that the partial work
346+
// represents an intermediate state, which we don't want to show to the user.
347+
// And by spending extra time finishing it, we're increasing the amount of
348+
// time it takes to show the final state, which is what they are actually
349+
// waiting for.
341350
//
342351
// For those exceptions where entanglement is semantically important, like
343352
// useMutableSource, we should ensure that there is no partial work at the
@@ -547,34 +556,23 @@ export function findUpdateLane(
547556
);
548557
}
549558

550-
// To ensure consistency across multiple updates in the same event, this should
551-
// be pure function, so that it always returns the same lane for given inputs.
552-
export function findTransitionLane(wipLanes: Lanes, pendingLanes: Lanes): Lane {
553-
// First look for lanes that are completely unclaimed, i.e. have no
554-
// pending work.
555-
let lane = pickArbitraryLane(TransitionLanes & ~pendingLanes);
556-
if (lane === NoLane) {
557-
// If all lanes have pending work, look for a lane that isn't currently
558-
// being worked on.
559-
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
560-
if (lane === NoLane) {
561-
// If everything is being worked on, pick any lane. This has the
562-
// effect of interrupting the current work-in-progress.
563-
lane = pickArbitraryLane(TransitionLanes);
564-
}
559+
export function claimNextTransitionLane(): Lane {
560+
// Cycle through the lanes, assigning each new transition to the next lane.
561+
// In most cases, this means every transition gets its own lane, until we
562+
// run out of lanes and cycle back to the beginning.
563+
const lane = nextTransitionLane;
564+
nextTransitionLane <<= 1;
565+
if ((nextTransitionLane & TransitionLanes) === 0) {
566+
nextTransitionLane = SomeTransitionLane;
565567
}
566568
return lane;
567569
}
568570

569-
// To ensure consistency across multiple updates in the same event, this should
570-
// be pure function, so that it always returns the same lane for given inputs.
571-
export function findRetryLane(wipLanes: Lanes): Lane {
572-
// This is a fork of `findUpdateLane` designed specifically for Suspense
573-
// "retries" — a special update that attempts to flip a Suspense boundary
574-
// from its placeholder state to its primary/resolved state.
575-
let lane = pickArbitraryLane(RetryLanes & ~wipLanes);
576-
if (lane === NoLane) {
577-
lane = pickArbitraryLane(RetryLanes);
571+
export function claimNextRetryLane(): Lane {
572+
const lane = nextRetryLane;
573+
nextRetryLane <<= 1;
574+
if ((nextRetryLane & RetryLanes) === 0) {
575+
nextRetryLane = SomeRetryLane;
578576
}
579577
return lane;
580578
}
@@ -761,16 +759,32 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
761759
}
762760

763761
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
764-
root.entangledLanes |= entangledLanes;
762+
// In addition to entangling each of the given lanes with each other, we also
763+
// have to consider _transitive_ entanglements. For each lane that is already
764+
// entangled with *any* of the given lanes, that lane is now transitively
765+
// entangled with *all* the given lanes.
766+
//
767+
// Translated: If C is entangled with A, then entangling A with B also
768+
// entangles C with B.
769+
//
770+
// If this is hard to grasp, it might help to intentionally break this
771+
// function and look at the tests that fail in ReactTransition-test.js. Try
772+
// commenting out one of the conditions below.
765773

774+
const rootEntangledLanes = (root.entangledLanes |= entangledLanes);
766775
const entanglements = root.entanglements;
767-
let lanes = entangledLanes;
768-
while (lanes > 0) {
776+
let lanes = rootEntangledLanes;
777+
while (lanes) {
769778
const index = pickArbitraryLaneIndex(lanes);
770779
const lane = 1 << index;
771-
772-
entanglements[index] |= entangledLanes;
773-
780+
if (
781+
// Is this one of the newly entangled lanes?
782+
(lane & entangledLanes) |
783+
// Is this lane transitively entangled with the newly entangled lanes?
784+
(entanglements[index] & entangledLanes)
785+
) {
786+
entanglements[index] |= entangledLanes;
787+
}
774788
lanes &= ~lane;
775789
}
776790
}

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

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const DefaultLanes: Lanes = /* */ 0b0000000000000000000
9292

9393
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
9494
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
95+
const SomeTransitionLane: Lane = /* */ 0b0000000000000000010000000000000;
9596

9697
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
9798

@@ -110,6 +111,9 @@ export const NoTimestamp = -1;
110111

111112
let currentUpdateLanePriority: LanePriority = NoLanePriority;
112113

114+
let nextTransitionLane: Lane = SomeTransitionLane;
115+
let nextRetryLane: Lane = SomeRetryLane;
116+
113117
export function getCurrentUpdateLanePriority(): LanePriority {
114118
return currentUpdateLanePriority;
115119
}
@@ -338,6 +342,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
338342
// entanglement is usually "best effort": we'll try our best to render the
339343
// lanes in the same batch, but it's not worth throwing out partially
340344
// completed work in order to do it.
345+
// TODO: Reconsider this. The counter-argument is that the partial work
346+
// represents an intermediate state, which we don't want to show to the user.
347+
// And by spending extra time finishing it, we're increasing the amount of
348+
// time it takes to show the final state, which is what they are actually
349+
// waiting for.
341350
//
342351
// For those exceptions where entanglement is semantically important, like
343352
// useMutableSource, we should ensure that there is no partial work at the
@@ -547,34 +556,23 @@ export function findUpdateLane(
547556
);
548557
}
549558

550-
// To ensure consistency across multiple updates in the same event, this should
551-
// be pure function, so that it always returns the same lane for given inputs.
552-
export function findTransitionLane(wipLanes: Lanes, pendingLanes: Lanes): Lane {
553-
// First look for lanes that are completely unclaimed, i.e. have no
554-
// pending work.
555-
let lane = pickArbitraryLane(TransitionLanes & ~pendingLanes);
556-
if (lane === NoLane) {
557-
// If all lanes have pending work, look for a lane that isn't currently
558-
// being worked on.
559-
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
560-
if (lane === NoLane) {
561-
// If everything is being worked on, pick any lane. This has the
562-
// effect of interrupting the current work-in-progress.
563-
lane = pickArbitraryLane(TransitionLanes);
564-
}
559+
export function claimNextTransitionLane(): Lane {
560+
// Cycle through the lanes, assigning each new transition to the next lane.
561+
// In most cases, this means every transition gets its own lane, until we
562+
// run out of lanes and cycle back to the beginning.
563+
const lane = nextTransitionLane;
564+
nextTransitionLane <<= 1;
565+
if ((nextTransitionLane & TransitionLanes) === 0) {
566+
nextTransitionLane = SomeTransitionLane;
565567
}
566568
return lane;
567569
}
568570

569-
// To ensure consistency across multiple updates in the same event, this should
570-
// be pure function, so that it always returns the same lane for given inputs.
571-
export function findRetryLane(wipLanes: Lanes): Lane {
572-
// This is a fork of `findUpdateLane` designed specifically for Suspense
573-
// "retries" — a special update that attempts to flip a Suspense boundary
574-
// from its placeholder state to its primary/resolved state.
575-
let lane = pickArbitraryLane(RetryLanes & ~wipLanes);
576-
if (lane === NoLane) {
577-
lane = pickArbitraryLane(RetryLanes);
571+
export function claimNextRetryLane(): Lane {
572+
const lane = nextRetryLane;
573+
nextRetryLane <<= 1;
574+
if ((nextRetryLane & RetryLanes) === 0) {
575+
nextRetryLane = SomeRetryLane;
578576
}
579577
return lane;
580578
}
@@ -761,16 +759,32 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
761759
}
762760

763761
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
764-
root.entangledLanes |= entangledLanes;
762+
// In addition to entangling each of the given lanes with each other, we also
763+
// have to consider _transitive_ entanglements. For each lane that is already
764+
// entangled with *any* of the given lanes, that lane is now transitively
765+
// entangled with *all* the given lanes.
766+
//
767+
// Translated: If C is entangled with A, then entangling A with B also
768+
// entangles C with B.
769+
//
770+
// If this is hard to grasp, it might help to intentionally break this
771+
// function and look at the tests that fail in ReactTransition-test.js. Try
772+
// commenting out one of the conditions below.
765773

774+
const rootEntangledLanes = (root.entangledLanes |= entangledLanes);
766775
const entanglements = root.entanglements;
767-
let lanes = entangledLanes;
768-
while (lanes > 0) {
776+
let lanes = rootEntangledLanes;
777+
while (lanes) {
769778
const index = pickArbitraryLaneIndex(lanes);
770779
const lane = 1 << index;
771-
772-
entanglements[index] |= entangledLanes;
773-
780+
if (
781+
// Is this one of the newly entangled lanes?
782+
(lane & entangledLanes) |
783+
// Is this lane transitively entangled with the newly entangled lanes?
784+
(entanglements[index] & entangledLanes)
785+
) {
786+
entanglements[index] |= entangledLanes;
787+
}
774788
lanes &= ~lane;
775789
}
776790
}

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

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ import {
145145
SyncBatchedLane,
146146
NoTimestamp,
147147
findUpdateLane,
148-
findTransitionLane,
149-
findRetryLane,
148+
claimNextTransitionLane,
149+
claimNextRetryLane,
150150
includesSomeLane,
151151
isSubsetOfLanes,
152152
mergeLanes,
@@ -302,8 +302,6 @@ let workInProgressRootUpdatedLanes: Lanes = NoLanes;
302302
// Lanes that were pinged (in an interleaved event) during this render.
303303
let workInProgressRootPingedLanes: Lanes = NoLanes;
304304

305-
let mostRecentlyUpdatedRoot: FiberRoot | null = null;
306-
307305
// The most recent time we committed a fallback. This lets us ensure a train
308306
// model where we don't commit new loading states in too quick succession.
309307
let globalMostRecentFallbackTime: number = 0;
@@ -360,7 +358,7 @@ let spawnedWorkDuringRender: null | Array<Lane | Lanes> = null;
360358
// between the first and second call.
361359
let currentEventTime: number = NoTimestamp;
362360
let currentEventWipLanes: Lanes = NoLanes;
363-
let currentEventPendingLanes: Lanes = NoLanes;
361+
let currentEventTransitionLane: Lanes = NoLanes;
364362

365363
// Dev only flag that tracks if passive effects are currently being flushed.
366364
// We warn about state updates for unmounted components differently in this case.
@@ -428,20 +426,17 @@ export function requestUpdateLane(fiber: Fiber): Lane {
428426
// event. Then reset the cached values once we can be sure the event is over.
429427
// Our heuristic for that is whenever we enter a concurrent work loop.
430428
//
431-
// We'll do the same for `currentEventPendingLanes` below.
429+
// We'll do the same for `currentEventTransitionLane` below.
432430
if (currentEventWipLanes === NoLanes) {
433431
currentEventWipLanes = workInProgressRootIncludedLanes;
434432
}
435433

436434
const isTransition = requestCurrentTransition() !== NoTransition;
437435
if (isTransition) {
438-
if (currentEventPendingLanes !== NoLanes) {
439-
currentEventPendingLanes =
440-
mostRecentlyUpdatedRoot !== null
441-
? mostRecentlyUpdatedRoot.pendingLanes
442-
: NoLanes;
436+
if (currentEventTransitionLane === NoLane) {
437+
currentEventTransitionLane = claimNextTransitionLane();
443438
}
444-
return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);
439+
return currentEventTransitionLane;
445440
}
446441

447442
// TODO: Remove this dependency on the Scheduler priority.
@@ -494,7 +489,8 @@ function requestRetryLane(fiber: Fiber) {
494489
if (currentEventWipLanes === NoLanes) {
495490
currentEventWipLanes = workInProgressRootIncludedLanes;
496491
}
497-
return findRetryLane(currentEventWipLanes);
492+
493+
return claimNextRetryLane();
498494
}
499495

500496
export function scheduleUpdateOnFiber(
@@ -618,13 +614,6 @@ export function scheduleUpdateOnFiber(
618614
schedulePendingInteractions(root, lane);
619615
}
620616

621-
// We use this when assigning a lane for a transition inside
622-
// `requestUpdateLane`. We assume it's the same as the root being updated,
623-
// since in the common case of a single root app it probably is. If it's not
624-
// the same root, then it's not a huge deal, we just might batch more stuff
625-
// together more than necessary.
626-
mostRecentlyUpdatedRoot = root;
627-
628617
return root;
629618
}
630619

@@ -793,7 +782,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
793782
// event time. The next update will compute a new event time.
794783
currentEventTime = NoTimestamp;
795784
currentEventWipLanes = NoLanes;
796-
currentEventPendingLanes = NoLanes;
785+
currentEventTransitionLane = NoLanes;
797786

798787
invariant(
799788
(executionContext & (RenderContext | CommitContext)) === NoContext,
@@ -2461,6 +2450,8 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
24612450
// suspended it has resolved, which means at least part of the tree was
24622451
// likely unblocked. Try rendering again, at a new expiration time.
24632452
if (retryLane === NoLane) {
2453+
// TODO: Assign this to `suspenseState.retryLane`? to avoid
2454+
// unnecessary entanglement?
24642455
retryLane = requestRetryLane(boundaryFiber);
24652456
}
24662457
// TODO: Special case idle priority?

0 commit comments

Comments
 (0)