From c9c7f801c5db19723641d2084393fec703ae1758 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 18 Feb 2020 00:28:00 -0800 Subject: [PATCH 01/27] implement initial reification functionality --- .../react-reconciler/src/ReactChildFiber.js | 38 ++++++---- packages/react-reconciler/src/ReactFiber.js | 6 ++ .../src/ReactFiberBeginWork.js | 65 +++++++++++++++++ .../src/ReactFiberCompleteWork.js | 22 ++++++ .../src/ReactFiberWorkLoop.js | 65 +++++++++++++++++ .../src/__tests__/ReactLazyReconciler-test.js | 73 +++++++++++++++++++ .../babel/transform-prevent-infinite-loops.js | 4 +- 7 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index f5203c7e0db39..cb0101387b5fe 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -1427,6 +1427,9 @@ function ChildReconciler(shouldTrackSideEffects) { export const reconcileChildFibers = ChildReconciler(true); export const mountChildFibers = ChildReconciler(false); +// @TODO this function currently eagerly clones children before beginning work +// on them. should figure out how to start work on child fibers without creating +// workInProgress untill we know we need to export function cloneChildFibers( current: Fiber | null, workInProgress: Fiber, @@ -1440,25 +1443,32 @@ export function cloneChildFibers( return; } + console.log('cloning child fibers', workInProgress.tag); + let currentChild = workInProgress.child; - let newChild = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); - workInProgress.child = newChild; + currentChild.return = workInProgress; + currentChild.reify = true; + // let newChild = createWorkInProgress( + // currentChild, + // currentChild.pendingProps, + // currentChild.expirationTime, + // ); + // workInProgress.child = newChild; + // + // newChild.return = workInProgress; - newChild.return = workInProgress; while (currentChild.sibling !== null) { currentChild = currentChild.sibling; - newChild = newChild.sibling = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); - newChild.return = workInProgress; + // newChild = newChild.sibling = createWorkInProgress( + // currentChild, + // currentChild.pendingProps, + // currentChild.expirationTime, + // ); + // newChild.return = workInProgress; + currentChild.return = workInProgress; + currentChild.reify = true; } - newChild.sibling = null; + // newChild.sibling = null; } // Reset a workInProgress child set to prepare it for a second pass. diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index a51116219927f..d857f8776b811 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -221,6 +221,11 @@ export type Fiber = {| // memory if we need to. alternate: Fiber | null, + // This boolean tells the workloop whether workInProgress was deferred. if + // later workInProgress is required we need to go back an reify any fibers + // where this is true + reify: boolean, + // Time spent rendering this Fiber and its descendants for the current update. // This tells us how well the tree makes use of sCU for memoization. // It is reset to 0 each time we render and only updated when we don't bailout. @@ -298,6 +303,7 @@ function FiberNode( this.childExpirationTime = NoWork; this.alternate = null; + this.reify = false; if (enableProfilerTimer) { // Note: The following is done to avoid a v8 performance cliff. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 9db5886a6c2b4..2a8f067dfcbdd 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2885,11 +2885,68 @@ function remountFiber( } } +function reifyWorkInProgress(current: Fiber) { + invariant( + current.reify === true, + 'reifyWorkInProgress was called on a fiber that is not mared for reification', + ); + + let parent = current.return; + + if (parent.child === null) { + return; + } + + console.log('reifying child fibers of', fiberName(parent)); + + let currentChild = parent.child; + // currentChild.return = workInProgress; + // currentChild.reify = true; + let newChild = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + parent.child = newChild; + + newChild.return = parent; + + while (currentChild.sibling !== null) { + currentChild = currentChild.sibling; + newChild = newChild.sibling = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + newChild.return = parent; + // currentChild.return = workInProgress; + // currentChild.reify = true; + } + newChild.sibling = null; + + return current.alternate; +} + +function fiberName(fiber) { + if (fiber.tag === 3) return 'HostRoot'; + if (fiber.tag === 6) return 'HostText'; + if (fiber.tag === 10) return 'ContextProvider'; + return typeof fiber.type === 'function' ? fiber.type.name : fiber.elementType; +} + function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { + console.log( + 'beginWork', + fiberName(workInProgress), + workInProgress.tag, + current && current.tag, + workInProgress.reify, + current && current.reify, + ); const updateExpirationTime = workInProgress.expirationTime; if (__DEV__) { @@ -2910,6 +2967,14 @@ function beginWork( } } + if (workInProgress.reify === true) { + console.log( + 'workInProgress.reify true, if we are going to do work we need to reify first', + ); + current = workInProgress; + workInProgress = reifyWorkInProgress(current); + } + if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 1138dc57116b9..1f410bfaf0ce8 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -633,11 +633,33 @@ function cutOffTailIfNeeded( } } +function fiberName(fiber) { + if (fiber.tag === 3) return 'HostRoot'; + if (fiber.tag === 6) return 'HostText'; + if (fiber.tag === 10) return 'ContextProvider'; + return typeof fiber.type === 'function' ? fiber.type.name : fiber.elementType; +} + function completeWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { + console.log( + 'completeWork', + fiberName(workInProgress), + workInProgress.tag, + current && current.tag, + workInProgress.reify, + current && current.reify, + ); + + // reset reify state. it would have been dealt with already if it needed to by by now + workInProgress.reify = false; + if (current !== null) { + current.reify = false; + } + const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index f647ccb88b2ce..ffaeba755a879 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -527,6 +527,10 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { const lastExpiredTime = root.lastExpiredTime; if (lastExpiredTime !== NoWork) { + console.log( + '***** getNextRootExpirationTimeToWorkOn has lastExpiredTime', + lastExpiredTime, + ); return lastExpiredTime; } @@ -535,6 +539,10 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { const firstPendingTime = root.firstPendingTime; if (!isRootSuspendedAtTime(root, firstPendingTime)) { // The highest priority pending time is not suspended. Let's work on that. + console.log( + '***** getNextRootExpirationTimeToWorkOn has firstPendingTime', + firstPendingTime, + ); return firstPendingTime; } @@ -555,6 +563,10 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // Don't work on Idle/Never priority unless everything else is committed. return NoWork; } + console.log( + '***** getNextRootExpirationTimeToWorkOn returning next level to work on', + nextLevel, + ); return nextLevel; } @@ -564,6 +576,7 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // the next level that the root has work on. This function is called on every // update, and right before exiting a task. function ensureRootIsScheduled(root: FiberRoot) { + console.log('ensureRootIsScheduled'); const lastExpiredTime = root.lastExpiredTime; if (lastExpiredTime !== NoWork) { // Special case: Expired work should flush synchronously. @@ -647,6 +660,8 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // event time. The next update will compute a new event time. currentEventTime = NoWork; + console.log('performConcurrentWorkOnRoot'); + if (didTimeout) { // The render task took too long to complete. Mark the current time as // expired to synchronously render all expired work in a single batch. @@ -747,6 +762,7 @@ function finishConcurrentRender( exitStatus, expirationTime, ) { + console.log('finishConcurrentRender'); // Set this to null to indicate there's no in-progress render. workInProgressRoot = null; @@ -775,6 +791,7 @@ function finishConcurrentRender( break; } case RootSuspended: { + console.log('RootSuspended'); markRootSuspendedAtTime(root, expirationTime); const lastSuspendedTime = root.lastSuspendedTime; if (expirationTime === lastSuspendedTime) { @@ -819,6 +836,7 @@ function finishConcurrentRender( } } + console.log('finishConcurrentRender about to get expiration time'); const nextTime = getNextRootExpirationTimeToWorkOn(root); if (nextTime !== NoWork && nextTime !== expirationTime) { // There's additional work on this root. @@ -850,6 +868,8 @@ function finishConcurrentRender( break; } case RootSuspendedWithDelay: { + console.log('RootSuspendedWithDelay'); + markRootSuspendedAtTime(root, expirationTime); const lastSuspendedTime = root.lastSuspendedTime; if (expirationTime === lastSuspendedTime) { @@ -946,6 +966,8 @@ function finishConcurrentRender( break; } case RootCompleted: { + console.log('RootCompleted'); + // The work completed. Ready to commit. if ( // do not delay if we're inside an act() scope @@ -995,6 +1017,8 @@ function performSyncWorkOnRoot(root) { 'Should not already be working.', ); + console.log('performSyncWorkOnRoot', lastExpiredTime, expirationTime); + flushPassiveEffects(); // If the root or expiration time have changed, throw out the existing stack @@ -1028,6 +1052,8 @@ function performSyncWorkOnRoot(root) { popInteractions(((prevInteractions: any): Set)); } + console.log('workInProgressRootExitStatus', workInProgressRootExitStatus); + if (workInProgressRootExitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; stopInterruptedWorkLoopTimer(); @@ -1045,10 +1071,15 @@ function performSyncWorkOnRoot(root) { 'bug in React. Please file an issue.', ); } else { + console.log('finishing sync render'); // We now have a consistent tree. Because this is a sync render, we // will commit it even if something suspended. stopFinishedWorkLoopTimer(); root.finishedWork = (root.current.alternate: any); + console.log( + '&&&& finishedWork.expirationTime', + root.finishedWork.expirationTime, + ); root.finishedExpirationTime = expirationTime; finishSyncRender(root); } @@ -1628,12 +1659,22 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { function getRemainingExpirationTime(fiber: Fiber) { const updateExpirationTime = fiber.expirationTime; const childExpirationTime = fiber.childExpirationTime; + console.log( + '^^^^ getRemainingExpirationTime updateExpirationTime, childExpirationTime', + updateExpirationTime, + childExpirationTime, + ); return updateExpirationTime > childExpirationTime ? updateExpirationTime : childExpirationTime; } function resetChildExpirationTime(completedWork: Fiber) { + console.log( + '((((( resetChildExpirationTime', + completedWork.expirationTime, + completedWork.childExpirationTime, + ); if ( renderExpirationTime !== Never && completedWork.childExpirationTime === Never @@ -1700,6 +1741,7 @@ function resetChildExpirationTime(completedWork: Fiber) { } function commitRoot(root) { + console.log('committing root'); const renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority( ImmediatePriority, @@ -1730,6 +1772,11 @@ function commitRootImpl(root, renderPriorityLevel) { if (finishedWork === null) { return null; } + console.log( + '&&&&& commitRootImpl finishedWork expirationTime', + finishedWork.expirationTime, + expirationTime, + ); root.finishedWork = null; root.finishedExpirationTime = NoWork; @@ -1760,11 +1807,13 @@ function commitRootImpl(root, renderPriorityLevel) { ); if (root === workInProgressRoot) { + console.log('root was workInProgressRoot'); // We can reset these now that they are finished. workInProgressRoot = null; workInProgress = null; renderExpirationTime = NoWork; } else { + console.log('root was different'); // This indicates that the last root we worked on is not the same one that // we're committing now. This most commonly happens when a suspended root // times out. @@ -1773,6 +1822,8 @@ function commitRootImpl(root, renderPriorityLevel) { // Get the list of effects. let firstEffect; if (finishedWork.effectTag > PerformedWork) { + console.log('**** work was performed'); + // A fiber's effect list consists only of its children, not itself. So if // the root has an effect, we need to add it to the end of the list. The // resulting list is the set that would belong to the root's parent, if it @@ -1785,6 +1836,8 @@ function commitRootImpl(root, renderPriorityLevel) { } } else { // There is no effect on the root. + console.log('**** no effects on th root'); + firstEffect = finishedWork.firstEffect; } @@ -1913,6 +1966,8 @@ function commitRootImpl(root, renderPriorityLevel) { } executionContext = prevExecutionContext; } else { + console.log('**** no effects at all'); + // No effects. root.current = finishedWork; // Measure these anyway so the flamegraph explicitly shows that there were @@ -1934,6 +1989,8 @@ function commitRootImpl(root, renderPriorityLevel) { const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; if (rootDoesHavePassiveEffects) { + console.log('**** YES passive effects on root'); + // This commit has passive effects. Stash a reference to them. But don't // schedule a callback until after flushing layout work. rootDoesHavePassiveEffects = false; @@ -1941,6 +1998,8 @@ function commitRootImpl(root, renderPriorityLevel) { pendingPassiveEffectsExpirationTime = expirationTime; pendingPassiveEffectsRenderPriority = renderPriorityLevel; } else { + console.log('**** NO passive effects on root'); + // We are done with the effect chain at this point so let's clear the // nextEffect pointers to assist with GC. If we have passive effects, we'll // clear this in flushPassiveEffects. @@ -1954,6 +2013,8 @@ function commitRootImpl(root, renderPriorityLevel) { // Check if there's remaining work on this root const remainingExpirationTime = root.firstPendingTime; + console.log('**** remainingExpirationTime', remainingExpirationTime); + if (remainingExpirationTime !== NoWork) { if (enableSchedulerTracing) { if (spawnedWorkDuringRender !== null) { @@ -2002,6 +2063,7 @@ function commitRootImpl(root, renderPriorityLevel) { // Always call this before exiting `commitRoot`, to ensure that any // additional work on this root is scheduled. + console.log('^^^ calling ensureRootIsScheduled from commitRootImpl'); ensureRootIsScheduled(root); if (hasUncaughtError) { @@ -2361,6 +2423,7 @@ function captureCommitPhaseErrorOnRoot( sourceFiber: Fiber, error: mixed, ) { + console.log('^^^^^ captureCommitPhaseErrorOnRoot', error); const errorInfo = createCapturedValue(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); enqueueUpdate(rootFiber, update); @@ -2372,6 +2435,7 @@ function captureCommitPhaseErrorOnRoot( } export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { + console.log('^^^^^ captureCommitPhaseError', error); if (sourceFiber.tag === HostRoot) { // Error was thrown at the root. There is no parent, so the root // itself should capture it. @@ -2483,6 +2547,7 @@ function retryTimedOutBoundary( boundaryFiber: Fiber, retryTime: ExpirationTime, ) { + console.log('^^^^ retryTimedOutBoundary'); // The boundary fiber (a Suspense component or SuspenseList component) // previously was rendered in its fallback state. One of the promises that // suspended it has resolved, which means at least part of the tree was diff --git a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js new file mode 100644 index 0000000000000..0f1edabdac0dd --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js @@ -0,0 +1,73 @@ +let React; +let ReactFeatureFlags; +let ReactNoop; +let Scheduler; +let ReactCache; +let Suspense; +let TextResource; + +describe('ReactLazyReconciler', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + // ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + // ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + ReactCache = require('react-cache'); + Suspense = React.Suspense; + }); + + it('yields text', () => { + const root = ReactNoop.createBlockingRoot(); + + const Ctx = React.createContext(1); + + function Text(props) { + let ctx = React.useContext(Ctx); + Scheduler.unstable_yieldValue(props.text); + Scheduler.unstable_yieldValue(ctx); + return props.text; + } + + function Nothing({children}) { + return children; + } + + let trigger = null; + + function App() { + let [val, setVal] = React.useState(1); + let texts = React.useMemo( + () => ( + + + + + + + + ), + [], + ); + trigger = setVal; + return {texts}; + } + + root.render(); + + // Nothing should have rendered yet + expect(root).toMatchRenderedOutput(null); + + // Everything should render immediately in the next event + expect(Scheduler).toFlushExpired(['A', 1, 'B', 1, 'C', 1]); + expect(root).toMatchRenderedOutput('ABC'); + + ReactNoop.act(() => trigger(2)); + + // Everything should render immediately in the next event + expect(Scheduler).toHaveYielded(['A', 2, 'B', 2, 'C', 2]); + expect(root).toMatchRenderedOutput('ABC'); + }); +}); diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index ac8ef57b8a40d..26ef925f7af7f 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,10 +13,10 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 1500; +const MAX_SOURCE_ITERATIONS = 10; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. -const MAX_TEST_ITERATIONS = 5000; +const MAX_TEST_ITERATIONS = 10; module.exports = ({types: t, template}) => { // We set a global so that we can later fail the test From 6e237801474dfb845f25c956b732acb5d6f5dfc7 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 18 Feb 2020 23:33:32 -0800 Subject: [PATCH 02/27] implement a working reification for function and context provider components --- packages/react-reconciler/src/ReactFiber.js | 6 + .../src/ReactFiberBeginWork.js | 343 +++++++++++++----- .../src/ReactFiberCompleteWork.js | 23 +- .../react-reconciler/src/ReactFiberStack.js | 6 +- .../src/__tests__/ReactLazyReconciler-test.js | 6 + 5 files changed, 276 insertions(+), 108 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index d857f8776b811..ef69078b09184 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -276,6 +276,9 @@ function FiberNode( this.type = null; this.stateNode = null; + // @TODO remove this. used for debugging output only + this.version = 0; + // Fiber this.return = null; this.child = null; @@ -429,6 +432,9 @@ export function createWorkInProgress( workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; + // @TODO remove this. used for debugging output only + workInProgress.version = current.version + 1; + if (__DEV__) { // DEV-only fields if (enableUserTimingAPI) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 2a8f067dfcbdd..f395cc0069ba4 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -600,6 +600,13 @@ function markRef(current: Fiber | null, workInProgress: Fiber) { } } +function preemptiveHooksBailout() { + console.log( + '#### preemptiveHooksBailout checking to see if context selection or states have changed', + ); + return false; +} + function updateFunctionComponent( current, workInProgress, @@ -624,71 +631,110 @@ function updateFunctionComponent( } } - let context; - if (!disableLegacyContext) { - const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); - context = getMaskedContext(workInProgress, unmaskedContext); - } + try { + if (workInProgress.reify) { + console.log( + '???? updateFunctionComponent called with a current fiber for workInProgress', + current.memoizedProps, + ); + if (preemptiveHooksBailout()) { + console.log('hooks have not changed, we can bail out of update'); + } + let parent = workInProgress.return; + workInProgress = createWorkInProgress( + current, + current.memoizedProps, + NoWork, + ); + workInProgress.return = parent; + } - let nextChildren; - prepareToReadContext(workInProgress, renderExpirationTime); - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - setCurrentPhase('render'); - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderExpirationTime, + console.log( + 'workInProgress & current', + fiberName(workInProgress), + fiberName(current), ); - if ( - debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode - ) { - // Only double-render components with Hooks - if (workInProgress.memoizedState !== null) { - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderExpirationTime, - ); + + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext( + workInProgress, + Component, + true, + ); + context = getMaskedContext(workInProgress, unmaskedContext); + } + + let nextChildren; + prepareToReadContext(workInProgress, renderExpirationTime); + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setCurrentPhase('render'); + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + // Only double-render components with Hooks + if (workInProgress.memoizedState !== null) { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + } } + setCurrentPhase(null); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); } - setCurrentPhase(null); - } else { - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderExpirationTime, - ); - } - if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + + if (current.reify === true) { + console.log( + 'function component is updating so reifying the work in progress tree', + ); + reifyWorkInProgress(current, workInProgress); + } + + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren( current, workInProgress, + nextChildren, renderExpirationTime, ); + return workInProgress.child; + } catch (e) { + console.log(e); + throw e; } - - // React DevTools reads this flag. - workInProgress.effectTag |= PerformedWork; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); - return workInProgress.child; } function updateChunk( @@ -2885,53 +2931,127 @@ function remountFiber( } } -function reifyWorkInProgress(current: Fiber) { +function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { + const originalCurrent = current; + invariant( current.reify === true, 'reifyWorkInProgress was called on a fiber that is not mared for reification', ); - let parent = current.return; - - if (parent.child === null) { - return; - } - - console.log('reifying child fibers of', fiberName(parent)); - - let currentChild = parent.child; - // currentChild.return = workInProgress; - // currentChild.reify = true; - let newChild = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); - parent.child = newChild; + let parent, parentWorkInProgress; + parentWorkInProgress = parent = current.return; + if (parent.reify === true) { + parentWorkInProgress = createWorkInProgress( + parent, + parent.memoizedProps, + parent.expirationTime, + ); + parentWorkInProgress.return = parent.return; + } + + try { + do { + // clone parent WIP's child. use the current's workInProgress if the currentChild is + // the current fiber + let currentChild = parentWorkInProgress.child; + console.log( + ')))) reifying', + fiberName(parentWorkInProgress), + 'starting with', + fiberName(currentChild), + ); + let newChild; + if (workInProgress !== null && currentChild === current) { + console.log('using workInProgress of', fiberName(workInProgress)); + newChild = workInProgress; + } else { + newChild = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + } + parentWorkInProgress.child = newChild; + currentChild.reify = false; + + newChild.return = parentWorkInProgress; + + // for each sibling of the parent's workInProgress create a workInProgress. + // use current's workInProgress if the sibiling is the current fiber + while (currentChild.sibling !== null) { + currentChild = currentChild.sibling; + console.log( + ')))) reifying', + fiberName(parentWorkInProgress), + 'now with', + fiberName(currentChild), + ); + if (workInProgress !== null && currentChild === current) { + console.log( + 'using ephemeralWorkInProgress of', + fiberName(workInProgress), + ); + newChild = newChild.sibling = workInProgress; + } else { + newChild = newChild.sibling = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + } + newChild.return = parentWorkInProgress; + currentChild.reify = false; + } + newChild.sibling = null; - newChild.return = parent; + console.log( + 'workInProgress pointing at', + fiberName(workInProgress.return), + ); + console.log('current pointing at', fiberName(current.return)); + console.log('parent pointing at', fiberName(parent.return)); + console.log( + 'parentWorkInProgress pointing at', + fiberName(parentWorkInProgress.return), + ); - while (currentChild.sibling !== null) { - currentChild = currentChild.sibling; - newChild = newChild.sibling = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); - newChild.return = parent; - // currentChild.return = workInProgress; - // currentChild.reify = true; + workInProgress = parentWorkInProgress; + current = parent; + parentWorkInProgress = parent = current.return; + if (parent !== null && parent.reify === true) { + parentWorkInProgress = createWorkInProgress( + parent, + parent.pendingProps, + parent.expirationTime, + ); + parentWorkInProgress.return = parent.return; + } + } while (parent !== null && current.reify === true); + } catch (e) { + console.log('error in refiy', e); } - newChild.sibling = null; - return current.alternate; + console.log( + 'returning originalCurrent.alternate', + fiberName(originalCurrent.alternate), + ); + return originalCurrent.alternate; } function fiberName(fiber) { - if (fiber.tag === 3) return 'HostRoot'; - if (fiber.tag === 6) return 'HostText'; - if (fiber.tag === 10) return 'ContextProvider'; - return typeof fiber.type === 'function' ? fiber.type.name : fiber.elementType; + if (fiber == null) return fiber; + let version = fiber.version; + let reify = fiber.reify ? 'r' : ''; + let back = `-${version}${reify}`; + let front = ''; + if (fiber.tag === 3) front = 'HostRoot'; + else if (fiber.tag === 6) front = 'HostText'; + else if (fiber.tag === 10) front = 'ContextProvider'; + else if (typeof fiber.type === 'function') front = fiber.type.name; + else front = 'tag' + fiber.tag; + + return front + back; } function beginWork( @@ -2941,12 +3061,17 @@ function beginWork( ): Fiber | null { console.log( 'beginWork', - fiberName(workInProgress), - workInProgress.tag, - current && current.tag, - workInProgress.reify, - current && current.reify, + fiberName(workInProgress) + '->' + fiberName(workInProgress.return), + current && fiberName(current) + '->' + fiberName(current.return), ); + + // we may have been passed a current fiber as workInProgress if the parent fiber + // bailed out. in this case set the current to workInProgress. they will be the same + // fiber for now until and unless we determine we need to reify it into real workInProgress + if (workInProgress.reify === true) { + current = workInProgress; + } + const updateExpirationTime = workInProgress.expirationTime; if (__DEV__) { @@ -2971,13 +3096,23 @@ function beginWork( console.log( 'workInProgress.reify true, if we are going to do work we need to reify first', ); - current = workInProgress; - workInProgress = reifyWorkInProgress(current); + if ( + workInProgress.tag !== ContextProvider && + workInProgress.tag !== FunctionComponent + ) { + console.log( + 'workInProgress is something other than a ContextProvider or FunctionComponent, reifying immediately', + ); + workInProgress = reifyWorkInProgress(current, null); + } } if (current !== null) { + // when we are on a current fiber as WIP we use memoizedProps since the pendingProps may be out of date const oldProps = current.memoizedProps; - const newProps = workInProgress.pendingProps; + const newProps = workInProgress.reify + ? workInProgress.memoizedProps + : workInProgress.pendingProps; if ( oldProps !== newProps || @@ -2985,10 +3120,16 @@ function beginWork( // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { + console.log( + '--- props different or legacy context changed. lets do update', + ); // If props or context changed, mark the fiber as having performed work. // This may be unset if the props are determined to be equal later (memo). didReceiveUpdate = true; } else if (updateExpirationTime < renderExpirationTime) { + console.log( + '--- props same, no legacy context, no work scheduled. lets push onto stack and bailout', + ); didReceiveUpdate = false; // This fiber does not have any pending work. Bailout without entering // the begin phase. There's still some bookkeeping we that needs to be done @@ -3162,9 +3303,15 @@ function beginWork( // nor legacy context. Set this to false. If an update queue or context // consumer produces a changed value, it will set this to true. Otherwise, // the component will assume the children have not changed and bail out. + console.log( + '--- props same, no legacy context, this fiber has work scheduled so we will see if it produces an update', + ); didReceiveUpdate = false; } } else { + console.log( + '--- current fiber null, likely this was a newly mounted component', + ); didReceiveUpdate = false; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 1f410bfaf0ce8..02f2d64bfa8b5 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -634,10 +634,18 @@ function cutOffTailIfNeeded( } function fiberName(fiber) { - if (fiber.tag === 3) return 'HostRoot'; - if (fiber.tag === 6) return 'HostText'; - if (fiber.tag === 10) return 'ContextProvider'; - return typeof fiber.type === 'function' ? fiber.type.name : fiber.elementType; + if (fiber == null) return fiber; + let version = fiber.version; + let reify = fiber.reify ? 'r' : ''; + let back = `-${version}${reify}`; + let front = ''; + if (fiber.tag === 3) front = 'HostRoot'; + else if (fiber.tag === 6) front = 'HostText'; + else if (fiber.tag === 10) front = 'ContextProvider'; + else if (typeof fiber.type === 'function') front = fiber.type.name; + else front = 'tag' + fiber.tag; + + return front + back; } function completeWork( @@ -647,11 +655,8 @@ function completeWork( ): Fiber | null { console.log( 'completeWork', - fiberName(workInProgress), - workInProgress.tag, - current && current.tag, - workInProgress.reify, - current && current.reify, + fiberName(workInProgress) + '->' + fiberName(workInProgress.return), + current && fiberName(current) + '->' + fiberName(current.return), ); // reset reify state. it would have been dealt with already if it needed to by by now diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index f27aeb65039be..0c7ca56e6a981 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -40,7 +40,11 @@ function pop(cursor: StackCursor, fiber: Fiber): void { } if (__DEV__) { - if (fiber !== fiberStack[index]) { + if ( + fiber !== fiberStack[index] && + fiber.altnerate !== null && + fiber.alternate !== fiberStack[index] + ) { console.error('Unexpected Fiber popped.'); } } diff --git a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js index 0f1edabdac0dd..10374ec9b587f 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js +++ b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js @@ -69,5 +69,11 @@ describe('ReactLazyReconciler', () => { // Everything should render immediately in the next event expect(Scheduler).toHaveYielded(['A', 2, 'B', 2, 'C', 2]); expect(root).toMatchRenderedOutput('ABC'); + + ReactNoop.act(() => trigger(3)); + + // Everything should render immediately in the next event + expect(Scheduler).toHaveYielded(['A', 3, 'B', 3, 'C', 3]); + expect(root).toMatchRenderedOutput('ABC'); }); }); From eae61cb2c6588870bbe934f0b14e974153ec459e Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 19 Feb 2020 00:28:40 -0800 Subject: [PATCH 03/27] hacky hook bailout to demo deferred bailouts --- .../src/ReactFiberBeginWork.js | 67 ++++++++++++++++++- .../react-reconciler/src/ReactFiberHooks.js | 2 + .../src/__tests__/ReactLazyReconciler-test.js | 33 +++++++-- 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index f395cc0069ba4..17e188d0383f1 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -600,11 +600,66 @@ function markRef(current: Fiber | null, workInProgress: Fiber) { } } -function preemptiveHooksBailout() { +function preemptiveHooksBailout( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +) { console.log( '#### preemptiveHooksBailout checking to see if context selection or states have changed', ); - return false; + let hook = workInProgress.memoizedState; + console.log('hook', hook); + console.log('workInProgress.dependencies', workInProgress.dependencies); + + if (workInProgress.dependencies) { + console.log( + 'workInProgress.dependencies exist so not bailing out', + workInProgress.dependencies, + ); + return false; + } + + while (hook !== null) { + const queue = hook.queue; + + if (queue) { + // The last rebase update that is NOT part of the base state. + let baseQueue = hook.baseQueue; + let reducer = queue.lastRenderedReducer; + + // The last pending update that hasn't been processed yet. + let pendingQueue = queue.pending; + if (pendingQueue !== null) { + // We have new updates that haven't been processed yet. + // We'll add them to the base queue. + baseQueue = pendingQueue; + } + + if (baseQueue !== null) { + // We have a queue to process. + let first = baseQueue.next; + let newState = hook.baseState; + + let update = first; + do { + // Process this update. + const action = update.action; + newState = reducer(newState, action); + update = update.next; + } while (update !== null && update !== first); + + // if newState is different from the current state do not bailout + if (newState !== hook.memoizedState) { + console.log('found a new state. not bailing out of hooks'); + return false; + } + } + } + + hook = hook.next; + } + console.log('bailing out of update based on hooks preemptively'); + return true; } function updateFunctionComponent( @@ -637,9 +692,15 @@ function updateFunctionComponent( '???? updateFunctionComponent called with a current fiber for workInProgress', current.memoizedProps, ); - if (preemptiveHooksBailout()) { + if (preemptiveHooksBailout(workInProgress, renderExpirationTime)) { console.log('hooks have not changed, we can bail out of update'); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } + console.log('???? creating a new workInProgress', current.memoizedProps); let parent = workInProgress.return; workInProgress = createWorkInProgress( current, diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 851e22f8fe835..33e1336094042 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1320,6 +1320,7 @@ function dispatchAction( currentlyRenderingFiber.expirationTime = renderExpirationTime; } else { if ( + false && // turn off eager computation with bailout fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) ) { @@ -1365,6 +1366,7 @@ function dispatchAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } + console.log('++++++++++++ scheduling work for reducer hook'); scheduleWork(fiber, expirationTime); } } diff --git a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js index 10374ec9b587f..9323e9e579722 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js +++ b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js @@ -31,11 +31,17 @@ describe('ReactLazyReconciler', () => { return props.text; } + let triggerState = null; + function Nothing({children}) { + let [state, setState] = React.useState(0); + triggerState = () => setState(s => s); + console.log('^^^^ rendering Nothing Component'); + Scheduler.unstable_yieldValue('Nothing'); return children; } - let trigger = null; + let triggerCtx = null; function App() { let [val, setVal] = React.useState(1); @@ -51,7 +57,7 @@ describe('ReactLazyReconciler', () => { ), [], ); - trigger = setVal; + triggerCtx = setVal; return {texts}; } @@ -61,19 +67,36 @@ describe('ReactLazyReconciler', () => { expect(root).toMatchRenderedOutput(null); // Everything should render immediately in the next event - expect(Scheduler).toFlushExpired(['A', 1, 'B', 1, 'C', 1]); + expect(Scheduler).toFlushExpired([ + 'Nothing', + 'Nothing', + 'Nothing', // double renders for components with hooks that are indeterminant + 'Nothing', + 'A', + 1, + 'B', + 1, + 'C', + 1, + ]); expect(root).toMatchRenderedOutput('ABC'); - ReactNoop.act(() => trigger(2)); + ReactNoop.act(() => triggerCtx(2)); // Everything should render immediately in the next event expect(Scheduler).toHaveYielded(['A', 2, 'B', 2, 'C', 2]); expect(root).toMatchRenderedOutput('ABC'); - ReactNoop.act(() => trigger(3)); + ReactNoop.act(() => triggerCtx(3)); // Everything should render immediately in the next event expect(Scheduler).toHaveYielded(['A', 3, 'B', 3, 'C', 3]); expect(root).toMatchRenderedOutput('ABC'); + + ReactNoop.act(() => triggerState()); + + // Everything should render immediately in the next event + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('ABC'); }); }); From fbeccb0ae7a290931f6ca3e152bd1ec9b489a646 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 19 Feb 2020 23:38:41 -0800 Subject: [PATCH 04/27] tune up speculative work bailout. implement context selectors --- .../src/ReactFiberBeginWork.js | 11 +- .../react-reconciler/src/ReactFiberHooks.js | 223 ++++++++++++++++-- .../src/ReactFiberNewContext.js | 4 + .../src/__tests__/ReactLazyReconciler-test.js | 59 ++++- packages/react/src/ReactHooks.js | 2 +- 5 files changed, 271 insertions(+), 28 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 17e188d0383f1..9652ac6d6bb66 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -137,7 +137,11 @@ import { calculateChangedBits, scheduleWorkOnParentPath, } from './ReactFiberNewContext'; -import {renderWithHooks, bailoutHooks} from './ReactFiberHooks'; +import { + renderWithHooks, + bailoutHooks, + bailoutSpeculativeWorkWithHooks, +} from './ReactFiberHooks'; import {stopProfilerTimerIfRunning} from './ReactProfilerTimer'; import { getMaskedContext, @@ -692,7 +696,10 @@ function updateFunctionComponent( '???? updateFunctionComponent called with a current fiber for workInProgress', current.memoizedProps, ); - if (preemptiveHooksBailout(workInProgress, renderExpirationTime)) { + // if (preemptiveHooksBailout(workInProgress, renderExpirationTime)) { + if ( + bailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) + ) { console.log('hooks have not changed, we can bail out of update'); return bailoutOnAlreadyFinishedWork( current, diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 33e1336094042..866673c73f479 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -21,7 +21,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {NoWork, Sync} from './ReactFiberExpirationTime'; -import {readContext} from './ReactFiberNewContext'; +import {readContext, peekContext} from './ReactFiberNewContext'; import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents'; import { Update as UpdateEffect, @@ -343,6 +343,27 @@ function areHookInputsEqual( return true; } +export function bailoutSpeculativeWorkWithHooks( + current: Fiber, + nextRenderExpirationTime: renderExpirationTime, +): boolean { + console.log('====== bailoutSpeculativeWorkWithHooks'); + let hook = current.memoizedState; + while (hook !== null) { + console.log('hook', hook); + if (typeof hook.bailout === 'function') { + let didBailout = hook.bailout(hook, nextRenderExpirationTime); + if (didBailout === false) { + console.log('====== bailoutSpeculativeWorkWithHooks returning false'); + return false; + } + } + hook = hook.next; + } + console.log('====== bailoutSpeculativeWorkWithHooks returning true'); + return true; +} + export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, @@ -480,6 +501,7 @@ export function renderWithHooks( return children; } +// @TODO may need to reset context hooks now that they have memoizedState export function bailoutHooks( current: Fiber, workInProgress: Fiber, @@ -492,6 +514,7 @@ export function bailoutHooks( } } +// @TODO may need to reset context hooks now that they have memoizedState export function resetHooksAfterThrow(): void { // We can assume the previous dispatcher is always this one, since we set it // at the beginning of the render phase and there's no re-entrancy. @@ -540,6 +563,8 @@ function mountWorkInProgressHook(): Hook { baseQueue: null, queue: null, + bailout: null, + next: null, }; @@ -600,6 +625,8 @@ function updateWorkInProgressHook(): Hook { baseQueue: currentHook.baseQueue, queue: currentHook.queue, + bailout: currentHook.bailout, + next: null, }; @@ -614,6 +641,122 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } +type ObservedBits = void | number | boolean; + +function mountContext( + context: ReactContext, + selector?: ObservedBits | (C => any), +): any { + const hook = mountWorkInProgressHook(); + if (typeof selector === 'function') { + let contextValue = readContext(context); + let selection = selector(contextValue); + hook.memoizedState = { + context, + contextValue, + selector, + selection, + stashedContextValue: EMPTY, + stashedSelection: EMPTY, + }; + hook.bailout = bailoutContext; + return selection; + } else { + // selector is the legacy observedBits api + let contextValue = readContext(context, selector); + hook.memoizedState = { + context, + contextValue, + selector: null, + selection: EMPTY, + stashedContextValue: EMPTY, + stashedSelection: EMPTY, + }; + hook.bailout = bailoutContext; + return contextValue; + } +} + +let EMPTY = Symbol('empty'); + +function updateContext( + context: ReactContext, + selector?: ObservedBits | (C => any), +): any { + const hook = updateWorkInProgressHook(); + const memoizedState = hook.memoizedState; + if (memoizedState.stashedSelection !== EMPTY) { + // context and selector could not have changed since we attempted a bailout + memoizedState.contextValue = memoizedState.stashedContextValue; + memoizedState.selection = memoizedState.stashedSelection; + memoizedState.stashedContextValue = EMPTY; + memoizedState.stashedSelection = EMPTY; + return memoizedState.selection; + } + memoizedState.context = context; + if (typeof selector === 'function') { + let selection = memoizedState.selection; + let contextValue = readContext(context); + + if ( + contextValue !== memoizedState.contextValue || + selector !== memoizedState.selector + ) { + selection = selector(contextValue); + memoizedState.contextValue = contextValue; + memoizedState.selector = selector; + memoizedState.selection = selection; + } + return selection; + } else { + // selector is actually legacy observedBits + let contextValue = readContext(context, selector); + memoizedState.contextValue = contextValue; + return contextValue; + } +} + +function bailoutContext(hook: Hook): boolean { + console.log('}}}}} bailoutContext', hook); + const memoizedState = hook.memoizedState; + let selector = memoizedState.selector; + let peekedContextValue = peekContext(memoizedState.context); + let previousContextValue = memoizedState.contextValue; + + if (selector !== null) { + console.log('}}}}} bailoutContext we have a selector'); + if (previousContextValue !== peekedContextValue) { + console.log( + '}}}}} bailoutContext we have different context values', + previousContextValue, + peekedContextValue, + ); + let stashedSelection = selector(peekedContextValue); + if (stashedSelection !== memoizedState.selection) { + console.log( + '}}}}} bailoutContext we have different context SELECTIONS', + memoizedState.selection, + stashedSelection, + ); + memoizedState.stashedSelection = stashedSelection; + memoizedState.stashedContextValue = peekedContextValue; + return false; + } + } + } else { + console.log('}}}}} bailoutContext we are not using selectors'); + if (previousContextValue !== peekedContextValue) { + console.log( + '}}}}} bailoutContext we have different context values', + previousContextValue, + peekedContextValue, + ); + return false; + } + } + return true; +} + function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue { return { lastEffect: null, @@ -644,6 +787,8 @@ function mountReducer( lastRenderedReducer: reducer, lastRenderedState: (initialState: any), }); + hook.bailout = bailoutReducer; + console.log('mountReducer hook', hook); const dispatch: Dispatch = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, @@ -837,6 +982,46 @@ function rerenderReducer( return [newState, dispatch]; } +function bailoutReducer(hook, renderExpirationTime): boolean { + console.log('}}}} bailoutReducer', hook); + + const queue = hook.queue; + + // The last rebase update that is NOT part of the base state. + let baseQueue = hook.baseQueue; + let reducer = queue.lastRenderedReducer; + + // The last pending update that hasn't been processed yet. + let pendingQueue = queue.pending; + if (pendingQueue !== null) { + // We have new updates that haven't been processed yet. + // We'll add them to the base queue. + baseQueue = pendingQueue; + } + + if (baseQueue !== null) { + // We have a queue to process. + let first = baseQueue.next; + let newState = hook.baseState; + + let update = first; + do { + // Process this update. + const action = update.action; + newState = reducer(newState, action); + update = update.next; + } while (update !== null && update !== first); + + // if newState is different from the current state do not bailout + if (newState !== hook.memoizedState) { + console.log('found a new state. not bailing out of hooks'); + return false; + } + } + console.log('state is the same so we can maybe bail out'); + return true; +} + function mountState( initialState: (() => S) | S, ): [S, Dispatch>] { @@ -852,6 +1037,8 @@ function mountState( lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }); + hook.bailout = bailoutReducer; + console.log('mountState hook', hook); const dispatch: Dispatch< BasicStateAction, > = (queue.dispatch = (dispatchAction.bind( @@ -1393,7 +1580,7 @@ const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, - useContext: readContext, + useContext: mountContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, @@ -1411,7 +1598,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, - useContext: readContext, + useContext: updateContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, @@ -1429,7 +1616,7 @@ const HooksDispatcherOnRerender: Dispatcher = { readContext, useCallback: updateCallback, - useContext: readContext, + useContext: updateContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, @@ -1486,11 +1673,11 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; mountHookTypesDev(); - return readContext(context, observedBits); + return mountContext(context, selector); }, useEffect( create: () => (() => void) | void, @@ -1607,11 +1794,11 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); - return readContext(context, observedBits); + return mountContext(context, selector); }, useEffect( create: () => (() => void) | void, @@ -1724,11 +1911,11 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); - return readContext(context, observedBits); + return updateContext(context, selector); }, useEffect( create: () => (() => void) | void, @@ -1841,11 +2028,11 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; updateHookTypesDev(); - return readContext(context, observedBits); + return updateContext(context, selector); }, useEffect( create: () => (() => void) | void, @@ -1960,12 +2147,12 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); mountHookTypesDev(); - return readContext(context, observedBits); + return mountContext(context, selector); }, useEffect( create: () => (() => void) | void, @@ -2091,12 +2278,12 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); - return readContext(context, observedBits); + return updateContext(context, selector); }, useEffect( create: () => (() => void) | void, @@ -2222,12 +2409,12 @@ if (__DEV__) { }, useContext( context: ReactContext, - observedBits: void | number | boolean, + selector: ObservedBits | (T => any), ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); updateHookTypesDev(); - return readContext(context, observedBits); + return updateContext(context, selector); }, useEffect( create: () => (() => void) | void, diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 510c1eb674ad1..68663c1875647 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -335,6 +335,10 @@ export function prepareToReadContext( } } +export function peekContext(context: ReactContext): T { + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} + export function readContext( context: ReactContext, observedBits: void | number | boolean, diff --git a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js index 9323e9e579722..cfa5453de7d2a 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js +++ b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js @@ -24,8 +24,13 @@ describe('ReactLazyReconciler', () => { const Ctx = React.createContext(1); + let selectorTest = false; + function Text(props) { - let ctx = React.useContext(Ctx); + let ctx = React.useContext(Ctx, v => { + selectorTest && Scheduler.unstable_yieldValue('selector'); + return Math.floor(v / 2); + }); Scheduler.unstable_yieldValue(props.text); Scheduler.unstable_yieldValue(ctx); return props.text; @@ -44,7 +49,7 @@ describe('ReactLazyReconciler', () => { let triggerCtx = null; function App() { - let [val, setVal] = React.useState(1); + let [val, setVal] = React.useState(2); let texts = React.useMemo( () => ( @@ -67,30 +72,70 @@ describe('ReactLazyReconciler', () => { expect(root).toMatchRenderedOutput(null); // Everything should render immediately in the next event + // double renders for each component with hooks expect(Scheduler).toFlushExpired([ 'Nothing', 'Nothing', - 'Nothing', // double renders for components with hooks that are indeterminant 'Nothing', + 'Nothing', + 'A', + 1, 'A', 1, 'B', 1, + 'B', + 1, + 'C', + 1, 'C', 1, ]); expect(root).toMatchRenderedOutput('ABC'); - ReactNoop.act(() => triggerCtx(2)); + ReactNoop.act(() => triggerCtx(4)); // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded(['A', 2, 'B', 2, 'C', 2]); + expect(Scheduler).toHaveYielded([ + 'A', + 2, + 'A', + 2, + 'B', + 2, + 'B', + 2, + 'C', + 2, + 'C', + 2, + ]); expect(root).toMatchRenderedOutput('ABC'); - ReactNoop.act(() => triggerCtx(3)); + selectorTest = true; + ReactNoop.act(() => triggerCtx(5)); + selectorTest = false; + // nothing should render (below app) because the value will be the same + expect(Scheduler).toHaveYielded(['selector', 'selector', 'selector']); + expect(root).toMatchRenderedOutput('ABC'); + + ReactNoop.act(() => triggerCtx(6)); // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded(['A', 3, 'B', 3, 'C', 3]); + expect(Scheduler).toHaveYielded([ + 'A', + 3, + 'A', + 3, + 'B', + 3, + 'B', + 3, + 'C', + 3, + 'C', + 3, + ]); expect(root).toMatchRenderedOutput('ABC'); ReactNoop.act(() => triggerState()); diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index c725b0e7c54e0..5ad4a0926e2fa 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -40,7 +40,7 @@ export function useContext( ) { const dispatcher = resolveDispatcher(); if (__DEV__) { - if (unstable_observedBits !== undefined) { + if (false && unstable_observedBits !== undefined) { console.error( 'useContext() second argument is reserved for future ' + 'use in React. Passing it is not supported. ' + From 38f3f9eda8ce5349adc14dfbfa2d240b4663d455 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 20 Feb 2020 10:19:52 -0800 Subject: [PATCH 05/27] handle effects properly --- .../src/ReactFiberCompleteWork.js | 14 ++++++-------- .../react-reconciler/src/ReactFiberHooks.js | 9 ++++----- .../react-reconciler/src/ReactFiberWorkLoop.js | 18 +++++++++++++++--- .../src/__tests__/ReactLazyReconciler-test.js | 18 ++++++++++-------- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 02f2d64bfa8b5..6ba616ae6a10d 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -653,19 +653,17 @@ function completeWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { + let wasSpeculative = workInProgress.reify; + console.log( - 'completeWork', + wasSpeculative ? 'completeWork -speculative' : 'completeWork', fiberName(workInProgress) + '->' + fiberName(workInProgress.return), current && fiberName(current) + '->' + fiberName(current.return), ); - // reset reify state. it would have been dealt with already if it needed to by by now - workInProgress.reify = false; - if (current !== null) { - current.reify = false; - } - - const newProps = workInProgress.pendingProps; + const newProps = wasSpeculative + ? workInProgress.memoizedProps + : workInProgress.pendingProps; switch (workInProgress.tag) { case IndeterminateComponent: diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 866673c73f479..b72310edc1dd6 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -350,7 +350,7 @@ export function bailoutSpeculativeWorkWithHooks( console.log('====== bailoutSpeculativeWorkWithHooks'); let hook = current.memoizedState; while (hook !== null) { - console.log('hook', hook); + console.log('hook'); if (typeof hook.bailout === 'function') { let didBailout = hook.bailout(hook, nextRenderExpirationTime); if (didBailout === false) { @@ -717,7 +717,7 @@ function updateContext( } function bailoutContext(hook: Hook): boolean { - console.log('}}}}} bailoutContext', hook); + console.log('}}}}} bailoutContext'); const memoizedState = hook.memoizedState; let selector = memoizedState.selector; let peekedContextValue = peekContext(memoizedState.context); @@ -788,7 +788,7 @@ function mountReducer( lastRenderedState: (initialState: any), }); hook.bailout = bailoutReducer; - console.log('mountReducer hook', hook); + console.log('mountReducer hook'); const dispatch: Dispatch = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, @@ -983,7 +983,7 @@ function rerenderReducer( } function bailoutReducer(hook, renderExpirationTime): boolean { - console.log('}}}} bailoutReducer', hook); + console.log('}}}} bailoutReducer'); const queue = hook.queue; @@ -1038,7 +1038,6 @@ function mountState( lastRenderedState: (initialState: any), }); hook.bailout = bailoutReducer; - console.log('mountState hook', hook); const dispatch: Dispatch< BasicStateAction, > = (queue.dispatch = (dispatchAction.bind( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index ffaeba755a879..534e276b4da24 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1532,6 +1532,8 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // need an additional field on the work in progress. const current = workInProgress.alternate; const returnFiber = workInProgress.return; + const workWasSpeculative = workInProgress.reify; + workInProgress.reify = false; // Check if the work completed or if something threw. if ((workInProgress.effectTag & Incomplete) === NoEffect) { @@ -1560,7 +1562,9 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { if ( returnFiber !== null && // Do not append effects to parents if a sibling failed to complete - (returnFiber.effectTag & Incomplete) === NoEffect + (returnFiber.effectTag & Incomplete) === NoEffect && + // Do not append effects to parent if workInProgress was speculative + workWasSpeculative !== true ) { // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the @@ -1836,12 +1840,20 @@ function commitRootImpl(root, renderPriorityLevel) { } } else { // There is no effect on the root. - console.log('**** no effects on th root'); - firstEffect = finishedWork.firstEffect; } if (firstEffect !== null) { + console.log('root finishedWork has effects'); + { + let count = 0; + let effect = firstEffect; + while (effect !== null) { + count++; + effect = effect.nextEffect; + } + console.log(`finished work had ${count} effects`); + } const prevExecutionContext = executionContext; executionContext |= CommitContext; const prevInteractions = pushInteractions(root); diff --git a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js index cfa5453de7d2a..a073e5d1dab31 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js +++ b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js @@ -33,13 +33,15 @@ describe('ReactLazyReconciler', () => { }); Scheduler.unstable_yieldValue(props.text); Scheduler.unstable_yieldValue(ctx); - return props.text; + return props.text + (props.plusValue ? ctx : ''); } let triggerState = null; function Nothing({children}) { let [state, setState] = React.useState(0); + // trigger state will result in an identical state. it is used to see + // where how and if the state bailout is working triggerState = () => setState(s => s); console.log('^^^^ rendering Nothing Component'); Scheduler.unstable_yieldValue('Nothing'); @@ -54,8 +56,8 @@ describe('ReactLazyReconciler', () => { () => ( - - + + @@ -91,7 +93,7 @@ describe('ReactLazyReconciler', () => { 'C', 1, ]); - expect(root).toMatchRenderedOutput('ABC'); + expect(root).toMatchRenderedOutput('A1B1C'); ReactNoop.act(() => triggerCtx(4)); @@ -110,14 +112,14 @@ describe('ReactLazyReconciler', () => { 'C', 2, ]); - expect(root).toMatchRenderedOutput('ABC'); + expect(root).toMatchRenderedOutput('A2B2C'); selectorTest = true; ReactNoop.act(() => triggerCtx(5)); selectorTest = false; // nothing should render (below app) because the value will be the same expect(Scheduler).toHaveYielded(['selector', 'selector', 'selector']); - expect(root).toMatchRenderedOutput('ABC'); + expect(root).toMatchRenderedOutput('A2B2C'); ReactNoop.act(() => triggerCtx(6)); @@ -136,12 +138,12 @@ describe('ReactLazyReconciler', () => { 'C', 3, ]); - expect(root).toMatchRenderedOutput('ABC'); + expect(root).toMatchRenderedOutput('A3B3C'); ReactNoop.act(() => triggerState()); // Everything should render immediately in the next event expect(Scheduler).toHaveYielded([]); - expect(root).toMatchRenderedOutput('ABC'); + expect(root).toMatchRenderedOutput('A3B3C'); }); }); From 59ee9cbe72c78bfa1b6e7361598225d9a6f66e37 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 20 Feb 2020 22:34:33 -0800 Subject: [PATCH 06/27] remove reify fiber property in lieu of speculativeWorkMode --- .../react-reconciler/src/ReactChildFiber.js | 38 ++-- packages/react-reconciler/src/ReactFiber.js | 6 - .../src/ReactFiberBeginWork.js | 166 ++++++++---------- .../src/ReactFiberCompleteWork.js | 11 +- .../src/ReactFiberWorkLoop.js | 11 +- 5 files changed, 100 insertions(+), 132 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index cb0101387b5fe..f5203c7e0db39 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -1427,9 +1427,6 @@ function ChildReconciler(shouldTrackSideEffects) { export const reconcileChildFibers = ChildReconciler(true); export const mountChildFibers = ChildReconciler(false); -// @TODO this function currently eagerly clones children before beginning work -// on them. should figure out how to start work on child fibers without creating -// workInProgress untill we know we need to export function cloneChildFibers( current: Fiber | null, workInProgress: Fiber, @@ -1443,32 +1440,25 @@ export function cloneChildFibers( return; } - console.log('cloning child fibers', workInProgress.tag); - let currentChild = workInProgress.child; - currentChild.return = workInProgress; - currentChild.reify = true; - // let newChild = createWorkInProgress( - // currentChild, - // currentChild.pendingProps, - // currentChild.expirationTime, - // ); - // workInProgress.child = newChild; - // - // newChild.return = workInProgress; + let newChild = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + workInProgress.child = newChild; + newChild.return = workInProgress; while (currentChild.sibling !== null) { currentChild = currentChild.sibling; - // newChild = newChild.sibling = createWorkInProgress( - // currentChild, - // currentChild.pendingProps, - // currentChild.expirationTime, - // ); - // newChild.return = workInProgress; - currentChild.return = workInProgress; - currentChild.reify = true; + newChild = newChild.sibling = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + newChild.return = workInProgress; } - // newChild.sibling = null; + newChild.sibling = null; } // Reset a workInProgress child set to prepare it for a second pass. diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index ef69078b09184..f2387a6692199 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -221,11 +221,6 @@ export type Fiber = {| // memory if we need to. alternate: Fiber | null, - // This boolean tells the workloop whether workInProgress was deferred. if - // later workInProgress is required we need to go back an reify any fibers - // where this is true - reify: boolean, - // Time spent rendering this Fiber and its descendants for the current update. // This tells us how well the tree makes use of sCU for memoization. // It is reset to 0 each time we render and only updated when we don't bailout. @@ -306,7 +301,6 @@ function FiberNode( this.childExpirationTime = NoWork; this.alternate = null; - this.reify = false; if (enableProfilerTimer) { // Note: The following is done to avoid a v8 performance cliff. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 9652ac6d6bb66..54fa6165d8325 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -215,6 +215,22 @@ if (__DEV__) { didWarnAboutDefaultPropsOnFunctionComponent = {}; } +let speculativeWorkRootFiber: Fiber | null = null; + +export function inSpeculativeWorkMode() { + return speculativeWorkRootFiber !== null; +} + +export function endSpeculationWorkIfRootFiber(fiber: Fiber) { + if (fiber === speculativeWorkRootFiber) { + speculativeWorkRootFiber = null; + console.log( + ']]] ending speculation: speculativeWorkRootFiber', + fiberName(speculativeWorkRootFiber), + ); + } +} + export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, @@ -604,68 +620,6 @@ function markRef(current: Fiber | null, workInProgress: Fiber) { } } -function preemptiveHooksBailout( - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, -) { - console.log( - '#### preemptiveHooksBailout checking to see if context selection or states have changed', - ); - let hook = workInProgress.memoizedState; - console.log('hook', hook); - console.log('workInProgress.dependencies', workInProgress.dependencies); - - if (workInProgress.dependencies) { - console.log( - 'workInProgress.dependencies exist so not bailing out', - workInProgress.dependencies, - ); - return false; - } - - while (hook !== null) { - const queue = hook.queue; - - if (queue) { - // The last rebase update that is NOT part of the base state. - let baseQueue = hook.baseQueue; - let reducer = queue.lastRenderedReducer; - - // The last pending update that hasn't been processed yet. - let pendingQueue = queue.pending; - if (pendingQueue !== null) { - // We have new updates that haven't been processed yet. - // We'll add them to the base queue. - baseQueue = pendingQueue; - } - - if (baseQueue !== null) { - // We have a queue to process. - let first = baseQueue.next; - let newState = hook.baseState; - - let update = first; - do { - // Process this update. - const action = update.action; - newState = reducer(newState, action); - update = update.next; - } while (update !== null && update !== first); - - // if newState is different from the current state do not bailout - if (newState !== hook.memoizedState) { - console.log('found a new state. not bailing out of hooks'); - return false; - } - } - } - - hook = hook.next; - } - console.log('bailing out of update based on hooks preemptively'); - return true; -} - function updateFunctionComponent( current, workInProgress, @@ -691,7 +645,7 @@ function updateFunctionComponent( } try { - if (workInProgress.reify) { + if (inSpeculativeWorkMode()) { console.log( '???? updateFunctionComponent called with a current fiber for workInProgress', current.memoizedProps, @@ -783,7 +737,7 @@ function updateFunctionComponent( ); } - if (current.reify === true) { + if (inSpeculativeWorkMode()) { console.log( 'function component is updating so reifying the work in progress tree', ); @@ -2899,6 +2853,22 @@ export function markWorkInProgressReceivedUpdate() { didReceiveUpdate = true; } +function enterSpeculativeWorkMode(workInProgress: Fiber) { + invariant( + speculativeWorkRootFiber === null, + 'enterSpeculativeWorkMode called when we are already in that mode. this is likely a bug with React itself', + ); + + speculativeWorkRootFiber = workInProgress; + + // set each child to return to this workInProgress; + let childFiber = workInProgress.child; + while (childFiber !== null) { + childFiber.return = workInProgress; + childFiber = childFiber.sibling; + } +} + function bailoutOnAlreadyFinishedWork( current: Fiber | null, workInProgress: Fiber, @@ -2931,7 +2901,12 @@ function bailoutOnAlreadyFinishedWork( } else { // This fiber doesn't have work, but its subtree does. Clone the child // fibers and continue. - cloneChildFibers(current, workInProgress); + if (speculativeWorkRootFiber === null) { + enterSpeculativeWorkMode(workInProgress); + } + // else { + // cloneChildFibers(current, workInProgress); + // } return workInProgress.child; } } @@ -3003,13 +2978,13 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { const originalCurrent = current; invariant( - current.reify === true, - 'reifyWorkInProgress was called on a fiber that is not mared for reification', + speculativeWorkRootFiber !== null, + 'reifyWorkInProgress was called when we are not in speculative work mode', ); let parent, parentWorkInProgress; parentWorkInProgress = parent = current.return; - if (parent.reify === true) { + if (parent !== null && parent !== speculativeWorkRootFiber) { parentWorkInProgress = createWorkInProgress( parent, parent.memoizedProps, @@ -3020,6 +2995,15 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { try { do { + // if the parent we're cloning child fibers from is the speculativeWorkRootFiber + // unset it so we don't go any higher + if (parentWorkInProgress === speculativeWorkRootFiber) { + console.log( + 'we have found the speculativeWorkRootFiber during reification. this is the last pass', + ); + speculativeWorkRootFiber = null; + } + // clone parent WIP's child. use the current's workInProgress if the currentChild is // the current fiber let currentChild = parentWorkInProgress.child; @@ -3041,7 +3025,6 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { ); } parentWorkInProgress.child = newChild; - currentChild.reify = false; newChild.return = parentWorkInProgress; @@ -3069,7 +3052,6 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { ); } newChild.return = parentWorkInProgress; - currentChild.reify = false; } newChild.sibling = null; @@ -3086,16 +3068,18 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { workInProgress = parentWorkInProgress; current = parent; - parentWorkInProgress = parent = current.return; - if (parent !== null && parent.reify === true) { - parentWorkInProgress = createWorkInProgress( - parent, - parent.pendingProps, - parent.expirationTime, - ); - parentWorkInProgress.return = parent.return; + if (speculativeWorkRootFiber !== null) { + parentWorkInProgress = parent = current.return; + if (parent !== null && parent !== speculativeWorkRootFiber) { + parentWorkInProgress = createWorkInProgress( + parent, + parent.pendingProps, + parent.expirationTime, + ); + parentWorkInProgress.return = parent.return; + } } - } while (parent !== null && current.reify === true); + } while (speculativeWorkRootFiber !== null && parent !== null); } catch (e) { console.log('error in refiy', e); } @@ -3104,14 +3088,15 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { 'returning originalCurrent.alternate', fiberName(originalCurrent.alternate), ); + speculativeWorkRootFiber = null; return originalCurrent.alternate; } function fiberName(fiber) { if (fiber == null) return fiber; let version = fiber.version; - let reify = fiber.reify ? 'r' : ''; - let back = `-${version}${reify}`; + let speculative = inSpeculativeWorkMode() ? 's' : ''; + let back = `-${version}${speculative}`; let front = ''; if (fiber.tag === 3) front = 'HostRoot'; else if (fiber.tag === 6) front = 'HostText'; @@ -3131,12 +3116,13 @@ function beginWork( 'beginWork', fiberName(workInProgress) + '->' + fiberName(workInProgress.return), current && fiberName(current) + '->' + fiberName(current.return), + 'speculativeWorkRootFiber: ' + fiberName(speculativeWorkRootFiber), ); - // we may have been passed a current fiber as workInProgress if the parent fiber - // bailed out. in this case set the current to workInProgress. they will be the same - // fiber for now until and unless we determine we need to reify it into real workInProgress - if (workInProgress.reify === true) { + // we may have been passed a current fiber as workInProgress if we are doing + // speculative work. if this is the case we need to assign the work to the current + // because they are the same + if (inSpeculativeWorkMode()) { current = workInProgress; } @@ -3160,9 +3146,9 @@ function beginWork( } } - if (workInProgress.reify === true) { + if (inSpeculativeWorkMode()) { console.log( - 'workInProgress.reify true, if we are going to do work we need to reify first', + 'we are in speculaive work mode, if we are going to do work we need to reify first', ); if ( workInProgress.tag !== ContextProvider && @@ -3176,11 +3162,8 @@ function beginWork( } if (current !== null) { - // when we are on a current fiber as WIP we use memoizedProps since the pendingProps may be out of date const oldProps = current.memoizedProps; - const newProps = workInProgress.reify - ? workInProgress.memoizedProps - : workInProgress.pendingProps; + const newProps = workInProgress.pendingProps; if ( oldProps !== newProps || @@ -3383,6 +3366,7 @@ function beginWork( didReceiveUpdate = false; } + // @TODO pretty sure we need to not do this if we are in speculativeMode. perhaps make it part of reification? // Before entering the begin phase, clear the expiration time. workInProgress.expirationTime = NoWork; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 6ba616ae6a10d..99e8d799e0b2f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -636,8 +636,7 @@ function cutOffTailIfNeeded( function fiberName(fiber) { if (fiber == null) return fiber; let version = fiber.version; - let reify = fiber.reify ? 'r' : ''; - let back = `-${version}${reify}`; + let back = `-${version}`; let front = ''; if (fiber.tag === 3) front = 'HostRoot'; else if (fiber.tag === 6) front = 'HostText'; @@ -653,17 +652,13 @@ function completeWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - let wasSpeculative = workInProgress.reify; - console.log( - wasSpeculative ? 'completeWork -speculative' : 'completeWork', + 'completeWork', fiberName(workInProgress) + '->' + fiberName(workInProgress.return), current && fiberName(current) + '->' + fiberName(current.return), ); - const newProps = wasSpeculative - ? workInProgress.memoizedProps - : workInProgress.pendingProps; + const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { case IndeterminateComponent: diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 534e276b4da24..b79f4d4f475ae 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -121,7 +121,11 @@ import { Batched, Idle, } from './ReactFiberExpirationTime'; -import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; +import { + beginWork as originalBeginWork, + inSpeculativeWorkMode, + endSpeculationWorkIfRootFiber, +} from './ReactFiberBeginWork'; import {completeWork} from './ReactFiberCompleteWork'; import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork'; import { @@ -1283,6 +1287,7 @@ function prepareFreshStack(root, expirationTime) { } workInProgressRoot = root; workInProgress = createWorkInProgress(root.current, null, expirationTime); + console.log('root.current'); renderExpirationTime = expirationTime; workInProgressRootExitStatus = RootIncomplete; workInProgressRootFatalError = null; @@ -1532,8 +1537,8 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // need an additional field on the work in progress. const current = workInProgress.alternate; const returnFiber = workInProgress.return; - const workWasSpeculative = workInProgress.reify; - workInProgress.reify = false; + endSpeculationWorkIfRootFiber(workInProgress); + const workWasSpeculative = inSpeculativeWorkMode(); // Check if the work completed or if something threw. if ((workInProgress.effectTag & Incomplete) === NoEffect) { From c17f5324f65652a0acf5e117e0ec5b8de4e53da1 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 21 Feb 2020 00:40:31 -0800 Subject: [PATCH 07/27] add feature flag and restore original work mode --- .../src/ReactFiberBeginWork.js | 442 ++++++++++-------- .../src/ReactFiberCompleteWork.js | 13 +- .../react-reconciler/src/ReactFiberHooks.js | 107 +++-- .../src/ReactFiberWorkLoop.js | 114 +++-- .../src/__tests__/ReactLazyReconciler-test.js | 149 ------ .../__tests__/ReactSpeculativeWork-test.js | 222 +++++++++ packages/shared/ReactFeatureFlags.js | 6 + .../babel/transform-prevent-infinite-loops.js | 4 +- 8 files changed, 602 insertions(+), 455 deletions(-) delete mode 100644 packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js create mode 100644 packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 54fa6165d8325..888fef8f24d9e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -66,6 +66,8 @@ import { warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, enableChunksAPI, + enableSpeculativeWork, + enableSpeculativeWorkTracing, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -140,7 +142,7 @@ import { import { renderWithHooks, bailoutHooks, - bailoutSpeculativeWorkWithHooks, + canBailoutSpeculativeWorkWithHooks, } from './ReactFiberHooks'; import {stopProfilerTimerIfRunning} from './ReactProfilerTimer'; import { @@ -224,10 +226,12 @@ export function inSpeculativeWorkMode() { export function endSpeculationWorkIfRootFiber(fiber: Fiber) { if (fiber === speculativeWorkRootFiber) { speculativeWorkRootFiber = null; - console.log( - ']]] ending speculation: speculativeWorkRootFiber', - fiberName(speculativeWorkRootFiber), - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + ']]] ending speculation: speculativeWorkRootFiber', + fiberName(speculativeWorkRootFiber), + ); + } } } @@ -644,119 +648,120 @@ function updateFunctionComponent( } } - try { - if (inSpeculativeWorkMode()) { + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( '???? updateFunctionComponent called with a current fiber for workInProgress', - current.memoizedProps, ); - // if (preemptiveHooksBailout(workInProgress, renderExpirationTime)) { - if ( - bailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) - ) { + } + if ( + canBailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) + ) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log('hooks have not changed, we can bail out of update'); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); } - console.log('???? creating a new workInProgress', current.memoizedProps); - let parent = workInProgress.return; - workInProgress = createWorkInProgress( + return bailoutOnAlreadyFinishedWork( current, - current.memoizedProps, - NoWork, + workInProgress, + renderExpirationTime, ); - workInProgress.return = parent; } + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '???? speculative bailout failed: creating a new workInProgress', + ); + } + let parent = workInProgress.return; - console.log( - 'workInProgress & current', - fiberName(workInProgress), - fiberName(current), + // this workInProgress is currently detatched from the actual workInProgress + // tree because the previous workInProgress is from the current tree and we + // don't yet have a return pointer to any fiber in the workInProgress tree + // it will get attached if/when we reify it and end our speculative work + workInProgress = createWorkInProgress( + current, + current.pendingProps, + NoWork, ); + workInProgress.return = parent; + } - let context; - if (!disableLegacyContext) { - const unmaskedContext = getUnmaskedContext( - workInProgress, - Component, - true, - ); - context = getMaskedContext(workInProgress, unmaskedContext); - } + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); + context = getMaskedContext(workInProgress, unmaskedContext); + } - let nextChildren; - prepareToReadContext(workInProgress, renderExpirationTime); - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - setCurrentPhase('render'); - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderExpirationTime, - ); - if ( - debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode - ) { - // Only double-render components with Hooks - if (workInProgress.memoizedState !== null) { - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderExpirationTime, - ); - } + let nextChildren; + prepareToReadContext(workInProgress, renderExpirationTime); + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setCurrentPhase('render'); + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + // Only double-render components with Hooks + if (workInProgress.memoizedState !== null) { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); } - setCurrentPhase(null); - } else { - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderExpirationTime, - ); } + setCurrentPhase(null); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + } - if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } - if (inSpeculativeWorkMode()) { + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( 'function component is updating so reifying the work in progress tree', ); - reifyWorkInProgress(current, workInProgress); } - - // React DevTools reads this flag. - workInProgress.effectTag |= PerformedWork; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); - return workInProgress.child; - } catch (e) { - console.log(e); - throw e; + // because there was an update and we were in speculative work mode we need + // to attach the temporary workInProgress we created earlier in this function + // to the workInProgress tree + reifyWorkInProgress(current, workInProgress); } + + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; } function updateChunk( @@ -2718,6 +2723,19 @@ function updateContextProvider( pushProvider(workInProgress, newValue); + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + 'updateContextProvider: we are in speculative work mode so we can bail out eagerly', + ); + } + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + if (oldProps !== null) { const oldValue = oldProps.value; const changedBits = calculateChangedBits(context, newValue, oldValue); @@ -2899,15 +2917,20 @@ function bailoutOnAlreadyFinishedWork( // a work-in-progress set. If so, we need to transfer their effects. return null; } else { - // This fiber doesn't have work, but its subtree does. Clone the child - // fibers and continue. - if (speculativeWorkRootFiber === null) { - enterSpeculativeWorkMode(workInProgress); - } - // else { - // cloneChildFibers(current, workInProgress); - // } - return workInProgress.child; + if (enableSpeculativeWork) { + // This fiber doesn't have work, but its subtree does. enter speculative + // work mode if not already in it and continue. + if (speculativeWorkRootFiber === null) { + enterSpeculativeWorkMode(workInProgress); + } + // this child wasn't cloned so it is from the current tree + return workInProgress.child; + } else { + // This fiber doesn't have work, but its subtree does. Clone the child + // fibers and continue. + cloneChildFibers(current, workInProgress); + return workInProgress.child; + } } } @@ -2993,102 +3016,102 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { parentWorkInProgress.return = parent.return; } - try { - do { - // if the parent we're cloning child fibers from is the speculativeWorkRootFiber - // unset it so we don't go any higher - if (parentWorkInProgress === speculativeWorkRootFiber) { + do { + // if the parent we're cloning child fibers from is the speculativeWorkRootFiber + // unset it so we don't go any higher + if (parentWorkInProgress === speculativeWorkRootFiber) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( 'we have found the speculativeWorkRootFiber during reification. this is the last pass', ); - speculativeWorkRootFiber = null; } + speculativeWorkRootFiber = null; + } - // clone parent WIP's child. use the current's workInProgress if the currentChild is - // the current fiber - let currentChild = parentWorkInProgress.child; + // clone parent WIP's child. use the current's workInProgress if the currentChild is + // the current fiber + let currentChild = parentWorkInProgress.child; + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( ')))) reifying', fiberName(parentWorkInProgress), 'starting with', fiberName(currentChild), ); - let newChild; - if (workInProgress !== null && currentChild === current) { + } + let newChild; + if (workInProgress !== null && currentChild === current) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log('using workInProgress of', fiberName(workInProgress)); - newChild = workInProgress; - } else { - newChild = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); } - parentWorkInProgress.child = newChild; + newChild = workInProgress; + } else { + newChild = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); + } + parentWorkInProgress.child = newChild; - newChild.return = parentWorkInProgress; + newChild.return = parentWorkInProgress; - // for each sibling of the parent's workInProgress create a workInProgress. - // use current's workInProgress if the sibiling is the current fiber - while (currentChild.sibling !== null) { - currentChild = currentChild.sibling; + // for each sibling of the parent's workInProgress create a workInProgress. + // use current's workInProgress if the sibiling is the current fiber + while (currentChild.sibling !== null) { + currentChild = currentChild.sibling; + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( ')))) reifying', fiberName(parentWorkInProgress), 'now with', fiberName(currentChild), ); - if (workInProgress !== null && currentChild === current) { + } + if (workInProgress !== null && currentChild === current) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( 'using ephemeralWorkInProgress of', fiberName(workInProgress), ); - newChild = newChild.sibling = workInProgress; - } else { - newChild = newChild.sibling = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); } - newChild.return = parentWorkInProgress; + newChild = newChild.sibling = workInProgress; + } else { + newChild = newChild.sibling = createWorkInProgress( + currentChild, + currentChild.pendingProps, + currentChild.expirationTime, + ); } - newChild.sibling = null; - - console.log( - 'workInProgress pointing at', - fiberName(workInProgress.return), - ); - console.log('current pointing at', fiberName(current.return)); - console.log('parent pointing at', fiberName(parent.return)); - console.log( - 'parentWorkInProgress pointing at', - fiberName(parentWorkInProgress.return), - ); - - workInProgress = parentWorkInProgress; - current = parent; - if (speculativeWorkRootFiber !== null) { - parentWorkInProgress = parent = current.return; - if (parent !== null && parent !== speculativeWorkRootFiber) { - parentWorkInProgress = createWorkInProgress( - parent, - parent.pendingProps, - parent.expirationTime, - ); - parentWorkInProgress.return = parent.return; - } + newChild.return = parentWorkInProgress; + } + newChild.sibling = null; + + workInProgress = parentWorkInProgress; + current = parent; + if (speculativeWorkRootFiber !== null) { + parentWorkInProgress = parent = current.return; + if (parent !== null && parent !== speculativeWorkRootFiber) { + parentWorkInProgress = createWorkInProgress( + parent, + parent.pendingProps, + parent.expirationTime, + ); + parentWorkInProgress.return = parent.return; } - } while (speculativeWorkRootFiber !== null && parent !== null); - } catch (e) { - console.log('error in refiy', e); - } + } + } while (speculativeWorkRootFiber !== null && parent !== null); - console.log( - 'returning originalCurrent.alternate', - fiberName(originalCurrent.alternate), - ); speculativeWorkRootFiber = null; + + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + 'returning originalCurrent.alternate', + fiberName(originalCurrent.alternate), + ); + } + // returning alternate of original current because in some cases + // this function is called with no initialWorkInProgress return originalCurrent.alternate; } @@ -3112,17 +3135,17 @@ function beginWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - console.log( - 'beginWork', - fiberName(workInProgress) + '->' + fiberName(workInProgress.return), - current && fiberName(current) + '->' + fiberName(current.return), - 'speculativeWorkRootFiber: ' + fiberName(speculativeWorkRootFiber), - ); - - // we may have been passed a current fiber as workInProgress if we are doing - // speculative work. if this is the case we need to assign the work to the current - // because they are the same - if (inSpeculativeWorkMode()) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + 'beginWork', + fiberName(workInProgress) + '->' + fiberName(workInProgress.return), + current && fiberName(current) + '->' + fiberName(current.return), + 'speculativeWorkRootFiber: ' + fiberName(speculativeWorkRootFiber), + ); + } + // in speculative work mode we have been passed the current fiber as the workInProgress + // if this is the case we need to assign the work to the current because they are the same + if (enableSpeculativeWork && inSpeculativeWorkMode()) { current = workInProgress; } @@ -3146,19 +3169,20 @@ function beginWork( } } - if (inSpeculativeWorkMode()) { - console.log( - 'we are in speculaive work mode, if we are going to do work we need to reify first', - ); - if ( - workInProgress.tag !== ContextProvider && - workInProgress.tag !== FunctionComponent - ) { + // for now only support speculative work with certain fiber types. + // support for more can and should be added + if ( + enableSpeculativeWork && + inSpeculativeWorkMode() && + workInProgress.tag !== ContextProvider && + workInProgress.tag !== FunctionComponent + ) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( - 'workInProgress is something other than a ContextProvider or FunctionComponent, reifying immediately', + 'in speculative work mode and workInProgress is something other than a ContextProvider or FunctionComponent, reifying immediately', ); - workInProgress = reifyWorkInProgress(current, null); } + workInProgress = reifyWorkInProgress(current, null); } if (current !== null) { @@ -3171,16 +3195,20 @@ function beginWork( // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { - console.log( - '--- props different or legacy context changed. lets do update', - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '--- props different or legacy context changed. lets do update', + ); + } // If props or context changed, mark the fiber as having performed work. // This may be unset if the props are determined to be equal later (memo). didReceiveUpdate = true; } else if (updateExpirationTime < renderExpirationTime) { - console.log( - '--- props same, no legacy context, no work scheduled. lets push onto stack and bailout', - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '--- props same, no legacy context, no work scheduled. lets push onto stack and bailout', + ); + } didReceiveUpdate = false; // This fiber does not have any pending work. Bailout without entering // the begin phase. There's still some bookkeeping we that needs to be done @@ -3354,15 +3382,19 @@ function beginWork( // nor legacy context. Set this to false. If an update queue or context // consumer produces a changed value, it will set this to true. Otherwise, // the component will assume the children have not changed and bail out. - console.log( - '--- props same, no legacy context, this fiber has work scheduled so we will see if it produces an update', - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '--- props same, no legacy context, this fiber has work scheduled so we will see if it produces an update', + ); + } didReceiveUpdate = false; } } else { - console.log( - '--- current fiber null, likely this was a newly mounted component', - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '--- current fiber null, likely this was a newly mounted component', + ); + } didReceiveUpdate = false; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 99e8d799e0b2f..636d29924143f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -120,6 +120,7 @@ import { enableFundamentalAPI, enableScopeAPI, enableChunksAPI, + enableSpeculativeWorkTracing, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -652,11 +653,13 @@ function completeWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - console.log( - 'completeWork', - fiberName(workInProgress) + '->' + fiberName(workInProgress.return), - current && fiberName(current) + '->' + fiberName(current.return), - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + 'completeWork', + fiberName(workInProgress) + '->' + fiberName(workInProgress.return), + current && fiberName(current) + '->' + fiberName(current.return), + ); + } const newProps = workInProgress.pendingProps; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index b72310edc1dd6..018318bb8e43d 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -21,7 +21,19 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {NoWork, Sync} from './ReactFiberExpirationTime'; -import {readContext, peekContext} from './ReactFiberNewContext'; +import { + readContext as originalReadContext, + peekContext, +} from './ReactFiberNewContext'; + +const readContext = originalReadContext; +const mountContext = enableSpeculativeWork + ? mountContextImpl + : originalReadContext; +const updateContext = enableSpeculativeWork + ? updateContextImpl + : originalReadContext; + import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents'; import { Update as UpdateEffect, @@ -42,6 +54,10 @@ import { markRenderEventTimeAndConfig, markUnprocessedUpdateTime, } from './ReactFiberWorkLoop'; +import { + enableSpeculativeWork, + enableSpeculativeWorkTracing, +} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import getComponentName from 'shared/getComponentName'; @@ -57,6 +73,8 @@ import { const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; +type ObservedBits = void | number | boolean; + export type Dispatcher = {| readContext( context: ReactContext, @@ -343,24 +361,32 @@ function areHookInputsEqual( return true; } -export function bailoutSpeculativeWorkWithHooks( +export function canBailoutSpeculativeWorkWithHooks( current: Fiber, nextRenderExpirationTime: renderExpirationTime, ): boolean { - console.log('====== bailoutSpeculativeWorkWithHooks'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('====== bailoutSpeculativeWorkWithHooks'); + } let hook = current.memoizedState; while (hook !== null) { - console.log('hook'); if (typeof hook.bailout === 'function') { let didBailout = hook.bailout(hook, nextRenderExpirationTime); if (didBailout === false) { - console.log('====== bailoutSpeculativeWorkWithHooks returning false'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '====== bailoutSpeculativeWorkWithHooks returning false', + hook.bailout.name, + ); + } return false; } } hook = hook.next; } - console.log('====== bailoutSpeculativeWorkWithHooks returning true'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('====== bailoutSpeculativeWorkWithHooks returning true'); + } return true; } @@ -641,9 +667,9 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } -type ObservedBits = void | number | boolean; +const EMPTY = Symbol('empty'); -function mountContext( +function mountContextImpl( context: ReactContext, selector?: ObservedBits | (C => any), ): any { @@ -677,9 +703,7 @@ function mountContext( } } -let EMPTY = Symbol('empty'); - -function updateContext( +function updateContextImpl( context: ReactContext, selector?: ObservedBits | (C => any), ): any { @@ -717,40 +741,43 @@ function updateContext( } function bailoutContext(hook: Hook): boolean { - console.log('}}}}} bailoutContext'); const memoizedState = hook.memoizedState; let selector = memoizedState.selector; let peekedContextValue = peekContext(memoizedState.context); let previousContextValue = memoizedState.contextValue; if (selector !== null) { - console.log('}}}}} bailoutContext we have a selector'); if (previousContextValue !== peekedContextValue) { - console.log( - '}}}}} bailoutContext we have different context values', - previousContextValue, - peekedContextValue, - ); - let stashedSelection = selector(peekedContextValue); - if (stashedSelection !== memoizedState.selection) { + if (__DEV__ && enableSpeculativeWorkTracing) { console.log( - '}}}}} bailoutContext we have different context SELECTIONS', - memoizedState.selection, - stashedSelection, + '}}}}} [selector mode] bailoutContext we have different context VALUES', + previousContextValue, + peekedContextValue, ); + } + let stashedSelection = selector(peekedContextValue); + if (stashedSelection !== memoizedState.selection) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '}}}}} [selector mode] bailoutContext we have different context SELECTIONS', + memoizedState.selection, + stashedSelection, + ); + } memoizedState.stashedSelection = stashedSelection; memoizedState.stashedContextValue = peekedContextValue; return false; } } } else { - console.log('}}}}} bailoutContext we are not using selectors'); if (previousContextValue !== peekedContextValue) { - console.log( - '}}}}} bailoutContext we have different context values', - previousContextValue, - peekedContextValue, - ); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '}}}}} [value mode] bailoutContext we have different context values', + previousContextValue, + peekedContextValue, + ); + } return false; } } @@ -788,7 +815,6 @@ function mountReducer( lastRenderedState: (initialState: any), }); hook.bailout = bailoutReducer; - console.log('mountReducer hook'); const dispatch: Dispatch = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, @@ -983,7 +1009,9 @@ function rerenderReducer( } function bailoutReducer(hook, renderExpirationTime): boolean { - console.log('}}}} bailoutReducer'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('}}}} bailoutReducer'); + } const queue = hook.queue; @@ -1014,11 +1042,15 @@ function bailoutReducer(hook, renderExpirationTime): boolean { // if newState is different from the current state do not bailout if (newState !== hook.memoizedState) { - console.log('found a new state. not bailing out of hooks'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('found a new state. not bailing out of hooks'); + } return false; } } - console.log('state is the same so we can maybe bail out'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('state is the same so we can maybe bail out'); + } return true; } @@ -1506,7 +1538,7 @@ function dispatchAction( currentlyRenderingFiber.expirationTime = renderExpirationTime; } else { if ( - false && // turn off eager computation with bailout + !enableSpeculativeWork && fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) ) { @@ -1530,6 +1562,9 @@ function dispatchAction( update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('++++++++++++ eagerly bailing out of reducer update'); + } // Fast path. We can bail out without scheduling React to re-render. // It's still possible that we'll need to rebase this update later, // if the component re-renders for a different reason and by that @@ -1552,7 +1587,9 @@ function dispatchAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } - console.log('++++++++++++ scheduling work for reducer hook'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('++++++++++++ scheduling work for reducer hook'); + } scheduleWork(fiber, expirationTime); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index b79f4d4f475ae..a6bae21a02d18 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -28,6 +28,8 @@ import { flushSuspenseFallbacksInTests, disableSchedulerTimeoutBasedOnReactExpirationTime, enableTrainModelFix, + enableSpeculativeWork, + enableSpeculativeWorkTracing, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -531,10 +533,6 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { const lastExpiredTime = root.lastExpiredTime; if (lastExpiredTime !== NoWork) { - console.log( - '***** getNextRootExpirationTimeToWorkOn has lastExpiredTime', - lastExpiredTime, - ); return lastExpiredTime; } @@ -543,10 +541,6 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { const firstPendingTime = root.firstPendingTime; if (!isRootSuspendedAtTime(root, firstPendingTime)) { // The highest priority pending time is not suspended. Let's work on that. - console.log( - '***** getNextRootExpirationTimeToWorkOn has firstPendingTime', - firstPendingTime, - ); return firstPendingTime; } @@ -567,10 +561,6 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // Don't work on Idle/Never priority unless everything else is committed. return NoWork; } - console.log( - '***** getNextRootExpirationTimeToWorkOn returning next level to work on', - nextLevel, - ); return nextLevel; } @@ -580,7 +570,9 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // the next level that the root has work on. This function is called on every // update, and right before exiting a task. function ensureRootIsScheduled(root: FiberRoot) { - console.log('ensureRootIsScheduled'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('ensureRootIsScheduled'); + } const lastExpiredTime = root.lastExpiredTime; if (lastExpiredTime !== NoWork) { // Special case: Expired work should flush synchronously. @@ -664,7 +656,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // event time. The next update will compute a new event time. currentEventTime = NoWork; - console.log('performConcurrentWorkOnRoot'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('performConcurrentWorkOnRoot'); + } if (didTimeout) { // The render task took too long to complete. Mark the current time as @@ -766,7 +760,9 @@ function finishConcurrentRender( exitStatus, expirationTime, ) { - console.log('finishConcurrentRender'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('finishConcurrentRender'); + } // Set this to null to indicate there's no in-progress render. workInProgressRoot = null; @@ -795,7 +791,9 @@ function finishConcurrentRender( break; } case RootSuspended: { - console.log('RootSuspended'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('RootSuspended'); + } markRootSuspendedAtTime(root, expirationTime); const lastSuspendedTime = root.lastSuspendedTime; if (expirationTime === lastSuspendedTime) { @@ -840,7 +838,6 @@ function finishConcurrentRender( } } - console.log('finishConcurrentRender about to get expiration time'); const nextTime = getNextRootExpirationTimeToWorkOn(root); if (nextTime !== NoWork && nextTime !== expirationTime) { // There's additional work on this root. @@ -872,7 +869,9 @@ function finishConcurrentRender( break; } case RootSuspendedWithDelay: { - console.log('RootSuspendedWithDelay'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('RootSuspendedWithDelay'); + } markRootSuspendedAtTime(root, expirationTime); const lastSuspendedTime = root.lastSuspendedTime; @@ -970,7 +969,9 @@ function finishConcurrentRender( break; } case RootCompleted: { - console.log('RootCompleted'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('RootCompleted'); + } // The work completed. Ready to commit. if ( @@ -1021,7 +1022,9 @@ function performSyncWorkOnRoot(root) { 'Should not already be working.', ); - console.log('performSyncWorkOnRoot', lastExpiredTime, expirationTime); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('performSyncWorkOnRoot', lastExpiredTime, expirationTime); + } flushPassiveEffects(); @@ -1056,7 +1059,9 @@ function performSyncWorkOnRoot(root) { popInteractions(((prevInteractions: any): Set)); } - console.log('workInProgressRootExitStatus', workInProgressRootExitStatus); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('workInProgressRootExitStatus', workInProgressRootExitStatus); + } if (workInProgressRootExitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; @@ -1075,15 +1080,13 @@ function performSyncWorkOnRoot(root) { 'bug in React. Please file an issue.', ); } else { - console.log('finishing sync render'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('finishing sync render'); + } // We now have a consistent tree. Because this is a sync render, we // will commit it even if something suspended. stopFinishedWorkLoopTimer(); root.finishedWork = (root.current.alternate: any); - console.log( - '&&&& finishedWork.expirationTime', - root.finishedWork.expirationTime, - ); root.finishedExpirationTime = expirationTime; finishSyncRender(root); } @@ -1287,7 +1290,6 @@ function prepareFreshStack(root, expirationTime) { } workInProgressRoot = root; workInProgress = createWorkInProgress(root.current, null, expirationTime); - console.log('root.current'); renderExpirationTime = expirationTime; workInProgressRootExitStatus = RootIncomplete; workInProgressRootFatalError = null; @@ -1668,22 +1670,12 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { function getRemainingExpirationTime(fiber: Fiber) { const updateExpirationTime = fiber.expirationTime; const childExpirationTime = fiber.childExpirationTime; - console.log( - '^^^^ getRemainingExpirationTime updateExpirationTime, childExpirationTime', - updateExpirationTime, - childExpirationTime, - ); return updateExpirationTime > childExpirationTime ? updateExpirationTime : childExpirationTime; } function resetChildExpirationTime(completedWork: Fiber) { - console.log( - '((((( resetChildExpirationTime', - completedWork.expirationTime, - completedWork.childExpirationTime, - ); if ( renderExpirationTime !== Never && completedWork.childExpirationTime === Never @@ -1750,7 +1742,9 @@ function resetChildExpirationTime(completedWork: Fiber) { } function commitRoot(root) { - console.log('committing root'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('committing root'); + } const renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority( ImmediatePriority, @@ -1781,11 +1775,6 @@ function commitRootImpl(root, renderPriorityLevel) { if (finishedWork === null) { return null; } - console.log( - '&&&&& commitRootImpl finishedWork expirationTime', - finishedWork.expirationTime, - expirationTime, - ); root.finishedWork = null; root.finishedExpirationTime = NoWork; @@ -1816,13 +1805,17 @@ function commitRootImpl(root, renderPriorityLevel) { ); if (root === workInProgressRoot) { - console.log('root was workInProgressRoot'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('root was workInProgressRoot'); + } // We can reset these now that they are finished. workInProgressRoot = null; workInProgress = null; renderExpirationTime = NoWork; } else { - console.log('root was different'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('root was different'); + } // This indicates that the last root we worked on is not the same one that // we're committing now. This most commonly happens when a suspended root // times out. @@ -1831,7 +1824,9 @@ function commitRootImpl(root, renderPriorityLevel) { // Get the list of effects. let firstEffect; if (finishedWork.effectTag > PerformedWork) { - console.log('**** work was performed'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('**** work was performed'); + } // A fiber's effect list consists only of its children, not itself. So if // the root has an effect, we need to add it to the end of the list. The @@ -1849,8 +1844,7 @@ function commitRootImpl(root, renderPriorityLevel) { } if (firstEffect !== null) { - console.log('root finishedWork has effects'); - { + if (__DEV__ && enableSpeculativeWorkTracing) { let count = 0; let effect = firstEffect; while (effect !== null) { @@ -1859,6 +1853,7 @@ function commitRootImpl(root, renderPriorityLevel) { } console.log(`finished work had ${count} effects`); } + const prevExecutionContext = executionContext; executionContext |= CommitContext; const prevInteractions = pushInteractions(root); @@ -1983,7 +1978,9 @@ function commitRootImpl(root, renderPriorityLevel) { } executionContext = prevExecutionContext; } else { - console.log('**** no effects at all'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('**** no effects at all'); + } // No effects. root.current = finishedWork; @@ -2006,8 +2003,6 @@ function commitRootImpl(root, renderPriorityLevel) { const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; if (rootDoesHavePassiveEffects) { - console.log('**** YES passive effects on root'); - // This commit has passive effects. Stash a reference to them. But don't // schedule a callback until after flushing layout work. rootDoesHavePassiveEffects = false; @@ -2015,8 +2010,6 @@ function commitRootImpl(root, renderPriorityLevel) { pendingPassiveEffectsExpirationTime = expirationTime; pendingPassiveEffectsRenderPriority = renderPriorityLevel; } else { - console.log('**** NO passive effects on root'); - // We are done with the effect chain at this point so let's clear the // nextEffect pointers to assist with GC. If we have passive effects, we'll // clear this in flushPassiveEffects. @@ -2030,8 +2023,6 @@ function commitRootImpl(root, renderPriorityLevel) { // Check if there's remaining work on this root const remainingExpirationTime = root.firstPendingTime; - console.log('**** remainingExpirationTime', remainingExpirationTime); - if (remainingExpirationTime !== NoWork) { if (enableSchedulerTracing) { if (spawnedWorkDuringRender !== null) { @@ -2080,7 +2071,6 @@ function commitRootImpl(root, renderPriorityLevel) { // Always call this before exiting `commitRoot`, to ensure that any // additional work on this root is scheduled. - console.log('^^^ calling ensureRootIsScheduled from commitRootImpl'); ensureRootIsScheduled(root); if (hasUncaughtError) { @@ -2440,7 +2430,9 @@ function captureCommitPhaseErrorOnRoot( sourceFiber: Fiber, error: mixed, ) { - console.log('^^^^^ captureCommitPhaseErrorOnRoot', error); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('^^^^^ captureCommitPhaseErrorOnRoot', error); + } const errorInfo = createCapturedValue(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); enqueueUpdate(rootFiber, update); @@ -2452,7 +2444,9 @@ function captureCommitPhaseErrorOnRoot( } export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { - console.log('^^^^^ captureCommitPhaseError', error); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('^^^^^ captureCommitPhaseError', error); + } if (sourceFiber.tag === HostRoot) { // Error was thrown at the root. There is no parent, so the root // itself should capture it. @@ -2564,7 +2558,9 @@ function retryTimedOutBoundary( boundaryFiber: Fiber, retryTime: ExpirationTime, ) { - console.log('^^^^ retryTimedOutBoundary'); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('^^^^ retryTimedOutBoundary'); + } // The boundary fiber (a Suspense component or SuspenseList component) // previously was rendered in its fallback state. One of the promises that // suspended it has resolved, which means at least part of the tree was diff --git a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js b/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js deleted file mode 100644 index a073e5d1dab31..0000000000000 --- a/packages/react-reconciler/src/__tests__/ReactLazyReconciler-test.js +++ /dev/null @@ -1,149 +0,0 @@ -let React; -let ReactFeatureFlags; -let ReactNoop; -let Scheduler; -let ReactCache; -let Suspense; -let TextResource; - -describe('ReactLazyReconciler', () => { - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - // ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - // ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - React = require('react'); - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - ReactCache = require('react-cache'); - Suspense = React.Suspense; - }); - - it('yields text', () => { - const root = ReactNoop.createBlockingRoot(); - - const Ctx = React.createContext(1); - - let selectorTest = false; - - function Text(props) { - let ctx = React.useContext(Ctx, v => { - selectorTest && Scheduler.unstable_yieldValue('selector'); - return Math.floor(v / 2); - }); - Scheduler.unstable_yieldValue(props.text); - Scheduler.unstable_yieldValue(ctx); - return props.text + (props.plusValue ? ctx : ''); - } - - let triggerState = null; - - function Nothing({children}) { - let [state, setState] = React.useState(0); - // trigger state will result in an identical state. it is used to see - // where how and if the state bailout is working - triggerState = () => setState(s => s); - console.log('^^^^ rendering Nothing Component'); - Scheduler.unstable_yieldValue('Nothing'); - return children; - } - - let triggerCtx = null; - - function App() { - let [val, setVal] = React.useState(2); - let texts = React.useMemo( - () => ( - - - - - - - - ), - [], - ); - triggerCtx = setVal; - return {texts}; - } - - root.render(); - - // Nothing should have rendered yet - expect(root).toMatchRenderedOutput(null); - - // Everything should render immediately in the next event - // double renders for each component with hooks - expect(Scheduler).toFlushExpired([ - 'Nothing', - 'Nothing', - 'Nothing', - 'Nothing', - 'A', - 1, - 'A', - 1, - 'B', - 1, - 'B', - 1, - 'C', - 1, - 'C', - 1, - ]); - expect(root).toMatchRenderedOutput('A1B1C'); - - ReactNoop.act(() => triggerCtx(4)); - - // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded([ - 'A', - 2, - 'A', - 2, - 'B', - 2, - 'B', - 2, - 'C', - 2, - 'C', - 2, - ]); - expect(root).toMatchRenderedOutput('A2B2C'); - - selectorTest = true; - ReactNoop.act(() => triggerCtx(5)); - selectorTest = false; - // nothing should render (below app) because the value will be the same - expect(Scheduler).toHaveYielded(['selector', 'selector', 'selector']); - expect(root).toMatchRenderedOutput('A2B2C'); - - ReactNoop.act(() => triggerCtx(6)); - - // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded([ - 'A', - 3, - 'A', - 3, - 'B', - 3, - 'B', - 3, - 'C', - 3, - 'C', - 3, - ]); - expect(root).toMatchRenderedOutput('A3B3C'); - - ReactNoop.act(() => triggerState()); - - // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded([]); - expect(root).toMatchRenderedOutput('A3B3C'); - }); -}); diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js new file mode 100644 index 0000000000000..93f9a6a89ead7 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -0,0 +1,222 @@ +let React; +let ReactFeatureFlags; +let ReactNoop; +let Scheduler; +let ReactCache; +let Suspense; +let TextResource; + +describe('ReactSpeculativeWork', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableSpeculativeWork = false; + ReactFeatureFlags.enableSpeculativeWorkTracing = false; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + ReactCache = require('react-cache'); + Suspense = React.Suspense; + }); + + it('yields text', () => { + const Ctx = React.createContext(1); + + let selectorTest = false; + + function Text(props) { + let ctx = React.useContext(Ctx, v => { + selectorTest && Scheduler.unstable_yieldValue('selector'); + return Math.floor(v / 2); + }); + Scheduler.unstable_yieldValue(props.text); + Scheduler.unstable_yieldValue(ctx); + return props.text + (props.plusValue ? ctx : ''); + } + + let triggerState = null; + + function Nothing({children}) { + let [state, setState] = React.useState(0); + // trigger state will result in an identical state. it is used to see + // where how and if the state bailout is working + triggerState = () => setState(s => s); + // Scheduler.unstable_yieldValue('Nothing'); + return children; + } + + let triggerCtx = null; + + function App() { + let [val, setVal] = React.useState(2); + let texts = React.useMemo( + () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + [], + ); + triggerCtx = setVal; + return {texts}; + } + + for (let i = 0; i < 1000; i++) { + const root = ReactNoop.createBlockingRoot(); + + root.render(); + + // Nothing should have rendered yet + expect(root).toMatchRenderedOutput(null); + + // Everything should render immediately in the next event + if (ReactFeatureFlags.enableSpeculativeWork) { + // double renders for each component with hooks + expect(Scheduler).toFlushExpired([ + // 'Nothing', + // 'Nothing', + // 'Nothing', + // 'Nothing', + 'A', + 1, + 'A', + 1, + 'B', + 1, + 'B', + 1, + 'C', + 1, + 'C', + 1, + ]); + expect(root).toMatchRenderedOutput('A1B1C'); + } else { + // Text only uses useContext which does not persist a hook in non-spec feature + // this means no double render + // also no selector means we get the actual value + expect(Scheduler).toFlushExpired([ + // 'Nothing', + // 'Nothing', + // 'Nothing', + // 'Nothing', + 'A', + 2, + 'B', + 2, + 'C', + 2, + ]); + expect(root).toMatchRenderedOutput('A2B2C'); + } + + ReactNoop.act(() => triggerCtx(4)); + + if (ReactFeatureFlags.enableSpeculativeWork) { + // Everything should render immediately in the next event + expect(Scheduler).toHaveYielded([ + 'A', + 2, + 'A', + 2, + 'B', + 2, + 'B', + 2, + 'C', + 2, + 'C', + 2, + ]); + expect(root).toMatchRenderedOutput('A2B2C'); + } else { + // Everything should render immediately in the next event + expect(Scheduler).toHaveYielded(['A', 4, 'B', 4, 'C', 4]); + expect(root).toMatchRenderedOutput('A4B4C'); + } + + selectorTest = true; + ReactNoop.act(() => triggerCtx(5)); + selectorTest = false; + // nothing should render (below app) because the value will be the same + + if (ReactFeatureFlags.enableSpeculativeWork) { + expect(Scheduler).toHaveYielded(['selector', 'selector', 'selector']); + expect(root).toMatchRenderedOutput('A2B2C'); + } else { + expect(Scheduler).toHaveYielded(['A', 5, 'B', 5, 'C', 5]); + expect(root).toMatchRenderedOutput('A5B5C'); + } + + ReactNoop.act(() => triggerCtx(6)); + + if (ReactFeatureFlags.enableSpeculativeWork) { + expect(Scheduler).toHaveYielded([ + 'A', + 3, + 'A', + 3, + 'B', + 3, + 'B', + 3, + 'C', + 3, + 'C', + 3, + ]); + expect(root).toMatchRenderedOutput('A3B3C'); + } else { + expect(Scheduler).toHaveYielded(['A', 6, 'B', 6, 'C', 6]); + expect(root).toMatchRenderedOutput('A6B6C'); + } + + ReactNoop.act(() => triggerState()); + + if (ReactFeatureFlags.enableSpeculativeWork) { + // Everything should render immediately in the next event + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('A3B3C'); + } else { + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('A6B6C'); + } + } + }); +}); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 0dd89f7674a5b..3fc30df026a3d 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -130,3 +130,9 @@ export const warnUnstableRenderSubtreeIntoContainer = false; // Disables ReactDOM.unstable_createPortal export const disableUnstableCreatePortal = false; + +// Turns on speculative work mode features and context selector api support +export const enableSpeculativeWork = true; + +// TEMPORARY turns on tracing of speculative work mode features +export const enableSpeculativeWorkTracing = false; diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index 26ef925f7af7f..5146adb3d66c5 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,10 +13,10 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 10; +const MAX_SOURCE_ITERATIONS = 15000; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. -const MAX_TEST_ITERATIONS = 10; +const MAX_TEST_ITERATIONS = 50000; module.exports = ({types: t, template}) => { // We set a global so that we can later fail the test From adf1f5bbf34200039129f132c3af0d7eabe0f666 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 21 Feb 2020 01:00:36 -0800 Subject: [PATCH 08/27] fixup context error warning and reduce test load --- .../src/__tests__/ReactSpeculativeWork-test.js | 15 ++++++++++----- packages/react/src/ReactHooks.js | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 93f9a6a89ead7..8cebf2ba6f241 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -24,11 +24,16 @@ describe('ReactSpeculativeWork', () => { let selectorTest = false; + let selector = v => { + selectorTest && Scheduler.unstable_yieldValue('selector'); + return Math.floor(v / 2); + }; + function Text(props) { - let ctx = React.useContext(Ctx, v => { - selectorTest && Scheduler.unstable_yieldValue('selector'); - return Math.floor(v / 2); - }); + let ctx = React.useContext( + Ctx, + ReactFeatureFlags.enableSpeculativeWork ? selector : undefined, + ); Scheduler.unstable_yieldValue(props.text); Scheduler.unstable_yieldValue(ctx); return props.text + (props.plusValue ? ctx : ''); @@ -97,7 +102,7 @@ describe('ReactSpeculativeWork', () => { return {texts}; } - for (let i = 0; i < 1000; i++) { + for (let i = 0; i < 100; i++) { const root = ReactNoop.createBlockingRoot(); root.render(); diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 5ad4a0926e2fa..1f0fa48c3d6a6 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -12,6 +12,7 @@ import type { ReactEventResponder, ReactEventResponderListener, } from 'shared/ReactTypes'; +import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols'; @@ -40,7 +41,7 @@ export function useContext( ) { const dispatcher = resolveDispatcher(); if (__DEV__) { - if (false && unstable_observedBits !== undefined) { + if (!enableSpeculativeWork && unstable_observedBits !== undefined) { console.error( 'useContext() second argument is reserved for future ' + 'use in React. Passing it is not supported. ' + From ce71231f43e6c4443cb48a2e166ca6c4da4b9187 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 21 Feb 2020 23:05:35 -0800 Subject: [PATCH 09/27] fix bug in completeUnitOfWork and get ReactNewContext tests working --- .../src/ReactFiberBeginWork.js | 76 ++++++++++++ .../src/ReactFiberWorkLoop.js | 109 ++++++++++++++++++ .../ReactNewContext-test.internal.js | 59 ++++++---- packages/react/src/ReactHooks.js | 15 +++ .../babel/transform-prevent-infinite-loops.js | 4 +- 5 files changed, 240 insertions(+), 23 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 888fef8f24d9e..8ceb03e62c4eb 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -218,6 +218,7 @@ if (__DEV__) { } let speculativeWorkRootFiber: Fiber | null = null; +let didReifySpeculativeWork: boolean = false; export function inSpeculativeWorkMode() { return speculativeWorkRootFiber !== null; @@ -235,6 +236,10 @@ export function endSpeculationWorkIfRootFiber(fiber: Fiber) { } } +export function didReifySpeculativeWorkDuringThisStep(): boolean { + return didReifySpeculativeWork; +} + export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, @@ -847,6 +852,13 @@ function updateClassComponent( nextProps, renderExpirationTime: ExpirationTime, ) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + 'updateClassComponent wip & current', + fiberName(workInProgress), + fiberName(current), + ); + } if (__DEV__) { if (workInProgress.type !== workInProgress.elementType) { // Lazy component props can't be validated in createElement @@ -956,6 +968,9 @@ function finishClassComponent( markRef(current, workInProgress); const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect; + if (__DEV__ && enableSpeculativeWorkTracing && didCaptureError) { + console.log('___ finishClassComponent didCaptureError!!!'); + } if (!shouldUpdate && !didCaptureError) { // Context providers should defer to sCU for rendering @@ -979,6 +994,9 @@ function finishClassComponent( didCaptureError && typeof Component.getDerivedStateFromError !== 'function' ) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('### captued an error but not derivedStateFromError handler'); + } // If we captured an error, but getDerivedStateFromError is not defined, // unmount all the children. componentDidCatch will schedule an update to // re-render a fallback. This is temporary until we migrate everyone to @@ -2997,7 +3015,52 @@ function remountFiber( } } +function printEffectChain(label, fiber) { + let effect = fiber; + let firstEffect = fiber.firstEffect; + let lastEffect = fiber.lastEffect; + + let cache = new Set(); + let buffer = ''; + do { + let name = fiberName(effect); + if (effect === firstEffect) { + name = 'F(' + name + ')'; + } + if (effect === lastEffect) { + name = 'L(' + name + ')'; + } + buffer += name + ' -> '; + cache.add(effect); + effect = effect.nextEffect; + } while (effect !== null && !cache.has(effect)); + if (cache.has(effect)) { + buffer = '[Circular] !!! : ' + buffer; + } + console.log(`printEffectChain(${label}) self`, buffer); + buffer = ''; + effect = fiber.firstEffect; + cache = new Set(); + while (effect !== null && !cache.has(effect)) { + let name = fiberName(effect); + if (effect === firstEffect) { + name = 'F(' + name + ')'; + } + if (effect === lastEffect) { + name = 'L(' + name + ')'; + } + buffer += name + ' -> '; + cache.add(effect); + effect = effect.nextEffect; + } + if (cache.has(effect)) { + buffer = '[Circular] !!! : ' + buffer; + } + console.log(`printEffectChain(${label}) firstEffect`, buffer); +} + function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { + didReifySpeculativeWork = true; const originalCurrent = current; invariant( @@ -3112,6 +3175,9 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { } // returning alternate of original current because in some cases // this function is called with no initialWorkInProgress + if (__DEV__ && enableSpeculativeWorkTracing) { + printEffectChain('reified leaf fiber', originalCurrent.alternate); + } return originalCurrent.alternate; } @@ -3135,6 +3201,13 @@ function beginWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { + if (enableSpeculativeWork) { + // this value is read from work loop to determine if we need to swap + // the unitOfWork for it's altnerate when we return null from beginWork + // on each step we set it to false. and only set it to true if reification + // occurs + didReifySpeculativeWork = false; + } if (__DEV__ && enableSpeculativeWorkTracing) { console.log( 'beginWork', @@ -3183,6 +3256,9 @@ function beginWork( ); } workInProgress = reifyWorkInProgress(current, null); + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('reified workInProgress as', fiberName(workInProgress)); + } } if (current !== null) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index a6bae21a02d18..0032d9719943f 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -127,6 +127,7 @@ import { beginWork as originalBeginWork, inSpeculativeWorkMode, endSpeculationWorkIfRootFiber, + didReifySpeculativeWorkDuringThisStep, } from './ReactFiberBeginWork'; import {completeWork} from './ReactFiberCompleteWork'; import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork'; @@ -1310,8 +1311,12 @@ function prepareFreshStack(root, expirationTime) { function handleError(root, thrownValue) { do { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('!!!! handleError with thrownValue', thrownValue); + } try { // Reset module-level state that was set during the render phase. + // @TODO may need to do something about speculativeMode here resetContextDependencies(); resetHooksAfterThrow(); resetCurrentDebugFiberInDEV(); @@ -1492,6 +1497,66 @@ function workLoopSync() { } } +function fiberName(fiber) { + if (fiber == null) return fiber; + let version = fiber.version; + let speculative = inSpeculativeWorkMode() ? 's' : ''; + let back = `-${version}${speculative}`; + let front = ''; + if (fiber.tag === 3) front = 'HostRoot'; + else if (fiber.tag === 6) front = 'HostText'; + else if (fiber.tag === 10) front = 'ContextProvider'; + else if (typeof fiber.type === 'function') front = fiber.type.name; + else front = 'tag' + fiber.tag; + + return front + back; +} + +function printEffectChain(label, fiber) { + return; + let effect = fiber; + let firstEffect = fiber.firstEffect; + let lastEffect = fiber.lastEffect; + + let cache = new Set(); + let buffer = ''; + do { + let name = fiberName(effect); + if (effect === firstEffect) { + name = 'F(' + name + ')'; + } + if (effect === lastEffect) { + name = 'L(' + name + ')'; + } + buffer += name + ' -> '; + cache.add(effect); + effect = effect.nextEffect; + } while (effect !== null && !cache.has(effect)); + if (cache.has(effect)) { + buffer = '[Circular] !!! : ' + buffer; + } + console.log(`printEffectChain(${label}) self`, buffer); + buffer = ''; + effect = fiber.firstEffect; + cache = new Set(); + while (effect !== null && !cache.has(effect)) { + let name = fiberName(effect); + if (effect === firstEffect) { + name = 'F(' + name + ')'; + } + if (effect === lastEffect) { + name = 'L(' + name + ')'; + } + buffer += name + ' -> '; + cache.add(effect); + effect = effect.nextEffect; + } + if (cache.has(effect)) { + buffer = '[Circular] !!! : ' + buffer; + } + console.log(`printEffectChain(${label}) firstEffect`, buffer); +} + /** @noinline */ function workLoopConcurrent() { // Perform work until Scheduler asks us to yield @@ -1506,6 +1571,10 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { // need an additional field on the work in progress. const current = unitOfWork.alternate; + if (__DEV__ && enableSpeculativeWorkTracing) { + printEffectChain('performUnitOfWork', unitOfWork); + } + startWorkTimer(unitOfWork); setCurrentDebugFiberInDEV(unitOfWork); @@ -1518,6 +1587,21 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { next = beginWork(current, unitOfWork, renderExpirationTime); } + if (enableSpeculativeWork) { + if (didReifySpeculativeWorkDuringThisStep()) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + 'beginWork started speculatively but ended up reifying. we need to swap unitOfWork with the altnerate', + ); + } + unitOfWork = unitOfWork.alternate; + } + } + + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('beginWork returned as next work', fiberName(next)); + } + resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { @@ -1544,6 +1628,12 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // Check if the work completed or if something threw. if ((workInProgress.effectTag & Incomplete) === NoEffect) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log( + '**** completeUnitOfWork: fiber completed', + fiberName(workInProgress), + ); + } setCurrentDebugFiberInDEV(workInProgress); let next; if ( @@ -1573,6 +1663,11 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // Do not append effects to parent if workInProgress was speculative workWasSpeculative !== true ) { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('appending effects to return fiber'); + printEffectChain('initial workInProgress', workInProgress); + printEffectChain('initial returnFiber', returnFiber); + } // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the // side-effect order. @@ -1605,8 +1700,17 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { } returnFiber.lastEffect = workInProgress; } + if (__DEV__ && enableSpeculativeWorkTracing) { + printEffectChain('final workInProgress', workInProgress); + printEffectChain('final returnFiber', returnFiber); + } + } else if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('NOT appending effects to return fiber'); } } else { + if (__DEV__ && enableSpeculativeWorkTracing) { + console.log('**** fiber did not complete because something threw'); + } // 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. @@ -1845,10 +1949,15 @@ function commitRootImpl(root, renderPriorityLevel) { if (firstEffect !== null) { if (__DEV__ && enableSpeculativeWorkTracing) { + printEffectChain('firstEffect', firstEffect); let count = 0; let effect = firstEffect; while (effect !== null) { count++; + console.log(`effect ${count}: ${effect.tag}`); + if (effect.nextEffect === effect) { + console.log(`next effect is the same!!!`); + } effect = effect.nextEffect; } console.log(`finished work had ${count} effects`); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 34d854f78cdd6..03729505135e6 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -54,14 +54,18 @@ describe('ReactNewContext', () => { function Consumer(props) { const observedBits = props.unstable_observedBits; let contextValue; - expect(() => { + if (ReactFeatureFlags.enableSpeculativeWork) { contextValue = useContext(Context, observedBits); - }).toErrorDev( - observedBits !== undefined - ? 'useContext() second argument is reserved for future use in React. ' + - `Passing it is not supported. You passed: ${observedBits}.` - : [], - ); + } else { + expect(() => { + contextValue = useContext(Context, observedBits); + }).toErrorDev( + observedBits !== undefined + ? 'useContext() second argument is reserved for future use in React. ' + + `Passing it is not supported. You passed: ${observedBits}.` + : [], + ); + } const render = props.children; return render(contextValue); }, @@ -70,14 +74,18 @@ describe('ReactNewContext', () => { React.forwardRef(function Consumer(props, ref) { const observedBits = props.unstable_observedBits; let contextValue; - expect(() => { + if (ReactFeatureFlags.enableSpeculativeWork) { contextValue = useContext(Context, observedBits); - }).toErrorDev( - observedBits !== undefined - ? 'useContext() second argument is reserved for future use in React. ' + - `Passing it is not supported. You passed: ${observedBits}.` - : [], - ); + } else { + expect(() => { + contextValue = useContext(Context, observedBits); + }).toErrorDev( + observedBits !== undefined + ? 'useContext() second argument is reserved for future use in React. ' + + `Passing it is not supported. You passed: ${observedBits}.` + : [], + ); + } const render = props.children; return render(contextValue); }), @@ -86,14 +94,18 @@ describe('ReactNewContext', () => { React.memo(function Consumer(props) { const observedBits = props.unstable_observedBits; let contextValue; - expect(() => { + if (ReactFeatureFlags.enableSpeculativeWork) { contextValue = useContext(Context, observedBits); - }).toErrorDev( - observedBits !== undefined - ? 'useContext() second argument is reserved for future use in React. ' + - `Passing it is not supported. You passed: ${observedBits}.` - : [], - ); + } else { + expect(() => { + contextValue = useContext(Context, observedBits); + }).toErrorDev( + observedBits !== undefined + ? 'useContext() second argument is reserved for future use in React. ' + + `Passing it is not supported. You passed: ${observedBits}.` + : [], + ); + } const render = props.children; return render(contextValue); }), @@ -1326,6 +1338,11 @@ describe('ReactNewContext', () => { }); describe('readContext', () => { + // @TODO this API is not currently supported when enableSpeculativeWork is true + // this is because with speculative work the fiber itself must hold necessary + // state to determine all the sources that could disallow a bailout + // readContext is not a hook per se and does not leave a path for using the + // same kind of hook bailout logic required by actual hooks it('can read the same context multiple times in the same function', () => { const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => { let result = 0; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1f0fa48c3d6a6..25e7cb4547c5e 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -54,6 +54,21 @@ export function useContext( : '', ); } + if ( + enableSpeculativeWork && + typeof unstable_observedBits === 'number' && + Array.isArray(arguments[2]) + ) { + console.error( + 'useContext() second argument is reserved for future ' + + 'use in React. Passing it is not supported. ' + + 'You passed: %s.%s', + unstable_observedBits, + '\n\nDid you call array.map(useContext)? ' + + 'Calling Hooks inside a loop is not supported. ' + + 'Learn more at https://fb.me/rules-of-hooks', + ); + } // TODO: add a more generic warning for invalid values. if ((Context: any)._context !== undefined) { diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index 5146adb3d66c5..7b9fa5bbeb1ff 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,10 +13,10 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 15000; +const MAX_SOURCE_ITERATIONS = 1500; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. -const MAX_TEST_ITERATIONS = 50000; +const MAX_TEST_ITERATIONS = 4999; module.exports = ({types: t, template}) => { // We set a global so that we can later fail the test From 344972a307719c11058286620afb5e9db86fceab Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 21 Feb 2020 23:57:49 -0800 Subject: [PATCH 10/27] remove tracing --- packages/react-reconciler/src/ReactFiber.js | 6 - .../src/ReactFiberBeginWork.js | 181 +----------------- .../src/ReactFiberCompleteWork.js | 23 --- .../react-reconciler/src/ReactFiberHooks.js | 106 ++++------ .../react-reconciler/src/ReactFiberStack.js | 7 +- .../src/ReactFiberWorkLoop.js | 179 +---------------- packages/shared/ReactFeatureFlags.js | 3 - .../babel/transform-prevent-infinite-loops.js | 2 +- 8 files changed, 44 insertions(+), 463 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index f2387a6692199..a51116219927f 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -271,9 +271,6 @@ function FiberNode( this.type = null; this.stateNode = null; - // @TODO remove this. used for debugging output only - this.version = 0; - // Fiber this.return = null; this.child = null; @@ -426,9 +423,6 @@ export function createWorkInProgress( workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; - // @TODO remove this. used for debugging output only - workInProgress.version = current.version + 1; - if (__DEV__) { // DEV-only fields if (enableUserTimingAPI) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8ceb03e62c4eb..98938e09b0a8b 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -67,7 +67,6 @@ import { enableScopeAPI, enableChunksAPI, enableSpeculativeWork, - enableSpeculativeWorkTracing, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -227,12 +226,6 @@ export function inSpeculativeWorkMode() { export function endSpeculationWorkIfRootFiber(fiber: Fiber) { if (fiber === speculativeWorkRootFiber) { speculativeWorkRootFiber = null; - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - ']]] ending speculation: speculativeWorkRootFiber', - fiberName(speculativeWorkRootFiber), - ); - } } } @@ -654,30 +647,15 @@ function updateFunctionComponent( } if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '???? updateFunctionComponent called with a current fiber for workInProgress', - ); - } if ( canBailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('hooks have not changed, we can bail out of update'); - } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '???? speculative bailout failed: creating a new workInProgress', - ); - } - let parent = workInProgress.return; - // this workInProgress is currently detatched from the actual workInProgress // tree because the previous workInProgress is from the current tree and we // don't yet have a return pointer to any fiber in the workInProgress tree @@ -687,7 +665,7 @@ function updateFunctionComponent( current.pendingProps, NoWork, ); - workInProgress.return = parent; + workInProgress.return = current.return; } let context; @@ -747,11 +725,6 @@ function updateFunctionComponent( } if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'function component is updating so reifying the work in progress tree', - ); - } // because there was an update and we were in speculative work mode we need // to attach the temporary workInProgress we created earlier in this function // to the workInProgress tree @@ -852,13 +825,6 @@ function updateClassComponent( nextProps, renderExpirationTime: ExpirationTime, ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'updateClassComponent wip & current', - fiberName(workInProgress), - fiberName(current), - ); - } if (__DEV__) { if (workInProgress.type !== workInProgress.elementType) { // Lazy component props can't be validated in createElement @@ -968,9 +934,6 @@ function finishClassComponent( markRef(current, workInProgress); const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect; - if (__DEV__ && enableSpeculativeWorkTracing && didCaptureError) { - console.log('___ finishClassComponent didCaptureError!!!'); - } if (!shouldUpdate && !didCaptureError) { // Context providers should defer to sCU for rendering @@ -994,9 +957,6 @@ function finishClassComponent( didCaptureError && typeof Component.getDerivedStateFromError !== 'function' ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('### captued an error but not derivedStateFromError handler'); - } // If we captured an error, but getDerivedStateFromError is not defined, // unmount all the children. componentDidCatch will schedule an update to // re-render a fallback. This is temporary until we migrate everyone to @@ -2742,11 +2702,6 @@ function updateContextProvider( pushProvider(workInProgress, newValue); if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'updateContextProvider: we are in speculative work mode so we can bail out eagerly', - ); - } return bailoutOnAlreadyFinishedWork( current, workInProgress, @@ -3015,50 +2970,6 @@ function remountFiber( } } -function printEffectChain(label, fiber) { - let effect = fiber; - let firstEffect = fiber.firstEffect; - let lastEffect = fiber.lastEffect; - - let cache = new Set(); - let buffer = ''; - do { - let name = fiberName(effect); - if (effect === firstEffect) { - name = 'F(' + name + ')'; - } - if (effect === lastEffect) { - name = 'L(' + name + ')'; - } - buffer += name + ' -> '; - cache.add(effect); - effect = effect.nextEffect; - } while (effect !== null && !cache.has(effect)); - if (cache.has(effect)) { - buffer = '[Circular] !!! : ' + buffer; - } - console.log(`printEffectChain(${label}) self`, buffer); - buffer = ''; - effect = fiber.firstEffect; - cache = new Set(); - while (effect !== null && !cache.has(effect)) { - let name = fiberName(effect); - if (effect === firstEffect) { - name = 'F(' + name + ')'; - } - if (effect === lastEffect) { - name = 'L(' + name + ')'; - } - buffer += name + ' -> '; - cache.add(effect); - effect = effect.nextEffect; - } - if (cache.has(effect)) { - buffer = '[Circular] !!! : ' + buffer; - } - console.log(`printEffectChain(${label}) firstEffect`, buffer); -} - function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { didReifySpeculativeWork = true; const originalCurrent = current; @@ -3083,30 +2994,14 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { // if the parent we're cloning child fibers from is the speculativeWorkRootFiber // unset it so we don't go any higher if (parentWorkInProgress === speculativeWorkRootFiber) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'we have found the speculativeWorkRootFiber during reification. this is the last pass', - ); - } speculativeWorkRootFiber = null; } // clone parent WIP's child. use the current's workInProgress if the currentChild is // the current fiber let currentChild = parentWorkInProgress.child; - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - ')))) reifying', - fiberName(parentWorkInProgress), - 'starting with', - fiberName(currentChild), - ); - } let newChild; if (workInProgress !== null && currentChild === current) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('using workInProgress of', fiberName(workInProgress)); - } newChild = workInProgress; } else { newChild = createWorkInProgress( @@ -3123,21 +3018,7 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { // use current's workInProgress if the sibiling is the current fiber while (currentChild.sibling !== null) { currentChild = currentChild.sibling; - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - ')))) reifying', - fiberName(parentWorkInProgress), - 'now with', - fiberName(currentChild), - ); - } if (workInProgress !== null && currentChild === current) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'using ephemeralWorkInProgress of', - fiberName(workInProgress), - ); - } newChild = newChild.sibling = workInProgress; } else { newChild = newChild.sibling = createWorkInProgress( @@ -3167,35 +3048,11 @@ function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { speculativeWorkRootFiber = null; - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'returning originalCurrent.alternate', - fiberName(originalCurrent.alternate), - ); - } // returning alternate of original current because in some cases // this function is called with no initialWorkInProgress - if (__DEV__ && enableSpeculativeWorkTracing) { - printEffectChain('reified leaf fiber', originalCurrent.alternate); - } return originalCurrent.alternate; } -function fiberName(fiber) { - if (fiber == null) return fiber; - let version = fiber.version; - let speculative = inSpeculativeWorkMode() ? 's' : ''; - let back = `-${version}${speculative}`; - let front = ''; - if (fiber.tag === 3) front = 'HostRoot'; - else if (fiber.tag === 6) front = 'HostText'; - else if (fiber.tag === 10) front = 'ContextProvider'; - else if (typeof fiber.type === 'function') front = fiber.type.name; - else front = 'tag' + fiber.tag; - - return front + back; -} - function beginWork( current: Fiber | null, workInProgress: Fiber, @@ -3208,14 +3065,6 @@ function beginWork( // occurs didReifySpeculativeWork = false; } - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'beginWork', - fiberName(workInProgress) + '->' + fiberName(workInProgress.return), - current && fiberName(current) + '->' + fiberName(current.return), - 'speculativeWorkRootFiber: ' + fiberName(speculativeWorkRootFiber), - ); - } // in speculative work mode we have been passed the current fiber as the workInProgress // if this is the case we need to assign the work to the current because they are the same if (enableSpeculativeWork && inSpeculativeWorkMode()) { @@ -3250,15 +3099,7 @@ function beginWork( workInProgress.tag !== ContextProvider && workInProgress.tag !== FunctionComponent ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'in speculative work mode and workInProgress is something other than a ContextProvider or FunctionComponent, reifying immediately', - ); - } workInProgress = reifyWorkInProgress(current, null); - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('reified workInProgress as', fiberName(workInProgress)); - } } if (current !== null) { @@ -3271,20 +3112,10 @@ function beginWork( // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '--- props different or legacy context changed. lets do update', - ); - } // If props or context changed, mark the fiber as having performed work. // This may be unset if the props are determined to be equal later (memo). didReceiveUpdate = true; } else if (updateExpirationTime < renderExpirationTime) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '--- props same, no legacy context, no work scheduled. lets push onto stack and bailout', - ); - } didReceiveUpdate = false; // This fiber does not have any pending work. Bailout without entering // the begin phase. There's still some bookkeeping we that needs to be done @@ -3458,19 +3289,9 @@ function beginWork( // nor legacy context. Set this to false. If an update queue or context // consumer produces a changed value, it will set this to true. Otherwise, // the component will assume the children have not changed and bail out. - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '--- props same, no legacy context, this fiber has work scheduled so we will see if it produces an update', - ); - } didReceiveUpdate = false; } } else { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '--- current fiber null, likely this was a newly mounted component', - ); - } didReceiveUpdate = false; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 636d29924143f..1138dc57116b9 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -120,7 +120,6 @@ import { enableFundamentalAPI, enableScopeAPI, enableChunksAPI, - enableSpeculativeWorkTracing, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -634,33 +633,11 @@ function cutOffTailIfNeeded( } } -function fiberName(fiber) { - if (fiber == null) return fiber; - let version = fiber.version; - let back = `-${version}`; - let front = ''; - if (fiber.tag === 3) front = 'HostRoot'; - else if (fiber.tag === 6) front = 'HostText'; - else if (fiber.tag === 10) front = 'ContextProvider'; - else if (typeof fiber.type === 'function') front = fiber.type.name; - else front = 'tag' + fiber.tag; - - return front + back; -} - function completeWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'completeWork', - fiberName(workInProgress) + '->' + fiberName(workInProgress.return), - current && fiberName(current) + '->' + fiberName(current.return), - ); - } - const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 018318bb8e43d..1f45f3581ad32 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -26,14 +26,6 @@ import { peekContext, } from './ReactFiberNewContext'; -const readContext = originalReadContext; -const mountContext = enableSpeculativeWork - ? mountContextImpl - : originalReadContext; -const updateContext = enableSpeculativeWork - ? updateContextImpl - : originalReadContext; - import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents'; import { Update as UpdateEffect, @@ -54,10 +46,7 @@ import { markRenderEventTimeAndConfig, markUnprocessedUpdateTime, } from './ReactFiberWorkLoop'; -import { - enableSpeculativeWork, - enableSpeculativeWorkTracing, -} from 'shared/ReactFeatureFlags'; +import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import getComponentName from 'shared/getComponentName'; @@ -73,6 +62,14 @@ import { const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; +const readContext = originalReadContext; +const mountContext = enableSpeculativeWork + ? mountContextImpl + : originalReadContext; +const updateContext = enableSpeculativeWork + ? updateContextImpl + : originalReadContext; + type ObservedBits = void | number | boolean; export type Dispatcher = {| @@ -365,28 +362,17 @@ export function canBailoutSpeculativeWorkWithHooks( current: Fiber, nextRenderExpirationTime: renderExpirationTime, ): boolean { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('====== bailoutSpeculativeWorkWithHooks'); - } let hook = current.memoizedState; while (hook !== null) { + // hooks without a bailout are assumed to permit bailing out + // any hook which can instigate work needs to implement the bailout API if (typeof hook.bailout === 'function') { - let didBailout = hook.bailout(hook, nextRenderExpirationTime); - if (didBailout === false) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '====== bailoutSpeculativeWorkWithHooks returning false', - hook.bailout.name, - ); - } + if (!hook.bailout(hook, nextRenderExpirationTime)) { return false; } } hook = hook.next; } - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('====== bailoutSpeculativeWorkWithHooks returning true'); - } return true; } @@ -667,6 +653,7 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } +// use a Symbol to allow for any value including null and undefined to be memoized const EMPTY = Symbol('empty'); function mountContextImpl( @@ -689,7 +676,8 @@ function mountContextImpl( return selection; } else { // selector is the legacy observedBits api - let contextValue = readContext(context, selector); + let observedBits = selector; + let contextValue = readContext(context, observedBits); hook.memoizedState = { context, contextValue, @@ -722,6 +710,8 @@ function updateContextImpl( let selection = memoizedState.selection; let contextValue = readContext(context); + // when using a selector only recomput if the context value is different or + // the selector is different than the memoized state if ( contextValue !== memoizedState.contextValue || selector !== memoizedState.selector @@ -733,8 +723,9 @@ function updateContextImpl( } return selection; } else { - // selector is actually legacy observedBits - let contextValue = readContext(context, selector); + // selector is actually observedBits + let observedBits = selector; + let contextValue = readContext(context, observedBits); memoizedState.contextValue = contextValue; return contextValue; } @@ -742,44 +733,25 @@ function updateContextImpl( function bailoutContext(hook: Hook): boolean { const memoizedState = hook.memoizedState; - let selector = memoizedState.selector; - let peekedContextValue = peekContext(memoizedState.context); - let previousContextValue = memoizedState.contextValue; + const selector = memoizedState.selector; + const peekedContextValue = peekContext(memoizedState.context); + const previousContextValue = memoizedState.contextValue; + // if this context hook uses a selector we need to check if the selection + // has changed with a new context value if (selector !== null) { if (previousContextValue !== peekedContextValue) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '}}}}} [selector mode] bailoutContext we have different context VALUES', - previousContextValue, - peekedContextValue, - ); - } - let stashedSelection = selector(peekedContextValue); + const stashedSelection = selector(peekedContextValue); if (stashedSelection !== memoizedState.selection) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '}}}}} [selector mode] bailoutContext we have different context SELECTIONS', - memoizedState.selection, - stashedSelection, - ); - } + // stashed selections will get applied to the hook's memoized state as + // part of the render phase of the workInProgress fiber memoizedState.stashedSelection = stashedSelection; memoizedState.stashedContextValue = peekedContextValue; return false; } } - } else { - if (previousContextValue !== peekedContextValue) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '}}}}} [value mode] bailoutContext we have different context values', - previousContextValue, - peekedContextValue, - ); - } - return false; - } + } else if (previousContextValue !== peekedContextValue) { + return false; } return true; } @@ -1008,11 +980,9 @@ function rerenderReducer( return [newState, dispatch]; } +// @TODO this is a completely broken bailout implementation. figure out how +// dispatchAction and updateReducer really work and implement something proper function bailoutReducer(hook, renderExpirationTime): boolean { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('}}}} bailoutReducer'); - } - const queue = hook.queue; // The last rebase update that is NOT part of the base state. @@ -1042,15 +1012,9 @@ function bailoutReducer(hook, renderExpirationTime): boolean { // if newState is different from the current state do not bailout if (newState !== hook.memoizedState) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('found a new state. not bailing out of hooks'); - } return false; } } - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('state is the same so we can maybe bail out'); - } return true; } @@ -1562,9 +1526,6 @@ function dispatchAction( update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('++++++++++++ eagerly bailing out of reducer update'); - } // Fast path. We can bail out without scheduling React to re-render. // It's still possible that we'll need to rebase this update later, // if the component re-renders for a different reason and by that @@ -1587,9 +1548,6 @@ function dispatchAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('++++++++++++ scheduling work for reducer hook'); - } scheduleWork(fiber, expirationTime); } } diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index 0c7ca56e6a981..87d7cb5db3c13 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -8,6 +8,7 @@ */ import type {Fiber} from './ReactFiber'; +import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; export type StackCursor = {|current: T|}; @@ -40,10 +41,12 @@ function pop(cursor: StackCursor, fiber: Fiber): void { } if (__DEV__) { + // when enableSpeculativeWork is activated we need to allow for a fiber's + // alternate to be popped off the stack due to reification of speculative work if ( fiber !== fiberStack[index] && - fiber.altnerate !== null && - fiber.alternate !== fiberStack[index] + (!enableSpeculativeWork || + (fiber.altnerate !== null && fiber.alternate !== fiberStack[index])) ) { console.error('Unexpected Fiber popped.'); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 0032d9719943f..c80b086e600d1 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -29,7 +29,6 @@ import { disableSchedulerTimeoutBasedOnReactExpirationTime, enableTrainModelFix, enableSpeculativeWork, - enableSpeculativeWorkTracing, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -571,9 +570,6 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime { // the next level that the root has work on. This function is called on every // update, and right before exiting a task. function ensureRootIsScheduled(root: FiberRoot) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('ensureRootIsScheduled'); - } const lastExpiredTime = root.lastExpiredTime; if (lastExpiredTime !== NoWork) { // Special case: Expired work should flush synchronously. @@ -657,10 +653,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // event time. The next update will compute a new event time. currentEventTime = NoWork; - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('performConcurrentWorkOnRoot'); - } - if (didTimeout) { // The render task took too long to complete. Mark the current time as // expired to synchronously render all expired work in a single batch. @@ -761,9 +753,6 @@ function finishConcurrentRender( exitStatus, expirationTime, ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('finishConcurrentRender'); - } // Set this to null to indicate there's no in-progress render. workInProgressRoot = null; @@ -792,9 +781,6 @@ function finishConcurrentRender( break; } case RootSuspended: { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('RootSuspended'); - } markRootSuspendedAtTime(root, expirationTime); const lastSuspendedTime = root.lastSuspendedTime; if (expirationTime === lastSuspendedTime) { @@ -870,10 +856,6 @@ function finishConcurrentRender( break; } case RootSuspendedWithDelay: { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('RootSuspendedWithDelay'); - } - markRootSuspendedAtTime(root, expirationTime); const lastSuspendedTime = root.lastSuspendedTime; if (expirationTime === lastSuspendedTime) { @@ -970,10 +952,6 @@ function finishConcurrentRender( break; } case RootCompleted: { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('RootCompleted'); - } - // The work completed. Ready to commit. if ( // do not delay if we're inside an act() scope @@ -1023,10 +1001,6 @@ function performSyncWorkOnRoot(root) { 'Should not already be working.', ); - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('performSyncWorkOnRoot', lastExpiredTime, expirationTime); - } - flushPassiveEffects(); // If the root or expiration time have changed, throw out the existing stack @@ -1060,10 +1034,6 @@ function performSyncWorkOnRoot(root) { popInteractions(((prevInteractions: any): Set)); } - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('workInProgressRootExitStatus', workInProgressRootExitStatus); - } - if (workInProgressRootExitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; stopInterruptedWorkLoopTimer(); @@ -1081,9 +1051,6 @@ function performSyncWorkOnRoot(root) { 'bug in React. Please file an issue.', ); } else { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('finishing sync render'); - } // We now have a consistent tree. Because this is a sync render, we // will commit it even if something suspended. stopFinishedWorkLoopTimer(); @@ -1311,9 +1278,6 @@ function prepareFreshStack(root, expirationTime) { function handleError(root, thrownValue) { do { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('!!!! handleError with thrownValue', thrownValue); - } try { // Reset module-level state that was set during the render phase. // @TODO may need to do something about speculativeMode here @@ -1497,66 +1461,6 @@ function workLoopSync() { } } -function fiberName(fiber) { - if (fiber == null) return fiber; - let version = fiber.version; - let speculative = inSpeculativeWorkMode() ? 's' : ''; - let back = `-${version}${speculative}`; - let front = ''; - if (fiber.tag === 3) front = 'HostRoot'; - else if (fiber.tag === 6) front = 'HostText'; - else if (fiber.tag === 10) front = 'ContextProvider'; - else if (typeof fiber.type === 'function') front = fiber.type.name; - else front = 'tag' + fiber.tag; - - return front + back; -} - -function printEffectChain(label, fiber) { - return; - let effect = fiber; - let firstEffect = fiber.firstEffect; - let lastEffect = fiber.lastEffect; - - let cache = new Set(); - let buffer = ''; - do { - let name = fiberName(effect); - if (effect === firstEffect) { - name = 'F(' + name + ')'; - } - if (effect === lastEffect) { - name = 'L(' + name + ')'; - } - buffer += name + ' -> '; - cache.add(effect); - effect = effect.nextEffect; - } while (effect !== null && !cache.has(effect)); - if (cache.has(effect)) { - buffer = '[Circular] !!! : ' + buffer; - } - console.log(`printEffectChain(${label}) self`, buffer); - buffer = ''; - effect = fiber.firstEffect; - cache = new Set(); - while (effect !== null && !cache.has(effect)) { - let name = fiberName(effect); - if (effect === firstEffect) { - name = 'F(' + name + ')'; - } - if (effect === lastEffect) { - name = 'L(' + name + ')'; - } - buffer += name + ' -> '; - cache.add(effect); - effect = effect.nextEffect; - } - if (cache.has(effect)) { - buffer = '[Circular] !!! : ' + buffer; - } - console.log(`printEffectChain(${label}) firstEffect`, buffer); -} - /** @noinline */ function workLoopConcurrent() { // Perform work until Scheduler asks us to yield @@ -1571,10 +1475,6 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { // need an additional field on the work in progress. const current = unitOfWork.alternate; - if (__DEV__ && enableSpeculativeWorkTracing) { - printEffectChain('performUnitOfWork', unitOfWork); - } - startWorkTimer(unitOfWork); setCurrentDebugFiberInDEV(unitOfWork); @@ -1587,19 +1487,11 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { next = beginWork(current, unitOfWork, renderExpirationTime); } - if (enableSpeculativeWork) { - if (didReifySpeculativeWorkDuringThisStep()) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - 'beginWork started speculatively but ended up reifying. we need to swap unitOfWork with the altnerate', - ); - } - unitOfWork = unitOfWork.alternate; - } - } - - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('beginWork returned as next work', fiberName(next)); + if (enableSpeculativeWork && didReifySpeculativeWorkDuringThisStep()) { + // we need to switch to the alternate becuase the original unitOfWork + // was from the prior commit and the alternate is reified workInProgress + // which we need to send to completeUnitOfWork + unitOfWork = unitOfWork.alternate; } resetCurrentDebugFiberInDEV(); @@ -1628,12 +1520,6 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // Check if the work completed or if something threw. if ((workInProgress.effectTag & Incomplete) === NoEffect) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log( - '**** completeUnitOfWork: fiber completed', - fiberName(workInProgress), - ); - } setCurrentDebugFiberInDEV(workInProgress); let next; if ( @@ -1663,11 +1549,6 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // Do not append effects to parent if workInProgress was speculative workWasSpeculative !== true ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('appending effects to return fiber'); - printEffectChain('initial workInProgress', workInProgress); - printEffectChain('initial returnFiber', returnFiber); - } // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the // side-effect order. @@ -1700,17 +1581,8 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { } returnFiber.lastEffect = workInProgress; } - if (__DEV__ && enableSpeculativeWorkTracing) { - printEffectChain('final workInProgress', workInProgress); - printEffectChain('final returnFiber', returnFiber); - } - } else if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('NOT appending effects to return fiber'); } } else { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('**** fiber did not complete because something threw'); - } // 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. @@ -1846,9 +1718,6 @@ function resetChildExpirationTime(completedWork: Fiber) { } function commitRoot(root) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('committing root'); - } const renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority( ImmediatePriority, @@ -1909,17 +1778,11 @@ function commitRootImpl(root, renderPriorityLevel) { ); if (root === workInProgressRoot) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('root was workInProgressRoot'); - } // We can reset these now that they are finished. workInProgressRoot = null; workInProgress = null; renderExpirationTime = NoWork; } else { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('root was different'); - } // This indicates that the last root we worked on is not the same one that // we're committing now. This most commonly happens when a suspended root // times out. @@ -1928,10 +1791,6 @@ function commitRootImpl(root, renderPriorityLevel) { // Get the list of effects. let firstEffect; if (finishedWork.effectTag > PerformedWork) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('**** work was performed'); - } - // A fiber's effect list consists only of its children, not itself. So if // the root has an effect, we need to add it to the end of the list. The // resulting list is the set that would belong to the root's parent, if it @@ -1948,21 +1807,6 @@ function commitRootImpl(root, renderPriorityLevel) { } if (firstEffect !== null) { - if (__DEV__ && enableSpeculativeWorkTracing) { - printEffectChain('firstEffect', firstEffect); - let count = 0; - let effect = firstEffect; - while (effect !== null) { - count++; - console.log(`effect ${count}: ${effect.tag}`); - if (effect.nextEffect === effect) { - console.log(`next effect is the same!!!`); - } - effect = effect.nextEffect; - } - console.log(`finished work had ${count} effects`); - } - const prevExecutionContext = executionContext; executionContext |= CommitContext; const prevInteractions = pushInteractions(root); @@ -2087,10 +1931,6 @@ function commitRootImpl(root, renderPriorityLevel) { } executionContext = prevExecutionContext; } else { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('**** no effects at all'); - } - // No effects. root.current = finishedWork; // Measure these anyway so the flamegraph explicitly shows that there were @@ -2539,9 +2379,6 @@ function captureCommitPhaseErrorOnRoot( sourceFiber: Fiber, error: mixed, ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('^^^^^ captureCommitPhaseErrorOnRoot', error); - } const errorInfo = createCapturedValue(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, Sync); enqueueUpdate(rootFiber, update); @@ -2553,9 +2390,6 @@ function captureCommitPhaseErrorOnRoot( } export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('^^^^^ captureCommitPhaseError', error); - } if (sourceFiber.tag === HostRoot) { // Error was thrown at the root. There is no parent, so the root // itself should capture it. @@ -2667,9 +2501,6 @@ function retryTimedOutBoundary( boundaryFiber: Fiber, retryTime: ExpirationTime, ) { - if (__DEV__ && enableSpeculativeWorkTracing) { - console.log('^^^^ retryTimedOutBoundary'); - } // The boundary fiber (a Suspense component or SuspenseList component) // previously was rendered in its fallback state. One of the promises that // suspended it has resolved, which means at least part of the tree was diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3fc30df026a3d..201060636e03c 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -133,6 +133,3 @@ export const disableUnstableCreatePortal = false; // Turns on speculative work mode features and context selector api support export const enableSpeculativeWork = true; - -// TEMPORARY turns on tracing of speculative work mode features -export const enableSpeculativeWorkTracing = false; diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index 7b9fa5bbeb1ff..ac8ef57b8a40d 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -16,7 +16,7 @@ const MAX_SOURCE_ITERATIONS = 1500; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. -const MAX_TEST_ITERATIONS = 4999; +const MAX_TEST_ITERATIONS = 5000; module.exports = ({types: t, template}) => { // We set a global so that we can later fail the test From fae3f57001b25fbab97c611a8bf7054734a76a02 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sat, 22 Feb 2020 23:56:15 -0800 Subject: [PATCH 11/27] crude perf measure --- .../__tests__/ReactSpeculativeWork-test.js | 84 ++++++++++++++++++- .../babel/transform-prevent-infinite-loops.js | 4 +- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 8cebf2ba6f241..9f2c41167ccad 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -6,11 +6,15 @@ let ReactCache; let Suspense; let TextResource; +let levels = 8; +let expansion = 3; +let leaves = expansion ** levels; + describe('ReactSpeculativeWork', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSpeculativeWork = false; + ReactFeatureFlags.enableSpeculativeWork = true; ReactFeatureFlags.enableSpeculativeWorkTracing = false; React = require('react'); ReactNoop = require('react-noop-renderer'); @@ -224,4 +228,82 @@ describe('ReactSpeculativeWork', () => { } } }); + + it.only('WARMUP with selector: stresses the createWorkInProgress less', () => { + ReactFeatureFlags.enableSpeculativeWork = false; + runTest('warmup'); + ReactFeatureFlags.enableSpeculativeWork = true; + runTest('warmup'); + runTest('warmup', true); + }); + + it.only('regular: stresses the createWorkInProgress less', () => { + ReactFeatureFlags.enableSpeculativeWork = false; + runTest('regular'); + }); + + it.only('speculative: stresses the createWorkInProgress less', () => { + ReactFeatureFlags.enableSpeculativeWork = true; + runTest('speculative'); + }); + + it.only('speculative with selector: stresses the createWorkInProgress less', () => { + ReactFeatureFlags.enableSpeculativeWork = true; + runTest('selector', true); + }); }); + +function runTest(label, withSelector) { + let Context = React.createContext(0); + let renderCount = 0; + + let selector = withSelector ? c => 1 : undefined; + let Consumer = () => { + let value = React.useContext(Context, selector); + renderCount++; + return Consumer; + }; + + let Expansion = ({level}) => { + if (level > 0) { + return ( + <> + + + + + ); + } else { + return ; + } + }; + + let externalSetValue; + + let App = () => { + let [value, setValue] = React.useState(0); + externalSetValue = setValue; + let child = React.useMemo(() => , [levels]); + return {child}; + }; + + let root = ReactNoop.createRoot(); + + root.render(); + + expect(root).toMatchRenderedOutput(null); + expect(Scheduler).toFlushAndYield([]); + expect(root.getChildren().length).toBe(leaves); + + ReactNoop.act(() => externalSetValue(1)); + expect(Scheduler).toFlushAndYield([]); + expect(root.getChildren().length).toBe(leaves); + + for (let i = 2; i < 30; i++) { + ReactNoop.act(() => externalSetValue(i)); + expect(Scheduler).toFlushAndYield([]); + } + expect(root.getChildren().length).toBe(leaves); + + // console.log(`${label}: renderCount`, renderCount); +} diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index ac8ef57b8a40d..a24ca6bcdf0ce 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,10 +13,10 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 1500; +const MAX_SOURCE_ITERATIONS = 150003; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. -const MAX_TEST_ITERATIONS = 5000; +const MAX_TEST_ITERATIONS = 500003; module.exports = ({types: t, template}) => { // We set a global so that we can later fail the test From 6abfd740189e7c96bf1f6e2b8cf4cb239d39b578 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 23 Feb 2020 22:52:06 -0800 Subject: [PATCH 12/27] fix dependency bug when applying stashed selection in updateContext hook --- packages/react-reconciler/src/ReactFiberHooks.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 1f45f3581ad32..769e217ad608d 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -703,6 +703,8 @@ function updateContextImpl( memoizedState.selection = memoizedState.stashedSelection; memoizedState.stashedContextValue = EMPTY; memoizedState.stashedSelection = EMPTY; + // need to readContext to reset dependency + readContext(context); return memoizedState.selection; } memoizedState.context = context; From ff55c0ab313b32af90ab201601145117c4965f35 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 24 Feb 2020 01:46:52 -0800 Subject: [PATCH 13/27] add experimental nextChildren residue bailout --- packages/react-reconciler/src/ReactFiber.js | 7 +++ .../src/ReactFiberBeginWork.js | 43 ++++++++++++++++--- .../react-reconciler/src/ReactFiberHooks.js | 8 +++- .../__tests__/ReactSpeculativeWork-test.js | 30 ++++++++----- packages/react/src/ReactElement.js | 7 +++ 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index a51116219927f..e60318a772e4a 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -169,6 +169,9 @@ export type Fiber = {| sibling: Fiber | null, index: number, + // identifier of last rendered children + residue: any, + // The ref last used to attach this node. // I'll avoid adding an owner field for prod and model that as functions. ref: @@ -277,6 +280,8 @@ function FiberNode( this.sibling = null; this.index = 0; + this.residue = null; + this.ref = null; this.pendingProps = pendingProps; @@ -465,6 +470,8 @@ export function createWorkInProgress( workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; + workInProgress.residue = current.residue; + // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 98938e09b0a8b..3d7870c42ae59 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -191,6 +191,7 @@ import { const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; let didReceiveUpdate: boolean = false; +let didPushEffect: boolean = false; let didWarnAboutBadClass; let didWarnAboutModulePatternComponent; @@ -239,6 +240,7 @@ export function reconcileChildren( nextChildren: any, renderExpirationTime: ExpirationTime, ) { + workInProgress.residue = (nextChildren && nextChildren.residue) || null; if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, @@ -715,6 +717,31 @@ function updateFunctionComponent( ); } + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + if (current !== null && !didPushEffect) { + // if nextChildren have the same residue as previous children and there + // were not pushed effects which had an effect we can bail out. + // bailing out in this way is actually useful now with speculative work + // since we can avoid large reifications whereas with the old method a bailout + // here is really no better than bailing out in nextChildren's reconciliation + if ( + current && + current.residue === + ((nextChildren && nextChildren.residue) || nextChildren) + ) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + } + // either effects were pushed or nextChildren had a different residue + // we need to reify + reifyWorkInProgress(current, workInProgress); + } + if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderExpirationTime); return bailoutOnAlreadyFinishedWork( @@ -724,13 +751,6 @@ function updateFunctionComponent( ); } - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - // because there was an update and we were in speculative work mode we need - // to attach the temporary workInProgress we created earlier in this function - // to the workInProgress tree - reifyWorkInProgress(current, workInProgress); - } - // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; reconcileChildren( @@ -2844,6 +2864,10 @@ export function markWorkInProgressReceivedUpdate() { didReceiveUpdate = true; } +export function markWorkInProgressPushedEffect() { + didPushEffect = true; +} + function enterSpeculativeWorkMode(workInProgress: Fiber) { invariant( speculativeWorkRootFiber === null, @@ -3064,6 +3088,11 @@ function beginWork( // on each step we set it to false. and only set it to true if reification // occurs didReifySpeculativeWork = false; + + // this value is used to determine if we can bailout when nextChildren is identical + // to memoized children when we are in speculative mode and would prefer to bailout + // rather than reify the workInProgress tree + didPushEffect = false; } // in speculative work mode we have been passed the current fiber as the workInProgress // if this is the case we need to assign the work to the current because they are the same diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 769e217ad608d..811554f95af62 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -51,7 +51,10 @@ import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import getComponentName from 'shared/getComponentName'; import is from 'shared/objectIs'; -import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; +import { + markWorkInProgressReceivedUpdate, + markWorkInProgressPushedEffect, +} from './ReactFiberBeginWork'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; import { UserBlockingPriority, @@ -1059,6 +1062,9 @@ function rerenderState( } function pushEffect(tag, create, destroy, deps) { + if (enableSpeculativeWork && tag & HookHasEffect) { + markWorkInProgressPushedEffect(); + } const effect: Effect = { tag, create, diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 9f2c41167ccad..5b045ca870296 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -6,7 +6,7 @@ let ReactCache; let Suspense; let TextResource; -let levels = 8; +let levels = 6; let expansion = 3; let leaves = expansion ** levels; @@ -257,20 +257,25 @@ function runTest(label, withSelector) { let Context = React.createContext(0); let renderCount = 0; + let span = Consumer; let selector = withSelector ? c => 1 : undefined; let Consumer = () => { let value = React.useContext(Context, selector); + let reduced = Math.floor(value / 2); + // whenever this effect has a HasEffect tag we won't bail out of updates. currently 50% of the time + React.useEffect(() => {}, [reduced]); renderCount++; - return Consumer; + // with residue feature this static element will enable bailouts even if we do a render + return span; }; let Expansion = ({level}) => { if (level > 0) { return ( <> - - - + {new Array(expansion).fill(0).map((_, i) => ( + + ))} ); } else { @@ -289,21 +294,24 @@ function runTest(label, withSelector) { let root = ReactNoop.createRoot(); - root.render(); + ReactNoop.act(() => root.render()); - expect(root).toMatchRenderedOutput(null); expect(Scheduler).toFlushAndYield([]); expect(root.getChildren().length).toBe(leaves); - ReactNoop.act(() => externalSetValue(1)); + ReactNoop.act(() => { + externalSetValue(1); + }); expect(Scheduler).toFlushAndYield([]); expect(root.getChildren().length).toBe(leaves); - for (let i = 2; i < 30; i++) { - ReactNoop.act(() => externalSetValue(i)); + for (let i = 2; i < 10; i++) { + ReactNoop.act(() => { + externalSetValue(i); + }); expect(Scheduler).toFlushAndYield([]); } expect(root.getChildren().length).toBe(leaves); - // console.log(`${label}: renderCount`, renderCount); + console.log(`${label}: renderCount`, renderCount); } diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 9a0f208696d67..451b81271c358 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -6,6 +6,7 @@ */ import getComponentName from 'shared/getComponentName'; +import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; @@ -158,6 +159,12 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { _owner: owner, }; + if (enableSpeculativeWork) { + // allows referential equality checking without having to hang onto old + // elements + element.residue = {}; + } + if (__DEV__) { // The validation flag is currently mutative. We put it on // an external backing store so that we can freeze the whole object. From e3bd3386d5849d6b4b8867f81a423bf56abe20bd Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 3 Mar 2020 15:49:10 -0800 Subject: [PATCH 14/27] support additional fiber types for specualtive work --- packages/react-reconciler/src/ReactFiber.js | 2 + .../src/ReactFiberBeginWork.js | 261 +++++++++++++----- .../src/ReactFiberCompleteWork.js | 13 + .../react-reconciler/src/ReactFiberHooks.js | 8 +- .../src/ReactFiberHostContext.js | 8 +- .../react-reconciler/src/ReactFiberStack.js | 5 +- .../src/ReactFiberWorkLoop.js | 13 +- .../__tests__/ReactSpeculativeWork-test.js | 206 +++++++++++++- 8 files changed, 432 insertions(+), 84 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index e60318a772e4a..b8efea4e46d12 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -372,6 +372,8 @@ const createFiber = function( return new FiberNode(tag, pendingProps, key, mode); }; +export const voidFiber = createFiber(0, null, null, 0); + function shouldConstruct(Component: Function) { const prototype = Component.prototype; return !!(prototype && prototype.isReactComponent); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 3d7870c42ae59..c1df75cc22140 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -86,6 +86,7 @@ import { resolveClassForHotReloading, } from './ReactFiberHotReloading'; +import {voidFiber} from './ReactFiber'; import { mountChildFibers, reconcileChildFibers, @@ -217,7 +218,15 @@ if (__DEV__) { didWarnAboutDefaultPropsOnFunctionComponent = {}; } +// holds the last workInProgress fiber before entering speculative mode +// if we work our way back to this fiber without reifying we leave speculative +// mode. if we reify we also clear this fiber upong reification let speculativeWorkRootFiber: Fiber | null = null; + +// used to tell whether we reified during a particular beginWork execution. This +// is useful to reorient the unitOfWork in the work loop since it may have started +// off as the current (in speculative mode) and needs to become the workInProgress +// if reification happened let didReifySpeculativeWork: boolean = false; export function inSpeculativeWorkMode() { @@ -331,6 +340,28 @@ function updateForwardRef( const render = Component.render; const ref = workInProgress.ref; + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + if ( + canBailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) + ) { + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + // this workInProgress is currently detatched from the actual workInProgress + // tree because the previous workInProgress is from the current tree and we + // don't yet have a return pointer to any fiber in the workInProgress tree + // it will get attached if/when we reify it and end our speculative work + workInProgress = createWorkInProgress( + current, + current.pendingProps, + NoWork, + ); + workInProgress.return = current.return; + } + // The rest is a fork of updateFunctionComponent let nextChildren; prepareToReadContext(workInProgress, renderExpirationTime); @@ -373,6 +404,31 @@ function updateForwardRef( ); } + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + if (current !== null && !didPushEffect) { + // if nextChildren have the same residue as previous children and there + // were not pushed effects which had an effect we can bail out. + // bailing out in this way is actually useful now with speculative work + // since we can avoid large reifications whereas with the old method a bailout + // here is really no better than bailing out in nextChildren's reconciliation + if ( + current && + current.residue === + ((nextChildren && nextChildren.residue) || nextChildren) + ) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + } + // either effects were pushed or nextChildren had a different residue + // we need to reify + reifyWorkInProgress(current, workInProgress); + } + if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderExpirationTime); return bailoutOnAlreadyFinishedWork( @@ -457,6 +513,16 @@ function updateMemoComponent( workInProgress.child = child; return child; } + // If we are in speculative work mode then we've rendered at least once + // and the props could not have changed so we don't need to check them + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + didReceiveUpdate = false; + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } if (__DEV__) { const type = Component.type; const innerPropTypes = type.propTypes; @@ -537,6 +603,21 @@ function updateSimpleMemoComponent( // Inner propTypes will be validated in the function component path. } } + // If we are in speculative work mode then we've rendered at least once + // and the props could not have changed so we don't need to check them and + // can bail out of work if there is no update + if ( + enableSpeculativeWork && + inSpeculativeWorkMode() && + updateExpirationTime < renderExpirationTime + ) { + didReceiveUpdate = false; + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } if (current !== null) { const prevProps = current.memoizedProps; if ( @@ -2868,6 +2949,20 @@ export function markWorkInProgressPushedEffect() { didPushEffect = true; } +function markReturnsForSpeculativeWorkMode(speculativeWorkInProgress: Fiber) { + invariant( + speculativeWorkRootFiber !== null, + 'markReturnsForSpeculativeWorkMode called when we are not yet in that mode. this is likely a bug with React itself', + ); + + // set each child to return to this workInProgress; + let childFiber = speculativeWorkInProgress.child; + while (childFiber !== null) { + childFiber.return = speculativeWorkInProgress; + childFiber = childFiber.sibling; + } +} + function enterSpeculativeWorkMode(workInProgress: Fiber) { invariant( speculativeWorkRootFiber === null, @@ -2919,6 +3014,8 @@ function bailoutOnAlreadyFinishedWork( // work mode if not already in it and continue. if (speculativeWorkRootFiber === null) { enterSpeculativeWorkMode(workInProgress); + } else { + markReturnsForSpeculativeWorkMode(workInProgress); } // this child wasn't cloned so it is from the current tree return workInProgress.child; @@ -2996,85 +3093,104 @@ function remountFiber( function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { didReifySpeculativeWork = true; - const originalCurrent = current; + + if (workInProgress === null) { + // intentionally not returning anywhere yet + workInProgress = createWorkInProgress( + current, + current.pendingProps, + NoWork, + ); + workInProgress.return = null; + } + let originalWorkInProgress = workInProgress; invariant( speculativeWorkRootFiber !== null, 'reifyWorkInProgress was called when we are not in speculative work mode', ); - let parent, parentWorkInProgress; - parentWorkInProgress = parent = current.return; - if (parent !== null && parent !== speculativeWorkRootFiber) { - parentWorkInProgress = createWorkInProgress( - parent, - parent.memoizedProps, - parent.expirationTime, - ); - parentWorkInProgress.return = parent.return; - } - - do { - // if the parent we're cloning child fibers from is the speculativeWorkRootFiber - // unset it so we don't go any higher - if (parentWorkInProgress === speculativeWorkRootFiber) { - speculativeWorkRootFiber = null; - } - - // clone parent WIP's child. use the current's workInProgress if the currentChild is - // the current fiber - let currentChild = parentWorkInProgress.child; - let newChild; - if (workInProgress !== null && currentChild === current) { - newChild = workInProgress; + while (true) { + // fibers in the part of the tree already visited by beginWork need to have + // their expirationTime set to NoWork. fibers we have yet to visit in the reified + // tree need to maintain their expirationTime. This might need to be reworked + // where we don't reify to-be-visited fibers but that requires tracking speculative + // mode on the fiber.mode property + let alreadyCompletedSpeculativeChildFibers = true; + + // when we encounter the reifiedCurrentChild we need to assign the reifiedNewChild rather + // than construct a new workInProgress + let reifiedNewChild = workInProgress; + let reifiedCurrentChild = current; + + // special logic required when we are reifying into the speculativeWorkRootFiber + // this is because current.return will actually be pointing at the last workInProgress + // created before speculative work began whereas in other cases it will be pointing + // to it's parent from the current tree + if (current.return === speculativeWorkRootFiber) { + workInProgress = current.return; + current = current.return = workInProgress.alternate; } else { - newChild = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, + current = current.return; + workInProgress = createWorkInProgress( + current, + current.pendingProps, + NoWork, ); + workInProgress.return = null; } - parentWorkInProgress.child = newChild; - newChild.return = parentWorkInProgress; - - // for each sibling of the parent's workInProgress create a workInProgress. - // use current's workInProgress if the sibiling is the current fiber - while (currentChild.sibling !== null) { - currentChild = currentChild.sibling; - if (workInProgress !== null && currentChild === current) { - newChild = newChild.sibling = workInProgress; + // visit each child of workInProgress and create a newChild + let child = workInProgress.child; + let parent = workInProgress; + let newChild = voidFiber; + while (child !== null) { + if (child === reifiedCurrentChild) { + newChild = newChild.sibling = parent.child = reifiedNewChild; + alreadyCompletedSpeculativeChildFibers = false; } else { - newChild = newChild.sibling = createWorkInProgress( - currentChild, - currentChild.pendingProps, - currentChild.expirationTime, - ); - } - newChild.return = parentWorkInProgress; - } - newChild.sibling = null; - - workInProgress = parentWorkInProgress; - current = parent; - if (speculativeWorkRootFiber !== null) { - parentWorkInProgress = parent = current.return; - if (parent !== null && parent !== speculativeWorkRootFiber) { - parentWorkInProgress = createWorkInProgress( - parent, - parent.pendingProps, - parent.expirationTime, + newChild = newChild.sibling = parent.child = createWorkInProgress( + child, + child.pendingProps, + child.expirationTime, ); - parentWorkInProgress.return = parent.return; + newChild.return = null; + if (alreadyCompletedSpeculativeChildFibers) { + resetWorkInProgressFromCompletedSpeculativFiberExpirationTimes( + newChild, + ); + } } + // parent should only be workInProgress on first pass. use voidFiber + // as dummy for subsequent passes + parent = voidFiber; + newChild.return = workInProgress; + child = child.sibling; + } + voidFiber.child = null; + voidFiber.sibling = null; + + if (workInProgress === speculativeWorkRootFiber) { + speculativeWorkRootFiber = null; + break; } - } while (speculativeWorkRootFiber !== null && parent !== null); + } - speculativeWorkRootFiber = null; + return originalWorkInProgress; +} - // returning alternate of original current because in some cases - // this function is called with no initialWorkInProgress - return originalCurrent.alternate; +function resetWorkInProgressFromCompletedSpeculativFiberExpirationTimes( + reifyingFiber: Fiber, +) { + // when a workInProgress is created from a current fiber after that fiber was completed in + // speculative mode we need to reflect that the work completed on this fiber's subtree is + // complete. that would not be reflected in the expiration times of the current tree and we + // want to avoid mutating them so we can effectively restart work. This is probably not the + // the right implementation however because it is possible there are lower priority expiration + // times in the speculative tree and marking these as NoWork effective hides those updates + // @TODO come back to this + reifyingFiber.expirationTime = NoWork; + reifyingFiber.childExpirationTime = NoWork; } function beginWork( @@ -3126,7 +3242,11 @@ function beginWork( enableSpeculativeWork && inSpeculativeWorkMode() && workInProgress.tag !== ContextProvider && - workInProgress.tag !== FunctionComponent + workInProgress.tag !== FunctionComponent && + workInProgress.tag !== ForwardRef && + workInProgress.tag !== SimpleMemoComponent && + workInProgress.tag !== MemoComponent && + workInProgress.tag > 6 ) { workInProgress = reifyWorkInProgress(current, null); } @@ -3324,6 +3444,19 @@ function beginWork( didReceiveUpdate = false; } + // for now only support speculative work with certain fiber types. + // support for more can and should be added + if ( + enableSpeculativeWork && + inSpeculativeWorkMode() && + workInProgress.tag !== FunctionComponent && + workInProgress.tag !== ForwardRef && + workInProgress.tag !== SimpleMemoComponent && + workInProgress.tag !== MemoComponent + ) { + workInProgress = reifyWorkInProgress(current, null); + } + // @TODO pretty sure we need to not do this if we are in speculativeMode. perhaps make it part of reification? // Before entering the begin phase, clear the expiration time. workInProgress.expirationTime = NoWork; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 1138dc57116b9..7bd30317f8223 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -54,6 +54,7 @@ import { Chunk, } from 'shared/ReactWorkTags'; import {NoMode, BlockingMode} from './ReactTypeOfMode'; +import {NoWork} from './ReactFiberExpirationTime'; import { Ref, Update, @@ -82,6 +83,7 @@ import { cloneFundamentalInstance, shouldUpdateFundamentalComponent, } from './ReactFiberHostConfig'; +import {inSpeculativeWorkMode} from './ReactFiberBeginWork'; import { getRootHostContainer, popHostContext, @@ -120,6 +122,7 @@ import { enableFundamentalAPI, enableScopeAPI, enableChunksAPI, + enableSpeculativeWork, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -638,6 +641,16 @@ function completeWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { + if (enableSpeculativeWork && inSpeculativeWorkMode()) { + // the workInProgress is the current fiber. + current = workInProgress; + // if we completed and we're still in speculative mode that means there + // was either no update on this fiber or we had updates but they bailed + // out and therefore we can safely reset work + // @TODO this should be moved elsewhere and account for render phase work + // as well as lower priority work + workInProgress.expirationTime = NoWork; + } const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 811554f95af62..2effef2a53e1e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -747,11 +747,11 @@ function bailoutContext(hook: Hook): boolean { if (selector !== null) { if (previousContextValue !== peekedContextValue) { const stashedSelection = selector(peekedContextValue); + // stashed selections will get applied to the hook's memoized state as + // part of the render phase of the workInProgress fiber + memoizedState.stashedSelection = stashedSelection; + memoizedState.stashedContextValue = peekedContextValue; if (stashedSelection !== memoizedState.selection) { - // stashed selections will get applied to the hook's memoized state as - // part of the render phase of the workInProgress fiber - memoizedState.stashedSelection = stashedSelection; - memoizedState.stashedContextValue = peekedContextValue; return false; } } diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 6b7eed3dd52db..8da5f6a27b3d1 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -12,6 +12,7 @@ import type {StackCursor} from './ReactFiberStack'; import type {Container, HostContext} from './ReactFiberHostConfig'; import invariant from 'shared/invariant'; +import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import {getChildHostContext, getRootHostContext} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack'; @@ -95,7 +96,12 @@ function pushHostContext(fiber: Fiber): void { function popHostContext(fiber: Fiber): void { // Do not pop unless this Fiber provided the current context. // pushHostContext() only pushes Fibers that provide unique contexts. - if (contextFiberStackCursor.current !== fiber) { + if ( + contextFiberStackCursor.current !== fiber && + (!enableSpeculativeWork || + (contextFiberStackCursor.current.alternate !== null && + contextFiberStackCursor.current.alternate !== fiber)) + ) { return; } diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index 87d7cb5db3c13..0b045fbd673da 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -44,9 +44,10 @@ function pop(cursor: StackCursor, fiber: Fiber): void { // when enableSpeculativeWork is activated we need to allow for a fiber's // alternate to be popped off the stack due to reification of speculative work if ( - fiber !== fiberStack[index] && + fiberStack[index] !== fiber && (!enableSpeculativeWork || - (fiber.altnerate !== null && fiber.alternate !== fiberStack[index])) + (fiberStack[index].alternate !== null && + fiberStack[index].alternate !== fiber)) ) { console.error('Unexpected Fiber popped.'); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c80b086e600d1..c27bce5dbd402 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1495,6 +1495,7 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { } resetCurrentDebugFiberInDEV(); + // may not want to memoize props here when we did not reifyWork unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // If this doesn't spawn new work, complete the current work. @@ -1515,8 +1516,16 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // need an additional field on the work in progress. const current = workInProgress.alternate; const returnFiber = workInProgress.return; - endSpeculationWorkIfRootFiber(workInProgress); - const workWasSpeculative = inSpeculativeWorkMode(); + let workWasSpeculative; + if (enableSpeculativeWork) { + // this whole thing needs to be reworked. I think using mode on the fiber + // makes the most sense. we should clean up the speculative mode of children + // while completing work. this works b/c roots will never be speculative + // this will leave our tree with no speculative mode fibers but will allow + // for more natural logic than what you see below + endSpeculationWorkIfRootFiber(workInProgress); + workWasSpeculative = inSpeculativeWorkMode(); + } // Check if the work completed or if something threw. if ((workInProgress.effectTag & Incomplete) === NoEffect) { diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 5b045ca870296..6e58fd969c5cf 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -6,7 +6,7 @@ let ReactCache; let Suspense; let TextResource; -let levels = 6; +let levels = 7; let expansion = 3; let leaves = expansion ** levels; @@ -251,6 +251,191 @@ describe('ReactSpeculativeWork', () => { ReactFeatureFlags.enableSpeculativeWork = true; runTest('selector', true); }); + + it('breaks', () => { + let App = () => { + return ( +
+ +
+ + +
+
+
+ ); + }; + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + + render() { + return this.props.children; + } + } + + let setOne; + let setTwo; + + let ThingOne = () => { + let [val, setVal] = React.useState(0); + setOne = setVal; + let thing = React.useMemo(() => { + return thing one; + }, [Math.floor(val / 2)]); + return thing; + }; + + let ThingTwo = () => { + let [val, setVal] = React.useState(0); + setTwo = setVal; + let thing = React.useMemo(() => { + return thing two; + }, [Math.floor(val / 3)]); + return thing; + }; + + let root = ReactNoop.createRoot(); + + console.log('-------------------------- root render'); + ReactNoop.act(() => root.render()); + + console.log('-------------------------- first update'); + ReactNoop.act(() => { + setOne(1); + setTwo(1); + }); + + console.log('-------------------------- second update'); + ReactNoop.act(() => { + setOne(2); + setTwo(2); + }); + }); + + it.only('breaks (redux benchmark)', () => { + let Context = React.createContext(0); + + let c = { + TREE_DEPTH: 15, + NUMBER_OF_SLICES: 250, + }; + + let state = {}; + let listeners = new Set(); + + let notify = val => + listeners.forEach(l => { + l(val); + }); + + let store = { + getState: () => state, + updateRandomId: val => + val == null + ? notify(Math.floor(Math.random() * listeners.size)) + : notify(val), + subscribe: l => { + listeners.add(l); + return () => listeners.delete(l); + }, + }; + + let counters = 0; + + const ConnectedCounter = () => { + let store = React.useContext(Context); + let id = React.useRef(null); + React.useEffect(() => { + id.current = counters++; + }, []); + let [counter, setCounter] = React.useState(0); + React.useEffect(() => { + return store.subscribe(v => { + v === id.current && setCounter(c => c + 1); + }); + }); + + return ; + }; + + const Counter = ({value}) => { + return
Value: {value}
; + }; + + class Slice extends React.Component { + state = {}; + + componentDidMount = () => { + //this.props.fillPairs(this.props.idx); + }; + + render() { + const {remainingDepth, idx} = this.props; + + if (remainingDepth > 0) { + return ( +
+ {idx}.{remainingDepth} +
+ +
+
+ ); + } + + return ; + } + } + Slice.displayName = 'Slice'; + + class App extends React.Component { + render() { + return ( +
+ +
+ {this.props.slices.map((slice, idx) => { + return ( +
+ +
+ ); + })} +
+
+ ); + } + } + App.displayName = 'App'; + + ReactNoop.act(() => + ReactNoop.render( + + + idx)} + /> + + , + ), + ); + console.log('--------------------- about to update random id'); + ReactNoop.act(() => store.updateRandomId(1)); + console.log('*******--------------------- about to update random id'); + ReactNoop.act(() => store.updateRandomId(0)); + ReactNoop.act(() => store.updateRandomId(0)); + + expect(true).toBe(true); + }); }); function runTest(label, withSelector) { @@ -259,29 +444,29 @@ function runTest(label, withSelector) { let span = Consumer; let selector = withSelector ? c => 1 : undefined; - let Consumer = () => { + let Consumer = React.forwardRef(() => { let value = React.useContext(Context, selector); let reduced = Math.floor(value / 2); // whenever this effect has a HasEffect tag we won't bail out of updates. currently 50% of the time - React.useEffect(() => {}, [reduced]); + // React.useEffect(() => {}, [reduced]); renderCount++; // with residue feature this static element will enable bailouts even if we do a render return span; - }; + }); - let Expansion = ({level}) => { + let Expansion = React.memo(({level}) => { if (level > 0) { return ( - <> + {new Array(expansion).fill(0).map((_, i) => ( ))} - + ); } else { return ; } - }; + }); let externalSetValue; @@ -297,13 +482,12 @@ function runTest(label, withSelector) { ReactNoop.act(() => root.render()); expect(Scheduler).toFlushAndYield([]); - expect(root.getChildren().length).toBe(leaves); ReactNoop.act(() => { externalSetValue(1); }); expect(Scheduler).toFlushAndYield([]); - expect(root.getChildren().length).toBe(leaves); + // expect(root.getChildren().length).toBe(leaves); for (let i = 2; i < 10; i++) { ReactNoop.act(() => { @@ -311,7 +495,7 @@ function runTest(label, withSelector) { }); expect(Scheduler).toFlushAndYield([]); } - expect(root.getChildren().length).toBe(leaves); + // expect(root.getChildren().length).toBe(leaves); console.log(`${label}: renderCount`, renderCount); } From 26fdebd7c68cd3a4662bb264c5378456951ece5a Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 13 Mar 2020 22:45:39 -0700 Subject: [PATCH 15/27] implement context reader propagation --- packages/react-reconciler/src/ReactFiber.js | 64 +- .../src/ReactFiberCommitWork.js | 8 + .../src/ReactFiberCompleteWork.js | 6 + .../src/ReactFiberNewContext.js | 339 +++++++++- .../__tests__/ReactSpeculativeWork-test.js | 582 ++++++------------ packages/shared/ReactFeatureFlags.js | 3 + 6 files changed, 585 insertions(+), 417 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index b8efea4e46d12..9dc7103fac3d8 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -34,6 +34,7 @@ import { enableUserTimingAPI, enableScopeAPI, enableChunksAPI, + enableContextReaderPropagation, } from 'shared/ReactFeatureFlags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; import {ConcurrentRoot, BlockingRoot} from 'shared/ReactRootTags'; @@ -116,6 +117,9 @@ if (__DEV__) { export type Dependencies = { expirationTime: ExpirationTime, firstContext: ContextDependency | null, + previousFirstContext?: ContextDependency | null, + contextSet?: Set> | null, + cleanupSet?: Set<() => mixed> | null, responders: Map< ReactEventResponder, ReactEventResponderInstance, @@ -477,14 +481,28 @@ export function createWorkInProgress( // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; - workInProgress.dependencies = - currentDependencies === null - ? null - : { - expirationTime: currentDependencies.expirationTime, - firstContext: currentDependencies.firstContext, - responders: currentDependencies.responders, - }; + if (enableContextReaderPropagation) { + workInProgress.dependencies = + currentDependencies === null + ? null + : { + expirationTime: currentDependencies.expirationTime, + firstContext: currentDependencies.firstContext, + previousFirstContext: currentDependencies.firstContext, + contextSet: currentDependencies.contextSet, + cleanupSet: currentDependencies.cleanupSet, + responders: currentDependencies.responders, + }; + } else { + workInProgress.dependencies = + currentDependencies === null + ? null + : { + expirationTime: currentDependencies.expirationTime, + firstContext: currentDependencies.firstContext, + responders: currentDependencies.responders, + }; + } // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; @@ -572,14 +590,28 @@ export function resetWorkInProgress( // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; - workInProgress.dependencies = - currentDependencies === null - ? null - : { - expirationTime: currentDependencies.expirationTime, - firstContext: currentDependencies.firstContext, - responders: currentDependencies.responders, - }; + if (enableContextReaderPropagation) { + workInProgress.dependencies = + currentDependencies === null + ? null + : { + expirationTime: currentDependencies.expirationTime, + firstContext: currentDependencies.firstContext, + previousFirstContext: currentDependencies.firstContext, + contextSet: currentDependencies.contextSet, + cleanupSet: currentDependencies.cleanupSet, + responders: currentDependencies.responders, + }; + } else { + workInProgress.dependencies = + currentDependencies === null + ? null + : { + expirationTime: currentDependencies.expirationTime, + firstContext: currentDependencies.firstContext, + responders: currentDependencies.responders, + }; + } if (enableProfilerTimer) { // Note: We don't reset the actualTime counts. It's useful to accumulate diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 9d9d220f33d52..aeccc6c526520 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -34,6 +34,7 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, + enableContextReaderPropagation, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -77,6 +78,7 @@ import {logCapturedError} from './ReactFiberErrorLogger'; import {resolveDefaultProps} from './ReactFiberLazyComponent'; import {getCommitTime} from './ReactProfilerTimer'; import {commitUpdateQueue} from './ReactUpdateQueue'; +import {cleanupReadersOnUnmount} from './ReactFiberNewContext'; import { getPublicInstance, supportsMutation, @@ -789,6 +791,9 @@ function commitUnmount( case MemoComponent: case SimpleMemoComponent: case Chunk: { + if (enableContextReaderPropagation) { + cleanupReadersOnUnmount(current); + } const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); if (updateQueue !== null) { const lastEffect = updateQueue.lastEffect; @@ -841,6 +846,9 @@ function commitUnmount( return; } case ClassComponent: { + if (enableContextReaderPropagation) { + cleanupReadersOnUnmount(current); + } safelyDetachRef(current); const instance = current.stateNode; if (typeof instance.componentWillUnmount === 'function') { diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 7bd30317f8223..e2ebc20f9bba3 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -123,6 +123,7 @@ import { enableScopeAPI, enableChunksAPI, enableSpeculativeWork, + enableContextReaderPropagation, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -986,6 +987,11 @@ function completeWork( updateHostContainer(workInProgress); return null; case ContextProvider: + if (enableContextReaderPropagation) { + // capture readers and store on memoizedState + workInProgress.memoizedState = + workInProgress.type._context._currentReaders; + } // Pop provider fiber popProvider(workInProgress); return null; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 68663c1875647..80206c8a13119 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -37,7 +37,10 @@ import { } from 'react-reconciler/src/ReactUpdateQueue'; import {NoWork} from './ReactFiberExpirationTime'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; -import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; +import { + enableSuspenseServerRenderer, + enableContextReaderPropagation, +} from 'shared/ReactFeatureFlags'; const valueCursor: StackCursor = createCursor(null); @@ -50,6 +53,12 @@ if (__DEV__) { let currentlyRenderingFiber: Fiber | null = null; let lastContextDependency: ContextDependency | null = null; let lastContextWithAllBitsObserved: ReactContext | null = null; +let lastPreviousContextDependency: ContextDependency | null; +if (enableContextReaderPropagation) { + // this module global tracks the context dependency in the same slot as the + // lastContextDependency from the previously committed render + lastPreviousContextDependency = null; +} let isDisallowedContextReadInDEV: boolean = false; @@ -79,6 +88,12 @@ export function exitDisallowedContextReadInDEV(): void { export function pushProvider(providerFiber: Fiber, nextValue: T): void { const context: ReactContext = providerFiber.type._context; + if (enableContextReaderPropagation) { + // push the previousReaders onto the stack + push(valueCursor, context._currentReaders, providerFiber); + context._currentReaders = providerFiber.memoizedState; + } + if (isPrimaryRenderer) { push(valueCursor, context._currentValue, providerFiber); @@ -127,6 +142,12 @@ export function popProvider(providerFiber: Fiber): void { } else { context._currentValue2 = currentValue; } + + if (enableContextReaderPropagation) { + // pop the previousReaders off the stack and restore + let previousReaders = pop(valueCursor, providerFiber); + context._currentReaders = previousReaders; + } } export function calculateChangedBits( @@ -193,6 +214,16 @@ export function propagateContextChange( changedBits: number, renderExpirationTime: ExpirationTime, ): void { + if (enableContextReaderPropagation) { + // instead of the traditional propagation we are going to use + // readers exclusively to fast path to dependent fibers + return propagateContextChangeToReaders( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); + } let fiber = workInProgress.child; if (fiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. @@ -320,10 +351,17 @@ export function prepareToReadContext( currentlyRenderingFiber = workInProgress; lastContextDependency = null; lastContextWithAllBitsObserved = null; + if (enableContextReaderPropagation) { + lastPreviousContextDependency = null; + } const dependencies = workInProgress.dependencies; if (dependencies !== null) { const firstContext = dependencies.firstContext; + if (enableContextReaderPropagation) { + dependencies.previousFirstContext = firstContext; + lastPreviousContextDependency = dependencies.previousFirstContext; + } if (firstContext !== null) { if (dependencies.expirationTime >= renderExpirationTime) { // Context list has a pending update. Mark that this fiber performed work. @@ -339,6 +377,176 @@ export function peekContext(context: ReactContext): T { return isPrimaryRenderer ? context._currentValue : context._currentValue2; } +function propagateContextChangeToReaders( + workInProgress: Fiber, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, +) { + let state = workInProgress.memoizedState; + if (state === null) { + // this Provider has no readers to propagate to + return; + } else { + let reader = state.firstReader; + while (reader !== null) { + // notify each read of the context change + reader.notify(context, changedBits, renderExpirationTime); + reader = reader.next; + } + } +} + +export function attachReader(contextItem) { + if (enableContextReaderPropagation) { + let context = contextItem.context; + // consider using bind on detachReader (the cleanup function) to avoid having to keep the closure alive + // for now we can just capture the currently rendering fiber for use in notify and cleanup + let readerFiber = currentlyRenderingFiber; + let currentReaders = context._currentReaders; + if (currentReaders == null) { + currentReaders = {firstReader: null}; + context._currentReaders = currentReaders; + } + let initialFirstReader = currentReaders.firstReader; + // readers are a doubly linked list of notify functions. the provide providers with direct access + // to fibers which currently do or have in the past read from this provider to allow avoiding the + // tree walk involved with propagation. This allows the time complexity of propagation to match the + // number of readers rather than the tree size + // it is doubly linked to allow for O(1) insert and removal. we could do singly + // and get O(1) insert and O(n) removal but for providers with many readers this felt more prudent + let reader = { + // @TODO switch to bind over using a closure + notify: (notifyingContext, changedBits, renderExpirationTime) => { + let list = readerFiber.dependencies; + let alternate = readerFiber.alternate; + let alternateList = alternate !== null ? alternate.dependencies : null; + // if the list already has the necessary expriation on it AND the alternate if it exists + // then we can bail out of notification + if ( + list.expirationTime >= renderExpirationTime && + readerFiber.expirationTime >= renderExpirationTime && + (alternate === null || + (alternateList !== null && + alternateList.expirationTime >= renderExpirationTime && + alternate.expriationTime >= renderExpirationTime)) + ) { + return; + } + let dependency = list.firstContext; + while (dependency !== null) { + // Check if the context matches. + if ( + dependency.context === notifyingContext && + (dependency.observedBits & changedBits) !== 0 + ) { + // Match! Schedule an update on this fiber. + + if (readerFiber.tag === ClassComponent) { + // Schedule a force update on the work-in-progress. + const update = createUpdate(renderExpirationTime, null); + 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(readerFiber, update); + } + + if (readerFiber.expirationTime < renderExpirationTime) { + readerFiber.expirationTime = renderExpirationTime; + } + if ( + alternate !== null && + alternate.expirationTime < renderExpirationTime + ) { + alternate.expirationTime = renderExpirationTime; + } + + scheduleWorkOnParentPath(readerFiber.return, renderExpirationTime); + + // Mark the expiration time on the list and if it exists the alternate's list + if (list.expirationTime < renderExpirationTime) { + list.expirationTime = renderExpirationTime; + } + if (alternateList !== null) { + if (alternateList.expirationTime < renderExpirationTime) { + alternateList.expirationTime = renderExpirationTime; + } + } + + // Since we already found a match, we can stop traversing the + // dependency list. + break; + } + dependency = dependency.next; + } + }, + next: null, + prev: null, + }; + if (__DEV__) { + // can be useful in distinguishing readers during debugging + // @TODO remove in the future + reader._tag = Math.random() + .toString(36) + .substring(2, 6); + reader._currentReaders = currentReaders; + } + currentReaders.firstReader = reader; + if (initialFirstReader !== null) { + reader.next = initialFirstReader; + initialFirstReader.prev = reader; + } + // return the cleanup function + // @TODO switch to bind instead of closure capture + return () => { + detachReader(currentReaders, reader); + }; + } +} + +function detachReader(currentReaders, reader) { + if (enableContextReaderPropagation) { + if (currentReaders.firstReader === reader) { + // if we are detaching the first item point our currentReaders + // to the next item first + currentReaders.firstReader = reader.next; + } + if (reader.next !== null) { + reader.next.prev = reader.prev; + } + if (reader.prev !== null) { + reader.prev.next = reader.next; + } + reader.prev = reader.next = null; + } +} + +export function cleanupReadersOnUnmount(fiber: Fiber) { + if (enableContextReaderPropagation) { + let dependencies = fiber.dependencies; + if (dependencies !== null) { + let {cleanupSet, firstContext} = dependencies; + if (cleanupSet !== null) { + // this fiber hosted a complex reader where cleanup functions were stored + // in a set + let iter = cleanupSet.values(); + for (let step = iter.next(); !step.done; step = iter.next()) { + step.value(); + } + } else if (firstContext !== null) { + // this fiber hosted a simple reader list + let contextItem = firstContext; + while (contextItem !== null) { + contextItem.cleanup(); + contextItem = contextItem.next; + } + } + } + } +} + export function readContext( context: ReactContext, observedBits: void | number | boolean, @@ -390,15 +598,134 @@ export function readContext( // This is the first dependency for this component. Create a new list. lastContextDependency = contextItem; - currentlyRenderingFiber.dependencies = { - expirationTime: NoWork, - firstContext: contextItem, - responders: null, - }; + + if (enableContextReaderPropagation) { + let existingContextSet = null; + let existingCleanupSet = null; + let previousFirstContext = null; + + let dependencies = currentlyRenderingFiber.dependencies; + if (dependencies !== null) { + existingContextSet = dependencies.contextSet; + existingCleanupSet = dependencies.cleanupSet; + previousFirstContext = dependencies.previousFirstContext; + } + + currentlyRenderingFiber.dependencies = { + expirationTime: NoWork, + firstContext: contextItem, + previousFirstContext: previousFirstContext, + contextSet: existingContextSet, + cleanupSet: existingCleanupSet, + responders: null, + }; + } else { + currentlyRenderingFiber.dependencies = { + expirationTime: NoWork, + firstContext: contextItem, + responders: null, + }; + } } else { // Append a new context item. lastContextDependency = lastContextDependency.next = contextItem; } + if (enableContextReaderPropagation) { + // there are two methods of tracking context reader fibers. For most fibers that + // read contexts the reads are stable over each render. ClassComponents, ContextConsumer, + // most uses of useContext, etc... However if you use readContext, or you change observedBits + // from render to render, or pass different contexts into useContext it is possible that the + // contexts you read from and their order in the dependency list won't be stable across renders + // + // the main goal is to attach readers during render so that we are included in the set of + // "fibers that need to know about new context values provided but they current provider" + // it is ok if we don't end up depending on that context in the future, it is safe to still + // be notified about future changes which is why this can be done during render phase + // + // cleaning up is trickier. we can only safely do that on umount because any given render could + // be yielded / thrown and not complete and we need to be able to restart without having + // a chance to restore the reader + // + // the algorithm here boils down to + // + // 1. if this is the first render we read from a context, attach readers for every context dependency. This may mean + // we have duplicates (especially in dev mode with useContext since the render function is called) + // twice + // + // 2. if this is not the first render on which we read from a context, check to see if each new + // dependency has the same context as the the dependency in this same slot of the list + // from the last committed render. if so we're in the stable case and can just copy the cleanup function + // over to the new contextItem; no need to call attach again. + // + // 3. instead if we find a conflicting context for this slot in the contextItem list + // we enter advanced tracking mode which creates a context Set and a cleanup Set + // these two sets will hold the maximal set of contexts attached to and cleanup + // functions related to said attachment gathered from the previous list, in addition + // to the current contextItem, attaching if necessary. + // + // 4. instead if we were already in advanced tracking mode we simply see if the current + // contextItem has a novel context and we attach it and store the cleanup function as necessary + // + // Note about memory leaks: + // this implementation does allow for some leakage. in particular if you read from fewer contexts + // on a second render we will 'lose' the initial cleanup functions since we do not activate advancedMode + // in that case. for the time being I'm not tackling that but there are a few ways I can imagine we do + // namely, move the list checking to the prepare step or calling a finalizer to complement the prepare + // step after the render invocation is finished + // additionally, it is technically not necessary to keep dead readers around (the reader for a context) + // no longer read from, but cleaning that up would add more code complexity, possibly lengthen the commit phase + // and leaving them is generally harmless since the nofication won't result in any work being scheduled + // + // @TODO the cleanupSet does not need to be a set, make it an array and simply push to it + if (lastPreviousContextDependency !== null) { + // previous list exists, we can see if we need to enter advanced + let dependencies = currentlyRenderingFiber.dependencies; + let {contextSet, cleanupSet} = dependencies; + if (contextSet !== null) { + // this fiber is already in complex tracking mode. let's attach if needed and add to sets + if (!contextSet.has(context)) { + cleanupSet.add(attachReader(contextItem)); + contextSet.add(context); + } + } else if ( + dependencies.contextSet === null && + lastContextDependency.context !== + lastPreviousContextDependency.context + ) { + // this fiber needs to switch to advanced tracking mode + contextSet = new Set(); + cleanupSet = new Set(); + + // fill the sets with everything from the previous commit + let currentDependency = dependencies.previousFirstContext; + while (currentDependency !== null) { + contextSet.add(currentDependency.context); + cleanupSet.add(currentDependency.cleanup); + currentDependency = currentDependency.next; + } + // attach and add this latest context if necessary + if (!contextSet.has(context)) { + cleanupSet.add(attachReader(contextItem)); + contextSet.add(context); + } + currentlyRenderingFiber.dependencies.contextSet = contextSet; + currentlyRenderingFiber.dependencies.cleanupSet = cleanupSet; + } else { + // in quick mode and context dependency has not changed, copy cleanup over + contextItem.cleanup = lastPreviousContextDependency.cleanup; + } + + // advance the lastPreviousContextDependency pointer in conjunction with each new contextItem. + // if it is null we don't advance it which means if another readContext happens in this pass + // for a different context we will end up entering advanced mode + if (lastPreviousContextDependency.next !== null) { + lastPreviousContextDependency = lastPreviousContextDependency.next; + } + } else { + // lastPreviousContextDependency does not exist so treating it like a mount and attaching readers + contextItem.cleanup = attachReader(contextItem); + } + } } return isPrimaryRenderer ? context._currentValue : context._currentValue2; } diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 6e58fd969c5cf..29715825dc8b9 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -6,9 +6,8 @@ let ReactCache; let Suspense; let TextResource; -let levels = 7; +let levels = 4; let expansion = 3; -let leaves = expansion ** levels; describe('ReactSpeculativeWork', () => { beforeEach(() => { @@ -23,247 +22,18 @@ describe('ReactSpeculativeWork', () => { Suspense = React.Suspense; }); - it('yields text', () => { - const Ctx = React.createContext(1); - - let selectorTest = false; - - let selector = v => { - selectorTest && Scheduler.unstable_yieldValue('selector'); - return Math.floor(v / 2); - }; - - function Text(props) { - let ctx = React.useContext( - Ctx, - ReactFeatureFlags.enableSpeculativeWork ? selector : undefined, - ); - Scheduler.unstable_yieldValue(props.text); - Scheduler.unstable_yieldValue(ctx); - return props.text + (props.plusValue ? ctx : ''); - } - - let triggerState = null; - - function Nothing({children}) { - let [state, setState] = React.useState(0); - // trigger state will result in an identical state. it is used to see - // where how and if the state bailout is working - triggerState = () => setState(s => s); - // Scheduler.unstable_yieldValue('Nothing'); - return children; - } - - let triggerCtx = null; - - function App() { - let [val, setVal] = React.useState(2); - let texts = React.useMemo( - () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - [], - ); - triggerCtx = setVal; - return {texts}; - } - - for (let i = 0; i < 100; i++) { - const root = ReactNoop.createBlockingRoot(); - - root.render(); - - // Nothing should have rendered yet - expect(root).toMatchRenderedOutput(null); - - // Everything should render immediately in the next event - if (ReactFeatureFlags.enableSpeculativeWork) { - // double renders for each component with hooks - expect(Scheduler).toFlushExpired([ - // 'Nothing', - // 'Nothing', - // 'Nothing', - // 'Nothing', - 'A', - 1, - 'A', - 1, - 'B', - 1, - 'B', - 1, - 'C', - 1, - 'C', - 1, - ]); - expect(root).toMatchRenderedOutput('A1B1C'); - } else { - // Text only uses useContext which does not persist a hook in non-spec feature - // this means no double render - // also no selector means we get the actual value - expect(Scheduler).toFlushExpired([ - // 'Nothing', - // 'Nothing', - // 'Nothing', - // 'Nothing', - 'A', - 2, - 'B', - 2, - 'C', - 2, - ]); - expect(root).toMatchRenderedOutput('A2B2C'); - } - - ReactNoop.act(() => triggerCtx(4)); - - if (ReactFeatureFlags.enableSpeculativeWork) { - // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded([ - 'A', - 2, - 'A', - 2, - 'B', - 2, - 'B', - 2, - 'C', - 2, - 'C', - 2, - ]); - expect(root).toMatchRenderedOutput('A2B2C'); - } else { - // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded(['A', 4, 'B', 4, 'C', 4]); - expect(root).toMatchRenderedOutput('A4B4C'); - } - - selectorTest = true; - ReactNoop.act(() => triggerCtx(5)); - selectorTest = false; - // nothing should render (below app) because the value will be the same - - if (ReactFeatureFlags.enableSpeculativeWork) { - expect(Scheduler).toHaveYielded(['selector', 'selector', 'selector']); - expect(root).toMatchRenderedOutput('A2B2C'); - } else { - expect(Scheduler).toHaveYielded(['A', 5, 'B', 5, 'C', 5]); - expect(root).toMatchRenderedOutput('A5B5C'); - } - - ReactNoop.act(() => triggerCtx(6)); - - if (ReactFeatureFlags.enableSpeculativeWork) { - expect(Scheduler).toHaveYielded([ - 'A', - 3, - 'A', - 3, - 'B', - 3, - 'B', - 3, - 'C', - 3, - 'C', - 3, - ]); - expect(root).toMatchRenderedOutput('A3B3C'); - } else { - expect(Scheduler).toHaveYielded(['A', 6, 'B', 6, 'C', 6]); - expect(root).toMatchRenderedOutput('A6B6C'); - } - - ReactNoop.act(() => triggerState()); - - if (ReactFeatureFlags.enableSpeculativeWork) { - // Everything should render immediately in the next event - expect(Scheduler).toHaveYielded([]); - expect(root).toMatchRenderedOutput('A3B3C'); - } else { - expect(Scheduler).toHaveYielded([]); - expect(root).toMatchRenderedOutput('A6B6C'); - } - } - }); - - it.only('WARMUP with selector: stresses the createWorkInProgress less', () => { - ReactFeatureFlags.enableSpeculativeWork = false; - runTest('warmup'); - ReactFeatureFlags.enableSpeculativeWork = true; - runTest('warmup'); - runTest('warmup', true); - }); - - it.only('regular: stresses the createWorkInProgress less', () => { - ReactFeatureFlags.enableSpeculativeWork = false; - runTest('regular'); - }); - - it.only('speculative: stresses the createWorkInProgress less', () => { - ReactFeatureFlags.enableSpeculativeWork = true; - runTest('speculative'); - }); + it('enters advanced context tracking mode when you read from different contexts in different orders', () => { + const ContextProviderContext = React.createContext( + React.createContext('dummy'), + ); - it.only('speculative with selector: stresses the createWorkInProgress less', () => { - ReactFeatureFlags.enableSpeculativeWork = true; - runTest('selector', true); - }); + const NumberContext = React.createContext(0); + const StringContext = React.createContext('zero'); - it('breaks', () => { - let App = () => { - return ( -
- -
- - -
-
-
- ); + let Consumer = () => { + let ContextToUse = React.useContext(ContextProviderContext); + let value = React.useContext(ContextToUse); + return value; }; class Indirection extends React.Component { @@ -276,166 +46,175 @@ describe('ReactSpeculativeWork', () => { } } - let setOne; - let setTwo; - - let ThingOne = () => { - let [val, setVal] = React.useState(0); - setOne = setVal; - let thing = React.useMemo(() => { - return thing one; - }, [Math.floor(val / 2)]); - return thing; - }; - - let ThingTwo = () => { - let [val, setVal] = React.useState(0); - setTwo = setVal; - let thing = React.useMemo(() => { - return thing two; - }, [Math.floor(val / 3)]); - return thing; + let App = ({ContextToUse, numberValue, stringValue, keyValue}) => { + return ( + + + + + + + + + + ); }; let root = ReactNoop.createRoot(); - console.log('-------------------------- root render'); - ReactNoop.act(() => root.render()); + console.log('---------------------- initial render with NumberContext'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('1'); - console.log('-------------------------- first update'); - ReactNoop.act(() => { - setOne(1); - setTwo(1); - }); + console.log('---------------------- remount render with NumberContext'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('1'); - console.log('-------------------------- second update'); - ReactNoop.act(() => { - setOne(2); - setTwo(2); - }); - }); + console.log('---------------------- change numberValue render'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('2'); - it.only('breaks (redux benchmark)', () => { - let Context = React.createContext(0); + console.log('---------------------- switch to StringContext render'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('two'); - let c = { - TREE_DEPTH: 15, - NUMBER_OF_SLICES: 250, - }; + console.log('---------------------- remount on NumberContext render'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('3'); - let state = {}; - let listeners = new Set(); - - let notify = val => - listeners.forEach(l => { - l(val); - }); - - let store = { - getState: () => state, - updateRandomId: val => - val == null - ? notify(Math.floor(Math.random() * listeners.size)) - : notify(val), - subscribe: l => { - listeners.add(l); - return () => listeners.delete(l); - }, - }; + console.log('---------------------- switch to StringContext render'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('three'); - let counters = 0; - - const ConnectedCounter = () => { - let store = React.useContext(Context); - let id = React.useRef(null); - React.useEffect(() => { - id.current = counters++; - }, []); - let [counter, setCounter] = React.useState(0); - React.useEffect(() => { - return store.subscribe(v => { - v === id.current && setCounter(c => c + 1); - }); - }); - - return ; - }; + console.log('---------------------- switch back to NumberContext render'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('3'); - const Counter = ({value}) => { - return
Value: {value}
; - }; + console.log('---------------------- switch back to StringContext render'); + ReactNoop.act(() => + root.render( + , + ), + ); + expect(root).toMatchRenderedOutput('three'); + }); - class Slice extends React.Component { - state = {}; + let warmups = []; + let tests = []; - componentDidMount = () => { - //this.props.fillPairs(this.props.idx); - }; + function warmupAndRunTest(testFn, label) { + warmups.push(() => + it(`warmup(${label})`, () => testFn(`warmup(${label})`)), + ); + tests.push(() => it(label, () => testFn(label))); + } - render() { - const {remainingDepth, idx} = this.props; - - if (remainingDepth > 0) { - return ( -
- {idx}.{remainingDepth} -
- -
-
- ); - } - - return ; - } - } - Slice.displayName = 'Slice'; + warmupAndRunTest(label => { + ReactFeatureFlags.enableContextReaderPropagation = false; + ReactFeatureFlags.enableSpeculativeWork = false; + runTest(label); + }, 'regular(walk)'); - class App extends React.Component { - render() { - return ( -
- -
- {this.props.slices.map((slice, idx) => { - return ( -
- -
- ); - })} -
-
- ); - } - } - App.displayName = 'App'; + warmupAndRunTest(label => { + ReactFeatureFlags.enableContextReaderPropagation = true; + ReactFeatureFlags.enableSpeculativeWork = false; + runTest(label); + }, 'regular(reader)'); - ReactNoop.act(() => - ReactNoop.render( - - - idx)} - /> - - , - ), - ); - console.log('--------------------- about to update random id'); - ReactNoop.act(() => store.updateRandomId(1)); - console.log('*******--------------------- about to update random id'); - ReactNoop.act(() => store.updateRandomId(0)); - ReactNoop.act(() => store.updateRandomId(0)); + warmupAndRunTest(label => { + ReactFeatureFlags.enableContextReaderPropagation = false; + ReactFeatureFlags.enableSpeculativeWork = true; + runTest(label); + }, 'speculative(walk)'); - expect(true).toBe(true); - }); + warmupAndRunTest(label => { + ReactFeatureFlags.enableContextReaderPropagation = true; + ReactFeatureFlags.enableSpeculativeWork = true; + runTest(label); + }, 'speculative(reader)'); + + warmupAndRunTest(label => { + ReactFeatureFlags.enableContextReaderPropagation = false; + ReactFeatureFlags.enableSpeculativeWork = true; + runTest(label, true); + }, 'speculativeSelector(walk)'); + + warmupAndRunTest(label => { + ReactFeatureFlags.enableContextReaderPropagation = true; + ReactFeatureFlags.enableSpeculativeWork = true; + runTest(label, true); + }, 'speculativeSelector(reader)'); + + warmups.forEach(t => t()); + tests.forEach(t => t()); }); function runTest(label, withSelector) { @@ -443,12 +222,12 @@ function runTest(label, withSelector) { let renderCount = 0; let span = Consumer; - let selector = withSelector ? c => 1 : undefined; + let selector = withSelector ? value => 1 : undefined; let Consumer = React.forwardRef(() => { let value = React.useContext(Context, selector); - let reduced = Math.floor(value / 2); - // whenever this effect has a HasEffect tag we won't bail out of updates. currently 50% of the time - // React.useEffect(() => {}, [reduced]); + let reduced = withSelector ? Math.floor(value / 3) : value; + // whenever this effect has a HasEffect tag we won't bail out of updates. currently 33% of the time + React.useEffect(() => {}, [reduced]); renderCount++; // with residue feature this static element will enable bailouts even if we do a render return span; @@ -464,10 +243,29 @@ function runTest(label, withSelector) { ); } else { - return ; + return ( + <> + + + + ); } }); + let ExtraNodes = ({level}) => { + if (level > 0) { + return ( + + {new Array(expansion).fill(0).map((_, i) => ( + + ))} + + ); + } else { + return 'extra-leaf'; + } + }; + let externalSetValue; let App = () => { @@ -481,21 +279,15 @@ function runTest(label, withSelector) { ReactNoop.act(() => root.render()); - expect(Scheduler).toFlushAndYield([]); - ReactNoop.act(() => { externalSetValue(1); }); - expect(Scheduler).toFlushAndYield([]); - // expect(root.getChildren().length).toBe(leaves); for (let i = 2; i < 10; i++) { ReactNoop.act(() => { externalSetValue(i); }); - expect(Scheduler).toFlushAndYield([]); } - // expect(root.getChildren().length).toBe(leaves); console.log(`${label}: renderCount`, renderCount); } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 201060636e03c..4b47440c66650 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -133,3 +133,6 @@ export const disableUnstableCreatePortal = false; // Turns on speculative work mode features and context selector api support export const enableSpeculativeWork = true; + +// Turns on context reader propagation +export const enableContextReaderPropagation = true; From 585c539e8b1451c2e6a475797673e9e699ad4cda Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sat, 14 Mar 2020 23:36:01 -0700 Subject: [PATCH 16/27] in progress implementation of reifyNextWork as alternative to speculative mode --- .../src/ReactFiberBeginWork.js | 205 ++++++++++++++++++ .../react-reconciler/src/ReactFiberHooks.js | 11 +- .../src/ReactFiberWorkLoop.js | 7 + .../react-reconciler/src/ReactTypeOfMode.js | 11 +- .../__tests__/ReactSpeculativeWork-test.js | 60 ++++- packages/shared/ReactFeatureFlags.js | 8 +- 6 files changed, 286 insertions(+), 16 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c1df75cc22140..bcec237bd8ac7 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -67,6 +67,7 @@ import { enableScopeAPI, enableChunksAPI, enableSpeculativeWork, + enableReifyNextWork, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -109,6 +110,7 @@ import { ProfileMode, StrictMode, BlockingMode, + ReifiedWorkMode, } from './ReactTypeOfMode'; import { shouldSetTextContent, @@ -2979,6 +2981,183 @@ function enterSpeculativeWorkMode(workInProgress: Fiber) { } } +function fiberName(fiber) { + if (fiber == null) return fiber; + let front = ''; + let back = ''; + if (fiber.mode & ReifiedWorkMode) back = 'r'; + if (fiber.tag === 3) front = 'HostRoot'; + else if (fiber.tag === 6) front = 'HostText'; + else if (fiber.tag === 10) front = 'ContextProvider'; + else if (typeof fiber.type === 'function') front = fiber.type.name; + else front = 'tag' + fiber.tag; + if (back) { + back = `-(${back})`; + } + return `${front}${back}`; +} + +function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { + console.log(`reifyNextWork(${fiberName(workInProgress)})`); + + let loops = 0; + + let fiber = workInProgress.child; + if (fiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + fiber.return = workInProgress; + } + + try { + while (fiber !== null) { + let nextFiber; + + if (loops++ > 100) { + console.log('BREAKING LOOP'); + break; + } + + console.log( + `_______ see IF fiber(${fiberName(fiber)}) has work to reify`, + ); + + if (fiber.mode & ReifiedWorkMode) { + console.log( + `_______ fiber(${fiberName( + fiber, + )}) has already been reified, don't go deeper`, + ); + nextFiber = null; + } else { + fiber.mode |= ReifiedWorkMode; + + if (fiber.expirationTime >= renderExpirationTime) { + console.log( + `_______ fiber(${fiberName( + fiber, + )}) DOES HAVE work to reify, reifying`, + ); + let didBailout; + switch (fiber.tag) { + case FunctionComponent: { + didBailout = canBailoutSpeculativeWorkWithHooks( + fiber, + renderExpirationTime, + ); + break; + } + default: { + // in unsupported cases we cannot bail out of work and we + // consider the speculative work reified + didBailout = false; + } + } + if (didBailout) { + console.log( + `_______ fiber(${fiberName( + fiber, + )}) DOES NOT HAVE REIFIED work, go deeper`, + ); + fiber.expirationTime = NoWork; + nextFiber = fiber.child; + } else { + console.log( + `_______ fiber(${fiberName( + fiber, + )}) HAS REIFIED work, don't go deeper`, + ); + nextFiber = null; + } + } else if (fiber.childExpirationTime >= renderExpirationTime) { + console.log( + `_______ fiber(${fiberName( + fiber, + )}) has children with work to reify, go deeper`, + ); + nextFiber = fiber.child; + } else { + console.log( + `_______ there is NO WORK deeper than fiber(${fiberName( + fiber, + )}), don't go deeper`, + ); + nextFiber = null; + } + } + + if (nextFiber !== null) { + nextFiber.return = fiber; + } else { + console.log( + `_______ looking for siblingsOf(${fiberName(fiber)} and ancestors)`, + ); + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + console.log( + `_______ ______ we're back to the beginning, finish up`, + ); + nextFiber = null; + break; + } + let sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + console.log( + `_______ ______ we've found sibling(${fiberName( + sibling, + )}) of fiber(${fiberName( + nextFiber, + )}), check it for reifying work`, + ); + sibling.return = nextFiber.return; + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + console.log( + `_______ ______ there are no siblings to return back to return(${fiberName( + nextFiber.return, + )}) and reset expirationTimes`, + ); + nextFiber = nextFiber.return; + resetChildExpirationTime(nextFiber); + } + } + fiber = nextFiber; + } + } catch (e) { + console.log(e); + } +} + +function resetChildExpirationTime(fiber: Fiber) { + let newChildExpirationTime = NoWork; + + let child = fiber.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if (childUpdateExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childUpdateExpirationTime; + } + if (childChildExpirationTime > newChildExpirationTime) { + newChildExpirationTime = childChildExpirationTime; + } + child = child.sibling; + } + + console.log( + `_______ _____ the new child expriation time for fiber(${fiberName( + fiber, + )}) is ${newChildExpirationTime}`, + ); + + fiber.childExpirationTime = newChildExpirationTime; +} + function bailoutOnAlreadyFinishedWork( current: Fiber | null, workInProgress: Fiber, @@ -3001,12 +3180,36 @@ function bailoutOnAlreadyFinishedWork( markUnprocessedUpdateTime(updateExpirationTime); } + if (enableReifyNextWork) { + if (workInProgress.childExpirationTime >= renderExpirationTime) { + if (workInProgress.mode & ReifiedWorkMode) { + console.log( + `bailoutOnAlreadyFinishedWork(${fiberName( + workInProgress, + )}) children have unprocessed work but we have already reified it so we do not need to do that again`, + ); + } else { + console.log( + `bailoutOnAlreadyFinishedWork(${fiberName( + workInProgress, + )}) children has unprocessed updates and was not yet reified`, + ); + reifyNextWork(workInProgress, renderExpirationTime); + } + } + } + // Check if the children have any pending work. const childExpirationTime = workInProgress.childExpirationTime; if (childExpirationTime < renderExpirationTime) { // The children don't have any work either. We can skip them. // TODO: Once we add back resuming, we should check if the children are // a work-in-progress set. If so, we need to transfer their effects. + console.log( + `bailoutOnAlreadyFinishedWork(${fiberName( + workInProgress, + )}) there is no child work to do so not going any deeper with work`, + ); return null; } else { if (enableSpeculativeWork) { @@ -3092,6 +3295,7 @@ function remountFiber( } function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { + console.log('(((((((((((( reifyWorkInProgress ))))))))))))'); didReifySpeculativeWork = true; if (workInProgress === null) { @@ -3198,6 +3402,7 @@ function beginWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { + console.log(`beginWork(${fiberName(workInProgress)})`); if (enableSpeculativeWork) { // this value is read from work loop to determine if we need to swap // the unitOfWork for it's altnerate when we return null from beginWork diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 2effef2a53e1e..e1e9525290277 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -46,7 +46,11 @@ import { markRenderEventTimeAndConfig, markUnprocessedUpdateTime, } from './ReactFiberWorkLoop'; -import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; +import { + enableSpeculativeWork, + enableContextSelectors, + enableReifyNextWork, +} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import getComponentName from 'shared/getComponentName'; @@ -66,10 +70,10 @@ import { const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; const readContext = originalReadContext; -const mountContext = enableSpeculativeWork +const mountContext = enableContextSelectors ? mountContextImpl : originalReadContext; -const updateContext = enableSpeculativeWork +const updateContext = enableContextSelectors ? updateContextImpl : originalReadContext; @@ -1511,6 +1515,7 @@ function dispatchAction( } else { if ( !enableSpeculativeWork && + !enableReifyNextWork && fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) ) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c27bce5dbd402..1812387326cb8 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -79,6 +79,7 @@ import { ProfileMode, BlockingMode, ConcurrentMode, + ReifiedWorkMode, } from './ReactTypeOfMode'; import { HostRoot, @@ -461,10 +462,12 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { // Update the source fiber's expiration time if (fiber.expirationTime < expirationTime) { fiber.expirationTime = expirationTime; + fiber.mode &= !ReifiedWorkMode; } let alternate = fiber.alternate; if (alternate !== null && alternate.expirationTime < expirationTime) { alternate.expirationTime = expirationTime; + alternate.mode &= !ReifiedWorkMode; } // Walk the parent path to the root and update the child expiration time. let node = fiber.return; @@ -476,17 +479,20 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { alternate = node.alternate; if (node.childExpirationTime < expirationTime) { node.childExpirationTime = expirationTime; + node.mode &= !ReifiedWorkMode; if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; + alternate.mode &= !ReifiedWorkMode; } } else if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; + alternate.mode &= !ReifiedWorkMode; } if (node.return === null && node.tag === HostRoot) { root = node.stateNode; @@ -1237,6 +1243,7 @@ export function flushControlled(fn: () => mixed): void { } function prepareFreshStack(root, expirationTime) { + console.log('------------------------------------ prepareFreshStack'); root.finishedWork = null; root.finishedExpirationTime = NoWork; diff --git a/packages/react-reconciler/src/ReactTypeOfMode.js b/packages/react-reconciler/src/ReactTypeOfMode.js index 4f053e5bba474..c253dea6d653e 100644 --- a/packages/react-reconciler/src/ReactTypeOfMode.js +++ b/packages/react-reconciler/src/ReactTypeOfMode.js @@ -9,10 +9,11 @@ export type TypeOfMode = number; -export const NoMode = 0b0000; -export const StrictMode = 0b0001; +export const NoMode = 0b00000; +export const StrictMode = 0b00001; // TODO: Remove BlockingMode and ConcurrentMode by reading from the root // tag instead -export const BlockingMode = 0b0010; -export const ConcurrentMode = 0b0100; -export const ProfileMode = 0b1000; +export const BlockingMode = 0b00010; +export const ConcurrentMode = 0b00100; +export const ProfileMode = 0b01000; +export const ReifiedWorkMode = 0b10000; diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 29715825dc8b9..110f6794563d4 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -2,9 +2,6 @@ let React; let ReactFeatureFlags; let ReactNoop; let Scheduler; -let ReactCache; -let Suspense; -let TextResource; let levels = 4; let expansion = 3; @@ -13,13 +10,62 @@ describe('ReactSpeculativeWork', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSpeculativeWork = true; - ReactFeatureFlags.enableSpeculativeWorkTracing = false; React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); - ReactCache = require('react-cache'); - Suspense = React.Suspense; + }); + + it.only('exercises reifyNextWork', () => { + let externalSetValue = () => {}; + + let App = () => { + return ( + + + + + + + + + + + + + + + ); + }; + + let Intermediate = ({children}) => children || null; + let BeforeUpdatingLeafBranch = ({children}) => children || null; + let AfterUpdatingLeafBranch = ({children}) => children || null; + let Leaf = () => null; + + let UpdatingLeaf = () => { + let [value, setValue] = React.useState('leaf'); + Scheduler.unstable_yieldValue(value); + externalSetValue = setValue; + return value; + }; + + let root = ReactNoop.createRoot(); + + ReactNoop.act(() => root.render()); + expect(Scheduler).toHaveYielded(['leaf']); + expect(root).toMatchRenderedOutput('leaf'); + + ReactNoop.act(() => externalSetValue('leaf')); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('leaf'); + + ReactNoop.act(() => externalSetValue('bar')); + expect(Scheduler).toHaveYielded(['bar']); + expect(root).toMatchRenderedOutput('bar'); + + ReactNoop.act(() => externalSetValue('baz')); + expect(Scheduler).toHaveYielded(['baz']); + expect(root).toMatchRenderedOutput('baz'); }); it('enters advanced context tracking mode when you read from different contexts in different orders', () => { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 4b47440c66650..25ae0383b6143 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -132,7 +132,13 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; // Turns on speculative work mode features and context selector api support -export const enableSpeculativeWork = true; +export const enableSpeculativeWork = false; + +// Turns on speculative work mode features and context selector api support +export const enableContextSelectors = true; // Turns on context reader propagation export const enableContextReaderPropagation = true; + +// Turns on a work reification implementation +export const enableReifyNextWork = true; From b59c99b36539b4287d09476163b644f6d1f68d35 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 15 Mar 2020 00:30:09 -0700 Subject: [PATCH 17/27] support contexts in reifyingNextWork --- .../src/ReactFiberBeginWork.js | 23 +++++- .../src/ReactFiberNewContext.js | 28 +++++++ .../src/ReactFiberWorkLoop.js | 21 +++-- .../__tests__/ReactSpeculativeWork-test.js | 78 ++++++++++++++----- packages/react/src/ReactHooks.js | 6 +- 5 files changed, 127 insertions(+), 29 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index bcec237bd8ac7..85630d219d71e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -135,6 +135,7 @@ import { import {findFirstSuspended} from './ReactFiberSuspenseComponent'; import { pushProvider, + popProvider, propagateContextChange, readContext, prepareToReadContext, @@ -3085,6 +3086,16 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { } } + if (fiber.tag === ContextProvider) { + console.log( + `_______ fiber(${fiberName( + fiber, + )}) is a ContextProvider, pushing its value onto stack`, + ); + const newValue = fiber.memoizedProps.value; + pushProvider(fiber, newValue); + } + if (nextFiber !== null) { nextFiber.return = fiber; } else { @@ -3102,6 +3113,14 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { nextFiber = null; break; } + if (nextFiber.tag === ContextProvider) { + console.log( + `_______ ______ the return fiber return(${fiberName( + nextFiber, + )}) is a ContextProvider, pop its value off the stack`, + ); + popProvider(nextFiber); + } let sibling = nextFiber.sibling; if (sibling !== null) { // Set the return pointer of the sibling to the work-in-progress fiber. @@ -3117,12 +3136,12 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { break; } // No more siblings. Traverse up. + nextFiber = nextFiber.return; console.log( `_______ ______ there are no siblings to return back to return(${fiberName( - nextFiber.return, + nextFiber, )}) and reset expirationTimes`, ); - nextFiber = nextFiber.return; resetChildExpirationTime(nextFiber); } } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 80206c8a13119..26b3a30fd2243 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -11,6 +11,7 @@ import type {ReactContext} from 'shared/ReactTypes'; import type {Fiber} from './ReactFiber'; import type {StackCursor} from './ReactFiberStack'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import {ReifiedWorkMode} from './ReactTypeOfMode'; export type ContextDependency = { context: ReactContext, @@ -40,6 +41,7 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; import { enableSuspenseServerRenderer, enableContextReaderPropagation, + enableReifyNextWork, } from 'shared/ReactFeatureFlags'; const valueCursor: StackCursor = createCursor(null); @@ -85,9 +87,13 @@ export function exitDisallowedContextReadInDEV(): void { } } +let pushedCount = 0; + export function pushProvider(providerFiber: Fiber, nextValue: T): void { const context: ReactContext = providerFiber.type._context; + console.log(`<====PUSH========= ${++pushedCount} pushed providers`); + if (enableContextReaderPropagation) { // push the previousReaders onto the stack push(valueCursor, context._currentReaders, providerFiber); @@ -133,6 +139,7 @@ export function pushProvider(providerFiber: Fiber, nextValue: T): void { export function popProvider(providerFiber: Fiber): void { const currentValue = valueCursor.current; + console.log(`======POP========> ${--pushedCount} pushed providers`); pop(valueCursor, providerFiber); @@ -188,17 +195,26 @@ export function scheduleWorkOnParentPath( let alternate = node.alternate; if (node.childExpirationTime < renderExpirationTime) { node.childExpirationTime = renderExpirationTime; + if (enableReifyNextWork) { + node.mode &= !ReifiedWorkMode; + } if ( alternate !== null && alternate.childExpirationTime < renderExpirationTime ) { alternate.childExpirationTime = renderExpirationTime; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } } else if ( alternate !== null && alternate.childExpirationTime < renderExpirationTime ) { alternate.childExpirationTime = renderExpirationTime; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } else { // Neither alternate was updated, which means the rest of the // ancestor path already has sufficient priority. @@ -259,6 +275,9 @@ export function propagateContextChange( if (fiber.expirationTime < renderExpirationTime) { fiber.expirationTime = renderExpirationTime; + if (enableReifyNextWork) { + fiber.mode &= !ReifiedWorkMode; + } } let alternate = fiber.alternate; if ( @@ -266,6 +285,9 @@ export function propagateContextChange( alternate.expirationTime < renderExpirationTime ) { alternate.expirationTime = renderExpirationTime; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } scheduleWorkOnParentPath(fiber.return, renderExpirationTime); @@ -455,12 +477,18 @@ export function attachReader(contextItem) { if (readerFiber.expirationTime < renderExpirationTime) { readerFiber.expirationTime = renderExpirationTime; + if (enableReifyNextWork) { + readerFiber.mode &= !ReifiedWorkMode; + } } if ( alternate !== null && alternate.expirationTime < renderExpirationTime ) { alternate.expirationTime = renderExpirationTime; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } scheduleWorkOnParentPath(readerFiber.return, renderExpirationTime); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 1812387326cb8..d538e38abe510 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -29,6 +29,7 @@ import { disableSchedulerTimeoutBasedOnReactExpirationTime, enableTrainModelFix, enableSpeculativeWork, + enableReifyNextWork, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -462,12 +463,16 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { // Update the source fiber's expiration time if (fiber.expirationTime < expirationTime) { fiber.expirationTime = expirationTime; - fiber.mode &= !ReifiedWorkMode; + if (enableReifyNextWork) { + fiber.mode &= !ReifiedWorkMode; + } } let alternate = fiber.alternate; if (alternate !== null && alternate.expirationTime < expirationTime) { alternate.expirationTime = expirationTime; - alternate.mode &= !ReifiedWorkMode; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } // Walk the parent path to the root and update the child expiration time. let node = fiber.return; @@ -479,20 +484,26 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { alternate = node.alternate; if (node.childExpirationTime < expirationTime) { node.childExpirationTime = expirationTime; - node.mode &= !ReifiedWorkMode; + if (enableReifyNextWork) { + node.mode &= !ReifiedWorkMode; + } if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; - alternate.mode &= !ReifiedWorkMode; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } } else if ( alternate !== null && alternate.childExpirationTime < expirationTime ) { alternate.childExpirationTime = expirationTime; - alternate.mode &= !ReifiedWorkMode; + if (enableReifyNextWork) { + alternate.mode &= !ReifiedWorkMode; + } } if (node.return === null && node.tag === HostRoot) { root = node.stateNode; diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 110f6794563d4..11d4e1611674d 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -16,56 +16,96 @@ describe('ReactSpeculativeWork', () => { }); it.only('exercises reifyNextWork', () => { - let externalSetValue = () => {}; + let externalSetValue; + let externalSetMyContextValue; let App = () => { + let ctxVal = React.useContext(MyContext); + let [value, setMyContextValue] = React.useState(ctxVal); + externalSetMyContextValue = setMyContextValue; + return ( - - - - - + + - + + + + + + + + + + + + - - - - - - + + ); }; + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + + render() { + return this.props.children; + } + } + let Intermediate = ({children}) => children || null; let BeforeUpdatingLeafBranch = ({children}) => children || null; let AfterUpdatingLeafBranch = ({children}) => children || null; let Leaf = () => null; + let MyContext = React.createContext(0); + let UpdatingLeaf = () => { let [value, setValue] = React.useState('leaf'); + let isEven = React.useContext(MyContext, v => v % 2 === 0); Scheduler.unstable_yieldValue(value); externalSetValue = setValue; - return value; + return `${value}-${isEven ? 'even' : 'odd'}`; }; let root = ReactNoop.createRoot(); ReactNoop.act(() => root.render()); expect(Scheduler).toHaveYielded(['leaf']); - expect(root).toMatchRenderedOutput('leaf'); + expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetValue('leaf')); expect(Scheduler).toHaveYielded([]); - expect(root).toMatchRenderedOutput('leaf'); + expect(root).toMatchRenderedOutput('leaf-even'); + + ReactNoop.act(() => externalSetMyContextValue(2)); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('leaf-even'); - ReactNoop.act(() => externalSetValue('bar')); + ReactNoop.act(() => { + externalSetValue('leaf'); + externalSetMyContextValue(4); + }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput('leaf-even'); + + ReactNoop.act(() => externalSetMyContextValue(5)); + expect(Scheduler).toHaveYielded(['leaf']); + expect(root).toMatchRenderedOutput('leaf-odd'); + + ReactNoop.act(() => { + externalSetValue('bar'); + externalSetMyContextValue(4); + }); expect(Scheduler).toHaveYielded(['bar']); - expect(root).toMatchRenderedOutput('bar'); + expect(root).toMatchRenderedOutput('bar-even'); ReactNoop.act(() => externalSetValue('baz')); expect(Scheduler).toHaveYielded(['baz']); - expect(root).toMatchRenderedOutput('baz'); + expect(root).toMatchRenderedOutput('baz-even'); }); it('enters advanced context tracking mode when you read from different contexts in different orders', () => { diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 25e7cb4547c5e..f49cc72071f07 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -12,7 +12,7 @@ import type { ReactEventResponder, ReactEventResponderListener, } from 'shared/ReactTypes'; -import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; +import {enableContextSelectors} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols'; @@ -41,7 +41,7 @@ export function useContext( ) { const dispatcher = resolveDispatcher(); if (__DEV__) { - if (!enableSpeculativeWork && unstable_observedBits !== undefined) { + if (!enableContextSelectors && unstable_observedBits !== undefined) { console.error( 'useContext() second argument is reserved for future ' + 'use in React. Passing it is not supported. ' + @@ -55,7 +55,7 @@ export function useContext( ); } if ( - enableSpeculativeWork && + enableContextSelectors && typeof unstable_observedBits === 'number' && Array.isArray(arguments[2]) ) { From 03490c61272f193aff4cdc12917eead01fe36c23 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 15 Mar 2020 01:03:03 -0700 Subject: [PATCH 18/27] fix bitwise not bug --- .../react-reconciler/src/ReactFiberBeginWork.js | 3 +-- .../react-reconciler/src/ReactFiberNewContext.js | 14 +++++++------- .../react-reconciler/src/ReactFiberWorkLoop.js | 10 +++++----- .../src/__tests__/ReactSpeculativeWork-test.js | 1 + 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 85630d219d71e..2ade081617fc7 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1612,7 +1612,6 @@ function mountIndeterminateComponent( getComponentName(Component) || 'Unknown', ); } - if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode @@ -3013,7 +3012,7 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { while (fiber !== null) { let nextFiber; - if (loops++ > 100) { + if (loops++ > 1000) { console.log('BREAKING LOOP'); break; } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 26b3a30fd2243..a7304312bf68f 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -196,7 +196,7 @@ export function scheduleWorkOnParentPath( if (node.childExpirationTime < renderExpirationTime) { node.childExpirationTime = renderExpirationTime; if (enableReifyNextWork) { - node.mode &= !ReifiedWorkMode; + node.mode &= ~ReifiedWorkMode; } if ( alternate !== null && @@ -204,7 +204,7 @@ export function scheduleWorkOnParentPath( ) { alternate.childExpirationTime = renderExpirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } } else if ( @@ -213,7 +213,7 @@ export function scheduleWorkOnParentPath( ) { alternate.childExpirationTime = renderExpirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } else { // Neither alternate was updated, which means the rest of the @@ -276,7 +276,7 @@ export function propagateContextChange( if (fiber.expirationTime < renderExpirationTime) { fiber.expirationTime = renderExpirationTime; if (enableReifyNextWork) { - fiber.mode &= !ReifiedWorkMode; + fiber.mode &= ~ReifiedWorkMode; } } let alternate = fiber.alternate; @@ -286,7 +286,7 @@ export function propagateContextChange( ) { alternate.expirationTime = renderExpirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } @@ -478,7 +478,7 @@ export function attachReader(contextItem) { if (readerFiber.expirationTime < renderExpirationTime) { readerFiber.expirationTime = renderExpirationTime; if (enableReifyNextWork) { - readerFiber.mode &= !ReifiedWorkMode; + readerFiber.mode &= ~ReifiedWorkMode; } } if ( @@ -487,7 +487,7 @@ export function attachReader(contextItem) { ) { alternate.expirationTime = renderExpirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index d538e38abe510..643619e690a05 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -464,14 +464,14 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { if (fiber.expirationTime < expirationTime) { fiber.expirationTime = expirationTime; if (enableReifyNextWork) { - fiber.mode &= !ReifiedWorkMode; + fiber.mode &= ~ReifiedWorkMode; } } let alternate = fiber.alternate; if (alternate !== null && alternate.expirationTime < expirationTime) { alternate.expirationTime = expirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } // Walk the parent path to the root and update the child expiration time. @@ -485,7 +485,7 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { if (node.childExpirationTime < expirationTime) { node.childExpirationTime = expirationTime; if (enableReifyNextWork) { - node.mode &= !ReifiedWorkMode; + node.mode &= ~ReifiedWorkMode; } if ( alternate !== null && @@ -493,7 +493,7 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { ) { alternate.childExpirationTime = expirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } } else if ( @@ -502,7 +502,7 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { ) { alternate.childExpirationTime = expirationTime; if (enableReifyNextWork) { - alternate.mode &= !ReifiedWorkMode; + alternate.mode &= ~ReifiedWorkMode; } } if (node.return === null && node.tag === HostRoot) { diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 11d4e1611674d..bbc7392c6309b 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -10,6 +10,7 @@ describe('ReactSpeculativeWork', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); From 5c62e8c75e0104bcca9170adc50e8556cece1d97 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 15 Mar 2020 08:20:23 -0700 Subject: [PATCH 19/27] add support for forwardRef and memo apis --- .../src/ReactFiberBeginWork.js | 12 ++++++++- .../__tests__/ReactSpeculativeWork-test.js | 25 +++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 2ade081617fc7..449d9db85b300 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2989,7 +2989,8 @@ function fiberName(fiber) { if (fiber.tag === 3) front = 'HostRoot'; else if (fiber.tag === 6) front = 'HostText'; else if (fiber.tag === 10) front = 'ContextProvider'; - else if (typeof fiber.type === 'function') front = fiber.type.name; + else if (typeof fiber.type === 'function' && fiber.type.name) + front = fiber.type.name; else front = 'tag' + fiber.tag; if (back) { back = `-(${back})`; @@ -3039,6 +3040,8 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { ); let didBailout; switch (fiber.tag) { + case ForwardRef: + case SimpleMemoComponent: case FunctionComponent: { didBailout = canBailoutSpeculativeWorkWithHooks( fiber, @@ -3046,6 +3049,13 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { ); break; } + case ClassComponent: { + // class component is not yet supported for bailing out of context and state updates + // but it support should be possible + // @TODO implement a ClassComponent bailout here + didBailout = false; + break; + } default: { // in unsupported cases we cannot bail out of work and we // consider the speculative work reified diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index bbc7392c6309b..e18b78d9946c6 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -57,20 +57,23 @@ describe('ReactSpeculativeWork', () => { } } - let Intermediate = ({children}) => children || null; - let BeforeUpdatingLeafBranch = ({children}) => children || null; - let AfterUpdatingLeafBranch = ({children}) => children || null; - let Leaf = () => null; + let Intermediate = React.memo(({children}) => children || null); + let BeforeUpdatingLeafBranch = React.memo(({children}) => children || null); + let AfterUpdatingLeafBranch = React.memo(({children}) => children || null); + let Leaf = React.memo(() => null); let MyContext = React.createContext(0); - let UpdatingLeaf = () => { - let [value, setValue] = React.useState('leaf'); - let isEven = React.useContext(MyContext, v => v % 2 === 0); - Scheduler.unstable_yieldValue(value); - externalSetValue = setValue; - return `${value}-${isEven ? 'even' : 'odd'}`; - }; + let UpdatingLeaf = React.memo( + () => { + let [value, setValue] = React.useState('leaf'); + let isEven = React.useContext(MyContext, v => v % 2 === 0); + Scheduler.unstable_yieldValue(value); + externalSetValue = setValue; + return `${value}-${isEven ? 'even' : 'odd'}`; + }, + (prevProps, nextProps) => prevProps === nextProps, + ); let root = ReactNoop.createRoot(); From b7992f21a6d85c9547c8be9c830ce82a2a440661 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 15 Mar 2020 08:21:15 -0700 Subject: [PATCH 20/27] remove loop breaker --- packages/react-reconciler/src/ReactFiberBeginWork.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 449d9db85b300..0eb88d0baf6d5 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3001,8 +3001,6 @@ function fiberName(fiber) { function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { console.log(`reifyNextWork(${fiberName(workInProgress)})`); - let loops = 0; - let fiber = workInProgress.child; if (fiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. @@ -3013,11 +3011,6 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { while (fiber !== null) { let nextFiber; - if (loops++ > 1000) { - console.log('BREAKING LOOP'); - break; - } - console.log( `_______ see IF fiber(${fiberName(fiber)}) has work to reify`, ); From daaa863af26ffb92ce28c84304ef7a4e3d7f15d9 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 15 Mar 2020 23:54:10 -0700 Subject: [PATCH 21/27] be defensive about checking bailouts in reify step --- .../src/ReactFiberBeginWork.js | 302 +++++++++--------- .../react-reconciler/src/ReactFiberHooks.js | 9 +- .../src/ReactFiberNewContext.js | 4 +- .../src/ReactFiberWorkLoop.js | 2 +- .../src/__tests__/ReactHooks-test.internal.js | 9 +- .../__tests__/ReactSpeculativeWork-test.js | 32 +- 6 files changed, 186 insertions(+), 172 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 0eb88d0baf6d5..8cd8f29dcb5d9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2999,7 +2999,7 @@ function fiberName(fiber) { } function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { - console.log(`reifyNextWork(${fiberName(workInProgress)})`); + // console.log(`reifyNextWork(${fiberName(workInProgress)})`); let fiber = workInProgress.child; if (fiber !== null) { @@ -3007,151 +3007,159 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { fiber.return = workInProgress; } - try { - while (fiber !== null) { - let nextFiber; + // try { + while (fiber !== null) { + let nextFiber; - console.log( - `_______ see IF fiber(${fiberName(fiber)}) has work to reify`, - ); + // console.log( + // `_______ see IF fiber(${fiberName(fiber)}) has work to reify`, + // ); - if (fiber.mode & ReifiedWorkMode) { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) has already been reified, don't go deeper`, - ); - nextFiber = null; - } else { - fiber.mode |= ReifiedWorkMode; - - if (fiber.expirationTime >= renderExpirationTime) { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) DOES HAVE work to reify, reifying`, - ); - let didBailout; - switch (fiber.tag) { - case ForwardRef: - case SimpleMemoComponent: - case FunctionComponent: { + if (fiber.mode & ReifiedWorkMode) { + // console.log( + // `_______ fiber(${fiberName( + // fiber, + // )}) has already been reified, don't go deeper`, + // ); + nextFiber = null; + } else { + fiber.mode |= ReifiedWorkMode; + + if (fiber.expirationTime >= renderExpirationTime) { + // console.log( + // `_______ fiber(${fiberName( + // fiber, + // )}) DOES HAVE work to reify, reifying`, + // ); + let didBailout; + switch (fiber.tag) { + case ForwardRef: + case SimpleMemoComponent: + case FunctionComponent: { + try { didBailout = canBailoutSpeculativeWorkWithHooks( fiber, renderExpirationTime, ); - break; - } - case ClassComponent: { - // class component is not yet supported for bailing out of context and state updates - // but it support should be possible - // @TODO implement a ClassComponent bailout here - didBailout = false; - break; - } - default: { - // in unsupported cases we cannot bail out of work and we - // consider the speculative work reified + } catch (e) { + // suppress error and do not bailout. it should error again + // when the component renders when the context selector is run + // or when the reducer state is updated didBailout = false; } + + break; } - if (didBailout) { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) DOES NOT HAVE REIFIED work, go deeper`, - ); - fiber.expirationTime = NoWork; - nextFiber = fiber.child; - } else { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) HAS REIFIED work, don't go deeper`, - ); - nextFiber = null; + case ClassComponent: { + // class component is not yet supported for bailing out of context and state updates + // but it support should be possible + // @TODO implement a ClassComponent bailout here + didBailout = false; + break; } - } else if (fiber.childExpirationTime >= renderExpirationTime) { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) has children with work to reify, go deeper`, - ); + default: { + // in unsupported cases we cannot bail out of work and we + // consider the speculative work reified + didBailout = false; + } + } + if (didBailout) { + // console.log( + // `_______ fiber(${fiberName( + // fiber, + // )}) DOES NOT HAVE REIFIED work, go deeper`, + // ); + fiber.expirationTime = NoWork; nextFiber = fiber.child; } else { - console.log( - `_______ there is NO WORK deeper than fiber(${fiberName( - fiber, - )}), don't go deeper`, - ); + // console.log( + // `_______ fiber(${fiberName( + // fiber, + // )}) HAS REIFIED work, don't go deeper`, + // ); nextFiber = null; } + } else if (fiber.childExpirationTime >= renderExpirationTime) { + // console.log( + // `_______ fiber(${fiberName( + // fiber, + // )}) has children with work to reify, go deeper`, + // ); + nextFiber = fiber.child; + } else { + // console.log( + // `_______ there is NO WORK deeper than fiber(${fiberName( + // fiber, + // )}), don't go deeper`, + // ); + nextFiber = null; } + } - if (fiber.tag === ContextProvider) { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) is a ContextProvider, pushing its value onto stack`, - ); - const newValue = fiber.memoizedProps.value; - pushProvider(fiber, newValue); - } + if (fiber.tag === ContextProvider) { + // console.log( + // `_______ fiber(${fiberName( + // fiber, + // )}) is a ContextProvider, pushing its value onto stack`, + // ); + const newValue = fiber.memoizedProps.value; + pushProvider(fiber, newValue); + } - if (nextFiber !== null) { - nextFiber.return = fiber; - } else { - console.log( - `_______ looking for siblingsOf(${fiberName(fiber)} and ancestors)`, - ); - // No child. Traverse to next sibling. - nextFiber = fiber; - while (nextFiber !== null) { - if (nextFiber === workInProgress) { - // We're back to the root of this subtree. Exit. - console.log( - `_______ ______ we're back to the beginning, finish up`, - ); - nextFiber = null; - break; - } - if (nextFiber.tag === ContextProvider) { - console.log( - `_______ ______ the return fiber return(${fiberName( - nextFiber, - )}) is a ContextProvider, pop its value off the stack`, - ); - popProvider(nextFiber); - } - let sibling = nextFiber.sibling; - if (sibling !== null) { - // Set the return pointer of the sibling to the work-in-progress fiber. - console.log( - `_______ ______ we've found sibling(${fiberName( - sibling, - )}) of fiber(${fiberName( - nextFiber, - )}), check it for reifying work`, - ); - sibling.return = nextFiber.return; - nextFiber = sibling; - break; - } - // No more siblings. Traverse up. - nextFiber = nextFiber.return; - console.log( - `_______ ______ there are no siblings to return back to return(${fiberName( - nextFiber, - )}) and reset expirationTimes`, - ); - resetChildExpirationTime(nextFiber); + if (nextFiber !== null) { + nextFiber.return = fiber; + } else { + // console.log( + // `_______ looking for siblingsOf(${fiberName(fiber)} and ancestors)`, + // ); + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + // console.log( + // `_______ ______ we're back to the beginning, finish up`, + // ); + nextFiber = null; + break; + } + if (nextFiber.tag === ContextProvider) { + // console.log( + // `_______ ______ the return fiber return(${fiberName( + // nextFiber, + // )}) is a ContextProvider, pop its value off the stack`, + // ); + popProvider(nextFiber); } + let sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + // console.log( + // `_______ ______ we've found sibling(${fiberName( + // sibling, + // )}) of fiber(${fiberName( + // nextFiber, + // )}), check it for reifying work`, + // ); + sibling.return = nextFiber.return; + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + // console.log( + // `_______ ______ there are no siblings to return back to return(${fiberName( + // nextFiber, + // )}) and reset expirationTimes`, + // ); + resetChildExpirationTime(nextFiber); } - fiber = nextFiber; } - } catch (e) { - console.log(e); + fiber = nextFiber; } + // } catch (e) { + // console.log(e); + // } } function resetChildExpirationTime(fiber: Fiber) { @@ -3170,11 +3178,11 @@ function resetChildExpirationTime(fiber: Fiber) { child = child.sibling; } - console.log( - `_______ _____ the new child expriation time for fiber(${fiberName( - fiber, - )}) is ${newChildExpirationTime}`, - ); + // console.log( + // `_______ _____ the new child expriation time for fiber(${fiberName( + // fiber, + // )}) is ${newChildExpirationTime}`, + // ); fiber.childExpirationTime = newChildExpirationTime; } @@ -3204,17 +3212,17 @@ function bailoutOnAlreadyFinishedWork( if (enableReifyNextWork) { if (workInProgress.childExpirationTime >= renderExpirationTime) { if (workInProgress.mode & ReifiedWorkMode) { - console.log( - `bailoutOnAlreadyFinishedWork(${fiberName( - workInProgress, - )}) children have unprocessed work but we have already reified it so we do not need to do that again`, - ); + // console.log( + // `bailoutOnAlreadyFinishedWork(${fiberName( + // workInProgress, + // )}) children have unprocessed work but we have already reified it so we do not need to do that again`, + // ); } else { - console.log( - `bailoutOnAlreadyFinishedWork(${fiberName( - workInProgress, - )}) children has unprocessed updates and was not yet reified`, - ); + // console.log( + // `bailoutOnAlreadyFinishedWork(${fiberName( + // workInProgress, + // )}) children has unprocessed updates and was not yet reified`, + // ); reifyNextWork(workInProgress, renderExpirationTime); } } @@ -3226,11 +3234,11 @@ function bailoutOnAlreadyFinishedWork( // The children don't have any work either. We can skip them. // TODO: Once we add back resuming, we should check if the children are // a work-in-progress set. If so, we need to transfer their effects. - console.log( - `bailoutOnAlreadyFinishedWork(${fiberName( - workInProgress, - )}) there is no child work to do so not going any deeper with work`, - ); + // console.log( + // `bailoutOnAlreadyFinishedWork(${fiberName( + // workInProgress, + // )}) there is no child work to do so not going any deeper with work`, + // ); return null; } else { if (enableSpeculativeWork) { @@ -3316,7 +3324,7 @@ function remountFiber( } function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { - console.log('(((((((((((( reifyWorkInProgress ))))))))))))'); + // console.log('(((((((((((( reifyWorkInProgress ))))))))))))'); didReifySpeculativeWork = true; if (workInProgress === null) { @@ -3423,7 +3431,7 @@ function beginWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - console.log(`beginWork(${fiberName(workInProgress)})`); + // console.log(`beginWork(${fiberName(workInProgress)})`); if (enableSpeculativeWork) { // this value is read from work loop to determine if we need to swap // the unitOfWork for it's altnerate when we return null from beginWork diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index e1e9525290277..8749d968a463f 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -660,8 +660,11 @@ function updateWorkInProgressHook(): Hook { return workInProgressHook; } -// use a Symbol to allow for any value including null and undefined to be memoized -const EMPTY = Symbol('empty'); +// wanted to use a symbol here to represent no value given undefiend and null should be valid +// selections. However Symbol was causing errors in tests so using an empty object to get the +// same effect +// const EMPTY = Symbol('empty'); +const EMPTY = {}; function mountContextImpl( context: ReactContext, @@ -1020,7 +1023,7 @@ function bailoutReducer(hook, renderExpirationTime): boolean { } while (update !== null && update !== first); // if newState is different from the current state do not bailout - if (newState !== hook.memoizedState) { + if (!is(newState, hook.memoizedState)) { return false; } } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index a7304312bf68f..3b69ef8055f9f 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -92,7 +92,7 @@ let pushedCount = 0; export function pushProvider(providerFiber: Fiber, nextValue: T): void { const context: ReactContext = providerFiber.type._context; - console.log(`<====PUSH========= ${++pushedCount} pushed providers`); + // console.log(`<====PUSH========= ${++pushedCount} pushed providers`); if (enableContextReaderPropagation) { // push the previousReaders onto the stack @@ -139,7 +139,7 @@ export function pushProvider(providerFiber: Fiber, nextValue: T): void { export function popProvider(providerFiber: Fiber): void { const currentValue = valueCursor.current; - console.log(`======POP========> ${--pushedCount} pushed providers`); + // console.log(`======POP========> ${--pushedCount} pushed providers`); pop(valueCursor, providerFiber); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 643619e690a05..002891116bdc9 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1254,7 +1254,7 @@ export function flushControlled(fn: () => mixed): void { } function prepareFreshStack(root, expirationTime) { - console.log('------------------------------------ prepareFreshStack'); + // console.log('------------------------------------ prepareFreshStack'); root.finishedWork = null; root.finishedExpirationTime = NoWork; diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 2ebf8d16a85e3..55583383b175a 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -103,7 +103,8 @@ describe('ReactHooks', () => { // Update that bails out. act(() => setCounter1(1)); - expect(Scheduler).toHaveYielded(['Parent: 1, 1']); + // expect(Scheduler).toHaveYielded(['Parent: 1, 1']); + expect(Scheduler).toHaveYielded([]); // This time, one of the state updates but the other one doesn't. So we // can't bail out. @@ -130,7 +131,8 @@ describe('ReactHooks', () => { // Because the final values are the same as the current values, the // component bails out. - expect(Scheduler).toHaveYielded(['Parent: 1, 2']); + // expect(Scheduler).toHaveYielded(['Parent: 1, 2']); + expect(Scheduler).toHaveYielded([]); // prepare to check SameValue act(() => { @@ -152,7 +154,8 @@ describe('ReactHooks', () => { setCounter2(NaN); }); - expect(Scheduler).toHaveYielded(['Parent: 0, NaN']); + // expect(Scheduler).toHaveYielded(['Parent: 0, NaN']); + expect(Scheduler).toHaveYielded([]); // check if changing negative 0 to positive 0 does not bail out act(() => { diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index e18b78d9946c6..89e9b063eafc7 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -3,7 +3,7 @@ let ReactFeatureFlags; let ReactNoop; let Scheduler; -let levels = 4; +let levels = 5; let expansion = 3; describe('ReactSpeculativeWork', () => { @@ -16,7 +16,7 @@ describe('ReactSpeculativeWork', () => { Scheduler = require('scheduler'); }); - it.only('exercises reifyNextWork', () => { + it('exercises reifyNextWork', () => { let externalSetValue; let externalSetMyContextValue; @@ -68,7 +68,7 @@ describe('ReactSpeculativeWork', () => { () => { let [value, setValue] = React.useState('leaf'); let isEven = React.useContext(MyContext, v => v % 2 === 0); - Scheduler.unstable_yieldValue(value); + // Scheduler.unstable_yieldValue(value); externalSetValue = setValue; return `${value}-${isEven ? 'even' : 'odd'}`; }, @@ -78,37 +78,37 @@ describe('ReactSpeculativeWork', () => { let root = ReactNoop.createRoot(); ReactNoop.act(() => root.render()); - expect(Scheduler).toHaveYielded(['leaf']); + // expect(Scheduler).toHaveYielded(['leaf']); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetValue('leaf')); - expect(Scheduler).toHaveYielded([]); + // expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetMyContextValue(2)); - expect(Scheduler).toHaveYielded([]); + // expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => { externalSetValue('leaf'); externalSetMyContextValue(4); }); - expect(Scheduler).toHaveYielded([]); + // expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetMyContextValue(5)); - expect(Scheduler).toHaveYielded(['leaf']); + // expect(Scheduler).toHaveYielded(['leaf']); expect(root).toMatchRenderedOutput('leaf-odd'); ReactNoop.act(() => { externalSetValue('bar'); externalSetMyContextValue(4); }); - expect(Scheduler).toHaveYielded(['bar']); + // expect(Scheduler).toHaveYielded(['bar']); expect(root).toMatchRenderedOutput('bar-even'); ReactNoop.act(() => externalSetValue('baz')); - expect(Scheduler).toHaveYielded(['baz']); + // expect(Scheduler).toHaveYielded(['baz']); expect(root).toMatchRenderedOutput('baz-even'); }); @@ -269,37 +269,37 @@ describe('ReactSpeculativeWork', () => { warmupAndRunTest(label => { ReactFeatureFlags.enableContextReaderPropagation = false; - ReactFeatureFlags.enableSpeculativeWork = false; + ReactFeatureFlags.enableReifyNextWork = false; runTest(label); }, 'regular(walk)'); warmupAndRunTest(label => { ReactFeatureFlags.enableContextReaderPropagation = true; - ReactFeatureFlags.enableSpeculativeWork = false; + ReactFeatureFlags.enableReifyNextWork = false; runTest(label); }, 'regular(reader)'); warmupAndRunTest(label => { ReactFeatureFlags.enableContextReaderPropagation = false; - ReactFeatureFlags.enableSpeculativeWork = true; + ReactFeatureFlags.enableReifyNextWork = true; runTest(label); }, 'speculative(walk)'); warmupAndRunTest(label => { ReactFeatureFlags.enableContextReaderPropagation = true; - ReactFeatureFlags.enableSpeculativeWork = true; + ReactFeatureFlags.enableReifyNextWork = true; runTest(label); }, 'speculative(reader)'); warmupAndRunTest(label => { ReactFeatureFlags.enableContextReaderPropagation = false; - ReactFeatureFlags.enableSpeculativeWork = true; + ReactFeatureFlags.enableReifyNextWork = true; runTest(label, true); }, 'speculativeSelector(walk)'); warmupAndRunTest(label => { ReactFeatureFlags.enableContextReaderPropagation = true; - ReactFeatureFlags.enableSpeculativeWork = true; + ReactFeatureFlags.enableReifyNextWork = true; runTest(label, true); }, 'speculativeSelector(reader)'); From 7d9949f993f9c9afc6b8fc8be2c4fa665ae3ad42 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 17 Mar 2020 00:05:33 -0700 Subject: [PATCH 22/27] first pass at a real bailoutReducer --- .../src/ReactFiberBeginWork.js | 15 +- .../react-reconciler/src/ReactFiberHooks.js | 251 +++++++++++++++++- .../__tests__/ReactSpeculativeWork-test.js | 42 +++ 3 files changed, 299 insertions(+), 9 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8cd8f29dcb5d9..bfb173e93b6e2 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -826,7 +826,7 @@ function updateFunctionComponent( reifyWorkInProgress(current, workInProgress); } - if (current !== null && !didReceiveUpdate) { + if (!enableReifyNextWork && current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderExpirationTime); return bailoutOnAlreadyFinishedWork( current, @@ -2999,7 +2999,7 @@ function fiberName(fiber) { } function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { - // console.log(`reifyNextWork(${fiberName(workInProgress)})`); + console.log(`reifyNextWork(${fiberName(workInProgress)})`); let fiber = workInProgress.child; if (fiber !== null) { @@ -3026,11 +3026,11 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { fiber.mode |= ReifiedWorkMode; if (fiber.expirationTime >= renderExpirationTime) { - // console.log( - // `_______ fiber(${fiberName( - // fiber, - // )}) DOES HAVE work to reify, reifying`, - // ); + console.log( + `_______ fiber(${fiberName( + fiber, + )}) DOES HAVE work to reify, reifying`, + ); let didBailout; switch (fiber.tag) { case ForwardRef: @@ -3045,6 +3045,7 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { // suppress error and do not bailout. it should error again // when the component renders when the context selector is run // or when the reducer state is updated + console.log(e); didBailout = false; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 8749d968a463f..9d8289c9f5fee 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -369,12 +369,15 @@ export function canBailoutSpeculativeWorkWithHooks( current: Fiber, nextRenderExpirationTime: renderExpirationTime, ): boolean { + console.log( + `canBailoutSpeculativeWorkWithHooks current(${fiberName(current)})`, + ); let hook = current.memoizedState; while (hook !== null) { // hooks without a bailout are assumed to permit bailing out // any hook which can instigate work needs to implement the bailout API if (typeof hook.bailout === 'function') { - if (!hook.bailout(hook, nextRenderExpirationTime)) { + if (!hook.bailout(hook, current, nextRenderExpirationTime)) { return false; } } @@ -818,6 +821,11 @@ function updateReducer( queue !== null, 'Should have a queue. This is likely a bug in React. Please file an issue.', ); + console.log( + `bailoutReducer --------------------------- current(${fiberName( + currentlyRenderingFiber, + )})`, + ); queue.lastRenderedReducer = reducer; @@ -829,6 +837,11 @@ function updateReducer( // The last pending update that hasn't been processed yet. let pendingQueue = queue.pending; if (pendingQueue !== null) { + console.log( + `bailoutReducer current(${fiberName( + currentlyRenderingFiber, + )}), pending queue has updates`, + ); // We have new updates that haven't been processed yet. // We'll add them to the base queue. if (baseQueue !== null) { @@ -843,6 +856,11 @@ function updateReducer( } if (baseQueue !== null) { + console.log( + `bailoutReducer current(${fiberName( + currentlyRenderingFiber, + )}), base queue has updates`, + ); // We have a queue to process. let first = baseQueue.next; let newState = current.baseState; @@ -854,6 +872,11 @@ function updateReducer( do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { + console.log( + `_______ update for current(${fiberName( + currentlyRenderingFiber, + )}), update has insufficient priority`, + ); // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. @@ -877,6 +900,11 @@ function updateReducer( markUnprocessedUpdateTime(updateExpirationTime); } } else { + console.log( + `_______ update for current(${fiberName( + currentlyRenderingFiber, + )}), update has SUFFICIENT priority`, + ); // This update does have sufficient priority. if (newBaseQueueLast !== null) { @@ -927,6 +955,12 @@ function updateReducer( markWorkInProgressReceivedUpdate(); } + console.log( + `updateReducer current(${fiberName( + currentlyRenderingFiber, + )}), setting memoizedState to ${newState}`, + ); + hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; @@ -934,6 +968,12 @@ function updateReducer( queue.lastRenderedState = newState; } + console.log( + `updateReducer current(${fiberName( + currentlyRenderingFiber, + )}), about to return ${hook.memoizedState}`, + ); + const dispatch: Dispatch
= (queue.dispatch: any); return [hook.memoizedState, dispatch]; } @@ -992,9 +1032,216 @@ function rerenderReducer( return [newState, dispatch]; } +function fiberName(fiber) { + if (fiber == null) return fiber; + let front = ''; + let back = ''; + if (fiber.mode & 16) back = 'r'; + if (fiber.tag === 3) front = 'HostRoot'; + else if (fiber.tag === 6) front = 'HostText'; + else if (fiber.tag === 10) front = 'ContextProvider'; + else if (typeof fiber.type === 'function' && fiber.type.name) + front = fiber.type.name; + else front = 'tag' + fiber.tag; + if (back) { + back = `-(${back})`; + } + return `${front}${back}`; +} + +function _bailoutReducer( + hook: Hook, + current: Fiber, + renderExpirationTime: ExpirationTime, +): boolean { + // this is mostly a clone of updateReducer + // it has been modified to work without a workInProgress hook + // and to prepare a toBeApplied state on the next render + // @TODO consider what happens if work is yielded between the bailout attempt + // and the render attempt + const queue = hook.queue; + const reducer = queue.lastRenderedReducer; + + console.log( + `bailoutReducer --------------------------- current(${fiberName(current)})`, + ); + + // The last rebase update that is NOT part of the base state. + let baseQueue = hook.baseQueue; + + // The last pending update that hasn't been processed yet. + let pendingQueue = queue.pending; + if (pendingQueue !== null) { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) reducer hook has a pending queue so an update needs to be processed`, + ); + // We have new updates that haven't been processed yet. + // We'll add them to the base queue. + if (baseQueue !== null) { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) reducer hook has a baseQueue, merge the two`, + ); + // Merge the pending queue and the base queue. + let baseFirst = baseQueue.next; + let pendingFirst = pendingQueue.next; + baseQueue.next = pendingFirst; + pendingQueue.next = baseFirst; + } + hook.baseQueue = baseQueue = pendingQueue; + queue.pending = null; + } + + let canBailout; + + if (baseQueue !== null) { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) reducer hook has a baseQueue including pending updates if any, compute new states`, + ); + // We have a queue to process. + let first = baseQueue.next; + let newState = hook.baseState; + + let newBaseState = null; + let newBaseQueueFirst = null; + let newBaseQueueLast = null; + let update = first; + do { + console.log( + `------ update loop for current(${fiberName( + current, + )}), looping over each update ${update.expirationTime}`, + ); + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime < renderExpirationTime) { + console.log( + `------ update loop; update insufficient priority ${update.expirationTime} ${renderExpirationTime}`, + ); + // Priority is insufficient. Skip this update. If this is the first + // skipped update, the previous update/state is the new base + // update/state. + const clone: Update = { + expirationTime: update.expirationTime, + suspenseConfig: update.suspenseConfig, + action: update.action, + eagerReducer: update.eagerReducer, + eagerState: update.eagerState, + next: (null: any), + }; + if (newBaseQueueLast === null) { + console.log( + `------ update loop, insufficient priority; newBaseQueueLast is null, setting it to cloned update`, + ); + newBaseQueueFirst = newBaseQueueLast = clone; + newBaseState = newState; + } else { + console.log( + `------ update loop, insufficient priority; newBaseQueueLast exists null, appending clone to it`, + ); + newBaseQueueLast = newBaseQueueLast.next = clone; + } + // Update the remaining priority in the queue. + if (updateExpirationTime > current.expirationTime) { + console.log( + `------ update loop, insufficient priority; reset the expriation time on the fiber`, + ); + current.expirationTime = updateExpirationTime; + markUnprocessedUpdateTime(updateExpirationTime); + } + } else { + console.log( + `------ update loop; sufficient priority, process this update`, + ); + // This update does have sufficient priority. + + if (newBaseQueueLast !== null) { + console.log( + `------ update loop sufficient priority; a previous update was skipped so append this update to base queue as well`, + ); + const clone: Update = { + expirationTime: Sync, // This update is going to be committed so we never want uncommit it. + suspenseConfig: update.suspenseConfig, + action: update.action, + eagerReducer: update.eagerReducer, + eagerState: update.eagerState, + next: (null: any), + }; + newBaseQueueLast = newBaseQueueLast.next = clone; + } + + // Process this update. + const action = update.action; + newState = reducer(newState, action); + console.log( + `------ update loop sufficient priority; newState(${newState})`, + ); + } + update = update.next; + } while (update !== null && update !== first); + + if (is(newState, hook.memoizedState)) { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) the final newState is the same as the memoized state, we won't prevent a bailout`, + ); + // the new state is the same as the memoizedState, + // the updates in this queue do not require work + canBailout = true; + } else { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) the final newState is different, we can't bailout`, + ); + // the new state is different. we cannot bail out of work + canBailout = false; + } + + if (newBaseQueueLast === null) { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) since the newBaseQueue is empty we need to set the newBaseState to newState`, + ); + newBaseState = newState; + } else { + console.log( + `bailoutReducer current(${fiberName( + current, + )}) we need to make the newBaseQueue circular`, + ); + newBaseQueueLast.next = (newBaseQueueFirst: any); + } + + console.log( + `bailoutReducer current(${fiberName( + current, + )}), set hook memoizedState to newState, and the baseState and baseQueue`, + ); + hook.memoizedState = newState; + hook.baseState = newBaseState; + hook.baseQueue = newBaseQueueLast; + } + + console.log( + `bailoutReducer current(${fiberName( + current, + )}) in the end we said we could bailout ? ${canBailout}`, + ); + return canBailout; +} + +let bailoutReducer = _bailoutReducer; + // @TODO this is a completely broken bailout implementation. figure out how // dispatchAction and updateReducer really work and implement something proper -function bailoutReducer(hook, renderExpirationTime): boolean { +function __bailoutReducer(hook, renderExpirationTime): boolean { const queue = hook.queue; // The last rebase update that is NOT part of the base state. diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 89e9b063eafc7..eda52945a9d94 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -16,6 +16,48 @@ describe('ReactSpeculativeWork', () => { Scheduler = require('scheduler'); }); + it.only('exercises bailoutReducer', () => { + let _dispatch; + + let App = () => { + return ; + }; + + let Parent = () => { + return ; + }; + + let Child = () => { + let [value, dispatch] = React.useReducer(function noZs(s, a) { + if (a === 'z') return s; + return s + a; + }, ''); + _dispatch = dispatch; + return value; + }; + + console.log('------------------------------------ initial'); + const root = ReactNoop.createRoot(); + ReactNoop.act(() => root.render()); + expect(root).toMatchRenderedOutput(''); + + console.log('------------------------------------ dispatch a'); + ReactNoop.act(() => _dispatch('a')); + expect(root).toMatchRenderedOutput('a'); + + console.log('------------------------------------ dispatch b'); + ReactNoop.act(() => _dispatch('b')); + expect(root).toMatchRenderedOutput('ab'); + + console.log('------------------------------------ dispatch z'); + ReactNoop.act(() => _dispatch('z')); + expect(root).toMatchRenderedOutput('ab'); + + console.log('------------------------------------ dispatch c'); + ReactNoop.act(() => _dispatch('c')); + expect(root).toMatchRenderedOutput('abc'); + }); + it('exercises reifyNextWork', () => { let externalSetValue; let externalSetMyContextValue; From 8300bdeb4ba1522759233f72a782de9df9afd5f3 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 13 Apr 2020 22:45:16 -0700 Subject: [PATCH 23/27] add test for exercising reducer bailout --- .../__tests__/ReactSpeculativeWork-test.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index eda52945a9d94..fb13301db427e 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -32,6 +32,7 @@ describe('ReactSpeculativeWork', () => { if (a === 'z') return s; return s + a; }, ''); + Scheduler.unstable_yieldValue(value); _dispatch = dispatch; return value; }; @@ -39,23 +40,51 @@ describe('ReactSpeculativeWork', () => { console.log('------------------------------------ initial'); const root = ReactNoop.createRoot(); ReactNoop.act(() => root.render()); + expect(Scheduler).toHaveYielded(['']); expect(root).toMatchRenderedOutput(''); console.log('------------------------------------ dispatch a'); ReactNoop.act(() => _dispatch('a')); + expect(Scheduler).toHaveYielded(['a']); expect(root).toMatchRenderedOutput('a'); console.log('------------------------------------ dispatch b'); ReactNoop.act(() => _dispatch('b')); + expect(Scheduler).toHaveYielded(['ab']); expect(root).toMatchRenderedOutput('ab'); console.log('------------------------------------ dispatch z'); ReactNoop.act(() => _dispatch('z')); + expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('ab'); console.log('------------------------------------ dispatch c'); ReactNoop.act(() => _dispatch('c')); + expect(Scheduler).toHaveYielded(['abc']); expect(root).toMatchRenderedOutput('abc'); + + console.log('------------------------------------ dispatch zd'); + ReactNoop.act(() => { + _dispatch('z'); + _dispatch('d'); + }); + expect(Scheduler).toHaveYielded(['abcd']); + expect(root).toMatchRenderedOutput('abcd'); + + console.log('------------------------------------ dispatch zd'); + ReactNoop.act(() => { + _dispatch('e'); + _dispatch('z'); + _dispatch('z'); + _dispatch('z'); + _dispatch('z'); + _dispatch('f'); + _dispatch('z'); + _dispatch('z'); + _dispatch('z'); + }); + expect(Scheduler).toHaveYielded(['abcdef']); + expect(root).toMatchRenderedOutput('abcdef'); }); it('exercises reifyNextWork', () => { From 1272f5afb0eb2fb0b92f27c0a16e8ac56965aefe Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 13 Apr 2020 23:17:23 -0700 Subject: [PATCH 24/27] remove logging --- .../src/ReactFiberBeginWork.js | 92 +-------- .../react-reconciler/src/ReactFiberHooks.js | 177 +----------------- .../__tests__/ReactSpeculativeWork-test.js | 42 +++-- 3 files changed, 31 insertions(+), 280 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index bfb173e93b6e2..bda5a86f61f06 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2999,38 +2999,21 @@ function fiberName(fiber) { } function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { - console.log(`reifyNextWork(${fiberName(workInProgress)})`); - let fiber = workInProgress.child; if (fiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. fiber.return = workInProgress; } - // try { while (fiber !== null) { let nextFiber; - // console.log( - // `_______ see IF fiber(${fiberName(fiber)}) has work to reify`, - // ); - if (fiber.mode & ReifiedWorkMode) { - // console.log( - // `_______ fiber(${fiberName( - // fiber, - // )}) has already been reified, don't go deeper`, - // ); nextFiber = null; } else { fiber.mode |= ReifiedWorkMode; if (fiber.expirationTime >= renderExpirationTime) { - console.log( - `_______ fiber(${fiberName( - fiber, - )}) DOES HAVE work to reify, reifying`, - ); let didBailout; switch (fiber.tag) { case ForwardRef: @@ -3045,7 +3028,6 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { // suppress error and do not bailout. it should error again // when the component renders when the context selector is run // or when the reducer state is updated - console.log(e); didBailout = false; } @@ -3065,44 +3047,19 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { } } if (didBailout) { - // console.log( - // `_______ fiber(${fiberName( - // fiber, - // )}) DOES NOT HAVE REIFIED work, go deeper`, - // ); fiber.expirationTime = NoWork; nextFiber = fiber.child; } else { - // console.log( - // `_______ fiber(${fiberName( - // fiber, - // )}) HAS REIFIED work, don't go deeper`, - // ); nextFiber = null; } } else if (fiber.childExpirationTime >= renderExpirationTime) { - // console.log( - // `_______ fiber(${fiberName( - // fiber, - // )}) has children with work to reify, go deeper`, - // ); nextFiber = fiber.child; } else { - // console.log( - // `_______ there is NO WORK deeper than fiber(${fiberName( - // fiber, - // )}), don't go deeper`, - // ); nextFiber = null; } } if (fiber.tag === ContextProvider) { - // console.log( - // `_______ fiber(${fiberName( - // fiber, - // )}) is a ContextProvider, pushing its value onto stack`, - // ); const newValue = fiber.memoizedProps.value; pushProvider(fiber, newValue); } @@ -3110,57 +3067,31 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { if (nextFiber !== null) { nextFiber.return = fiber; } else { - // console.log( - // `_______ looking for siblingsOf(${fiberName(fiber)} and ancestors)`, - // ); // No child. Traverse to next sibling. nextFiber = fiber; while (nextFiber !== null) { if (nextFiber === workInProgress) { // We're back to the root of this subtree. Exit. - // console.log( - // `_______ ______ we're back to the beginning, finish up`, - // ); nextFiber = null; break; } if (nextFiber.tag === ContextProvider) { - // console.log( - // `_______ ______ the return fiber return(${fiberName( - // nextFiber, - // )}) is a ContextProvider, pop its value off the stack`, - // ); popProvider(nextFiber); } let sibling = nextFiber.sibling; if (sibling !== null) { // Set the return pointer of the sibling to the work-in-progress fiber. - // console.log( - // `_______ ______ we've found sibling(${fiberName( - // sibling, - // )}) of fiber(${fiberName( - // nextFiber, - // )}), check it for reifying work`, - // ); sibling.return = nextFiber.return; nextFiber = sibling; break; } // No more siblings. Traverse up. nextFiber = nextFiber.return; - // console.log( - // `_______ ______ there are no siblings to return back to return(${fiberName( - // nextFiber, - // )}) and reset expirationTimes`, - // ); resetChildExpirationTime(nextFiber); } } fiber = nextFiber; } - // } catch (e) { - // console.log(e); - // } } function resetChildExpirationTime(fiber: Fiber) { @@ -3179,12 +3110,6 @@ function resetChildExpirationTime(fiber: Fiber) { child = child.sibling; } - // console.log( - // `_______ _____ the new child expriation time for fiber(${fiberName( - // fiber, - // )}) is ${newChildExpirationTime}`, - // ); - fiber.childExpirationTime = newChildExpirationTime; } @@ -3213,17 +3138,8 @@ function bailoutOnAlreadyFinishedWork( if (enableReifyNextWork) { if (workInProgress.childExpirationTime >= renderExpirationTime) { if (workInProgress.mode & ReifiedWorkMode) { - // console.log( - // `bailoutOnAlreadyFinishedWork(${fiberName( - // workInProgress, - // )}) children have unprocessed work but we have already reified it so we do not need to do that again`, - // ); + // noop, we don't need to do any checking if we've already done it } else { - // console.log( - // `bailoutOnAlreadyFinishedWork(${fiberName( - // workInProgress, - // )}) children has unprocessed updates and was not yet reified`, - // ); reifyNextWork(workInProgress, renderExpirationTime); } } @@ -3235,11 +3151,6 @@ function bailoutOnAlreadyFinishedWork( // The children don't have any work either. We can skip them. // TODO: Once we add back resuming, we should check if the children are // a work-in-progress set. If so, we need to transfer their effects. - // console.log( - // `bailoutOnAlreadyFinishedWork(${fiberName( - // workInProgress, - // )}) there is no child work to do so not going any deeper with work`, - // ); return null; } else { if (enableSpeculativeWork) { @@ -3325,7 +3236,6 @@ function remountFiber( } function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { - // console.log('(((((((((((( reifyWorkInProgress ))))))))))))'); didReifySpeculativeWork = true; if (workInProgress === null) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 9d8289c9f5fee..ed91e6c9d6eb9 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -369,9 +369,6 @@ export function canBailoutSpeculativeWorkWithHooks( current: Fiber, nextRenderExpirationTime: renderExpirationTime, ): boolean { - console.log( - `canBailoutSpeculativeWorkWithHooks current(${fiberName(current)})`, - ); let hook = current.memoizedState; while (hook !== null) { // hooks without a bailout are assumed to permit bailing out @@ -821,11 +818,6 @@ function updateReducer( queue !== null, 'Should have a queue. This is likely a bug in React. Please file an issue.', ); - console.log( - `bailoutReducer --------------------------- current(${fiberName( - currentlyRenderingFiber, - )})`, - ); queue.lastRenderedReducer = reducer; @@ -837,11 +829,6 @@ function updateReducer( // The last pending update that hasn't been processed yet. let pendingQueue = queue.pending; if (pendingQueue !== null) { - console.log( - `bailoutReducer current(${fiberName( - currentlyRenderingFiber, - )}), pending queue has updates`, - ); // We have new updates that haven't been processed yet. // We'll add them to the base queue. if (baseQueue !== null) { @@ -856,11 +843,6 @@ function updateReducer( } if (baseQueue !== null) { - console.log( - `bailoutReducer current(${fiberName( - currentlyRenderingFiber, - )}), base queue has updates`, - ); // We have a queue to process. let first = baseQueue.next; let newState = current.baseState; @@ -872,11 +854,6 @@ function updateReducer( do { const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { - console.log( - `_______ update for current(${fiberName( - currentlyRenderingFiber, - )}), update has insufficient priority`, - ); // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. @@ -900,11 +877,6 @@ function updateReducer( markUnprocessedUpdateTime(updateExpirationTime); } } else { - console.log( - `_______ update for current(${fiberName( - currentlyRenderingFiber, - )}), update has SUFFICIENT priority`, - ); // This update does have sufficient priority. if (newBaseQueueLast !== null) { @@ -955,12 +927,6 @@ function updateReducer( markWorkInProgressReceivedUpdate(); } - console.log( - `updateReducer current(${fiberName( - currentlyRenderingFiber, - )}), setting memoizedState to ${newState}`, - ); - hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; @@ -968,12 +934,6 @@ function updateReducer( queue.lastRenderedState = newState; } - console.log( - `updateReducer current(${fiberName( - currentlyRenderingFiber, - )}), about to return ${hook.memoizedState}`, - ); - const dispatch: Dispatch = (queue.dispatch: any); return [hook.memoizedState, dispatch]; } @@ -1032,24 +992,7 @@ function rerenderReducer( return [newState, dispatch]; } -function fiberName(fiber) { - if (fiber == null) return fiber; - let front = ''; - let back = ''; - if (fiber.mode & 16) back = 'r'; - if (fiber.tag === 3) front = 'HostRoot'; - else if (fiber.tag === 6) front = 'HostText'; - else if (fiber.tag === 10) front = 'ContextProvider'; - else if (typeof fiber.type === 'function' && fiber.type.name) - front = fiber.type.name; - else front = 'tag' + fiber.tag; - if (back) { - back = `-(${back})`; - } - return `${front}${back}`; -} - -function _bailoutReducer( +function bailoutReducer( hook: Hook, current: Fiber, renderExpirationTime: ExpirationTime, @@ -1062,29 +1005,15 @@ function _bailoutReducer( const queue = hook.queue; const reducer = queue.lastRenderedReducer; - console.log( - `bailoutReducer --------------------------- current(${fiberName(current)})`, - ); - // The last rebase update that is NOT part of the base state. let baseQueue = hook.baseQueue; // The last pending update that hasn't been processed yet. let pendingQueue = queue.pending; if (pendingQueue !== null) { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) reducer hook has a pending queue so an update needs to be processed`, - ); // We have new updates that haven't been processed yet. // We'll add them to the base queue. if (baseQueue !== null) { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) reducer hook has a baseQueue, merge the two`, - ); // Merge the pending queue and the base queue. let baseFirst = baseQueue.next; let pendingFirst = pendingQueue.next; @@ -1095,14 +1024,10 @@ function _bailoutReducer( queue.pending = null; } - let canBailout; + // will be set to false if there are updates to process + let canBailout = true; if (baseQueue !== null) { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) reducer hook has a baseQueue including pending updates if any, compute new states`, - ); // We have a queue to process. let first = baseQueue.next; let newState = hook.baseState; @@ -1112,16 +1037,8 @@ function _bailoutReducer( let newBaseQueueLast = null; let update = first; do { - console.log( - `------ update loop for current(${fiberName( - current, - )}), looping over each update ${update.expirationTime}`, - ); const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { - console.log( - `------ update loop; update insufficient priority ${update.expirationTime} ${renderExpirationTime}`, - ); // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. @@ -1134,35 +1051,20 @@ function _bailoutReducer( next: (null: any), }; if (newBaseQueueLast === null) { - console.log( - `------ update loop, insufficient priority; newBaseQueueLast is null, setting it to cloned update`, - ); newBaseQueueFirst = newBaseQueueLast = clone; newBaseState = newState; } else { - console.log( - `------ update loop, insufficient priority; newBaseQueueLast exists null, appending clone to it`, - ); newBaseQueueLast = newBaseQueueLast.next = clone; } // Update the remaining priority in the queue. if (updateExpirationTime > current.expirationTime) { - console.log( - `------ update loop, insufficient priority; reset the expriation time on the fiber`, - ); current.expirationTime = updateExpirationTime; markUnprocessedUpdateTime(updateExpirationTime); } } else { - console.log( - `------ update loop; sufficient priority, process this update`, - ); // This update does have sufficient priority. if (newBaseQueueLast !== null) { - console.log( - `------ update loop sufficient priority; a previous update was skipped so append this update to base queue as well`, - ); const clone: Update = { expirationTime: Sync, // This update is going to be committed so we never want uncommit it. suspenseConfig: update.suspenseConfig, @@ -1177,106 +1079,33 @@ function _bailoutReducer( // Process this update. const action = update.action; newState = reducer(newState, action); - console.log( - `------ update loop sufficient priority; newState(${newState})`, - ); } update = update.next; } while (update !== null && update !== first); if (is(newState, hook.memoizedState)) { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) the final newState is the same as the memoized state, we won't prevent a bailout`, - ); // the new state is the same as the memoizedState, // the updates in this queue do not require work canBailout = true; } else { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) the final newState is different, we can't bailout`, - ); // the new state is different. we cannot bail out of work canBailout = false; } if (newBaseQueueLast === null) { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) since the newBaseQueue is empty we need to set the newBaseState to newState`, - ); newBaseState = newState; } else { - console.log( - `bailoutReducer current(${fiberName( - current, - )}) we need to make the newBaseQueue circular`, - ); newBaseQueueLast.next = (newBaseQueueFirst: any); } - console.log( - `bailoutReducer current(${fiberName( - current, - )}), set hook memoizedState to newState, and the baseState and baseQueue`, - ); hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; } - console.log( - `bailoutReducer current(${fiberName( - current, - )}) in the end we said we could bailout ? ${canBailout}`, - ); return canBailout; } -let bailoutReducer = _bailoutReducer; - -// @TODO this is a completely broken bailout implementation. figure out how -// dispatchAction and updateReducer really work and implement something proper -function __bailoutReducer(hook, renderExpirationTime): boolean { - const queue = hook.queue; - - // The last rebase update that is NOT part of the base state. - let baseQueue = hook.baseQueue; - let reducer = queue.lastRenderedReducer; - - // The last pending update that hasn't been processed yet. - let pendingQueue = queue.pending; - if (pendingQueue !== null) { - // We have new updates that haven't been processed yet. - // We'll add them to the base queue. - baseQueue = pendingQueue; - } - - if (baseQueue !== null) { - // We have a queue to process. - let first = baseQueue.next; - let newState = hook.baseState; - - let update = first; - do { - // Process this update. - const action = update.action; - newState = reducer(newState, action); - update = update.next; - } while (update !== null && update !== first); - - // if newState is different from the current state do not bailout - if (!is(newState, hook.memoizedState)) { - return false; - } - } - return true; -} - function mountState( initialState: (() => S) | S, ): [S, Dispatch>] { diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index fb13301db427e..66d001c1da526 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -16,7 +16,7 @@ describe('ReactSpeculativeWork', () => { Scheduler = require('scheduler'); }); - it.only('exercises bailoutReducer', () => { + it('exercises bailoutReducer', () => { let _dispatch; let App = () => { @@ -71,7 +71,7 @@ describe('ReactSpeculativeWork', () => { expect(Scheduler).toHaveYielded(['abcd']); expect(root).toMatchRenderedOutput('abcd'); - console.log('------------------------------------ dispatch zd'); + console.log('------------------------------------ dispatch ezzzzfzzz'); ReactNoop.act(() => { _dispatch('e'); _dispatch('z'); @@ -128,18 +128,30 @@ describe('ReactSpeculativeWork', () => { } } - let Intermediate = React.memo(({children}) => children || null); - let BeforeUpdatingLeafBranch = React.memo(({children}) => children || null); - let AfterUpdatingLeafBranch = React.memo(({children}) => children || null); - let Leaf = React.memo(() => null); + let Intermediate = React.memo(function Intermediate({children}) { + return children || null; + }); + let BeforeUpdatingLeafBranch = React.memo( + function BeforeUpdatingLeafBranch({children}) { + return children || null; + }, + ); + let AfterUpdatingLeafBranch = React.memo(function AfterUpdatingLeafBranch({ + children, + }) { + return children || null; + }); + let Leaf = React.memo(function Leaf() { + return null; + }); let MyContext = React.createContext(0); let UpdatingLeaf = React.memo( - () => { + function UpdatingLeaf() { let [value, setValue] = React.useState('leaf'); let isEven = React.useContext(MyContext, v => v % 2 === 0); - // Scheduler.unstable_yieldValue(value); + Scheduler.unstable_yieldValue(value); externalSetValue = setValue; return `${value}-${isEven ? 'even' : 'odd'}`; }, @@ -149,37 +161,37 @@ describe('ReactSpeculativeWork', () => { let root = ReactNoop.createRoot(); ReactNoop.act(() => root.render()); - // expect(Scheduler).toHaveYielded(['leaf']); + expect(Scheduler).toHaveYielded(['leaf']); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetValue('leaf')); - // expect(Scheduler).toHaveYielded([]); + expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetMyContextValue(2)); - // expect(Scheduler).toHaveYielded([]); + expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => { externalSetValue('leaf'); externalSetMyContextValue(4); }); - // expect(Scheduler).toHaveYielded([]); + expect(Scheduler).toHaveYielded([]); expect(root).toMatchRenderedOutput('leaf-even'); ReactNoop.act(() => externalSetMyContextValue(5)); - // expect(Scheduler).toHaveYielded(['leaf']); + expect(Scheduler).toHaveYielded(['leaf']); expect(root).toMatchRenderedOutput('leaf-odd'); ReactNoop.act(() => { externalSetValue('bar'); externalSetMyContextValue(4); }); - // expect(Scheduler).toHaveYielded(['bar']); + expect(Scheduler).toHaveYielded(['bar']); expect(root).toMatchRenderedOutput('bar-even'); ReactNoop.act(() => externalSetValue('baz')); - // expect(Scheduler).toHaveYielded(['baz']); + expect(Scheduler).toHaveYielded(['baz']); expect(root).toMatchRenderedOutput('baz-even'); }); From 5eb9508a9773dbc9c52c113e8af6b35de3dcc1b4 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 13 Apr 2020 23:47:42 -0700 Subject: [PATCH 25/27] remove speculative work implementation --- packages/react-reconciler/src/ReactFiber.js | 9 - .../src/ReactFiberBeginWork.js | 352 +----------------- .../src/ReactFiberCompleteWork.js | 11 - .../react-reconciler/src/ReactFiberHooks.js | 10 +- .../src/ReactFiberHostContext.js | 8 +- .../src/ReactFiberNewContext.js | 5 - .../react-reconciler/src/ReactFiberStack.js | 10 +- .../src/ReactFiberWorkLoop.js | 29 +- .../ReactNewContext-test.internal.js | 12 +- .../__tests__/ReactSpeculativeWork-test.js | 2 +- packages/react/src/ReactElement.js | 7 - packages/shared/ReactFeatureFlags.js | 3 - 12 files changed, 14 insertions(+), 444 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 9dc7103fac3d8..66e391dfb06b3 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -173,9 +173,6 @@ export type Fiber = {| sibling: Fiber | null, index: number, - // identifier of last rendered children - residue: any, - // The ref last used to attach this node. // I'll avoid adding an owner field for prod and model that as functions. ref: @@ -284,8 +281,6 @@ function FiberNode( this.sibling = null; this.index = 0; - this.residue = null; - this.ref = null; this.pendingProps = pendingProps; @@ -376,8 +371,6 @@ const createFiber = function( return new FiberNode(tag, pendingProps, key, mode); }; -export const voidFiber = createFiber(0, null, null, 0); - function shouldConstruct(Component: Function) { const prototype = Component.prototype; return !!(prototype && prototype.isReactComponent); @@ -476,8 +469,6 @@ export function createWorkInProgress( workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; - workInProgress.residue = current.residue; - // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index bda5a86f61f06..63a8fc22b8a90 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -66,7 +66,6 @@ import { warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, enableChunksAPI, - enableSpeculativeWork, enableReifyNextWork, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; @@ -87,7 +86,6 @@ import { resolveClassForHotReloading, } from './ReactFiberHotReloading'; -import {voidFiber} from './ReactFiber'; import { mountChildFibers, reconcileChildFibers, @@ -195,7 +193,6 @@ import { const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; let didReceiveUpdate: boolean = false; -let didPushEffect: boolean = false; let didWarnAboutBadClass; let didWarnAboutModulePatternComponent; @@ -252,7 +249,6 @@ export function reconcileChildren( nextChildren: any, renderExpirationTime: ExpirationTime, ) { - workInProgress.residue = (nextChildren && nextChildren.residue) || null; if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, @@ -343,28 +339,6 @@ function updateForwardRef( const render = Component.render; const ref = workInProgress.ref; - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if ( - canBailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) - ) { - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } - // this workInProgress is currently detatched from the actual workInProgress - // tree because the previous workInProgress is from the current tree and we - // don't yet have a return pointer to any fiber in the workInProgress tree - // it will get attached if/when we reify it and end our speculative work - workInProgress = createWorkInProgress( - current, - current.pendingProps, - NoWork, - ); - workInProgress.return = current.return; - } - // The rest is a fork of updateFunctionComponent let nextChildren; prepareToReadContext(workInProgress, renderExpirationTime); @@ -407,31 +381,6 @@ function updateForwardRef( ); } - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if (current !== null && !didPushEffect) { - // if nextChildren have the same residue as previous children and there - // were not pushed effects which had an effect we can bail out. - // bailing out in this way is actually useful now with speculative work - // since we can avoid large reifications whereas with the old method a bailout - // here is really no better than bailing out in nextChildren's reconciliation - if ( - current && - current.residue === - ((nextChildren && nextChildren.residue) || nextChildren) - ) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } - } - // either effects were pushed or nextChildren had a different residue - // we need to reify - reifyWorkInProgress(current, workInProgress); - } - if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderExpirationTime); return bailoutOnAlreadyFinishedWork( @@ -516,16 +465,6 @@ function updateMemoComponent( workInProgress.child = child; return child; } - // If we are in speculative work mode then we've rendered at least once - // and the props could not have changed so we don't need to check them - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - didReceiveUpdate = false; - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } if (__DEV__) { const type = Component.type; const innerPropTypes = type.propTypes; @@ -606,21 +545,6 @@ function updateSimpleMemoComponent( // Inner propTypes will be validated in the function component path. } } - // If we are in speculative work mode then we've rendered at least once - // and the props could not have changed so we don't need to check them and - // can bail out of work if there is no update - if ( - enableSpeculativeWork && - inSpeculativeWorkMode() && - updateExpirationTime < renderExpirationTime - ) { - didReceiveUpdate = false; - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } if (current !== null) { const prevProps = current.memoizedProps; if ( @@ -732,28 +656,6 @@ function updateFunctionComponent( } } - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if ( - canBailoutSpeculativeWorkWithHooks(workInProgress, renderExpirationTime) - ) { - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } - // this workInProgress is currently detatched from the actual workInProgress - // tree because the previous workInProgress is from the current tree and we - // don't yet have a return pointer to any fiber in the workInProgress tree - // it will get attached if/when we reify it and end our speculative work - workInProgress = createWorkInProgress( - current, - current.pendingProps, - NoWork, - ); - workInProgress.return = current.return; - } - let context; if (!disableLegacyContext) { const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); @@ -801,31 +703,6 @@ function updateFunctionComponent( ); } - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - if (current !== null && !didPushEffect) { - // if nextChildren have the same residue as previous children and there - // were not pushed effects which had an effect we can bail out. - // bailing out in this way is actually useful now with speculative work - // since we can avoid large reifications whereas with the old method a bailout - // here is really no better than bailing out in nextChildren's reconciliation - if ( - current && - current.residue === - ((nextChildren && nextChildren.residue) || nextChildren) - ) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } - } - // either effects were pushed or nextChildren had a different residue - // we need to reify - reifyWorkInProgress(current, workInProgress); - } - if (!enableReifyNextWork && current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderExpirationTime); return bailoutOnAlreadyFinishedWork( @@ -2804,14 +2681,6 @@ function updateContextProvider( pushProvider(workInProgress, newValue); - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); - } - if (oldProps !== null) { const oldValue = oldProps.value; const changedBits = calculateChangedBits(context, newValue, oldValue); @@ -2947,57 +2816,6 @@ export function markWorkInProgressReceivedUpdate() { didReceiveUpdate = true; } -export function markWorkInProgressPushedEffect() { - didPushEffect = true; -} - -function markReturnsForSpeculativeWorkMode(speculativeWorkInProgress: Fiber) { - invariant( - speculativeWorkRootFiber !== null, - 'markReturnsForSpeculativeWorkMode called when we are not yet in that mode. this is likely a bug with React itself', - ); - - // set each child to return to this workInProgress; - let childFiber = speculativeWorkInProgress.child; - while (childFiber !== null) { - childFiber.return = speculativeWorkInProgress; - childFiber = childFiber.sibling; - } -} - -function enterSpeculativeWorkMode(workInProgress: Fiber) { - invariant( - speculativeWorkRootFiber === null, - 'enterSpeculativeWorkMode called when we are already in that mode. this is likely a bug with React itself', - ); - - speculativeWorkRootFiber = workInProgress; - - // set each child to return to this workInProgress; - let childFiber = workInProgress.child; - while (childFiber !== null) { - childFiber.return = workInProgress; - childFiber = childFiber.sibling; - } -} - -function fiberName(fiber) { - if (fiber == null) return fiber; - let front = ''; - let back = ''; - if (fiber.mode & ReifiedWorkMode) back = 'r'; - if (fiber.tag === 3) front = 'HostRoot'; - else if (fiber.tag === 6) front = 'HostText'; - else if (fiber.tag === 10) front = 'ContextProvider'; - else if (typeof fiber.type === 'function' && fiber.type.name) - front = fiber.type.name; - else front = 'tag' + fiber.tag; - if (back) { - back = `-(${back})`; - } - return `${front}${back}`; -} - function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { let fiber = workInProgress.child; if (fiber !== null) { @@ -3153,22 +2971,10 @@ function bailoutOnAlreadyFinishedWork( // a work-in-progress set. If so, we need to transfer their effects. return null; } else { - if (enableSpeculativeWork) { - // This fiber doesn't have work, but its subtree does. enter speculative - // work mode if not already in it and continue. - if (speculativeWorkRootFiber === null) { - enterSpeculativeWorkMode(workInProgress); - } else { - markReturnsForSpeculativeWorkMode(workInProgress); - } - // this child wasn't cloned so it is from the current tree - return workInProgress.child; - } else { - // This fiber doesn't have work, but its subtree does. Clone the child - // fibers and continue. - cloneChildFibers(current, workInProgress); - return workInProgress.child; - } + // This fiber doesn't have work, but its subtree does. Clone the child + // fibers and continue. + cloneChildFibers(current, workInProgress); + return workInProgress.child; } } @@ -3235,132 +3041,11 @@ function remountFiber( } } -function reifyWorkInProgress(current: Fiber, workInProgress: Fiber | null) { - didReifySpeculativeWork = true; - - if (workInProgress === null) { - // intentionally not returning anywhere yet - workInProgress = createWorkInProgress( - current, - current.pendingProps, - NoWork, - ); - workInProgress.return = null; - } - let originalWorkInProgress = workInProgress; - - invariant( - speculativeWorkRootFiber !== null, - 'reifyWorkInProgress was called when we are not in speculative work mode', - ); - - while (true) { - // fibers in the part of the tree already visited by beginWork need to have - // their expirationTime set to NoWork. fibers we have yet to visit in the reified - // tree need to maintain their expirationTime. This might need to be reworked - // where we don't reify to-be-visited fibers but that requires tracking speculative - // mode on the fiber.mode property - let alreadyCompletedSpeculativeChildFibers = true; - - // when we encounter the reifiedCurrentChild we need to assign the reifiedNewChild rather - // than construct a new workInProgress - let reifiedNewChild = workInProgress; - let reifiedCurrentChild = current; - - // special logic required when we are reifying into the speculativeWorkRootFiber - // this is because current.return will actually be pointing at the last workInProgress - // created before speculative work began whereas in other cases it will be pointing - // to it's parent from the current tree - if (current.return === speculativeWorkRootFiber) { - workInProgress = current.return; - current = current.return = workInProgress.alternate; - } else { - current = current.return; - workInProgress = createWorkInProgress( - current, - current.pendingProps, - NoWork, - ); - workInProgress.return = null; - } - - // visit each child of workInProgress and create a newChild - let child = workInProgress.child; - let parent = workInProgress; - let newChild = voidFiber; - while (child !== null) { - if (child === reifiedCurrentChild) { - newChild = newChild.sibling = parent.child = reifiedNewChild; - alreadyCompletedSpeculativeChildFibers = false; - } else { - newChild = newChild.sibling = parent.child = createWorkInProgress( - child, - child.pendingProps, - child.expirationTime, - ); - newChild.return = null; - if (alreadyCompletedSpeculativeChildFibers) { - resetWorkInProgressFromCompletedSpeculativFiberExpirationTimes( - newChild, - ); - } - } - // parent should only be workInProgress on first pass. use voidFiber - // as dummy for subsequent passes - parent = voidFiber; - newChild.return = workInProgress; - child = child.sibling; - } - voidFiber.child = null; - voidFiber.sibling = null; - - if (workInProgress === speculativeWorkRootFiber) { - speculativeWorkRootFiber = null; - break; - } - } - - return originalWorkInProgress; -} - -function resetWorkInProgressFromCompletedSpeculativFiberExpirationTimes( - reifyingFiber: Fiber, -) { - // when a workInProgress is created from a current fiber after that fiber was completed in - // speculative mode we need to reflect that the work completed on this fiber's subtree is - // complete. that would not be reflected in the expiration times of the current tree and we - // want to avoid mutating them so we can effectively restart work. This is probably not the - // the right implementation however because it is possible there are lower priority expiration - // times in the speculative tree and marking these as NoWork effective hides those updates - // @TODO come back to this - reifyingFiber.expirationTime = NoWork; - reifyingFiber.childExpirationTime = NoWork; -} - function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - // console.log(`beginWork(${fiberName(workInProgress)})`); - if (enableSpeculativeWork) { - // this value is read from work loop to determine if we need to swap - // the unitOfWork for it's altnerate when we return null from beginWork - // on each step we set it to false. and only set it to true if reification - // occurs - didReifySpeculativeWork = false; - - // this value is used to determine if we can bailout when nextChildren is identical - // to memoized children when we are in speculative mode and would prefer to bailout - // rather than reify the workInProgress tree - didPushEffect = false; - } - // in speculative work mode we have been passed the current fiber as the workInProgress - // if this is the case we need to assign the work to the current because they are the same - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - current = workInProgress; - } - const updateExpirationTime = workInProgress.expirationTime; if (__DEV__) { @@ -3381,21 +3066,6 @@ function beginWork( } } - // for now only support speculative work with certain fiber types. - // support for more can and should be added - if ( - enableSpeculativeWork && - inSpeculativeWorkMode() && - workInProgress.tag !== ContextProvider && - workInProgress.tag !== FunctionComponent && - workInProgress.tag !== ForwardRef && - workInProgress.tag !== SimpleMemoComponent && - workInProgress.tag !== MemoComponent && - workInProgress.tag > 6 - ) { - workInProgress = reifyWorkInProgress(current, null); - } - if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; @@ -3589,20 +3259,6 @@ function beginWork( didReceiveUpdate = false; } - // for now only support speculative work with certain fiber types. - // support for more can and should be added - if ( - enableSpeculativeWork && - inSpeculativeWorkMode() && - workInProgress.tag !== FunctionComponent && - workInProgress.tag !== ForwardRef && - workInProgress.tag !== SimpleMemoComponent && - workInProgress.tag !== MemoComponent - ) { - workInProgress = reifyWorkInProgress(current, null); - } - - // @TODO pretty sure we need to not do this if we are in speculativeMode. perhaps make it part of reification? // Before entering the begin phase, clear the expiration time. workInProgress.expirationTime = NoWork; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e2ebc20f9bba3..e777b8f0de63a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -122,7 +122,6 @@ import { enableFundamentalAPI, enableScopeAPI, enableChunksAPI, - enableSpeculativeWork, enableContextReaderPropagation, } from 'shared/ReactFeatureFlags'; import { @@ -642,16 +641,6 @@ function completeWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { - if (enableSpeculativeWork && inSpeculativeWorkMode()) { - // the workInProgress is the current fiber. - current = workInProgress; - // if we completed and we're still in speculative mode that means there - // was either no update on this fiber or we had updates but they bailed - // out and therefore we can safely reset work - // @TODO this should be moved elsewhere and account for render phase work - // as well as lower priority work - workInProgress.expirationTime = NoWork; - } const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index ed91e6c9d6eb9..f9cdbf7d80ed5 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -47,7 +47,6 @@ import { markUnprocessedUpdateTime, } from './ReactFiberWorkLoop'; import { - enableSpeculativeWork, enableContextSelectors, enableReifyNextWork, } from 'shared/ReactFeatureFlags'; @@ -55,10 +54,7 @@ import { import invariant from 'shared/invariant'; import getComponentName from 'shared/getComponentName'; import is from 'shared/objectIs'; -import { - markWorkInProgressReceivedUpdate, - markWorkInProgressPushedEffect, -} from './ReactFiberBeginWork'; +import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; import { UserBlockingPriority, @@ -1145,9 +1141,6 @@ function rerenderState( } function pushEffect(tag, create, destroy, deps) { - if (enableSpeculativeWork && tag & HookHasEffect) { - markWorkInProgressPushedEffect(); - } const effect: Effect = { tag, create, @@ -1593,7 +1586,6 @@ function dispatchAction( currentlyRenderingFiber.expirationTime = renderExpirationTime; } else { if ( - !enableSpeculativeWork && !enableReifyNextWork && fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork) diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 8da5f6a27b3d1..6b7eed3dd52db 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -12,7 +12,6 @@ import type {StackCursor} from './ReactFiberStack'; import type {Container, HostContext} from './ReactFiberHostConfig'; import invariant from 'shared/invariant'; -import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import {getChildHostContext, getRootHostContext} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack'; @@ -96,12 +95,7 @@ function pushHostContext(fiber: Fiber): void { function popHostContext(fiber: Fiber): void { // Do not pop unless this Fiber provided the current context. // pushHostContext() only pushes Fibers that provide unique contexts. - if ( - contextFiberStackCursor.current !== fiber && - (!enableSpeculativeWork || - (contextFiberStackCursor.current.alternate !== null && - contextFiberStackCursor.current.alternate !== fiber)) - ) { + if (contextFiberStackCursor.current !== fiber) { return; } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 3b69ef8055f9f..5fd1e2b5a7a6f 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -87,13 +87,9 @@ export function exitDisallowedContextReadInDEV(): void { } } -let pushedCount = 0; - export function pushProvider(providerFiber: Fiber, nextValue: T): void { const context: ReactContext = providerFiber.type._context; - // console.log(`<====PUSH========= ${++pushedCount} pushed providers`); - if (enableContextReaderPropagation) { // push the previousReaders onto the stack push(valueCursor, context._currentReaders, providerFiber); @@ -139,7 +135,6 @@ export function pushProvider(providerFiber: Fiber, nextValue: T): void { export function popProvider(providerFiber: Fiber): void { const currentValue = valueCursor.current; - // console.log(`======POP========> ${--pushedCount} pushed providers`); pop(valueCursor, providerFiber); diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index 0b045fbd673da..0968421a9fbf7 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -8,7 +8,6 @@ */ import type {Fiber} from './ReactFiber'; -import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; export type StackCursor = {|current: T|}; @@ -41,14 +40,7 @@ function pop(cursor: StackCursor, fiber: Fiber): void { } if (__DEV__) { - // when enableSpeculativeWork is activated we need to allow for a fiber's - // alternate to be popped off the stack due to reification of speculative work - if ( - fiberStack[index] !== fiber && - (!enableSpeculativeWork || - (fiberStack[index].alternate !== null && - fiberStack[index].alternate !== fiber)) - ) { + if (fiberStack[index] !== fiber) { console.error('Unexpected Fiber popped.'); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 002891116bdc9..c18f2499982c8 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -28,7 +28,6 @@ import { flushSuspenseFallbacksInTests, disableSchedulerTimeoutBasedOnReactExpirationTime, enableTrainModelFix, - enableSpeculativeWork, enableReifyNextWork, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -124,12 +123,7 @@ import { Batched, Idle, } from './ReactFiberExpirationTime'; -import { - beginWork as originalBeginWork, - inSpeculativeWorkMode, - endSpeculationWorkIfRootFiber, - didReifySpeculativeWorkDuringThisStep, -} from './ReactFiberBeginWork'; +import {beginWork as originalBeginWork} from './ReactFiberBeginWork'; import {completeWork} from './ReactFiberCompleteWork'; import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork'; import { @@ -1505,13 +1499,6 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { next = beginWork(current, unitOfWork, renderExpirationTime); } - if (enableSpeculativeWork && didReifySpeculativeWorkDuringThisStep()) { - // we need to switch to the alternate becuase the original unitOfWork - // was from the prior commit and the alternate is reified workInProgress - // which we need to send to completeUnitOfWork - unitOfWork = unitOfWork.alternate; - } - resetCurrentDebugFiberInDEV(); // may not want to memoize props here when we did not reifyWork unitOfWork.memoizedProps = unitOfWork.pendingProps; @@ -1534,16 +1521,6 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // need an additional field on the work in progress. const current = workInProgress.alternate; const returnFiber = workInProgress.return; - let workWasSpeculative; - if (enableSpeculativeWork) { - // this whole thing needs to be reworked. I think using mode on the fiber - // makes the most sense. we should clean up the speculative mode of children - // while completing work. this works b/c roots will never be speculative - // this will leave our tree with no speculative mode fibers but will allow - // for more natural logic than what you see below - endSpeculationWorkIfRootFiber(workInProgress); - workWasSpeculative = inSpeculativeWorkMode(); - } // Check if the work completed or if something threw. if ((workInProgress.effectTag & Incomplete) === NoEffect) { @@ -1572,9 +1549,7 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { if ( returnFiber !== null && // Do not append effects to parents if a sibling failed to complete - (returnFiber.effectTag & Incomplete) === NoEffect && - // Do not append effects to parent if workInProgress was speculative - workWasSpeculative !== true + (returnFiber.effectTag & Incomplete) === NoEffect ) { // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 03729505135e6..669c2f816f229 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -54,7 +54,7 @@ describe('ReactNewContext', () => { function Consumer(props) { const observedBits = props.unstable_observedBits; let contextValue; - if (ReactFeatureFlags.enableSpeculativeWork) { + if (ReactFeatureFlags.enableContextSelectors) { contextValue = useContext(Context, observedBits); } else { expect(() => { @@ -74,7 +74,7 @@ describe('ReactNewContext', () => { React.forwardRef(function Consumer(props, ref) { const observedBits = props.unstable_observedBits; let contextValue; - if (ReactFeatureFlags.enableSpeculativeWork) { + if (ReactFeatureFlags.enableContextSelectors) { contextValue = useContext(Context, observedBits); } else { expect(() => { @@ -94,7 +94,7 @@ describe('ReactNewContext', () => { React.memo(function Consumer(props) { const observedBits = props.unstable_observedBits; let contextValue; - if (ReactFeatureFlags.enableSpeculativeWork) { + if (ReactFeatureFlags.enableContextSelectors) { contextValue = useContext(Context, observedBits); } else { expect(() => { @@ -1338,11 +1338,7 @@ describe('ReactNewContext', () => { }); describe('readContext', () => { - // @TODO this API is not currently supported when enableSpeculativeWork is true - // this is because with speculative work the fiber itself must hold necessary - // state to determine all the sources that could disallow a bailout - // readContext is not a hook per se and does not leave a path for using the - // same kind of hook bailout logic required by actual hooks + // @TODO revisit this test with enableReifyNextWork and enableContextSelectors it('can read the same context multiple times in the same function', () => { const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => { let result = 0; diff --git a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js index 66d001c1da526..af34905f6debd 100644 --- a/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSpeculativeWork-test.js @@ -6,7 +6,7 @@ let Scheduler; let levels = 5; let expansion = 3; -describe('ReactSpeculativeWork', () => { +xdescribe('ReactSpeculativeWork', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 451b81271c358..9a0f208696d67 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -6,7 +6,6 @@ */ import getComponentName from 'shared/getComponentName'; -import {enableSpeculativeWork} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; @@ -159,12 +158,6 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { _owner: owner, }; - if (enableSpeculativeWork) { - // allows referential equality checking without having to hang onto old - // elements - element.residue = {}; - } - if (__DEV__) { // The validation flag is currently mutative. We put it on // an external backing store so that we can freeze the whole object. diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 25ae0383b6143..d8a02a544c3a2 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -131,9 +131,6 @@ export const warnUnstableRenderSubtreeIntoContainer = false; // Disables ReactDOM.unstable_createPortal export const disableUnstableCreatePortal = false; -// Turns on speculative work mode features and context selector api support -export const enableSpeculativeWork = false; - // Turns on speculative work mode features and context selector api support export const enableContextSelectors = true; From c8a593790866e5f5483776c4f6fcb9b82765878d Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 13 Apr 2020 23:56:28 -0700 Subject: [PATCH 26/27] additional cleanup --- .../src/ReactFiberBeginWork.js | 25 ------------------- .../src/ReactFiberCompleteWork.js | 2 -- .../src/ReactFiberNewContext.js | 2 +- .../react-reconciler/src/ReactFiberStack.js | 2 +- .../src/ReactFiberWorkLoop.js | 3 --- .../babel/transform-prevent-infinite-loops.js | 4 +-- 6 files changed, 4 insertions(+), 34 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 63a8fc22b8a90..aa691c4474f55 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -218,31 +218,6 @@ if (__DEV__) { didWarnAboutDefaultPropsOnFunctionComponent = {}; } -// holds the last workInProgress fiber before entering speculative mode -// if we work our way back to this fiber without reifying we leave speculative -// mode. if we reify we also clear this fiber upong reification -let speculativeWorkRootFiber: Fiber | null = null; - -// used to tell whether we reified during a particular beginWork execution. This -// is useful to reorient the unitOfWork in the work loop since it may have started -// off as the current (in speculative mode) and needs to become the workInProgress -// if reification happened -let didReifySpeculativeWork: boolean = false; - -export function inSpeculativeWorkMode() { - return speculativeWorkRootFiber !== null; -} - -export function endSpeculationWorkIfRootFiber(fiber: Fiber) { - if (fiber === speculativeWorkRootFiber) { - speculativeWorkRootFiber = null; - } -} - -export function didReifySpeculativeWorkDuringThisStep(): boolean { - return didReifySpeculativeWork; -} - export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e777b8f0de63a..94e7a2ea4fb22 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -54,7 +54,6 @@ import { Chunk, } from 'shared/ReactWorkTags'; import {NoMode, BlockingMode} from './ReactTypeOfMode'; -import {NoWork} from './ReactFiberExpirationTime'; import { Ref, Update, @@ -83,7 +82,6 @@ import { cloneFundamentalInstance, shouldUpdateFundamentalComponent, } from './ReactFiberHostConfig'; -import {inSpeculativeWorkMode} from './ReactFiberBeginWork'; import { getRootHostContainer, popHostContext, diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 5fd1e2b5a7a6f..3ea3d3d8cce17 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -661,7 +661,7 @@ export function readContext( // contexts you read from and their order in the dependency list won't be stable across renders // // the main goal is to attach readers during render so that we are included in the set of - // "fibers that need to know about new context values provided but they current provider" + // "fibers that need to know about new context values provided by the current provider" // it is ok if we don't end up depending on that context in the future, it is safe to still // be notified about future changes which is why this can be done during render phase // diff --git a/packages/react-reconciler/src/ReactFiberStack.js b/packages/react-reconciler/src/ReactFiberStack.js index 0968421a9fbf7..f27aeb65039be 100644 --- a/packages/react-reconciler/src/ReactFiberStack.js +++ b/packages/react-reconciler/src/ReactFiberStack.js @@ -40,7 +40,7 @@ function pop(cursor: StackCursor, fiber: Fiber): void { } if (__DEV__) { - if (fiberStack[index] !== fiber) { + if (fiber !== fiberStack[index]) { console.error('Unexpected Fiber popped.'); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c18f2499982c8..4a7a82d3f681e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1248,7 +1248,6 @@ export function flushControlled(fn: () => mixed): void { } function prepareFreshStack(root, expirationTime) { - // console.log('------------------------------------ prepareFreshStack'); root.finishedWork = null; root.finishedExpirationTime = NoWork; @@ -1292,7 +1291,6 @@ function handleError(root, thrownValue) { do { try { // Reset module-level state that was set during the render phase. - // @TODO may need to do something about speculativeMode here resetContextDependencies(); resetHooksAfterThrow(); resetCurrentDebugFiberInDEV(); @@ -1500,7 +1498,6 @@ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { } resetCurrentDebugFiberInDEV(); - // may not want to memoize props here when we did not reifyWork unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // If this doesn't spawn new work, complete the current work. diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index a24ca6bcdf0ce..ac8ef57b8a40d 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,10 +13,10 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 150003; +const MAX_SOURCE_ITERATIONS = 1500; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. -const MAX_TEST_ITERATIONS = 500003; +const MAX_TEST_ITERATIONS = 5000; module.exports = ({types: t, template}) => { // We set a global so that we can later fail the test From c1c05293f64e433f806bdfabf6efe3770b4f132f Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 4 May 2020 21:11:41 -0700 Subject: [PATCH 27/27] explore force scheduling work to help with react fresh cases --- .../src/ReactFiberBeginWork.js | 26 ++++++++++- .../src/ReactFiberHotReloading.js | 2 +- .../src/ReactFiberWorkLoop.js | 45 ++++++++++++++++--- .../react-refresh/src/ReactFreshRuntime.js | 3 ++ 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index aa691c4474f55..c4c91166f0dde 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2792,6 +2792,7 @@ export function markWorkInProgressReceivedUpdate() { } function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { + // console.log('reifying next work from ', workInProgress.tag); let fiber = workInProgress.child; if (fiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. @@ -2802,11 +2803,32 @@ function reifyNextWork(workInProgress: Fiber, renderExpirationTime) { let nextFiber; if (fiber.mode & ReifiedWorkMode) { + // this fiber and it's sub tree have already been reified. whatever work exists + // there we need to progress forward with it. no need to delve deeper nextFiber = null; } else { fiber.mode |= ReifiedWorkMode; - if (fiber.expirationTime >= renderExpirationTime) { + // console.log( + // '-- checking', + // fiber.tag, + // fiber._debugNeedsRemount, + // fiber.type && fiber.type.name, + // ); + // fiber.alternate && + // console.log( + // '-- checking alternate', + // fiber.alternate.tag, + // fiber.alternate._debugNeedsRemount, + // fiber.type === fiber.alternate.type, + // ); + + if (__DEV__ && fiber._debugNeedsRemount) { + // this fiber needs to be remounted. we can bail out of the reify algo for this + // subtree and let normal work takes it's course + console.log('fiber', fiber.tag, 'has debug marker'); + nextFiber = null; + } else if (fiber.expirationTime >= renderExpirationTime) { let didBailout; switch (fiber.tag) { case ForwardRef: @@ -3023,6 +3045,8 @@ function beginWork( ): Fiber | null { const updateExpirationTime = workInProgress.expirationTime; + // console.log('beginWork', workInProgress.tag); + if (__DEV__) { if (workInProgress._debugNeedsRemount && current !== null) { // This will restart the begin phase with a new fiber. diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.js b/packages/react-reconciler/src/ReactFiberHotReloading.js index ec280e96567c7..94f9735231289 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.js @@ -319,7 +319,7 @@ function scheduleFibersWithFamiliesRecursively( fiber._debugNeedsRemount = true; } if (needsRemount || needsRender) { - scheduleWork(fiber, Sync); + scheduleWork(fiber, Sync, true); } if (child !== null && !needsRemount) { scheduleFibersWithFamiliesRecursively( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 4a7a82d3f681e..738412ae81318 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -380,11 +380,12 @@ export function computeExpirationForFiber( export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime, + force?: boolean, ) { checkForNestedUpdates(); warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber); - const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); + const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime, force); if (root === null) { warnAboutUpdateOnUnmountedFiberInDEV(fiber); return; @@ -453,19 +454,31 @@ export const scheduleWork = scheduleUpdateOnFiber; // work without treating it as a typical update that originates from an event; // e.g. retrying a Suspense boundary isn't an update, but it does schedule work // on a fiber. -function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { +function markUpdateTimeFromFiberToRoot(fiber, expirationTime, force) { // Update the source fiber's expiration time if (fiber.expirationTime < expirationTime) { fiber.expirationTime = expirationTime; if (enableReifyNextWork) { - fiber.mode &= ~ReifiedWorkMode; + if (force === true) { + // console.log('marking path as reified'); + fiber.mode |= ReifiedWorkMode; + } else { + // console.log('marking path as NOT reified'); + fiber.mode &= ~ReifiedWorkMode; + } } } let alternate = fiber.alternate; if (alternate !== null && alternate.expirationTime < expirationTime) { alternate.expirationTime = expirationTime; if (enableReifyNextWork) { - alternate.mode &= ~ReifiedWorkMode; + if (force === true) { + // console.log('marking alternate path as reified'); + alternate.mode |= ReifiedWorkMode; + } else { + // console.log('marking alternate path as NOT reified'); + alternate.mode &= ~ReifiedWorkMode; + } } } // Walk the parent path to the root and update the child expiration time. @@ -479,7 +492,13 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { if (node.childExpirationTime < expirationTime) { node.childExpirationTime = expirationTime; if (enableReifyNextWork) { - node.mode &= ~ReifiedWorkMode; + if (force === true) { + // console.log('marking path as reified'); + node.mode |= ReifiedWorkMode; + } else { + // console.log('marking path as NOT reified'); + node.mode &= ~ReifiedWorkMode; + } } if ( alternate !== null && @@ -487,7 +506,13 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { ) { alternate.childExpirationTime = expirationTime; if (enableReifyNextWork) { - alternate.mode &= ~ReifiedWorkMode; + if (force === true) { + // console.log('marking alternate path as reified'); + alternate.mode |= ReifiedWorkMode; + } else { + // console.log('marking alternate path as NOT reified'); + alternate.mode &= ~ReifiedWorkMode; + } } } } else if ( @@ -496,7 +521,13 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { ) { alternate.childExpirationTime = expirationTime; if (enableReifyNextWork) { - alternate.mode &= ~ReifiedWorkMode; + if (force === true) { + // console.log('marking alternate path as reified'); + alternate.mode |= ReifiedWorkMode; + } else { + // console.log('marking alternate path as NOT reified'); + alternate.mode &= ~ReifiedWorkMode; + } } } if (node.return === null && node.tag === HostRoot) { diff --git a/packages/react-refresh/src/ReactFreshRuntime.js b/packages/react-refresh/src/ReactFreshRuntime.js index 350363ce876ab..66c2185de28d7 100644 --- a/packages/react-refresh/src/ReactFreshRuntime.js +++ b/packages/react-refresh/src/ReactFreshRuntime.js @@ -149,11 +149,14 @@ function isReactClass(type) { function canPreserveStateBetween(prevType, nextType) { if (isReactClass(prevType) || isReactClass(nextType)) { + console.log('this update CANNOT preserve state'); return false; } if (haveEqualSignatures(prevType, nextType)) { + console.log('this update CAN preserve state'); return true; } + console.log('DEFAULT CASE this update CANNOT preserve state'); return false; }