From 6175521dcfa5c2f65b4046408f8728e05a7cb59c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 13 Mar 2023 12:25:22 -0400 Subject: [PATCH] Fork completeUnitOfWork into unwind and complete A mini-refactor to split completeUnitOfWork into two functions: completeUnitOfWork and unwindUnitOfWork. The existing function is already almost complete forked. I think splitting them up makes sense because it makes it easier to specialize the behavior. My practical motivation is that I'm going to change the "unwind" phase to synchronously unwind to the nearest Suspense/error boundary. This means we'll no longer prerender the siblings of a suspended tree. I'll address this in a subsequent step. --- .../src/ReactFiberWorkLoop.js | 200 +++++++++++------- 1 file changed, 125 insertions(+), 75 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 0bad69e81aa23..2dc18e603415b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -2085,10 +2085,10 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { break outer; } default: { - // Continue with the normal work loop. + // Unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } } @@ -2197,7 +2197,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } case SuspendedOnData: { @@ -2250,7 +2250,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Otherwise, unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); } break; } @@ -2261,7 +2261,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // always unwind. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } case SuspendedOnHydration: { @@ -2461,7 +2461,7 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { ReactCurrentOwner.current = null; } -function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { +function throwAndUnwindWorkLoop(unitOfWork: Fiber, thrownValue: mixed) { // This is a fork of performUnitOfWork specifcally for unwinding a fiber // that threw an exception. // @@ -2506,8 +2506,21 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { throw error; } - // Return to the normal work loop. - completeUnitOfWork(unitOfWork); + if (unitOfWork.flags & Incomplete) { + // Unwind the stack until we reach the nearest boundary. + unwindUnitOfWork(unitOfWork); + } else { + // Although the fiber suspended, we're intentionally going to commit it in + // an inconsistent state. We can do this safely in cases where we know the + // inconsistent tree will be hidden. + // + // This currently only applies to Legacy Suspense implementation, but we may + // port a version of this to concurrent roots, too, when performing a + // synchronous render. Because that will allow us to mutate the tree as we + // go instead of buffering mutations until the end. Though it's unclear if + // this particular path is how that would be implemented. + completeUnitOfWork(unitOfWork); + } } function completeUnitOfWork(unitOfWork: Fiber): void { @@ -2515,81 +2528,37 @@ function completeUnitOfWork(unitOfWork: Fiber): void { // sibling. If there are no more siblings, return to the parent fiber. let completedWork: Fiber = unitOfWork; do { + if ((completedWork.flags & Incomplete) !== NoFlags) { + // This fiber did not complete, because one of its children did not + // complete. Switch to unwinding the stack instead of completing it. + // + // The reason "unwind" and "complete" is interleaved + unwindUnitOfWork(completedWork); + return; + } + // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = completedWork.alternate; const returnFiber = completedWork.return; - // Check if the work completed or if something threw. - if ((completedWork.flags & Incomplete) === NoFlags) { - setCurrentDebugFiberInDEV(completedWork); - let next; - if ( - !enableProfilerTimer || - (completedWork.mode & ProfileMode) === NoMode - ) { - next = completeWork(current, completedWork, renderLanes); - } else { - startProfilerTimer(completedWork); - next = completeWork(current, completedWork, renderLanes); - // Update render duration assuming we didn't error. - stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); - } - resetCurrentDebugFiberInDEV(); - - if (next !== null) { - // Completing this fiber spawned new work. Work on that next. - workInProgress = next; - return; - } + setCurrentDebugFiberInDEV(completedWork); + let next; + if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) { + next = completeWork(current, completedWork, renderLanes); } else { - // 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. - const next = unwindWork(current, completedWork, renderLanes); - - // Because this fiber did not complete, don't reset its lanes. - - if (next !== null) { - // If completing this work spawned new work, do that next. We'll come - // back here again. - // Since we're restarting, remove anything that is not a host effect - // from the effect tag. - next.flags &= HostEffectMask; - workInProgress = next; - return; - } - - if ( - enableProfilerTimer && - (completedWork.mode & ProfileMode) !== NoMode - ) { - // Record the render duration for the fiber that errored. - stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); - - // Include the time spent working on failed children before continuing. - let actualDuration = completedWork.actualDuration; - let child = completedWork.child; - while (child !== null) { - // $FlowFixMe[unsafe-addition] addition with possible null/undefined value - actualDuration += child.actualDuration; - child = child.sibling; - } - completedWork.actualDuration = actualDuration; - } + startProfilerTimer(completedWork); + next = completeWork(current, completedWork, renderLanes); + // Update render duration assuming we didn't error. + stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); + } + resetCurrentDebugFiberInDEV(); - if (returnFiber !== null) { - // Mark the parent fiber as incomplete and clear its subtree flags. - returnFiber.flags |= Incomplete; - returnFiber.subtreeFlags = NoFlags; - returnFiber.deletions = null; - } else { - // We've unwound all the way to the root. - workInProgressRootExitStatus = RootDidNotComplete; - workInProgress = null; - return; - } + if (next !== null) { + // Completing this fiber spawned new work. Work on that next. + workInProgress = next; + return; } const siblingFiber = completedWork.sibling; @@ -2611,6 +2580,87 @@ function completeUnitOfWork(unitOfWork: Fiber): void { } } +function unwindUnitOfWork(unitOfWork: Fiber): void { + let incompleteWork: Fiber = unitOfWork; + do { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = incompleteWork.alternate; + + // 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. + const next = unwindWork(current, incompleteWork, renderLanes); + + // Because this fiber did not complete, don't reset its lanes. + + if (next !== null) { + // Found a boundary that can handle this exception. Re-renter the + // begin phase. This branch will return us to the normal work loop. + // + // Since we're restarting, remove anything that is not a host effect + // from the effect tag. + next.flags &= HostEffectMask; + workInProgress = next; + return; + } + + // Keep unwinding until we reach either a boundary or the root. + + if (enableProfilerTimer && (incompleteWork.mode & ProfileMode) !== NoMode) { + // Record the render duration for the fiber that errored. + stopProfilerTimerIfRunningAndRecordDelta(incompleteWork, false); + + // Include the time spent working on failed children before continuing. + let actualDuration = incompleteWork.actualDuration; + let child = incompleteWork.child; + while (child !== null) { + // $FlowFixMe[unsafe-addition] addition with possible null/undefined value + actualDuration += child.actualDuration; + child = child.sibling; + } + incompleteWork.actualDuration = actualDuration; + } + + // TODO: Once we stop prerendering siblings, instead of resetting the parent + // of the node being unwound, we should be able to reset node itself as we + // unwind the stack. Saves an additional null check. + const returnFiber = incompleteWork.return; + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its subtree flags. + // TODO: Once we stop prerendering siblings, we may be able to get rid of + // the Incomplete flag because unwinding to the nearest boundary will + // happen synchronously. + returnFiber.flags |= Incomplete; + returnFiber.subtreeFlags = NoFlags; + returnFiber.deletions = null; + } + + // If there are siblings, work on them now even though they're going to be + // replaced by a fallback. We're "prerendering" them. Historically our + // rationale for this behavior has been to initiate any lazy data requests + // in the siblings, and also to warm up the CPU cache. + // TODO: Don't prerender siblings. With `use`, we suspend the work loop + // until the data has resolved, anyway. + const siblingFiber = incompleteWork.sibling; + if (siblingFiber !== null) { + // This branch will return us to the normal work loop. + workInProgress = siblingFiber; + return; + } + // Otherwise, return to the parent + // $FlowFixMe[incompatible-type] we bail out when we get a null + incompleteWork = returnFiber; + // Update the next thing we're working on in case something throws. + workInProgress = incompleteWork; + } while (incompleteWork !== null); + + // We've unwound all the way to the root. + workInProgressRootExitStatus = RootDidNotComplete; + workInProgress = null; +} + function commitRoot( root: FiberRoot, recoverableErrors: null | Array>,