Skip to content

Commit fda85b8

Browse files
committed
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.
1 parent 31fd817 commit fda85b8

File tree

1 file changed

+129
-76
lines changed

1 file changed

+129
-76
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 129 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,10 +2173,10 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
21732173
break outer;
21742174
}
21752175
default: {
2176-
// Continue with the normal work loop.
2176+
// Unwind then continue with the normal work loop.
21772177
workInProgressSuspendedReason = NotSuspended;
21782178
workInProgressThrownValue = null;
2179-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2179+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
21802180
break;
21812181
}
21822182
}
@@ -2285,7 +2285,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
22852285
// Unwind then continue with the normal work loop.
22862286
workInProgressSuspendedReason = NotSuspended;
22872287
workInProgressThrownValue = null;
2288-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2288+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
22892289
break;
22902290
}
22912291
case SuspendedOnData: {
@@ -2343,7 +2343,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
23432343
// Otherwise, unwind then continue with the normal work loop.
23442344
workInProgressSuspendedReason = NotSuspended;
23452345
workInProgressThrownValue = null;
2346-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2346+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
23472347
}
23482348
break;
23492349
}
@@ -2400,7 +2400,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
24002400
// Otherwise, unwind then continue with the normal work loop.
24012401
workInProgressSuspendedReason = NotSuspended;
24022402
workInProgressThrownValue = null;
2403-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2403+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
24042404
break;
24052405
}
24062406
case SuspendedOnDeprecatedThrowPromise: {
@@ -2410,7 +2410,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
24102410
// always unwind.
24112411
workInProgressSuspendedReason = NotSuspended;
24122412
workInProgressThrownValue = null;
2413-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2413+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
24142414
break;
24152415
}
24162416
case SuspendedOnHydration: {
@@ -2610,7 +2610,7 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
26102610
ReactCurrentOwner.current = null;
26112611
}
26122612

2613-
function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2613+
function throwAndUnwindWorkLoop(unitOfWork: Fiber, thrownValue: mixed) {
26142614
// This is a fork of performUnitOfWork specifcally for unwinding a fiber
26152615
// that threw an exception.
26162616
//
@@ -2655,90 +2655,62 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
26552655
throw error;
26562656
}
26572657

2658-
// Return to the normal work loop.
2659-
completeUnitOfWork(unitOfWork);
2658+
if (unitOfWork.flags & Incomplete) {
2659+
// Unwind the stack until we reach the nearest boundary.
2660+
unwindUnitOfWork(unitOfWork);
2661+
} else {
2662+
// Although the fiber suspended, we're intentionally going to commit it in
2663+
// an inconsistent state. We can do this safely in cases where we know the
2664+
// inconsistent tree will be hidden.
2665+
//
2666+
// This currently only applies to Legacy Suspense implementation, but we may
2667+
// port a version of this to concurrent roots, too, when performing a
2668+
// synchronous render. Because that will allow us to mutate the tree as we
2669+
// go instead of buffering mutations until the end. Though it's unclear if
2670+
// this particular path is how that would be implemented.
2671+
completeUnitOfWork(unitOfWork);
2672+
}
26602673
}
26612674

26622675
function completeUnitOfWork(unitOfWork: Fiber): void {
26632676
// Attempt to complete the current unit of work, then move to the next
26642677
// sibling. If there are no more siblings, return to the parent fiber.
26652678
let completedWork: Fiber = unitOfWork;
26662679
do {
2680+
if ((completedWork.flags & Incomplete) !== NoFlags) {
2681+
// This fiber did not complete, because one of its children did not
2682+
// complete. Switch to unwinding the stack instead of completing it.
2683+
//
2684+
// The reason "unwind" and "complete" is interleaved is because when
2685+
// something suspends, we continue rendering the siblings even though
2686+
// they will be replaced by a fallback.
2687+
// TODO: Disable sibling prerendering, then remove this branch.
2688+
unwindUnitOfWork(completedWork);
2689+
return;
2690+
}
2691+
26672692
// The current, flushed, state of this fiber is the alternate. Ideally
26682693
// nothing should rely on this, but relying on it here means that we don't
26692694
// need an additional field on the work in progress.
26702695
const current = completedWork.alternate;
26712696
const returnFiber = completedWork.return;
26722697

2673-
// Check if the work completed or if something threw.
2674-
if ((completedWork.flags & Incomplete) === NoFlags) {
2675-
setCurrentDebugFiberInDEV(completedWork);
2676-
let next;
2677-
if (
2678-
!enableProfilerTimer ||
2679-
(completedWork.mode & ProfileMode) === NoMode
2680-
) {
2681-
next = completeWork(current, completedWork, renderLanes);
2682-
} else {
2683-
startProfilerTimer(completedWork);
2684-
next = completeWork(current, completedWork, renderLanes);
2685-
// Update render duration assuming we didn't error.
2686-
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
2687-
}
2688-
resetCurrentDebugFiberInDEV();
2689-
2690-
if (next !== null) {
2691-
// Completing this fiber spawned new work. Work on that next.
2692-
workInProgress = next;
2693-
return;
2694-
}
2698+
setCurrentDebugFiberInDEV(completedWork);
2699+
let next;
2700+
if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) {
2701+
next = completeWork(current, completedWork, renderLanes);
26952702
} else {
2696-
// This fiber did not complete because something threw. Pop values off
2697-
// the stack without entering the complete phase. If this is a boundary,
2698-
// capture values if possible.
2699-
const next = unwindWork(current, completedWork, renderLanes);
2700-
2701-
// Because this fiber did not complete, don't reset its lanes.
2702-
2703-
if (next !== null) {
2704-
// If completing this work spawned new work, do that next. We'll come
2705-
// back here again.
2706-
// Since we're restarting, remove anything that is not a host effect
2707-
// from the effect tag.
2708-
next.flags &= HostEffectMask;
2709-
workInProgress = next;
2710-
return;
2711-
}
2712-
2713-
if (
2714-
enableProfilerTimer &&
2715-
(completedWork.mode & ProfileMode) !== NoMode
2716-
) {
2717-
// Record the render duration for the fiber that errored.
2718-
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
2719-
2720-
// Include the time spent working on failed children before continuing.
2721-
let actualDuration = completedWork.actualDuration;
2722-
let child = completedWork.child;
2723-
while (child !== null) {
2724-
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
2725-
actualDuration += child.actualDuration;
2726-
child = child.sibling;
2727-
}
2728-
completedWork.actualDuration = actualDuration;
2729-
}
2703+
startProfilerTimer(completedWork);
2704+
next = completeWork(current, completedWork, renderLanes);
2705+
// Update render duration assuming we didn't error.
2706+
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
2707+
}
2708+
resetCurrentDebugFiberInDEV();
27302709

2731-
if (returnFiber !== null) {
2732-
// Mark the parent fiber as incomplete and clear its subtree flags.
2733-
returnFiber.flags |= Incomplete;
2734-
returnFiber.subtreeFlags = NoFlags;
2735-
returnFiber.deletions = null;
2736-
} else {
2737-
// We've unwound all the way to the root.
2738-
workInProgressRootExitStatus = RootDidNotComplete;
2739-
workInProgress = null;
2740-
return;
2741-
}
2710+
if (next !== null) {
2711+
// Completing this fiber spawned new work. Work on that next.
2712+
workInProgress = next;
2713+
return;
27422714
}
27432715

27442716
const siblingFiber = completedWork.sibling;
@@ -2760,6 +2732,87 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
27602732
}
27612733
}
27622734

2735+
function unwindUnitOfWork(unitOfWork: Fiber): void {
2736+
let incompleteWork: Fiber = unitOfWork;
2737+
do {
2738+
// The current, flushed, state of this fiber is the alternate. Ideally
2739+
// nothing should rely on this, but relying on it here means that we don't
2740+
// need an additional field on the work in progress.
2741+
const current = incompleteWork.alternate;
2742+
2743+
// This fiber did not complete because something threw. Pop values off
2744+
// the stack without entering the complete phase. If this is a boundary,
2745+
// capture values if possible.
2746+
const next = unwindWork(current, incompleteWork, renderLanes);
2747+
2748+
// Because this fiber did not complete, don't reset its lanes.
2749+
2750+
if (next !== null) {
2751+
// Found a boundary that can handle this exception. Re-renter the
2752+
// begin phase. This branch will return us to the normal work loop.
2753+
//
2754+
// Since we're restarting, remove anything that is not a host effect
2755+
// from the effect tag.
2756+
next.flags &= HostEffectMask;
2757+
workInProgress = next;
2758+
return;
2759+
}
2760+
2761+
// Keep unwinding until we reach either a boundary or the root.
2762+
2763+
if (enableProfilerTimer && (incompleteWork.mode & ProfileMode) !== NoMode) {
2764+
// Record the render duration for the fiber that errored.
2765+
stopProfilerTimerIfRunningAndRecordDelta(incompleteWork, false);
2766+
2767+
// Include the time spent working on failed children before continuing.
2768+
let actualDuration = incompleteWork.actualDuration;
2769+
let child = incompleteWork.child;
2770+
while (child !== null) {
2771+
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
2772+
actualDuration += child.actualDuration;
2773+
child = child.sibling;
2774+
}
2775+
incompleteWork.actualDuration = actualDuration;
2776+
}
2777+
2778+
// TODO: Once we stop prerendering siblings, instead of resetting the parent
2779+
// of the node being unwound, we should be able to reset node itself as we
2780+
// unwind the stack. Saves an additional null check.
2781+
const returnFiber = incompleteWork.return;
2782+
if (returnFiber !== null) {
2783+
// Mark the parent fiber as incomplete and clear its subtree flags.
2784+
// TODO: Once we stop prerendering siblings, we may be able to get rid of
2785+
// the Incomplete flag because unwinding to the nearest boundary will
2786+
// happen synchronously.
2787+
returnFiber.flags |= Incomplete;
2788+
returnFiber.subtreeFlags = NoFlags;
2789+
returnFiber.deletions = null;
2790+
}
2791+
2792+
// If there are siblings, work on them now even though they're going to be
2793+
// replaced by a fallback. We're "prerendering" them. Historically our
2794+
// rationale for this behavior has been to initiate any lazy data requests
2795+
// in the siblings, and also to warm up the CPU cache.
2796+
// TODO: Don't prerender siblings. With `use`, we suspend the work loop
2797+
// until the data has resolved, anyway.
2798+
const siblingFiber = incompleteWork.sibling;
2799+
if (siblingFiber !== null) {
2800+
// This branch will return us to the normal work loop.
2801+
workInProgress = siblingFiber;
2802+
return;
2803+
}
2804+
// Otherwise, return to the parent
2805+
// $FlowFixMe[incompatible-type] we bail out when we get a null
2806+
incompleteWork = returnFiber;
2807+
// Update the next thing we're working on in case something throws.
2808+
workInProgress = incompleteWork;
2809+
} while (incompleteWork !== null);
2810+
2811+
// We've unwound all the way to the root.
2812+
workInProgressRootExitStatus = RootDidNotComplete;
2813+
workInProgress = null;
2814+
}
2815+
27632816
function commitRoot(
27642817
root: FiberRoot,
27652818
recoverableErrors: null | Array<CapturedValue<mixed>>,

0 commit comments

Comments
 (0)