Skip to content

Commit

Permalink
Track entangled lanes separately from update lane
Browse files Browse the repository at this point in the history
A small refactor to how the lane entanglement mechanism works. We can
now distinguish between the lane that "spawned" a render task (i.e. a
new update) versus the lanes that it's entangled with. Both the update
lane and the entangled lanes will be included while rendering, but
by keeping them separate, we don't lose the original priority.

In practical terms, this means we can now entangle a low priority update
with a higher priority lane while rendering at the lower priority.

To do this, lanes that are entangled at the root are now tracked using
the same variable that we use to track the "base lanes" when revealing
a previously hidden tree — conceptually, they are the same thing. I
also renamed this variable (from subtreeLanes to entangledRenderLanes)
to better reflect how it's used.

My primary motivation is related to useDeferredValue, which I'll address
in a later PR.
  • Loading branch information
acdlite committed Oct 12, 2023
1 parent 18e7d91 commit bfc4dbc
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 41 deletions.
18 changes: 11 additions & 7 deletions packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,19 +726,23 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog(['Suspend! [/path/b]']);

await act(async () => {
resolvePromise();

// TODO: Since the transition previously suspended, there's no need for
// this transition to be rendered synchronously on susbequent attempts;
// if we fail to commit synchronously the first time, the scroll
// restoration state won't be restored anyway. We can improve this later.
// Since the transition previously suspended, there's no need for this
// transition to be rendered synchronously on susbequent attempts; if we
// fail to commit synchronously the first time, the scroll restoration
// state won't be restored anyway.
//
// Once this is implemented, update this test to yield in between each
// child to prove that it's concurrent.
// Yield in between each child to prove that it's concurrent.
await waitForMicrotasks();
assertLog(['Before', '/path/b', 'After']);
assertLog([]);

await waitFor(['Before']);
await waitFor(['/path/b']);
await waitFor(['After']);
});
assertLog([]);
expect(div.textContent).toBe('/path/b');
Expand Down
21 changes: 13 additions & 8 deletions packages/react-reconciler/src/ReactFiberHiddenContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import type {Lanes} from './ReactFiberLane';

import {createCursor, push, pop} from './ReactFiberStack';

import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop';
import {
getEntangledRenderLanes,
setEntangledRenderLanes,
} from './ReactFiberWorkLoop';
import {NoLanes, mergeLanes} from './ReactFiberLane';

// TODO: Remove `renderLanes` context in favor of hidden context
Expand All @@ -29,26 +32,28 @@ type HiddenContext = {
// InvisibleParentContext that is currently managed by SuspenseContext.
export const currentTreeHiddenStackCursor: StackCursor<HiddenContext | null> =
createCursor(null);
export const prevRenderLanesStackCursor: StackCursor<Lanes> =
export const prevEntangledRenderLanesCursor: StackCursor<Lanes> =
createCursor(NoLanes);

export function pushHiddenContext(fiber: Fiber, context: HiddenContext): void {
const prevRenderLanes = getRenderLanes();
push(prevRenderLanesStackCursor, prevRenderLanes, fiber);
const prevEntangledRenderLanes = getEntangledRenderLanes();
push(prevEntangledRenderLanesCursor, prevEntangledRenderLanes, fiber);
push(currentTreeHiddenStackCursor, context, fiber);

// When rendering a subtree that's currently hidden, we must include all
// lanes that would have rendered if the hidden subtree hadn't been deferred.
// That is, in order to reveal content from hidden -> visible, we must commit
// all the updates that we skipped when we originally hid the tree.
setRenderLanes(mergeLanes(prevRenderLanes, context.baseLanes));
setEntangledRenderLanes(
mergeLanes(prevEntangledRenderLanes, context.baseLanes),
);
}

export function reuseHiddenContextOnStack(fiber: Fiber): void {
// This subtree is not currently hidden, so we don't need to add any lanes
// to the render lanes. But we still need to push something to avoid a
// context mismatch. Reuse the existing context on the stack.
push(prevRenderLanesStackCursor, getRenderLanes(), fiber);
push(prevEntangledRenderLanesCursor, getEntangledRenderLanes(), fiber);
push(
currentTreeHiddenStackCursor,
currentTreeHiddenStackCursor.current,
Expand All @@ -58,10 +63,10 @@ export function reuseHiddenContextOnStack(fiber: Fiber): void {

export function popHiddenContext(fiber: Fiber): void {
// Restore the previous render lanes from the stack
setRenderLanes(prevRenderLanesStackCursor.current);
setEntangledRenderLanes(prevEntangledRenderLanesCursor.current);

pop(currentTreeHiddenStackCursor, fiber);
pop(prevRenderLanesStackCursor, fiber);
pop(prevEntangledRenderLanesCursor, fiber);
}

export function isCurrentTreeHidden(): boolean {
Expand Down
50 changes: 43 additions & 7 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000

export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;

export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
Expand Down Expand Up @@ -274,17 +275,23 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
}
}

return nextLanes;
}

export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
let entangledLanes = renderLanes;

if (
allowConcurrentByDefault &&
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
) {
// Do nothing, use the lanes as they were assigned.
} else if ((nextLanes & InputContinuousLane) !== NoLanes) {
} else if ((entangledLanes & InputContinuousLane) !== NoLanes) {
// When updates are sync by default, we entangle continuous priority updates
// and default updates, so they render in the same batch. The only reason
// they use separate lanes is because continuous updates should interrupt
// transitions, but default updates should not.
nextLanes |= pendingLanes & DefaultLane;
entangledLanes |= entangledLanes & DefaultLane;
}

// Check for entangled lanes and add them to the batch.
Expand All @@ -309,21 +316,21 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
// For those exceptions where entanglement is semantically important,
// we should ensure that there is no partial work at the
// time we apply the entanglement.
const entangledLanes = root.entangledLanes;
if (entangledLanes !== NoLanes) {
const allEntangledLanes = root.entangledLanes;
if (allEntangledLanes !== NoLanes) {
const entanglements = root.entanglements;
let lanes = nextLanes & entangledLanes;
let lanes = entangledLanes & allEntangledLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;

nextLanes |= entanglements[index];
entangledLanes |= entanglements[index];

lanes &= ~lane;
}
}

return nextLanes;
return entangledLanes;
}

function computeExpirationTime(lane: Lane, currentTime: number) {
Expand Down Expand Up @@ -404,6 +411,7 @@ export function markStarvedLanesAsExpired(
// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
// TODO: We should be able to replace this with upgradePendingLanesToSync
//
// We exclude retry lanes because those must always be time sliced, in order
// to unwrap uncached promises.
Expand Down Expand Up @@ -708,6 +716,34 @@ export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
}
}

export function upgradePendingLaneToSync(root: FiberRoot, lane: Lane) {
// Since we're upgrading the priority of the given lane, there is now pending
// sync work.
root.pendingLanes |= SyncLane;

// Entangle the sync lane with the lane we're upgrading. This means SyncLane
// will not be allowed to finish without also finishing the given lane.
root.entangledLanes |= SyncLane;
root.entanglements[SyncLaneIndex] |= lane;
}

export function upgradePendingLanesToSync(
root: FiberRoot,
lanesToUpgrade: Lanes,
) {
// Same as upgradePendingLaneToSync but accepts multiple lanes, so it's a
// bit slower.
root.pendingLanes |= SyncLane;
root.entangledLanes |= SyncLane;
let lanes = lanesToUpgrade;
while (lanes) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
root.entanglements[SyncLaneIndex] |= lane;
lanes &= ~lane;
}
}

export function markHiddenUpdate(
root: FiberRoot,
update: ConcurrentUpdate,
Expand Down
8 changes: 5 additions & 3 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import {
getNextLanes,
includesSyncLane,
markStarvedLanesAsExpired,
markRootEntangled,
mergeLanes,
upgradePendingLaneToSync,
claimNextTransitionLane,
} from './ReactFiberLane';
import {
Expand Down Expand Up @@ -250,7 +249,10 @@ function processRootScheduleInMicrotask() {
currentEventTransitionLane !== NoLane &&
shouldAttemptEagerTransition()
) {
markRootEntangled(root, mergeLanes(currentEventTransitionLane, SyncLane));
// A transition was scheduled during an event, but we're going to try to
// render it synchronously anyway. We do this during a popstate event to
// preserve the scroll position of the previous page.
upgradePendingLaneToSync(root, currentEventTransitionLane);
}

const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
Expand Down
42 changes: 26 additions & 16 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,12 @@ import {
includesBlockingLane,
includesExpiredLane,
getNextLanes,
getEntangledLanes,
getLanesToRetrySynchronouslyOnError,
markRootUpdated,
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
markRootPinged,
markRootEntangled,
upgradePendingLanesToSync,
markRootFinished,
addFiberToLanesMap,
movePendingFibersToMemoized,
Expand Down Expand Up @@ -349,8 +350,8 @@ let workInProgressRootDidAttachPingListener: boolean = false;
// HiddenContext module.
//
// Most things in the work loop should deal with workInProgressRootRenderLanes.
// Most things in begin/complete phases should deal with renderLanes.
export let renderLanes: Lanes = NoLanes;
// Most things in begin/complete phases should deal with entangledRenderLanes.
export let entangledRenderLanes: Lanes = NoLanes;

// Whether to root completed, errored, suspended, etc.
let workInProgressRootExitStatus: RootExitStatus = RootInProgress;
Expand Down Expand Up @@ -1335,7 +1336,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {

export function flushRoot(root: FiberRoot, lanes: Lanes) {
if (lanes !== NoLanes) {
markRootEntangled(root, mergeLanes(lanes, SyncLane));
upgradePendingLanesToSync(root, lanes);
ensureRootIsScheduled(root);
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
resetRenderTimer();
Expand Down Expand Up @@ -1471,12 +1472,12 @@ export function isInvalidExecutionContextForEventFunction(): boolean {
// hidden subtree. The stack logic is managed there because that's the only
// place that ever modifies it. Which module it lives in doesn't matter for
// performance because this function will get inlined regardless
export function setRenderLanes(subtreeRenderLanes: Lanes) {
renderLanes = subtreeRenderLanes;
export function setEntangledRenderLanes(newEntangledRenderLanes: Lanes) {
entangledRenderLanes = newEntangledRenderLanes;
}

export function getRenderLanes(): Lanes {
return renderLanes;
export function getEntangledRenderLanes(): Lanes {
return entangledRenderLanes;
}

function resetWorkInProgressStack() {
Expand Down Expand Up @@ -1526,7 +1527,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes;
workInProgressRootRenderLanes = lanes;
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
workInProgressRootDidAttachPingListener = false;
Expand All @@ -1539,6 +1540,15 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;

// Get the lanes that are entangled with whatever we're about to render. We
// track these separately so we can distinguish the priority of the render
// task from the priority of the lanes it is entangled with. For example, a
// transition may not be allowed to finish unless it includes the Sync lane,
// which is currently suspended. We should be able to render the Transition
// and Sync lane in the same batch, but at Transition priority, because the
// Sync lane already suspended.
entangledRenderLanes = getEntangledLanes(root, lanes);

finishQueueingConcurrentUpdates();

if (__DEV__) {
Expand Down Expand Up @@ -2249,10 +2259,10 @@ function performUnitOfWork(unitOfWork: Fiber): void {
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderLanes);
next = beginWork(current, unitOfWork, entangledRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderLanes);
next = beginWork(current, unitOfWork, entangledRenderLanes);
}

resetCurrentDebugFiberInDEV();
Expand Down Expand Up @@ -2359,9 +2369,9 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
unitOfWork = workInProgress = resetWorkInProgress(
unitOfWork,
renderLanes,
entangledRenderLanes,
);
next = beginWork(current, unitOfWork, renderLanes);
next = beginWork(current, unitOfWork, entangledRenderLanes);
break;
}
}
Expand Down Expand Up @@ -2471,10 +2481,10 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
setCurrentDebugFiberInDEV(completedWork);
let next;
if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) {
next = completeWork(current, completedWork, renderLanes);
next = completeWork(current, completedWork, entangledRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, renderLanes);
next = completeWork(current, completedWork, entangledRenderLanes);
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
Expand Down Expand Up @@ -2516,7 +2526,7 @@ function unwindUnitOfWork(unitOfWork: Fiber): void {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(current, incompleteWork, renderLanes);
const next = unwindWork(current, incompleteWork, entangledRenderLanes);

// Because this fiber did not complete, don't reset its lanes.

Expand Down

0 comments on commit bfc4dbc

Please sign in to comment.