From acb551e01645053c4c1d503af0100fdb3bbd1358 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 4 Feb 2020 16:31:04 -0800 Subject: [PATCH] Factor out render phase Splits the work loop and its surrounding enter/exit code into their own functions. Now we can do perform multiple render phase passes within a single call to performConcurrentWorkOnRoot or performSyncWorkOnRoot. This lets us get rid of the `didError` field. --- .../react-reconciler/src/ReactFiberRoot.js | 7 - .../src/ReactFiberWorkLoop.js | 330 +++++++++--------- 2 files changed, 162 insertions(+), 175 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index eb1db38e7ee67..e0de5ecfd8970 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -74,10 +74,6 @@ type BaseFiberRootProperties = {| // render again lastPingedTime: ExpirationTime, lastExpiredTime: ExpirationTime, - - // Set to true when the root completes with an error. We'll try rendering - // again. This field ensures we don't retry more than once. - didError: boolean, |}; // The following attributes are only used by interaction tracing builds. @@ -127,7 +123,6 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.nextKnownPendingLevel = NoWork; this.lastPingedTime = NoWork; this.lastExpiredTime = NoWork; - this.didError = false; if (enableSchedulerTracing) { this.interactionThreadID = unstable_getThreadID(); @@ -254,8 +249,6 @@ export function markRootFinishedAtTime( // Clear the expired time root.lastExpiredTime = NoWork; } - - root.didError = false; } export function markRootExpiredAtTime( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 899a1d8872255..7c860b29a7f7a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -647,7 +647,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) { currentEventTime = NoWork; // Check if the render expired. - const expirationTime = getNextRootExpirationTimeToWorkOn(root); 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. @@ -660,83 +659,52 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // Determine the next expiration time to work on, using the fields stored // on the root. - if (expirationTime !== NoWork) { - const originalCallbackNode = root.callbackNode; - invariant( - (executionContext & (RenderContext | CommitContext)) === NoContext, - 'Should not already be working.', - ); - - flushPassiveEffects(); + let expirationTime = getNextRootExpirationTimeToWorkOn(root); + if (expirationTime === NoWork) { + return null; + } + const originalCallbackNode = root.callbackNode; + invariant( + (executionContext & (RenderContext | CommitContext)) === NoContext, + 'Should not already be working.', + ); - // If the root or expiration time have changed, throw out the existing stack - // and prepare a fresh one. Otherwise we'll continue where we left off. - if ( - root !== workInProgressRoot || - expirationTime !== renderExpirationTime - ) { - prepareFreshStack(root, expirationTime); - startWorkOnPendingInteractions(root, expirationTime); - } - - // If we have a work-in-progress fiber, it means there's still work to do - // in this root. - if (workInProgress !== null) { - const prevExecutionContext = executionContext; - executionContext |= RenderContext; - const prevDispatcher = pushDispatcher(root); - const prevInteractions = pushInteractions(root); - startWorkLoopTimer(workInProgress); - do { - try { - workLoopConcurrent(); - break; - } catch (thrownValue) { - handleError(root, thrownValue); - } - } while (true); - resetContextDependencies(); - executionContext = prevExecutionContext; - popDispatcher(prevDispatcher); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } + flushPassiveEffects(); - if (workInProgressRootExitStatus === RootFatalErrored) { - const fatalError = workInProgressRootFatalError; - stopInterruptedWorkLoopTimer(); - prepareFreshStack(root, expirationTime); - markRootSuspendedAtTime(root, expirationTime); - ensureRootIsScheduled(root); - throw fatalError; - } + let exitStatus = renderRootConcurrent(root, expirationTime); - if (workInProgress !== null) { - // There's still work left over. Exit without committing. - stopInterruptedWorkLoopTimer(); - } else { - // We now have a consistent tree. The next step is either to commit it, - // or, if something suspended, wait to commit it after a timeout. - stopFinishedWorkLoopTimer(); - - const finishedWork: Fiber = ((root.finishedWork = - root.current.alternate): any); - root.finishedExpirationTime = expirationTime; - finishConcurrentRender( - root, - finishedWork, - workInProgressRootExitStatus, - expirationTime, - ); - } + if (exitStatus !== RootIncomplete) { + if (exitStatus === RootErrored) { + // If something threw an error, try rendering one more time. We'll + // render synchronously to block concurrent data mutations, and we'll + // render at Idle (or lower) so that all pending updates are included. + // If it still fails after the second attempt, we'll give up and commit + // the resulting tree. + expirationTime = expirationTime > Idle ? Idle : expirationTime; + exitStatus = renderRootSync(root, expirationTime); + } + if (exitStatus === RootFatalErrored) { + const fatalError = workInProgressRootFatalError; + prepareFreshStack(root, expirationTime); + markRootSuspendedAtTime(root, expirationTime); ensureRootIsScheduled(root); - if (root.callbackNode === originalCallbackNode) { - // The task node scheduled for this root is the same one that's - // currently executed. Need to return a continuation. - return performConcurrentWorkOnRoot.bind(null, root); - } + throw fatalError; } + + // We now have a consistent tree. The next step is either to commit it, + // or, if something suspended, wait to commit it after a timeout. + const finishedWork: Fiber = ((root.finishedWork = + root.current.alternate): any); + root.finishedExpirationTime = expirationTime; + finishConcurrentRender(root, finishedWork, exitStatus, expirationTime); + } + + ensureRootIsScheduled(root); + if (root.callbackNode === originalCallbackNode) { + // The task node scheduled for this root is the same one that's + // currently executed. Need to return a continuation. + return performConcurrentWorkOnRoot.bind(null, root); } return null; } @@ -747,9 +715,6 @@ function finishConcurrentRender( exitStatus, expirationTime, ) { - // Set this to null to indicate there's no in-progress render. - workInProgressRoot = null; - switch (exitStatus) { case RootIncomplete: case RootFatalErrored: { @@ -759,20 +724,9 @@ function finishConcurrentRender( // statement, but eslint doesn't know about invariant, so it complains // if I do. eslint-disable-next-line no-fallthrough case RootErrored: { - // If this was an async render, the error may have happened due to - // a mutation in a concurrent event. Try rendering one more time, - // synchronously, to see if the error goes away. If there are - // lower priority updates, let's include those, too, in case they - // fix the inconsistency. Render at Idle to include all updates. - // If it was Idle or Never or some not-yet-invented time, render - // at that time. - markRootExpiredAtTime( - root, - expirationTime > Idle ? Idle : expirationTime, - ); - root.didError = true; - // We assume that this second render pass will be synchronous - // and therefore not hit this path again. + // We should have already attempted to retry this tree. If we reached + // this point, it errored again. Commit it. + commitRoot(root); break; } case RootSuspended: { @@ -1012,98 +966,44 @@ function performSyncWorkOnRoot(root) { } else { // Start a fresh tree. expirationTime = lastExpiredTime; - prepareFreshStack(root, expirationTime); - startWorkOnPendingInteractions(root, expirationTime); } } else { // There's no expired work. This must be a new, synchronous render. expirationTime = Sync; - prepareFreshStack(root, expirationTime); - startWorkOnPendingInteractions(root, expirationTime); } - // If we have a work-in-progress fiber, it means there's still work to do - // in this root. - if (workInProgress !== null) { - const prevExecutionContext = executionContext; - executionContext |= RenderContext; - const prevDispatcher = pushDispatcher(root); - const prevInteractions = pushInteractions(root); - startWorkLoopTimer(workInProgress); - - do { - try { - workLoopSync(); - break; - } catch (thrownValue) { - handleError(root, thrownValue); - } - } while (true); - resetContextDependencies(); - executionContext = prevExecutionContext; - popDispatcher(prevDispatcher); - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - } - - if (workInProgressRootExitStatus === RootFatalErrored) { - const fatalError = workInProgressRootFatalError; - stopInterruptedWorkLoopTimer(); - prepareFreshStack(root, expirationTime); - markRootSuspendedAtTime(root, expirationTime); - ensureRootIsScheduled(root); - throw fatalError; - } + let exitStatus = renderRootSync(root, expirationTime); - if (workInProgress !== null) { - // This is a sync render, so we should have finished the whole tree. - invariant( - false, - 'Cannot commit an incomplete root. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - } else { - // 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); - root.finishedExpirationTime = expirationTime; - finishSyncRender(root); - } + if (root.tag !== LegacyRoot && exitStatus === RootErrored) { + // If something threw an error, try rendering one more time. We'll + // render synchronously to block concurrent data mutations, and we'll + // render at Idle (or lower) so that all pending updates are included. + // If it still fails after the second attempt, we'll give up and commit + // the resulting tree. + expirationTime = expirationTime > Idle ? Idle : expirationTime; + exitStatus = renderRootSync(root, expirationTime); + } - // Before exiting, make sure there's a callback scheduled for the next - // pending level. + if (exitStatus === RootFatalErrored) { + const fatalError = workInProgressRootFatalError; + prepareFreshStack(root, expirationTime); + markRootSuspendedAtTime(root, expirationTime); ensureRootIsScheduled(root); + throw fatalError; } - return null; -} + // We now have a consistent tree. Because this is a sync render, we + // will commit it even if something suspended. + root.finishedWork = (root.current.alternate: any); + root.finishedExpirationTime = expirationTime; -function finishSyncRender(root) { - // Set this to null to indicate there's no in-progress render. - workInProgressRoot = null; + commitRoot(root); - // Try rendering one more time, synchronously, to see if the error goes away. - // If there are lower priority updates, let's include those, too, in case they - // fix the inconsistency. Render at Idle to include all updates. If it was - // Idle or Never or some not-yet-invented time, render at that time. - if ( - workInProgressRootExitStatus === RootErrored && - // Legacy mode shouldn't retry, only blocking and concurrent mode - root.tag !== LegacyRoot && - // Don't retry more than once. - !root.didError - ) { - root.didError = true; - markRootExpiredAtTime( - root, - renderExpirationTime > Idle ? Idle : renderExpirationTime, - ); - } else { - // In all other cases, since this was a synchronous render, - // commit immediately. - commitRoot(root); - } + // Before exiting, make sure there's a callback scheduled for the next + // pending level. + ensureRootIsScheduled(root); + + return null; } export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) { @@ -1484,6 +1384,53 @@ function inferTimeFromExpirationTimeWithSuspenseConfig( ); } +function renderRootSync(root, expirationTime) { + const prevExecutionContext = executionContext; + executionContext |= RenderContext; + const prevDispatcher = pushDispatcher(root); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteractions(root, expirationTime); + } + + const prevInteractions = pushInteractions(root); + startWorkLoopTimer(workInProgress); + do { + try { + workLoopSync(); + break; + } catch (thrownValue) { + handleError(root, thrownValue); + } + } while (true); + resetContextDependencies(); + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + } + + executionContext = prevExecutionContext; + popDispatcher(prevDispatcher); + + if (workInProgress !== null) { + // This is a sync render, so we should have finished the whole tree. + invariant( + false, + 'Cannot commit an incomplete root. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); + } + + stopFinishedWorkLoopTimer(); + + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + return workInProgressRootExitStatus; +} + // The work loop is an extremely hot path. Tell Closure not to inline it. /** @noinline */ function workLoopSync() { @@ -1493,6 +1440,53 @@ function workLoopSync() { } } +function renderRootConcurrent(root, expirationTime) { + const prevExecutionContext = executionContext; + executionContext |= RenderContext; + const prevDispatcher = pushDispatcher(root); + + // If the root or expiration time have changed, throw out the existing stack + // and prepare a fresh one. Otherwise we'll continue where we left off. + if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) { + prepareFreshStack(root, expirationTime); + startWorkOnPendingInteractions(root, expirationTime); + } + + const prevInteractions = pushInteractions(root); + startWorkLoopTimer(workInProgress); + do { + try { + workLoopConcurrent(); + break; + } catch (thrownValue) { + handleError(root, thrownValue); + } + } while (true); + resetContextDependencies(); + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + } + + popDispatcher(prevDispatcher); + executionContext = prevExecutionContext; + + // Check if the tree has completed. + if (workInProgress !== null) { + // Still work remaining. + stopInterruptedWorkLoopTimer(); + return RootIncomplete; + } else { + // Completed the tree. + stopFinishedWorkLoopTimer(); + + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + + // Return the final exit status. + return workInProgressRootExitStatus; + } +} + /** @noinline */ function workLoopConcurrent() { // Perform work until Scheduler asks us to yield