Skip to content

Commit

Permalink
Entangle overlapping transitions per queue
Browse files Browse the repository at this point in the history
When multiple transitions update the same queue, only the most recent
one should be allowed to finish. We shouldn't show intermediate states.

See #17418 for background on why this is important.

The way this currently works is that we always assign the same lane to
all transitions. It's impossible for one transition to finish without
also finishing all the others.

The downside of the current approach is that it's too aggressive. Not
all transitions are related to each other, so one should not block
the other.

The new approach is to only entangle transitions if they update one or
more of the same state hooks (or class components), because this
indicates that they are related. If they are unrelated, then they can
finish in any order, as long as they have different lanes.

However, this commit does not change anything about how the lanes are
assigned. All it does is add the mechanism to entangle per queue. So it
doesn't actually change any behavior, yet. But it's a requirement for my
next step, which is to assign different lanes to consecutive transitions
until we run out and cycle back to the beginning.

To avoid too many combinations of behavior, I'm reusing the feature
flag I added in my last PR (enableTransitionEntanglement). I'll wait
until the existing behavior has rolled out before landing this one.
  • Loading branch information
acdlite committed Jan 27, 2021
1 parent 9c32622 commit 07ecda9
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 11 deletions.
16 changes: 13 additions & 3 deletions packages/react-reconciler/src/ReactFiberClassComponent.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {

import {
enqueueUpdate,
entangleTransitions,
processUpdateQueue,
checkHasForceUpdateAfterProcessing,
resetHasForceUpdateBeforeProcessing,
Expand Down Expand Up @@ -214,7 +215,10 @@ const classComponentUpdater = {
}

enqueueUpdate(fiber, update, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitions(root, fiber, lane);
}

if (__DEV__) {
if (enableDebugTracing) {
Expand Down Expand Up @@ -246,7 +250,10 @@ const classComponentUpdater = {
}

enqueueUpdate(fiber, update, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitions(root, fiber, lane);
}

if (__DEV__) {
if (enableDebugTracing) {
Expand Down Expand Up @@ -277,7 +284,10 @@ const classComponentUpdater = {
}

enqueueUpdate(fiber, update, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitions(root, fiber, lane);
}

if (__DEV__) {
if (enableDebugTracing) {
Expand Down
36 changes: 34 additions & 2 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import {
isSubsetOfLanes,
mergeLanes,
removeLanes,
intersectLanes,
isTransitionLane,
markRootEntangled,
markRootMutableRead,
getCurrentUpdateLanePriority,
Expand Down Expand Up @@ -104,7 +106,11 @@ import {getIsRendering} from './ReactCurrentFiber';
import {logStateUpdateScheduled} from './DebugTracing';
import {markStateUpdateScheduled} from './SchedulingProfiler';
import {CacheContext} from './ReactFiberCacheComponent.new';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
import {
createUpdate,
enqueueUpdate,
entangleTransitions,
} from './ReactUpdateQueue.new';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
Expand All @@ -121,6 +127,7 @@ type Update<S, A> = {|
export type UpdateQueue<S, A> = {|
pending: Update<S, A> | null,
interleaved: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
Expand Down Expand Up @@ -654,6 +661,7 @@ function mountReducer<S, I, A>(
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
Expand Down Expand Up @@ -811,6 +819,10 @@ function updateReducer<S, I, A>(
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}

const dispatch: Dispatch<A> = (queue.dispatch: any);
Expand Down Expand Up @@ -1102,6 +1114,7 @@ function useMutableSource<Source, Snapshot>(
const newQueue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: snapshot,
Expand Down Expand Up @@ -1158,6 +1171,7 @@ function mountState<S>(
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
Expand Down Expand Up @@ -1821,6 +1835,9 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
const lane = requestUpdateLane(provider);
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
if (root !== null) {
entangleTransitions(root, fiber, lane);
}

const seededCache = new Map();
if (seedKey !== null && seedKey !== undefined && root !== null) {
Expand Down Expand Up @@ -1960,7 +1977,22 @@ function dispatchAction<S, A>(
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (isTransitionLane(lane) && root !== null) {
let queueLanes = queue.lanes;

// If any entangled lanes are no longer pending on the root, then they
// must have finished. We can remove them from the
queueLanes = intersectLanes(queueLanes, root.pendingLanes);

// Entangle the new transition lane with the other transition lanes.
const newQueueLanes = mergeLanes(queueLanes, lane);
if (newQueueLanes !== queueLanes) {
queue.lanes = newQueueLanes;
markRootEntangled(root, newQueueLanes);
}
}
}

if (__DEV__) {
Expand Down
13 changes: 10 additions & 3 deletions packages/react-reconciler/src/ReactFiberLane.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
}

if (enableTransitionEntanglement) {
// We don't need to include higher priority lanes, because in this
// experiment we always unsuspend all transitions whenever we receive
// an update.
// We don't need to do anything extra here, because we apply per-lane
// transition entanglement in the entanglement loop below.
} else {
// If there are higher priority lanes, we'll include them even if they
// are suspended.
Expand Down Expand Up @@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) {
return (lanes & TransitionLanes) === lanes;
}

export function isTransitionLane(lane: Lane) {
return (lane & TransitionLanes) !== 0;
}

// To ensure consistency across multiple updates in the same event, this should
// be a pure function, so that it always returns the same lane for given inputs.
export function findUpdateLane(
Expand Down Expand Up @@ -634,6 +637,10 @@ export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}

export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b;
}

// Seems redundant, but it changes the type from a single lane (used for
// updates) to a group of lanes (used for flushing work).
export function laneToLanes(lane: Lane): Lanes {
Expand Down
11 changes: 9 additions & 2 deletions packages/react-reconciler/src/ReactFiberReconciler.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ import {
IsThisRendererActing,
act,
} from './ReactFiberWorkLoop.new';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
import {
createUpdate,
enqueueUpdate,
entangleTransitions,
} from './ReactUpdateQueue.new';
import {
isRendering as ReactCurrentFiberIsRendering,
current as ReactCurrentFiberCurrent,
Expand Down Expand Up @@ -315,7 +319,10 @@ export function updateContainer(
}

enqueueUpdate(current, update, lane);
scheduleUpdateOnFiber(current, lane, eventTime);
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}

return lane;
}
Expand Down
35 changes: 34 additions & 1 deletion packages/react-reconciler/src/ReactUpdateQueue.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,17 @@
// regardless of priority. Intermediate state may vary according to system
// resources, but the final state is always the same.

import type {Fiber} from './ReactInternalTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.new';

import {
NoLane,
NoLanes,
isSubsetOfLanes,
mergeLanes,
isTransitionLane,
intersectLanes,
markRootEntangled,
} from './ReactFiberLane.new';
import {
enterDisallowedContextReadInDEV,
Expand Down Expand Up @@ -128,6 +131,7 @@ export type Update<State> = {|
export type SharedQueue<State> = {|
pending: Update<State> | null,
interleaved: Update<State> | null,
lanes: Lanes,
|};

export type UpdateQueue<State> = {|
Expand Down Expand Up @@ -167,6 +171,7 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
shared: {
pending: null,
interleaved: null,
lanes: NoLanes,
},
effects: null,
};
Expand Down Expand Up @@ -260,6 +265,30 @@ export function enqueueUpdate<State>(
}
}

export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}

const sharedQueue: SharedQueue<mixed> = (updateQueue: any).shared;
if (isTransitionLane(lane) && root !== null) {
let queueLanes = sharedQueue.lanes;

// If any entangled lanes are no longer pending on the root, then they
// must have finished. We can remove them from the
queueLanes = intersectLanes(queueLanes, root.pendingLanes);

// Entangle the new transition lane with the other transition lanes.
const newQueueLanes = mergeLanes(queueLanes, lane);
if (newQueueLanes !== queueLanes) {
sharedQueue.lanes = newQueueLanes;
markRootEntangled(root, newQueueLanes);
}
}
}

export function enqueueCapturedUpdate<State>(
workInProgress: Fiber,
capturedUpdate: Update<State>,
Expand Down Expand Up @@ -595,6 +624,10 @@ export function processUpdateQueue<State>(
newLanes = mergeLanes(newLanes, interleaved.lane);
interleaved = ((interleaved: any).next: Update<State>);
} while (interleaved !== lastInterleaved);
} else if (firstBaseUpdate === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.shared.lanes = NoLanes;
}

// Set the remaining expiration time to be whatever is remaining in the queue.
Expand Down

0 comments on commit 07ecda9

Please sign in to comment.