Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronously restart when an error is thrown during async rendering #13041

Merged
merged 1 commit into from Jun 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/react-reconciler/src/ReactFiberPendingPriority.js
Expand Up @@ -10,14 +10,19 @@
import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';

import {NoWork} from './ReactFiberExpirationTime';
import {NoWork, Sync} from './ReactFiberExpirationTime';

// TODO: Offscreen updates

export function markPendingPriorityLevel(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
// If there's a gap between completing a failed root and retrying it,
// additional updates may be scheduled. Clear `didError`, in case the update
// is sufficient to fix the error.
root.didError = false;

// Update the latest and earliest pending times
const earliestPendingTime = root.earliestPendingTime;
if (earliestPendingTime === NoWork) {
Expand All @@ -43,6 +48,8 @@ export function markCommittedPriorityLevels(
currentTime: ExpirationTime,
earliestRemainingTime: ExpirationTime,
): void {
root.didError = false;

if (earliestRemainingTime === NoWork) {
// Fast path. There's no remaining work. Clear everything.
root.earliestPendingTime = NoWork;
Expand Down Expand Up @@ -111,10 +118,30 @@ export function markCommittedPriorityLevels(
findNextPendingPriorityLevel(root);
}

export function hasLowerPriorityWork(
root: FiberRoot,
renderExpirationTime: ExpirationTime,
) {
return (
renderExpirationTime !== root.latestPendingTime &&
renderExpirationTime !== root.latestSuspendedTime
);
}

export function markSuspendedPriorityLevel(
root: FiberRoot,
suspendedTime: ExpirationTime,
didError: boolean,
): void {
if (didError && !hasLowerPriorityWork(root, suspendedTime)) {
// TODO: When we add back resuming, we need to ensure the progressed work
// is thrown out and not reused during the restarted render. One way to
// invalidate the progressed work is to restart at expirationTime + 1.
root.didError = true;
findNextPendingPriorityLevel(root);
return;
}

// First, check the known pending levels and update them if needed.
const earliestPendingTime = root.earliestPendingTime;
const latestPendingTime = root.latestPendingTime;
Expand Down Expand Up @@ -191,6 +218,12 @@ function findNextPendingPriorityLevel(root) {
// nothing to work on.
nextExpirationTimeToWorkOn = expirationTime = root.latestPingedTime;
}

if (root.didError) {
// Revert to synchronous mode.
expirationTime = Sync;
}

root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn;
root.expirationTime = expirationTime;
}
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberRoot.js
Expand Up @@ -44,6 +44,11 @@ export type FiberRoot = {
// be retried.
latestPingedTime: ExpirationTime,

// If an error is thrown, and there are no more updates in the queue, we try
// rendering from the root one more time, synchronously, before handling
// the error.
didError: boolean,

pendingCommitExpirationTime: ExpirationTime,
// A finished work-in-progress HostRoot that's ready to be committed.
// TODO: The reason this is separate from isReadyForCommit is because the
Expand Down Expand Up @@ -86,6 +91,8 @@ export function createFiberRoot(
latestSuspendedTime: NoWork,
latestPingedTime: NoWork,

didError: false,

pendingCommitExpirationTime: NoWork,
finishedWork: null,
context: null,
Expand Down
10 changes: 9 additions & 1 deletion packages/react-reconciler/src/ReactFiberScheduler.js
Expand Up @@ -229,6 +229,7 @@ let nextRoot: FiberRoot | null = null;
// The time at which we're currently rendering work.
let nextRenderExpirationTime: ExpirationTime = NoWork;
let nextLatestTimeoutMs: number = -1;
let nextRenderDidError: boolean = false;

// The next fiber with an effect that we're currently committing.
let nextEffect: Fiber | null = null;
Expand Down Expand Up @@ -343,6 +344,7 @@ function resetStack() {
nextRoot = null;
nextRenderExpirationTime = NoWork;
nextLatestTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = null;
}

Expand Down Expand Up @@ -1007,6 +1009,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
nextRoot = root;
nextRenderExpirationTime = expirationTime;
nextLatestTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
Expand Down Expand Up @@ -1113,7 +1116,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
const didCompleteRoot = false;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
markSuspendedPriorityLevel(root, expirationTime);
markSuspendedPriorityLevel(root, expirationTime, nextRenderDidError);
const suspendedExpirationTime = expirationTime;
const newExpirationTime = root.expirationTime;
onSuspend(
Expand Down Expand Up @@ -1290,6 +1293,10 @@ function markTimeout(
}
}

function markError(root: FiberRoot) {
nextRenderDidError = true;
}

function retrySuspendedRoot(root: FiberRoot, suspendedTime: ExpirationTime) {
markPingedPriorityLevel(root, suspendedTime);
const retryTime = root.expirationTime;
Expand Down Expand Up @@ -1975,6 +1982,7 @@ export {
captureCommitPhaseError,
onUncaughtError,
markTimeout,
markError,
retrySuspendedRoot,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
Expand Down
30 changes: 16 additions & 14 deletions packages/react-reconciler/src/ReactFiberUnwindWork.js
Expand Up @@ -56,6 +56,7 @@ import {
} from './ReactProfilerTimer';
import {
markTimeout,
markError,
onUncaughtError,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
Expand All @@ -64,6 +65,7 @@ import {
scheduleWork,
retrySuspendedRoot,
} from './ReactFiberScheduler';
import {hasLowerPriorityWork} from './ReactFiberPendingPriority';

function createRootErrorUpdate(
fiber: Fiber,
Expand Down Expand Up @@ -150,20 +152,6 @@ function throwException(
// Its effect list is no longer valid.
sourceFiber.firstEffect = sourceFiber.lastEffect = null;

// Check if there is lower priority work, regardless of whether it's pending.
// If so, it may have the effect of fixing the exception that was just thrown.
const latestPendingTime = root.latestPendingTime;
const latestSuspendedTime = root.latestSuspendedTime;
if (
renderExpirationTime !== latestPendingTime &&
renderExpirationTime !== latestSuspendedTime &&
renderExpirationTime !== Never
) {
// There's lower priority work. Exit to suspend this render and retry at
// the lower priority.
return;
}

if (
enableSuspense &&
value !== null &&
Expand Down Expand Up @@ -259,6 +247,20 @@ function throwException(
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
} else {
// This is an error.
markError(root);
if (
// Retry (at the same priority) one more time before handling the error.
// The retry will flush synchronously. (Unless we're already rendering
// synchronously, in which case move to the next check.)
(!root.didError && renderExpirationTime !== Sync) ||
// There's lower priority work. If so, it may have the effect of fixing
// the exception that was just thrown.
hasLowerPriorityWork(root, renderExpirationTime)
) {
return;
}
}

// We didn't find a boundary that could handle this type of exception. Start
Expand Down