Skip to content

Commit 2eac58a

Browse files
committed
Move unwind after error into main work loop
I need to be able to yield to the main thread in between when an error is thrown and when the stack is unwound. (This is the motivation behind the refactor, but it isn't implemented in this commit.) Currently the unwind is inlined directly into `handleError`. Instead, I've moved the unwind logic into the main work loop. At the very beginning of the function, we check to see if the work-in-progress is in a "suspended" state — that is, whether it needs to be unwound. If it is, we will enter the unwind phase instead of the begin phase. We only need to perform this check when we first enter the work loop: at the beginning of a Scheduler chunk, or after something throws. We don't need to perform it after every unit of work.
1 parent fbb94d3 commit 2eac58a

File tree

2 files changed

+70
-4
lines changed

2 files changed

+70
-4
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,12 @@ let workInProgress: Fiber | null = null;
280280
// The lanes we're rendering
281281
let workInProgressRootRenderLanes: Lanes = NoLanes;
282282

283+
// When this is true, the work-in-progress fiber just suspended (or errored) and
284+
// we've yet to unwind the stack. In some cases, we may yield to the main thread
285+
// after this happens. If the fiber is pinged before we resume, we can retry
286+
// immediately instead of unwinding the stack.
287+
let workInProgressIsSuspended: boolean = false;
288+
283289
// A contextual version of workInProgressRootRenderLanes. It is a superset of
284290
// the lanes that we started working on at the root. When we enter a subtree
285291
// that is currently hidden, we add the lanes that would have committed if
@@ -1548,6 +1554,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
15481554
const rootWorkInProgress = createWorkInProgress(root.current, null);
15491555
workInProgress = rootWorkInProgress;
15501556
workInProgressRootRenderLanes = renderLanes = lanes;
1557+
workInProgressIsSuspended = false;
15511558
workInProgressRootExitStatus = RootInProgress;
15521559
workInProgressRootFatalError = null;
15531560
workInProgressRootSkippedLanes = NoLanes;
@@ -1632,7 +1639,11 @@ function handleError(root, thrownValue): void {
16321639
thrownValue,
16331640
workInProgressRootRenderLanes,
16341641
);
1635-
completeUnitOfWork(erroredWork);
1642+
// Setting this to `true` tells the work loop to unwind the stack instead
1643+
// of entering the begin phase. It's called "suspended" because it usually
1644+
// happens because of Suspense, but it also applies to errors. Think of it
1645+
// as suspending the execution of the work loop.
1646+
workInProgressIsSuspended = true;
16361647
} catch (yetAnotherThrownValue) {
16371648
// Something in the return path also threw.
16381649
thrownValue = yetAnotherThrownValue;
@@ -1810,7 +1821,14 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
18101821
// The work loop is an extremely hot path. Tell Closure not to inline it.
18111822
/** @noinline */
18121823
function workLoopSync() {
1813-
// Already timed out, so perform work without checking if we need to yield.
1824+
// Perform work without checking if we need to yield between fiber.
1825+
1826+
if (workInProgressIsSuspended && workInProgress !== null) {
1827+
// The current work-in-progress was already attempted. We need to unwind
1828+
// it before we continue the normal work loop.
1829+
resumeSuspendedUnitOfWork(workInProgress);
1830+
}
1831+
18141832
while (workInProgress !== null) {
18151833
performUnitOfWork(workInProgress);
18161834
}
@@ -1899,6 +1917,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
18991917
/** @noinline */
19001918
function workLoopConcurrent() {
19011919
// Perform work until Scheduler asks us to yield
1920+
1921+
if (workInProgressIsSuspended && workInProgress !== null) {
1922+
// The current work-in-progress was already attempted. We need to unwind
1923+
// it before we continue the normal work loop.
1924+
resumeSuspendedUnitOfWork(workInProgress);
1925+
}
1926+
19021927
while (workInProgress !== null && !shouldYield()) {
19031928
performUnitOfWork(workInProgress);
19041929
}
@@ -1932,6 +1957,14 @@ function performUnitOfWork(unitOfWork: Fiber): void {
19321957
ReactCurrentOwner.current = null;
19331958
}
19341959

1960+
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
1961+
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
1962+
// just suspended. It's a separate function to keep the additional logic out
1963+
// of the work loop's hot path.
1964+
workInProgressIsSuspended = false;
1965+
completeUnitOfWork(unitOfWork);
1966+
}
1967+
19351968
function completeUnitOfWork(unitOfWork: Fiber): void {
19361969
// Attempt to complete the current unit of work, then move to the next
19371970
// sibling. If there are no more siblings, return to the parent fiber.

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,12 @@ let workInProgress: Fiber | null = null;
280280
// The lanes we're rendering
281281
let workInProgressRootRenderLanes: Lanes = NoLanes;
282282

283+
// When this is true, the work-in-progress fiber just suspended (or errored) and
284+
// we've yet to unwind the stack. In some cases, we may yield to the main thread
285+
// after this happens. If the fiber is pinged before we resume, we can retry
286+
// immediately instead of unwinding the stack.
287+
let workInProgressIsSuspended: boolean = false;
288+
283289
// A contextual version of workInProgressRootRenderLanes. It is a superset of
284290
// the lanes that we started working on at the root. When we enter a subtree
285291
// that is currently hidden, we add the lanes that would have committed if
@@ -1548,6 +1554,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
15481554
const rootWorkInProgress = createWorkInProgress(root.current, null);
15491555
workInProgress = rootWorkInProgress;
15501556
workInProgressRootRenderLanes = renderLanes = lanes;
1557+
workInProgressIsSuspended = false;
15511558
workInProgressRootExitStatus = RootInProgress;
15521559
workInProgressRootFatalError = null;
15531560
workInProgressRootSkippedLanes = NoLanes;
@@ -1632,7 +1639,11 @@ function handleError(root, thrownValue): void {
16321639
thrownValue,
16331640
workInProgressRootRenderLanes,
16341641
);
1635-
completeUnitOfWork(erroredWork);
1642+
// Setting this to `true` tells the work loop to unwind the stack instead
1643+
// of entering the begin phase. It's called "suspended" because it usually
1644+
// happens because of Suspense, but it also applies to errors. Think of it
1645+
// as suspending the execution of the work loop.
1646+
workInProgressIsSuspended = true;
16361647
} catch (yetAnotherThrownValue) {
16371648
// Something in the return path also threw.
16381649
thrownValue = yetAnotherThrownValue;
@@ -1810,7 +1821,14 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
18101821
// The work loop is an extremely hot path. Tell Closure not to inline it.
18111822
/** @noinline */
18121823
function workLoopSync() {
1813-
// Already timed out, so perform work without checking if we need to yield.
1824+
// Perform work without checking if we need to yield between fiber.
1825+
1826+
if (workInProgressIsSuspended && workInProgress !== null) {
1827+
// The current work-in-progress was already attempted. We need to unwind
1828+
// it before we continue the normal work loop.
1829+
resumeSuspendedUnitOfWork(workInProgress);
1830+
}
1831+
18141832
while (workInProgress !== null) {
18151833
performUnitOfWork(workInProgress);
18161834
}
@@ -1899,6 +1917,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
18991917
/** @noinline */
19001918
function workLoopConcurrent() {
19011919
// Perform work until Scheduler asks us to yield
1920+
1921+
if (workInProgressIsSuspended && workInProgress !== null) {
1922+
// The current work-in-progress was already attempted. We need to unwind
1923+
// it before we continue the normal work loop.
1924+
resumeSuspendedUnitOfWork(workInProgress);
1925+
}
1926+
19021927
while (workInProgress !== null && !shouldYield()) {
19031928
performUnitOfWork(workInProgress);
19041929
}
@@ -1932,6 +1957,14 @@ function performUnitOfWork(unitOfWork: Fiber): void {
19321957
ReactCurrentOwner.current = null;
19331958
}
19341959

1960+
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
1961+
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
1962+
// just suspended. It's a separate function to keep the additional logic out
1963+
// of the work loop's hot path.
1964+
workInProgressIsSuspended = false;
1965+
completeUnitOfWork(unitOfWork);
1966+
}
1967+
19351968
function completeUnitOfWork(unitOfWork: Fiber): void {
19361969
// Attempt to complete the current unit of work, then move to the next
19371970
// sibling. If there are no more siblings, return to the parent fiber.

0 commit comments

Comments
 (0)