From 7cb9fd7ef822436aef13c8cbf648af1e21a5309a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 1 Feb 2021 18:05:42 -0600 Subject: [PATCH] Land interleaved updates change in main fork (#20710) * Land #20615 in main fork Includes change to interleaved updates. ``` yarn replace-fork ``` * Check deferRenderPhaseUpdateToNextBatch in test --- .../src/ReactFiberClassComponent.old.js | 6 +- .../src/ReactFiberHooks.old.js | 72 +++++++++++++++---- .../src/ReactFiberInterleavedUpdates.old.js | 55 ++++++++++++++ .../src/ReactFiberNewContext.old.js | 27 +++++-- .../src/ReactFiberReconciler.old.js | 2 +- .../src/ReactFiberThrow.old.js | 2 +- .../src/ReactFiberWorkLoop.old.js | 26 ++++++- .../src/ReactUpdateQueue.old.js | 58 ++++++++++++--- .../__tests__/ReactIncrementalUpdates-test.js | 11 ++- 9 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index 36bf2d22f75f6..5f366e7c253a2 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js @@ -214,7 +214,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update); + enqueueUpdate(fiber, update, lane); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { entangleTransitions(root, fiber, lane); @@ -249,7 +249,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update); + enqueueUpdate(fiber, update, lane); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { entangleTransitions(root, fiber, lane); @@ -283,7 +283,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update); + enqueueUpdate(fiber, update, lane); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root !== null) { entangleTransitions(root, fiber, lane); diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 35b0c2ca15482..17038f14b0904 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -77,6 +77,7 @@ import { warnIfNotCurrentlyActingUpdatesInDev, warnIfNotScopedWithMatchingAct, markSkippedUpdateLanes, + isInterleavedUpdate, } from './ReactFiberWorkLoop.old'; import invariant from 'shared/invariant'; @@ -110,6 +111,7 @@ import { enqueueUpdate, entangleTransitions, } from './ReactUpdateQueue.old'; +import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -122,8 +124,9 @@ type Update = {| priority?: ReactPriorityLevel, |}; -type UpdateQueue = {| +export type UpdateQueue = {| pending: Update | null, + interleaved: Update | null, lanes: Lanes, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, @@ -657,6 +660,7 @@ function mountReducer( hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { pending: null, + interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, @@ -800,7 +804,22 @@ function updateReducer( queue.lastRenderedState = newState; } - if (baseQueue === null) { + // Interleaved updates are stored on a separate queue. We aren't going to + // process them during this render, but we do need to track which lanes + // are remaining. + const lastInterleaved = queue.interleaved; + if (lastInterleaved !== null) { + let interleaved = lastInterleaved; + do { + const interleavedLane = interleaved.lane; + currentlyRenderingFiber.lanes = mergeLanes( + currentlyRenderingFiber.lanes, + interleavedLane, + ); + 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; @@ -1132,6 +1151,7 @@ function useMutableSource( // including any interleaving updates that occur. const newQueue = { pending: null, + interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, @@ -1188,6 +1208,7 @@ function mountState( hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { pending: null, + interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, @@ -1869,7 +1890,7 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { cache: seededCache, }; refreshUpdate.payload = payload; - enqueueUpdate(provider, refreshUpdate); + enqueueUpdate(provider, refreshUpdate, lane); return; } } @@ -1904,17 +1925,6 @@ function dispatchAction( next: (null: any), }; - // Append the update to the end of the list. - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - const alternate = fiber.alternate; if ( fiber === currentlyRenderingFiber || @@ -1924,7 +1934,41 @@ function dispatchAction( // queue -> linked list of updates. After this render pass, we'll restart // and apply the stashed updates on top of the work-in-progress hook. didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + queue.pending = update; } else { + if (isInterleavedUpdate(fiber, lane)) { + const interleaved = queue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transfered to the pending queue. + pushInterleavedQueue(queue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + queue.interleaved = update; + } else { + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + queue.pending = update; + } + if ( fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes) diff --git a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js new file mode 100644 index 0000000000000..20096794461f1 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {UpdateQueue as HookQueue} from './ReactFiberHooks.old'; +import type {SharedQueue as ClassQueue} from './ReactUpdateQueue.old'; + +// An array of all update queues that received updates during the current +// render. When this render exits, either because it finishes or because it is +// interrupted, the interleaved updates will be transfered onto the main part +// of the queue. +let interleavedQueues: Array< + HookQueue | ClassQueue, +> | null = null; + +export function pushInterleavedQueue( + queue: HookQueue | ClassQueue, +) { + if (interleavedQueues === null) { + interleavedQueues = [queue]; + } else { + interleavedQueues.push(queue); + } +} + +export function enqueueInterleavedUpdates() { + // Transfer the interleaved updates onto the main queue. Each queue has a + // `pending` field and an `interleaved` field. When they are not null, they + // point to the last node in a circular linked list. We need to append the + // interleaved list to the end of the pending list by joining them into a + // single, circular list. + if (interleavedQueues !== null) { + for (let i = 0; i < interleavedQueues.length; i++) { + const queue = interleavedQueues[i]; + const lastInterleavedUpdate = queue.interleaved; + if (lastInterleavedUpdate !== null) { + queue.interleaved = null; + const firstInterleavedUpdate = lastInterleavedUpdate.next; + const lastPendingUpdate = queue.pending; + if (lastPendingUpdate !== null) { + const firstPendingUpdate = lastPendingUpdate.next; + lastPendingUpdate.next = (firstInterleavedUpdate: any); + lastInterleavedUpdate.next = (firstPendingUpdate: any); + } + queue.pending = (lastInterleavedUpdate: any); + } + } + interleavedQueues = null; + } +} diff --git a/packages/react-reconciler/src/ReactFiberNewContext.old.js b/packages/react-reconciler/src/ReactFiberNewContext.old.js index 934bc6b3fcd5c..c1102b89f93f2 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.old.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.old.js @@ -11,6 +11,7 @@ import type {ReactContext} from 'shared/ReactTypes'; import type {Fiber, ContextDependency} from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack.old'; import type {Lanes} from './ReactFiberLane.old'; +import type {SharedQueue} from './ReactUpdateQueue.old'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack.old'; @@ -31,7 +32,7 @@ import { import invariant from 'shared/invariant'; import is from 'shared/objectIs'; -import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.old'; +import {createUpdate, ForceUpdate} from './ReactUpdateQueue.old'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.old'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; @@ -211,16 +212,30 @@ export function propagateContextChange( if (fiber.tag === ClassComponent) { // Schedule a force update on the work-in-progress. - const update = createUpdate( - NoTimestamp, - pickArbitraryLane(renderLanes), - ); + const lane = pickArbitraryLane(renderLanes); + const update = createUpdate(NoTimestamp, lane); update.tag = ForceUpdate; // TODO: Because we don't have a work-in-progress, this will add the // update to the current fiber, too, which means it will persist even if // this render is thrown away. Since it's a race condition, not sure it's // worth fixing. - enqueueUpdate(fiber, update); + + // Inlined `enqueueUpdate` to remove interleaved update check + const updateQueue = fiber.updateQueue; + if (updateQueue === null) { + // Only occurs if the fiber has been unmounted. + } else { + const sharedQueue: SharedQueue = (updateQueue: any).shared; + const pending = sharedQueue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + sharedQueue.pending = update; + } } fiber.lanes = mergeLanes(fiber.lanes, renderLanes); const alternate = fiber.alternate; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 1fc12435d64cf..64bbea2c4070f 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -318,7 +318,7 @@ export function updateContainer( update.callback = callback; } - enqueueUpdate(current, update); + enqueueUpdate(current, update, lane); const root = scheduleUpdateOnFiber(current, lane, eventTime); if (root !== null) { entangleTransitions(root, current, lane); diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index e7534497735c5..6ae53d02fc5ce 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -293,7 +293,7 @@ function throwException( // prevent a bail out. const update = createUpdate(NoTimestamp, SyncLane); update.tag = ForceUpdate; - enqueueUpdate(sourceFiber, update); + enqueueUpdate(sourceFiber, update, SyncLane); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index e1431de6431fd..0f99452a4c636 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -34,7 +34,6 @@ import { disableSchedulerTimeoutInWorkLoop, enableDoubleInvokingEffects, skipUnmountedBoundaries, - enableDiscreteEventMicroTasks, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -207,6 +206,7 @@ import { pop as popFromStack, createCursor, } from './ReactFiberStack.old'; +import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.old'; import { markNestedUpdateScheduled, @@ -217,6 +217,7 @@ import { syncNestedUpdateFlag, } from './ReactProfilerTimer.old'; +import {enableDiscreteEventMicroTasks} from 'shared/ReactFeatureFlags'; // DEV stuff import getComponentName from 'shared/getComponentName'; import ReactStrictModeWarnings from './ReactStrictModeWarnings.old'; @@ -537,6 +538,7 @@ export function scheduleUpdateOnFiber( } } + // TODO: Consolidate with `isInterleavedUpdate` check if (root === workInProgressRoot) { // Received an update to a tree that's in the middle of rendering. Mark // that there was an interleaved update work on this root. Unless the @@ -674,6 +676,22 @@ function markUpdateLaneFromFiberToRoot( } } +export function isInterleavedUpdate(fiber: Fiber, lane: Lane) { + return ( + // TODO: Optimize slightly by comparing to root that fiber belongs to. + // Requires some refactoring. Not a big deal though since it's rare for + // concurrent apps to have more than a single root. + workInProgressRoot !== null && + (fiber.mode & BlockingMode) !== NoMode && + // If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps), + // then don't treat this as an interleaved update. This pattern is + // accompanied by a warning but we haven't fully deprecated it yet. We can + // remove once the deferRenderPhaseUpdateToNextBatch flag is enabled. + (deferRenderPhaseUpdateToNextBatch || + (executionContext & RenderContext) === NoContext) + ); +} + // Use this function to schedule a task for a root. There's only one task per // root; if a task was already scheduled, we'll check to make sure the priority // of the existing task is the same as the priority of the next level that the @@ -1376,6 +1394,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) { workInProgressRootUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; + enqueueInterleavedUpdates(); + if (enableSchedulerTracing) { spawnedWorkDuringRender = null; } @@ -2307,7 +2327,7 @@ function captureCommitPhaseErrorOnRoot( ) { const errorInfo = createCapturedValue(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane)); - enqueueUpdate(rootFiber, update); + enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane)); if (root !== null) { @@ -2354,7 +2374,7 @@ export function captureCommitPhaseError( errorInfo, (SyncLane: Lane), ); - enqueueUpdate(fiber, update); + enqueueUpdate(fiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane)); if (root !== null) { diff --git a/packages/react-reconciler/src/ReactUpdateQueue.old.js b/packages/react-reconciler/src/ReactUpdateQueue.old.js index 1d71e14819d87..d1e82e10d7c39 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.old.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.old.js @@ -105,7 +105,11 @@ import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags'; import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags'; import {StrictMode} from './ReactTypeOfMode'; -import {markSkippedUpdateLanes} from './ReactFiberWorkLoop.old'; +import { + markSkippedUpdateLanes, + isInterleavedUpdate, +} from './ReactFiberWorkLoop.old'; +import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import invariant from 'shared/invariant'; @@ -124,8 +128,9 @@ export type Update = {| next: Update | null, |}; -type SharedQueue = {| +export type SharedQueue = {| pending: Update | null, + interleaved: Update | null, lanes: Lanes, |}; @@ -165,6 +170,7 @@ export function initializeUpdateQueue(fiber: Fiber): void { lastBaseUpdate: null, shared: { pending: null, + interleaved: null, lanes: NoLanes, }, effects: null, @@ -205,7 +211,11 @@ export function createUpdate(eventTime: number, lane: Lane): Update<*> { return update; } -export function enqueueUpdate(fiber: Fiber, update: Update) { +export function enqueueUpdate( + fiber: Fiber, + update: Update, + lane: Lane, +) { const updateQueue = fiber.updateQueue; if (updateQueue === null) { // Only occurs if the fiber has been unmounted. @@ -213,15 +223,31 @@ export function enqueueUpdate(fiber: Fiber, update: Update) { } const sharedQueue: SharedQueue = (updateQueue: any).shared; - const pending = sharedQueue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; + + if (isInterleavedUpdate(fiber, lane)) { + const interleaved = sharedQueue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transfered to the pending queue. + pushInterleavedQueue(sharedQueue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + sharedQueue.interleaved = update; } else { - update.next = pending.next; - pending.next = update; + const pending = sharedQueue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + sharedQueue.pending = update; } - sharedQueue.pending = update; if (__DEV__) { if ( @@ -591,7 +617,17 @@ export function processUpdateQueue( queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; - if (firstBaseUpdate === null) { + // Interleaved updates are stored on a separate queue. We aren't going to + // process them during this render, but we do need to track which lanes + // are remaining. + const lastInterleaved = queue.shared.interleaved; + if (lastInterleaved !== null) { + let interleaved = lastInterleaved; + do { + 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; diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index 2dd1cd18e7c50..e24e2576dde2f 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -320,7 +320,16 @@ describe('ReactIncrementalUpdates', () => { }); expect(instance.state).toEqual({a: 'a', b: 'b'}); - expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']); + + if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { + expect(Scheduler).toHaveYielded([ + 'componentWillReceiveProps', + 'render', + 'render', + ]); + } else { + expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']); + } }); it('updates triggered from inside a class setState updater', () => {