Skip to content

Commit

Permalink
Entangle overlapping transitions per queue (facebook#20670)
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 facebook#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.
  • Loading branch information
acdlite authored and koto committed Jun 15, 2021
1 parent 63fade0 commit 069e7b6
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 22 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
16 changes: 13 additions & 3 deletions packages/react-reconciler/src/ReactFiberClassComponent.old.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);
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);
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);
scheduleUpdateOnFiber(fiber, lane, eventTime);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitions(root, fiber, lane);
}

if (__DEV__) {
if (enableDebugTracing) {
Expand Down
39 changes: 37 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,25 @@ 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 shared queue, which
// represents a superset of the actually pending lanes. In some cases we
// may entangle more than we need to, but that's OK. In fact it's worse if
// we *don't* entangle when we should.
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
41 changes: 39 additions & 2 deletions packages/react-reconciler/src/ReactFiberHooks.old.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 @@ -103,7 +105,11 @@ import {getIsRendering} from './ReactCurrentFiber';
import {logStateUpdateScheduled} from './DebugTracing';
import {markStateUpdateScheduled} from './SchedulingProfiler';
import {CacheContext} from './ReactFiberCacheComponent.old';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
import {
createUpdate,
enqueueUpdate,
entangleTransitions,
} from './ReactUpdateQueue.old';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand All @@ -118,6 +124,7 @@ type Update<S, A> = {|

type UpdateQueue<S, A> = {|
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
Expand Down Expand Up @@ -650,6 +657,7 @@ function mountReducer<S, I, A>(
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
Expand Down Expand Up @@ -792,6 +800,12 @@ function updateReducer<S, I, A>(
queue.lastRenderedState = newState;
}

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);
return [hook.memoizedState, dispatch];
}
Expand Down Expand Up @@ -1080,6 +1094,7 @@ function useMutableSource<Source, Snapshot>(
// including any interleaving updates that occur.
const newQueue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: snapshot,
Expand Down Expand Up @@ -1135,6 +1150,7 @@ function mountState<S>(
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
Expand Down Expand Up @@ -1798,6 +1814,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 @@ -1914,7 +1933,25 @@ 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 shared queue, which
// represents a superset of the actually pending lanes. In some cases we
// may entangle more than we need to, but that's OK. In fact it's worse if
// we *don't* entangle when we should.
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
13 changes: 10 additions & 3 deletions packages/react-reconciler/src/ReactFiberLane.old.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
Loading

0 comments on commit 069e7b6

Please sign in to comment.