From 07ecda9d19b216aac2c21486157153d68cb94711 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 26 Jan 2021 23:19:25 -0600 Subject: [PATCH] Entangle overlapping transitions per queue 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. --- .../src/ReactFiberClassComponent.new.js | 16 +++++++-- .../src/ReactFiberHooks.new.js | 36 +++++++++++++++++-- .../src/ReactFiberLane.new.js | 13 +++++-- .../src/ReactFiberReconciler.new.js | 11 ++++-- .../src/ReactUpdateQueue.new.js | 35 +++++++++++++++++- 5 files changed, 100 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 9ed6dc23c9f16..389b4877852db 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -40,6 +40,7 @@ import { import { enqueueUpdate, + entangleTransitions, processUpdateQueue, checkHasForceUpdateAfterProcessing, resetHasForceUpdateBeforeProcessing, @@ -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) { @@ -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) { @@ -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) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 374d383030cdb..69f75d47925f3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -45,6 +45,8 @@ import { isSubsetOfLanes, mergeLanes, removeLanes, + intersectLanes, + isTransitionLane, markRootEntangled, markRootMutableRead, getCurrentUpdateLanePriority, @@ -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; @@ -121,6 +127,7 @@ type Update = {| export type UpdateQueue = {| pending: Update | null, interleaved: Update | null, + lanes: Lanes, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, lastRenderedState: S | null, @@ -654,6 +661,7 @@ function mountReducer( const queue = (hook.queue = { pending: null, interleaved: null, + lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: (initialState: any), @@ -811,6 +819,10 @@ function updateReducer( markSkippedUpdateLanes(interleavedLane); interleaved = ((interleaved: any).next: Update); } 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 = (queue.dispatch: any); @@ -1102,6 +1114,7 @@ function useMutableSource( const newQueue = { pending: null, interleaved: null, + lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: snapshot, @@ -1158,6 +1171,7 @@ function mountState( const queue = (hook.queue = { pending: null, interleaved: null, + lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), @@ -1821,6 +1835,9 @@ function refreshCache(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) { @@ -1960,7 +1977,22 @@ function dispatchAction( 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__) { diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 4570c10a62b42..88c4d1946cb6d 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -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. @@ -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( @@ -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 { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 08ec3c8eac87e..70923424e8870 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -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, @@ -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; } diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactUpdateQueue.new.js index 84575950c3465..d0bf5d1ea2ccd 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.new.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.new.js @@ -84,7 +84,7 @@ // 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 { @@ -92,6 +92,9 @@ import { NoLanes, isSubsetOfLanes, mergeLanes, + isTransitionLane, + intersectLanes, + markRootEntangled, } from './ReactFiberLane.new'; import { enterDisallowedContextReadInDEV, @@ -128,6 +131,7 @@ export type Update = {| export type SharedQueue = {| pending: Update | null, interleaved: Update | null, + lanes: Lanes, |}; export type UpdateQueue = {| @@ -167,6 +171,7 @@ export function initializeUpdateQueue(fiber: Fiber): void { shared: { pending: null, interleaved: null, + lanes: NoLanes, }, effects: null, }; @@ -260,6 +265,30 @@ export function enqueueUpdate( } } +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 = (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( workInProgress: Fiber, capturedUpdate: Update, @@ -595,6 +624,10 @@ export function processUpdateQueue( newLanes = mergeLanes(newLanes, interleaved.lane); interleaved = ((interleaved: any).next: Update); } 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.