Skip to content

Commit 3c2149f

Browse files
acdlitekoto
authored andcommitted
Parallel transitions: Assign different lanes to consecutive transitions (facebook#20672)
* Land enableTransitionEntanglement changes Leaving the flag though because I plan to reuse it for additional, similar changes. * 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 6f3a7f8 commit 3c2149f

6 files changed

+625
-250
lines changed

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

+60-86
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ export type Lane = number;
3636
export type LaneMap<T> = Array<T>;
3737

3838
import invariant from 'shared/invariant';
39-
import {
40-
enableCache,
41-
enableTransitionEntanglement,
42-
} from 'shared/ReactFeatureFlags';
39+
import {enableCache} from 'shared/ReactFeatureFlags';
4340

4441
import {
4542
ImmediatePriority as ImmediateSchedulerPriority,
@@ -95,6 +92,7 @@ export const DefaultLanes: Lanes = /* */ 0b0000000000000000000
9592

9693
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
9794
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
95+
const SomeTransitionLane: Lane = /* */ 0b0000000000000000010000000000000;
9896

9997
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
10098

@@ -113,6 +111,9 @@ export const NoTimestamp = -1;
113111

114112
let currentUpdateLanePriority: LanePriority = NoLanePriority;
115113

114+
let nextTransitionLane: Lane = SomeTransitionLane;
115+
let nextRetryLane: Lane = SomeRetryLane;
116+
116117
export function getCurrentUpdateLanePriority(): LanePriority {
117118
return currentUpdateLanePriority;
118119
}
@@ -309,15 +310,6 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
309310
return NoLanes;
310311
}
311312

312-
if (enableTransitionEntanglement) {
313-
// We don't need to do anything extra here, because we apply per-lane
314-
// transition entanglement in the entanglement loop below.
315-
} else {
316-
// If there are higher priority lanes, we'll include them even if they
317-
// are suspended.
318-
nextLanes = pendingLanes & getEqualOrHigherPriorityLanes(nextLanes);
319-
}
320-
321313
// If we're already in the middle of a render, switching lanes will interrupt
322314
// it and we'll lose our progress. We should only do this if the new lanes are
323315
// higher priority.
@@ -350,6 +342,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
350342
// entanglement is usually "best effort": we'll try our best to render the
351343
// lanes in the same batch, but it's not worth throwing out partially
352344
// 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.
353350
//
354351
// For those exceptions where entanglement is semantically important, like
355352
// useMutableSource, we should ensure that there is no partial work at the
@@ -559,34 +556,23 @@ export function findUpdateLane(
559556
);
560557
}
561558

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-
}
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;
577567
}
578568
return lane;
579569
}
580570

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);
571+
export function claimNextRetryLane(): Lane {
572+
const lane = nextRetryLane;
573+
nextRetryLane <<= 1;
574+
if ((nextRetryLane & RetryLanes) === 0) {
575+
nextRetryLane = SomeRetryLane;
590576
}
591577
return lane;
592578
}
@@ -595,16 +581,6 @@ function getHighestPriorityLane(lanes: Lanes) {
595581
return lanes & -lanes;
596582
}
597583

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-
608584
export function pickArbitraryLane(lanes: Lanes): Lane {
609585
// This wrapper function gets inlined. Only exists so to communicate that it
610586
// doesn't matter which bit is selected; you can pick any bit without
@@ -676,39 +652,21 @@ export function markRootUpdated(
676652
) {
677653
root.pendingLanes |= updateLane;
678654

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;
655+
// If there are any suspended transitions, it's possible this new update
656+
// could unblock them. Clear the suspended lanes so that we can try rendering
657+
// them again.
658+
//
659+
// TODO: We really only need to unsuspend only lanes that are in the
660+
// `subtreeLanes` of the updated fiber, or the update lanes of the return
661+
// path. This would exclude suspended updates in an unrelated sibling tree,
662+
// since there's no way for this update to unblock it.
663+
//
664+
// We don't do this if the incoming update is idle, because we never process
665+
// idle updates until after all the regular updates have finished; there's no
666+
// way it could unblock a transition.
667+
if ((updateLane & IdleLanes) === NoLanes) {
668+
root.suspendedLanes = NoLanes;
669+
root.pingedLanes = NoLanes;
712670
}
713671

714672
const eventTimes = root.eventTimes;
@@ -801,16 +759,32 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
801759
}
802760

803761
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
804-
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.
805773

774+
const rootEntangledLanes = (root.entangledLanes |= entangledLanes);
806775
const entanglements = root.entanglements;
807-
let lanes = entangledLanes;
808-
while (lanes > 0) {
776+
let lanes = rootEntangledLanes;
777+
while (lanes) {
809778
const index = pickArbitraryLaneIndex(lanes);
810779
const lane = 1 << index;
811-
812-
entanglements[index] |= entangledLanes;
813-
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+
}
814788
lanes &= ~lane;
815789
}
816790
}

0 commit comments

Comments
 (0)