Skip to content

Commit

Permalink
Fork completeUnitOfWork into unwind and complete
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Mar 13, 2023
1 parent cd20376 commit 5f4bfbc
Showing 1 changed file with 128 additions and 75 deletions.
203 changes: 128 additions & 75 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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: {
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -2506,90 +2506,62 @@ 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 {
// Attempt to complete the current unit of work, then move to the next
// 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 is because when
// something suspends, we continue rendering the siblings even though
// they will be replaced by a fallback.
// TODO: Disable sibling prerendering, then remove this branch.
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;
Expand All @@ -2611,6 +2583,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<CapturedValue<mixed>>,
Expand Down

0 comments on commit 5f4bfbc

Please sign in to comment.