Skip to content

Commit a40a8d0

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 3eb1832 commit a40a8d0

File tree

4 files changed

+426
-115
lines changed

4 files changed

+426
-115
lines changed

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

Lines changed: 42 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const DefaultLanes: Lanes = /* */ 0b0000000000000000000
9595

9696
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
9797
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
98+
const SomeTransitionLane: Lane = /* */ 0b0000000000000000010000000000000;
9899

99100
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
100101

@@ -113,6 +114,9 @@ export const NoTimestamp = -1;
113114

114115
let currentUpdateLanePriority: LanePriority = NoLanePriority;
115116

117+
let nextTransitionLane: Lane = SomeTransitionLane;
118+
let nextRetryLane: Lane = SomeRetryLane;
119+
116120
export function getCurrentUpdateLanePriority(): LanePriority {
117121
return currentUpdateLanePriority;
118122
}
@@ -313,9 +317,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
313317
// We don't need to do anything extra here, because we apply per-lane
314318
// transition entanglement in the entanglement loop below.
315319
} else {
316-
// If there are higher priority lanes, we'll include them even if they
317-
// are suspended.
318-
nextLanes = pendingLanes & getEqualOrHigherPriorityLanes(nextLanes);
320+
// When per-lane entanglement is not enabled, always entangle all pending
321+
// transitions together.
322+
if (nextLanes & TransitionLanes) {
323+
nextLanes |= pendingLanes & TransitionLanes;
324+
}
319325
}
320326

321327
// If we're already in the middle of a render, switching lanes will interrupt
@@ -350,6 +356,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
350356
// entanglement is usually "best effort": we'll try our best to render the
351357
// lanes in the same batch, but it's not worth throwing out partially
352358
// completed work in order to do it.
359+
// TODO: Reconsider this. The counter-argument is that the partial work
360+
// represents an intermediate state, which we don't want to show to the user.
361+
// And by spending extra time finishing it, we're increasing the amount of
362+
// time it takes to show the final state, which is what they are actually
363+
// waiting for.
353364
//
354365
// For those exceptions where entanglement is semantically important, like
355366
// useMutableSource, we should ensure that there is no partial work at the
@@ -559,34 +570,23 @@ export function findUpdateLane(
559570
);
560571
}
561572

562-
// To ensure consistency across multiple updates in the same event, this should
563-
// be pure function, so that it always returns the same lane for given inputs.
564-
export function findTransitionLane(wipLanes: Lanes, pendingLanes: Lanes): Lane {
565-
// First look for lanes that are completely unclaimed, i.e. have no
566-
// pending work.
567-
let lane = pickArbitraryLane(TransitionLanes & ~pendingLanes);
568-
if (lane === NoLane) {
569-
// If all lanes have pending work, look for a lane that isn't currently
570-
// being worked on.
571-
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
572-
if (lane === NoLane) {
573-
// If everything is being worked on, pick any lane. This has the
574-
// effect of interrupting the current work-in-progress.
575-
lane = pickArbitraryLane(TransitionLanes);
576-
}
573+
export function claimNextTransitionLane(): Lane {
574+
// Cycle through the lanes, assigning each new transition to the next lane.
575+
// In most cases, this means every transition gets its own lane, until we
576+
// run out of lanes and cycle back to the beginning.
577+
const lane = nextTransitionLane;
578+
nextTransitionLane <<= 1;
579+
if ((nextTransitionLane & TransitionLanes) === 0) {
580+
nextTransitionLane = SomeTransitionLane;
577581
}
578582
return lane;
579583
}
580584

581-
// To ensure consistency across multiple updates in the same event, this should
582-
// be pure function, so that it always returns the same lane for given inputs.
583-
export function findRetryLane(wipLanes: Lanes): Lane {
584-
// This is a fork of `findUpdateLane` designed specifically for Suspense
585-
// "retries" — a special update that attempts to flip a Suspense boundary
586-
// from its placeholder state to its primary/resolved state.
587-
let lane = pickArbitraryLane(RetryLanes & ~wipLanes);
588-
if (lane === NoLane) {
589-
lane = pickArbitraryLane(RetryLanes);
585+
export function claimNextRetryLane(): Lane {
586+
const lane = nextRetryLane;
587+
nextRetryLane <<= 1;
588+
if ((nextRetryLane & RetryLanes) === 0) {
589+
nextRetryLane = SomeRetryLane;
590590
}
591591
return lane;
592592
}
@@ -595,16 +595,6 @@ function getHighestPriorityLane(lanes: Lanes) {
595595
return lanes & -lanes;
596596
}
597597

598-
function getLowestPriorityLane(lanes: Lanes): Lane {
599-
// This finds the most significant non-zero bit.
600-
const index = 31 - clz32(lanes);
601-
return index < 0 ? NoLanes : 1 << index;
602-
}
603-
604-
function getEqualOrHigherPriorityLanes(lanes: Lanes | Lane): Lanes {
605-
return (getLowestPriorityLane(lanes) << 1) - 1;
606-
}
607-
608598
export function pickArbitraryLane(lanes: Lanes): Lane {
609599
// This wrapper function gets inlined. Only exists so to communicate that it
610600
// doesn't matter which bit is selected; you can pick any bit without
@@ -676,39 +666,21 @@ export function markRootUpdated(
676666
) {
677667
root.pendingLanes |= updateLane;
678668

679-
// TODO: Theoretically, any update to any lane can unblock any other lane. But
680-
// it's not practical to try every single possible combination. We need a
681-
// heuristic to decide which lanes to attempt to render, and in which batches.
682-
// For now, we use the same heuristic as in the old ExpirationTimes model:
683-
// retry any lane at equal or lower priority, but don't try updates at higher
684-
// priority without also including the lower priority updates. This works well
685-
// when considering updates across different priority levels, but isn't
686-
// sufficient for updates within the same priority, since we want to treat
687-
// those updates as parallel.
688-
689-
// Unsuspend any update at equal or lower priority.
690-
const higherPriorityLanes = updateLane - 1; // Turns 0b1000 into 0b0111
691-
692-
if (enableTransitionEntanglement) {
693-
// If there are any suspended transitions, it's possible this new update
694-
// could unblock them. Clear the suspended lanes so that we can try rendering
695-
// them again.
696-
//
697-
// TODO: We really only need to unsuspend only lanes that are in the
698-
// `subtreeLanes` of the updated fiber, or the update lanes of the return
699-
// path. This would exclude suspended updates in an unrelated sibling tree,
700-
// since there's no way for this update to unblock it.
701-
//
702-
// We don't do this if the incoming update is idle, because we never process
703-
// idle updates until after all the regular updates have finished; there's no
704-
// way it could unblock a transition.
705-
if ((updateLane & IdleLanes) === NoLanes) {
706-
root.suspendedLanes = NoLanes;
707-
root.pingedLanes = NoLanes;
708-
}
709-
} else {
710-
root.suspendedLanes &= higherPriorityLanes;
711-
root.pingedLanes &= higherPriorityLanes;
669+
// If there are any suspended transitions, it's possible this new update could
670+
// unblock them. Clear the suspended lanes so that we can try rendering
671+
// them again.
672+
//
673+
// TODO: We really only need to unsuspend only lanes that are in the
674+
// `subtreeLanes` of the updated fiber, or the update lanes of the return
675+
// path. This would exclude suspended updates in an unrelated sibling tree,
676+
// since there's no way for this update to unblock it.
677+
//
678+
// We don't do this if the incoming update is idle, because we never process
679+
// idle updates until after all the regular updates have finished; there's no
680+
// way it could unblock a transition.
681+
if ((updateLane & IdleLanes) === NoLanes) {
682+
root.suspendedLanes = NoLanes;
683+
root.pingedLanes = NoLanes;
712684
}
713685

714686
const eventTimes = root.eventTimes;

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

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

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

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

434432
const isTransition = requestCurrentTransition() !== NoTransition;
435433
if (isTransition) {
436-
if (currentEventPendingLanes !== NoLanes) {
437-
currentEventPendingLanes =
438-
mostRecentlyUpdatedRoot !== null
439-
? mostRecentlyUpdatedRoot.pendingLanes
440-
: NoLanes;
434+
if (currentEventTransitionLane === NoLane) {
435+
currentEventTransitionLane = claimNextTransitionLane();
441436
}
442-
return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);
437+
return currentEventTransitionLane;
443438
}
444439

445440
// TODO: Remove this dependency on the Scheduler priority.
@@ -492,7 +487,8 @@ function requestRetryLane(fiber: Fiber) {
492487
if (currentEventWipLanes === NoLanes) {
493488
currentEventWipLanes = workInProgressRootIncludedLanes;
494489
}
495-
return findRetryLane(currentEventWipLanes);
490+
491+
return claimNextRetryLane();
496492
}
497493

498494
export function scheduleUpdateOnFiber(
@@ -616,13 +612,6 @@ export function scheduleUpdateOnFiber(
616612
schedulePendingInteractions(root, lane);
617613
}
618614

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

@@ -770,7 +759,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
770759
// event time. The next update will compute a new event time.
771760
currentEventTime = NoTimestamp;
772761
currentEventWipLanes = NoLanes;
773-
currentEventPendingLanes = NoLanes;
762+
currentEventTransitionLane = NoLanes;
774763

775764
invariant(
776765
(executionContext & (RenderContext | CommitContext)) === NoContext,
@@ -2437,6 +2426,8 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
24372426
// suspended it has resolved, which means at least part of the tree was
24382427
// likely unblocked. Try rendering again, at a new expiration time.
24392428
if (retryLane === NoLane) {
2429+
// TODO: Assign this to `suspenseState.retryLane`? to avoid
2430+
// unnecessary entanglement?
24402431
retryLane = requestRetryLane(boundaryFiber);
24412432
}
24422433
// TODO: Special case idle priority?

packages/react-reconciler/src/__tests__/ReactExpiration-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ describe('ReactExpiration', () => {
733733
// Both normal pri updates should have expired.
734734
expect(Scheduler).toFlushExpired([
735735
'Sibling',
736-
gate(flags => flags.enableTransitionEntanglement)
736+
gate(flags => flags.enableTransitionEntanglement || flags.new)
737737
? // Notice that the high pri update didn't flush yet. Expiring one lane
738738
// doesn't affect other lanes. (Unless they are intentionally
739739
// entangled, like we do for overlapping transitions that affect the

0 commit comments

Comments
 (0)