Resume immediately pinged fiber without unwinding (#25074)

* Yield to main thread if continuation is returned

Instead of using an imperative method `requestYield` to ask Scheduler to
yield to the main thread, we can assume that any time a Scheduler task
returns a continuation callback, it's because it wants to yield to the
main thread. We can assume the task already checked some condition that
caused it to return a continuation, so we don't need to do any
additional checks — we can immediately yield and schedule a new task
for the continuation.

The replaces the `requestYield` API that I added in ca990e9.

* 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.

* Yield to main thread whenever a fiber suspends

When a fiber suspends, we should yield to the main thread in case the
data is already cached, to unblock a potential ping event.

By itself, this commit isn't useful because we don't do anything special
in the case where to do receive an immediate ping event. I've split this
out only to demonstrate that it doesn't break any existing behavior.

See the next commit for full context and motivation.

* Resume immediately pinged fiber without unwinding

If a fiber suspends, and is pinged immediately in a microtask (or a
regular task that fires before React resumes rendering), try rendering
the same fiber again without unwinding the stack. This can be super
helpful when working with promises and async-await, because even if the
outermost promise hasn't been cached before, the underlying data may
have been preloaded. In many cases, we can continue rendering
immediately without having to show a fallback.

This optimization should work during any concurrent (time-sliced)
render. It doesn't work during discrete updates because those are
semantically required to finish synchronously — those get the current
behavior.
This commit is contained in:
Andrew Clark 2022-08-11 22:01:56 -04:00 committed by GitHub
parent 4e60dbd905
commit 8ef3a7c08c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 538 additions and 199 deletions

View File

@ -357,7 +357,7 @@ function throwException(
sourceFiber: Fiber, sourceFiber: Fiber,
value: mixed, value: mixed,
rootRenderLanes: Lanes, rootRenderLanes: Lanes,
) { ): Wakeable | null {
// The source fiber did not complete. // The source fiber did not complete.
sourceFiber.flags |= Incomplete; sourceFiber.flags |= Incomplete;
@ -459,7 +459,7 @@ function throwException(
if (suspenseBoundary.mode & ConcurrentMode) { if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes); attachPingListener(root, wakeable, rootRenderLanes);
} }
return; return wakeable;
} else { } else {
// No boundary was found. Unless this is a sync update, this is OK. // No boundary was found. Unless this is a sync update, this is OK.
// We can suspend and wait for more data to arrive. // We can suspend and wait for more data to arrive.
@ -474,7 +474,7 @@ function throwException(
// This case also applies to initial hydration. // This case also applies to initial hydration.
attachPingListener(root, wakeable, rootRenderLanes); attachPingListener(root, wakeable, rootRenderLanes);
renderDidSuspendDelayIfPossible(); renderDidSuspendDelayIfPossible();
return; return wakeable;
} }
// This is a sync/discrete update. We treat this case like an error // This is a sync/discrete update. We treat this case like an error
@ -517,7 +517,7 @@ function throwException(
// Even though the user may not be affected by this error, we should // Even though the user may not be affected by this error, we should
// still log it so it can be fixed. // still log it so it can be fixed.
queueHydrationError(createCapturedValueAtFiber(value, sourceFiber)); queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
return; return null;
} }
} else { } else {
// Otherwise, fall through to the error path. // Otherwise, fall through to the error path.
@ -540,7 +540,7 @@ function throwException(
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane); workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane); const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update); enqueueCapturedUpdate(workInProgress, update);
return; return null;
} }
case ClassComponent: case ClassComponent:
// Capture and retry // Capture and retry
@ -564,7 +564,7 @@ function throwException(
lane, lane,
); );
enqueueCapturedUpdate(workInProgress, update); enqueueCapturedUpdate(workInProgress, update);
return; return null;
} }
break; break;
default: default:
@ -572,6 +572,7 @@ function throwException(
} }
workInProgress = workInProgress.return; workInProgress = workInProgress.return;
} while (workInProgress !== null); } while (workInProgress !== null);
return null;
} }
export {throwException, createRootErrorUpdate, createClassErrorUpdate}; export {throwException, createRootErrorUpdate, createClassErrorUpdate};

View File

@ -357,7 +357,7 @@ function throwException(
sourceFiber: Fiber, sourceFiber: Fiber,
value: mixed, value: mixed,
rootRenderLanes: Lanes, rootRenderLanes: Lanes,
) { ): Wakeable | null {
// The source fiber did not complete. // The source fiber did not complete.
sourceFiber.flags |= Incomplete; sourceFiber.flags |= Incomplete;
@ -459,7 +459,7 @@ function throwException(
if (suspenseBoundary.mode & ConcurrentMode) { if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes); attachPingListener(root, wakeable, rootRenderLanes);
} }
return; return wakeable;
} else { } else {
// No boundary was found. Unless this is a sync update, this is OK. // No boundary was found. Unless this is a sync update, this is OK.
// We can suspend and wait for more data to arrive. // We can suspend and wait for more data to arrive.
@ -474,7 +474,7 @@ function throwException(
// This case also applies to initial hydration. // This case also applies to initial hydration.
attachPingListener(root, wakeable, rootRenderLanes); attachPingListener(root, wakeable, rootRenderLanes);
renderDidSuspendDelayIfPossible(); renderDidSuspendDelayIfPossible();
return; return wakeable;
} }
// This is a sync/discrete update. We treat this case like an error // This is a sync/discrete update. We treat this case like an error
@ -517,7 +517,7 @@ function throwException(
// Even though the user may not be affected by this error, we should // Even though the user may not be affected by this error, we should
// still log it so it can be fixed. // still log it so it can be fixed.
queueHydrationError(createCapturedValueAtFiber(value, sourceFiber)); queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
return; return null;
} }
} else { } else {
// Otherwise, fall through to the error path. // Otherwise, fall through to the error path.
@ -540,7 +540,7 @@ function throwException(
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane); workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane); const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update); enqueueCapturedUpdate(workInProgress, update);
return; return null;
} }
case ClassComponent: case ClassComponent:
// Capture and retry // Capture and retry
@ -564,7 +564,7 @@ function throwException(
lane, lane,
); );
enqueueCapturedUpdate(workInProgress, update); enqueueCapturedUpdate(workInProgress, update);
return; return null;
} }
break; break;
default: default:
@ -572,6 +572,7 @@ function throwException(
} }
workInProgress = workInProgress.return; workInProgress = workInProgress.return;
} while (workInProgress !== null); } while (workInProgress !== null);
return null;
} }
export {throwException, createRootErrorUpdate, createClassErrorUpdate}; export {throwException, createRootErrorUpdate, createClassErrorUpdate};

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
let suspendedWakeable: Wakeable | null = null;
let wasPinged = false;
let adHocSuspendCount: number = 0;
const MAX_AD_HOC_SUSPEND_COUNT = 50;
export function suspendedWakeableWasPinged() {
return wasPinged;
}
export function trackSuspendedWakeable(wakeable: Wakeable) {
adHocSuspendCount++;
suspendedWakeable = wakeable;
}
export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
if (wakeable === suspendedWakeable) {
// This ping is from the wakeable that just suspended. Mark it as pinged.
// When the work loop resumes, we'll immediately try rendering the fiber
// again instead of unwinding the stack.
wasPinged = true;
return true;
}
return false;
}
export function resetWakeableState() {
suspendedWakeable = null;
wasPinged = false;
adHocSuspendCount = 0;
}
export function throwIfInfinitePingLoopDetected() {
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
// TODO: Guard against an infinite loop by throwing an error if the same
// component suspends too many times in a row. This should be thrown from
// the render phase so that it gets the component stack.
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
let suspendedWakeable: Wakeable | null = null;
let wasPinged = false;
let adHocSuspendCount: number = 0;
const MAX_AD_HOC_SUSPEND_COUNT = 50;
export function suspendedWakeableWasPinged() {
return wasPinged;
}
export function trackSuspendedWakeable(wakeable: Wakeable) {
adHocSuspendCount++;
suspendedWakeable = wakeable;
}
export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
if (wakeable === suspendedWakeable) {
// This ping is from the wakeable that just suspended. Mark it as pinged.
// When the work loop resumes, we'll immediately try rendering the fiber
// again instead of unwinding the stack.
wasPinged = true;
return true;
}
return false;
}
export function resetWakeableState() {
suspendedWakeable = null;
wasPinged = false;
adHocSuspendCount = 0;
}
export function throwIfInfinitePingLoopDetected() {
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
// TODO: Guard against an infinite loop by throwing an error if the same
// component suspends too many times in a row. This should be thrown from
// the render phase so that it gets the component stack.
}
}

View File

@ -86,6 +86,7 @@ import {
import { import {
createWorkInProgress, createWorkInProgress,
assignFiberPropertiesInDEV, assignFiberPropertiesInDEV,
resetWorkInProgress,
} from './ReactFiber.new'; } from './ReactFiber.new';
import {isRootDehydrated} from './ReactFiberShellHydration'; import {isRootDehydrated} from './ReactFiberShellHydration';
import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.new'; import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.new';
@ -245,6 +246,12 @@ import {
isConcurrentActEnvironment, isConcurrentActEnvironment,
} from './ReactFiberAct.new'; } from './ReactFiberAct.new';
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new'; import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
import {
resetWakeableState,
trackSuspendedWakeable,
suspendedWakeableWasPinged,
attemptToPingSuspendedWakeable,
} from './ReactFiberWakeable.new';
const ceil = Math.ceil; const ceil = Math.ceil;
@ -280,6 +287,12 @@ let workInProgress: Fiber | null = null;
// The lanes we're rendering // The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes; let workInProgressRootRenderLanes: Lanes = NoLanes;
// When this is true, the work-in-progress fiber just suspended (or errored) and
// we've yet to unwind the stack. In some cases, we may yield to the main thread
// after this happens. If the fiber is pinged before we resume, we can retry
// immediately instead of unwinding the stack.
let workInProgressIsSuspended: boolean = false;
// A contextual version of workInProgressRootRenderLanes. It is a superset of // A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree // the lanes that we started working on at the root. When we enter a subtree
// that is currently hidden, we add the lanes that would have committed if // that is currently hidden, we add the lanes that would have committed if
@ -1543,11 +1556,13 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
); );
interruptedWork = interruptedWork.return; interruptedWork = interruptedWork.return;
} }
resetWakeableState();
} }
workInProgressRoot = root; workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null); const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress; workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes; workInProgressRootRenderLanes = renderLanes = lanes;
workInProgressIsSuspended = false;
workInProgressRootExitStatus = RootInProgress; workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null; workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes; workInProgressRootSkippedLanes = NoLanes;
@ -1566,7 +1581,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
return rootWorkInProgress; return rootWorkInProgress;
} }
function handleError(root, thrownValue): void { function handleError(root, thrownValue): Wakeable | null {
do { do {
let erroredWork = workInProgress; let erroredWork = workInProgress;
try { try {
@ -1592,7 +1607,7 @@ function handleError(root, thrownValue): void {
// intentionally not calling those, we need set it here. // intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts. // TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null; workInProgress = null;
return; return null;
} }
if (enableProfilerTimer && erroredWork.mode & ProfileMode) { if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
@ -1625,14 +1640,21 @@ function handleError(root, thrownValue): void {
} }
} }
throwException( const maybeWakeable = throwException(
root, root,
erroredWork.return, erroredWork.return,
erroredWork, erroredWork,
thrownValue, thrownValue,
workInProgressRootRenderLanes, workInProgressRootRenderLanes,
); );
completeUnitOfWork(erroredWork); // Setting this to `true` tells the work loop to unwind the stack instead
// of entering the begin phase. It's called "suspended" because it usually
// happens because of Suspense, but it also applies to errors. Think of it
// as suspending the execution of the work loop.
workInProgressIsSuspended = true;
// Return to the normal work loop.
return maybeWakeable;
} catch (yetAnotherThrownValue) { } catch (yetAnotherThrownValue) {
// Something in the return path also threw. // Something in the return path also threw.
thrownValue = yetAnotherThrownValue; thrownValue = yetAnotherThrownValue;
@ -1646,8 +1668,6 @@ function handleError(root, thrownValue): void {
} }
continue; continue;
} }
// Return to the normal work loop.
return;
} while (true); } while (true);
} }
@ -1810,7 +1830,14 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// The work loop is an extremely hot path. Tell Closure not to inline it. // The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */ /** @noinline */
function workLoopSync() { function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield. // Perform work without checking if we need to yield between fiber.
if (workInProgressIsSuspended && workInProgress !== null) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
}
while (workInProgress !== null) { while (workInProgress !== null) {
performUnitOfWork(workInProgress); performUnitOfWork(workInProgress);
} }
@ -1860,7 +1887,14 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent(); workLoopConcurrent();
break; break;
} catch (thrownValue) { } catch (thrownValue) {
handleError(root, thrownValue); const maybeWakeable = handleError(root, thrownValue);
if (maybeWakeable !== null) {
// If this fiber just suspended, it's possible the data is already
// cached. Yield to the the main thread to give it a chance to ping. If
// it does, we can retry immediately without unwinding the stack.
trackSuspendedWakeable(maybeWakeable);
break;
}
} }
} while (true); } while (true);
resetContextDependencies(); resetContextDependencies();
@ -1899,6 +1933,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
/** @noinline */ /** @noinline */
function workLoopConcurrent() { function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield // Perform work until Scheduler asks us to yield
if (workInProgressIsSuspended && workInProgress !== null) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
}
while (workInProgress !== null && !shouldYield()) { while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress); performUnitOfWork(workInProgress);
} }
@ -1932,6 +1973,56 @@ function performUnitOfWork(unitOfWork: Fiber): void {
ReactCurrentOwner.current = null; ReactCurrentOwner.current = null;
} }
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
// just suspended. In some cases, we may choose to retry the fiber immediately
// instead of unwinding the stack. It's a separate function to keep the
// additional logic out of the work loop's hot path.
if (!suspendedWakeableWasPinged()) {
// The wakeable wasn't pinged. Return to the normal work loop. This will
// unwind the stack, and potentially result in showing a fallback.
workInProgressIsSuspended = false;
resetWakeableState();
completeUnitOfWork(unitOfWork);
return;
}
// The work-in-progress was immediately pinged. Instead of unwinding the
// stack and potentially showing a fallback, reset the fiber and try rendering
// it again.
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderLanes);
}
// The begin phase finished successfully without suspending. Reset the state
// used to track the fiber while it was suspended. Then return to the normal
// work loop.
workInProgressIsSuspended = false;
resetWakeableState();
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
function completeUnitOfWork(unitOfWork: Fiber): void { function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next // 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. // sibling. If there are no more siblings, return to the parent fiber.
@ -2743,27 +2834,31 @@ export function pingSuspendedRoot(
// Received a ping at the same priority level at which we're currently // Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror // rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes. // the logic of whether or not a root suspends once it completes.
const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
// TODO: If we're rendering sync either due to Sync, Batched or expired, if (didPingSuspendedWakeable) {
// we should probably never restart. // Successfully pinged the in-progress fiber. Don't unwind the stack.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else { } else {
// Even though we can't restart right now, we might get an // TODO: If we're rendering sync either due to Sync, Batched or expired,
// opportunity later. So we mark this render as having a ping. // we should probably never restart.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes, // If we're suspended with delay, or if it's a retry, we'll always suspend
pingedLanes, // so we can always restart.
); if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
} }
} }

View File

@ -86,6 +86,7 @@ import {
import { import {
createWorkInProgress, createWorkInProgress,
assignFiberPropertiesInDEV, assignFiberPropertiesInDEV,
resetWorkInProgress,
} from './ReactFiber.old'; } from './ReactFiber.old';
import {isRootDehydrated} from './ReactFiberShellHydration'; import {isRootDehydrated} from './ReactFiberShellHydration';
import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.old'; import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.old';
@ -245,6 +246,12 @@ import {
isConcurrentActEnvironment, isConcurrentActEnvironment,
} from './ReactFiberAct.old'; } from './ReactFiberAct.old';
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old'; import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
import {
resetWakeableState,
trackSuspendedWakeable,
suspendedWakeableWasPinged,
attemptToPingSuspendedWakeable,
} from './ReactFiberWakeable.old';
const ceil = Math.ceil; const ceil = Math.ceil;
@ -280,6 +287,12 @@ let workInProgress: Fiber | null = null;
// The lanes we're rendering // The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes; let workInProgressRootRenderLanes: Lanes = NoLanes;
// When this is true, the work-in-progress fiber just suspended (or errored) and
// we've yet to unwind the stack. In some cases, we may yield to the main thread
// after this happens. If the fiber is pinged before we resume, we can retry
// immediately instead of unwinding the stack.
let workInProgressIsSuspended: boolean = false;
// A contextual version of workInProgressRootRenderLanes. It is a superset of // A contextual version of workInProgressRootRenderLanes. It is a superset of
// the lanes that we started working on at the root. When we enter a subtree // the lanes that we started working on at the root. When we enter a subtree
// that is currently hidden, we add the lanes that would have committed if // that is currently hidden, we add the lanes that would have committed if
@ -1543,11 +1556,13 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
); );
interruptedWork = interruptedWork.return; interruptedWork = interruptedWork.return;
} }
resetWakeableState();
} }
workInProgressRoot = root; workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null); const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress; workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = renderLanes = lanes; workInProgressRootRenderLanes = renderLanes = lanes;
workInProgressIsSuspended = false;
workInProgressRootExitStatus = RootInProgress; workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null; workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes; workInProgressRootSkippedLanes = NoLanes;
@ -1566,7 +1581,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
return rootWorkInProgress; return rootWorkInProgress;
} }
function handleError(root, thrownValue): void { function handleError(root, thrownValue): Wakeable | null {
do { do {
let erroredWork = workInProgress; let erroredWork = workInProgress;
try { try {
@ -1592,7 +1607,7 @@ function handleError(root, thrownValue): void {
// intentionally not calling those, we need set it here. // intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts. // TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null; workInProgress = null;
return; return null;
} }
if (enableProfilerTimer && erroredWork.mode & ProfileMode) { if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
@ -1625,14 +1640,21 @@ function handleError(root, thrownValue): void {
} }
} }
throwException( const maybeWakeable = throwException(
root, root,
erroredWork.return, erroredWork.return,
erroredWork, erroredWork,
thrownValue, thrownValue,
workInProgressRootRenderLanes, workInProgressRootRenderLanes,
); );
completeUnitOfWork(erroredWork); // Setting this to `true` tells the work loop to unwind the stack instead
// of entering the begin phase. It's called "suspended" because it usually
// happens because of Suspense, but it also applies to errors. Think of it
// as suspending the execution of the work loop.
workInProgressIsSuspended = true;
// Return to the normal work loop.
return maybeWakeable;
} catch (yetAnotherThrownValue) { } catch (yetAnotherThrownValue) {
// Something in the return path also threw. // Something in the return path also threw.
thrownValue = yetAnotherThrownValue; thrownValue = yetAnotherThrownValue;
@ -1646,8 +1668,6 @@ function handleError(root, thrownValue): void {
} }
continue; continue;
} }
// Return to the normal work loop.
return;
} while (true); } while (true);
} }
@ -1810,7 +1830,14 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// The work loop is an extremely hot path. Tell Closure not to inline it. // The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */ /** @noinline */
function workLoopSync() { function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield. // Perform work without checking if we need to yield between fiber.
if (workInProgressIsSuspended && workInProgress !== null) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
}
while (workInProgress !== null) { while (workInProgress !== null) {
performUnitOfWork(workInProgress); performUnitOfWork(workInProgress);
} }
@ -1860,7 +1887,14 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
workLoopConcurrent(); workLoopConcurrent();
break; break;
} catch (thrownValue) { } catch (thrownValue) {
handleError(root, thrownValue); const maybeWakeable = handleError(root, thrownValue);
if (maybeWakeable !== null) {
// If this fiber just suspended, it's possible the data is already
// cached. Yield to the the main thread to give it a chance to ping. If
// it does, we can retry immediately without unwinding the stack.
trackSuspendedWakeable(maybeWakeable);
break;
}
} }
} while (true); } while (true);
resetContextDependencies(); resetContextDependencies();
@ -1899,6 +1933,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
/** @noinline */ /** @noinline */
function workLoopConcurrent() { function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield // Perform work until Scheduler asks us to yield
if (workInProgressIsSuspended && workInProgress !== null) {
// The current work-in-progress was already attempted. We need to unwind
// it before we continue the normal work loop.
resumeSuspendedUnitOfWork(workInProgress);
}
while (workInProgress !== null && !shouldYield()) { while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress); performUnitOfWork(workInProgress);
} }
@ -1932,6 +1973,56 @@ function performUnitOfWork(unitOfWork: Fiber): void {
ReactCurrentOwner.current = null; ReactCurrentOwner.current = null;
} }
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
// just suspended. In some cases, we may choose to retry the fiber immediately
// instead of unwinding the stack. It's a separate function to keep the
// additional logic out of the work loop's hot path.
if (!suspendedWakeableWasPinged()) {
// The wakeable wasn't pinged. Return to the normal work loop. This will
// unwind the stack, and potentially result in showing a fallback.
workInProgressIsSuspended = false;
resetWakeableState();
completeUnitOfWork(unitOfWork);
return;
}
// The work-in-progress was immediately pinged. Instead of unwinding the
// stack and potentially showing a fallback, reset the fiber and try rendering
// it again.
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderLanes);
}
// The begin phase finished successfully without suspending. Reset the state
// used to track the fiber while it was suspended. Then return to the normal
// work loop.
workInProgressIsSuspended = false;
resetWakeableState();
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
function completeUnitOfWork(unitOfWork: Fiber): void { function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next // 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. // sibling. If there are no more siblings, return to the parent fiber.
@ -2743,27 +2834,31 @@ export function pingSuspendedRoot(
// Received a ping at the same priority level at which we're currently // Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror // rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes. // the logic of whether or not a root suspends once it completes.
const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
// TODO: If we're rendering sync either due to Sync, Batched or expired, if (didPingSuspendedWakeable) {
// we should probably never restart. // Successfully pinged the in-progress fiber. Don't unwind the stack.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else { } else {
// Even though we can't restart right now, we might get an // TODO: If we're rendering sync either due to Sync, Batched or expired,
// opportunity later. So we mark this render as having a ping. // we should probably never restart.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes, // If we're suspended with delay, or if it's a retry, we'll always suspend
pingedLanes, // so we can always restart.
); if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
} }
} }

View File

@ -485,22 +485,33 @@ describe('ReactOffscreen', () => {
// In the same render, also hide the offscreen tree. // In the same render, also hide the offscreen tree.
root.render(<App show={false} />); root.render(<App show={false} />);
expect(Scheduler).toFlushUntilNextPaint([ if (gate(flags => flags.enableSyncDefaultUpdates)) {
// The outer update will commit, but the inner update is deferred until expect(Scheduler).toFlushUntilNextPaint([
// a later render. // The outer update will commit, but the inner update is deferred until
'Outer: 1', // a later render.
'Outer: 1',
// Something suspended. This means we won't commit immediately; there // Something suspended. This means we won't commit immediately; there
// will be an async gap between render and commit. In this test, we will // will be an async gap between render and commit. In this test, we will
// use this property to schedule a concurrent update. The fact that // use this property to schedule a concurrent update. The fact that
// we're using Suspense to schedule a concurrent update is not directly // we're using Suspense to schedule a concurrent update is not directly
// relevant to the test — we could also use time slicing, but I've // relevant to the test — we could also use time slicing, but I've
// chosen to use Suspense the because implementation details of time // chosen to use Suspense the because implementation details of time
// slicing are more volatile. // slicing are more volatile.
'Suspend! [Async: 1]', 'Suspend! [Async: 1]',
'Loading...',
]);
} else {
// When default updates are time sliced, React yields before preparing
// the fallback.
expect(Scheduler).toFlushUntilNextPaint([
'Outer: 1',
'Suspend! [Async: 1]',
]);
expect(Scheduler).toFlushUntilNextPaint(['Loading...']);
}
'Loading...',
]);
// Assert that we haven't committed quite yet // Assert that we haven't committed quite yet
expect(root).toMatchRenderedOutput( expect(root).toMatchRenderedOutput(
<> <>

View File

@ -963,33 +963,36 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// @gate enableCache // @gate enableCache
it('resolves successfully even if fallback render is pending', async () => { it('resolves successfully even if fallback render is pending', async () => {
ReactNoop.render( const root = ReactNoop.createRoot();
root.render(
<> <>
<Suspense fallback={<Text text="Loading..." />} /> <Suspense fallback={<Text text="Loading..." />} />
</>, </>,
); );
expect(Scheduler).toFlushAndYield([]); expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop.getChildren()).toEqual([]); expect(root).toMatchRenderedOutput(null);
if (gate(flags => flags.enableSyncDefaultUpdates)) { if (gate(flags => flags.enableSyncDefaultUpdates)) {
React.startTransition(() => { React.startTransition(() => {
ReactNoop.render( root.render(
<> <>
<Suspense fallback={<Text text="Loading..." />}> <Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" /> <AsyncText text="Async" />
<Text text="Sibling" />
</Suspense> </Suspense>
</>, </>,
); );
}); });
} else { } else {
ReactNoop.render( root.render(
<> <>
<Suspense fallback={<Text text="Loading..." />}> <Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" /> <AsyncText text="Async" />
<Text text="Sibling" />
</Suspense> </Suspense>
</>, </>,
); );
} }
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']); expect(Scheduler).toFlushAndYieldThrough(['Suspend! [Async]', 'Sibling']);
await resolveText('Async'); await resolveText('Async');
expect(Scheduler).toFlushAndYield([ expect(Scheduler).toFlushAndYield([
@ -998,8 +1001,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Loading...', 'Loading...',
// Once we've completed the boundary we restarted. // Once we've completed the boundary we restarted.
'Async', 'Async',
'Sibling',
]); ]);
expect(ReactNoop.getChildren()).toEqual([span('Async')]); expect(root).toMatchRenderedOutput(
<>
<span prop="Async" />
<span prop="Sibling" />
</>,
);
}); });
// @gate enableCache // @gate enableCache
@ -3859,7 +3868,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Suspend! [A2]', 'Suspend! [A2]',
'Loading...', 'Loading...',
'Suspend! [B2]', 'Suspend! [B2]',
'Loading...',
]); ]);
expect(root).toMatchRenderedOutput( expect(root).toMatchRenderedOutput(
<> <>

View File

@ -0,0 +1,67 @@
'use strict';
let React;
let ReactNoop;
let Scheduler;
let act;
let Suspense;
let startTransition;
describe('ReactWakeable', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('jest-react').act;
Suspense = React.Suspense;
startTransition = React.startTransition;
});
function Text(props) {
Scheduler.unstable_yieldValue(props.text);
return props.text;
}
test('if suspended fiber is pinged in a microtask, retry immediately without unwinding the stack', async () => {
let resolved = false;
function Async() {
if (resolved) {
return <Text text="Async" />;
}
Scheduler.unstable_yieldValue('Suspend!');
throw Promise.resolve().then(() => {
Scheduler.unstable_yieldValue('Resolve in microtask');
resolved = true;
});
}
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<Async />
</Suspense>
);
}
await act(async () => {
startTransition(() => {
ReactNoop.render(<App />);
});
// React will yield when the async component suspends.
expect(Scheduler).toFlushUntilNextPaint(['Suspend!']);
// Wait for microtasks to resolve
// TODO: The async form of `act` should automatically yield to microtasks
// when a continuation is returned, the way Scheduler does.
await null;
expect(Scheduler).toHaveYielded(['Resolve in microtask']);
});
// Finished rendering without unwinding the stack.
expect(Scheduler).toHaveYielded(['Async']);
});
});

View File

@ -54,13 +54,6 @@
); );
} }
function unstable_requestYield() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
this,
arguments
);
}
function unstable_runWithPriority() { function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this, this,
@ -123,7 +116,6 @@
unstable_cancelCallback: unstable_cancelCallback, unstable_cancelCallback: unstable_cancelCallback,
unstable_shouldYield: unstable_shouldYield, unstable_shouldYield: unstable_shouldYield,
unstable_requestPaint: unstable_requestPaint, unstable_requestPaint: unstable_requestPaint,
unstable_requestYield: unstable_requestYield,
unstable_runWithPriority: unstable_runWithPriority, unstable_runWithPriority: unstable_runWithPriority,
unstable_next: unstable_next, unstable_next: unstable_next,
unstable_wrapCallback: unstable_wrapCallback, unstable_wrapCallback: unstable_wrapCallback,

View File

@ -54,13 +54,6 @@
); );
} }
function unstable_requestYield() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
this,
arguments
);
}
function unstable_runWithPriority() { function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this, this,
@ -117,7 +110,6 @@
unstable_cancelCallback: unstable_cancelCallback, unstable_cancelCallback: unstable_cancelCallback,
unstable_shouldYield: unstable_shouldYield, unstable_shouldYield: unstable_shouldYield,
unstable_requestPaint: unstable_requestPaint, unstable_requestPaint: unstable_requestPaint,
unstable_requestYield: unstable_requestYield,
unstable_runWithPriority: unstable_runWithPriority, unstable_runWithPriority: unstable_runWithPriority,
unstable_next: unstable_next, unstable_next: unstable_next,
unstable_wrapCallback: unstable_wrapCallback, unstable_wrapCallback: unstable_wrapCallback,

View File

@ -54,13 +54,6 @@
); );
} }
function unstable_requestYield() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
this,
arguments
);
}
function unstable_runWithPriority() { function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply( return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this, this,
@ -117,7 +110,6 @@
unstable_cancelCallback: unstable_cancelCallback, unstable_cancelCallback: unstable_cancelCallback,
unstable_shouldYield: unstable_shouldYield, unstable_shouldYield: unstable_shouldYield,
unstable_requestPaint: unstable_requestPaint, unstable_requestPaint: unstable_requestPaint,
unstable_requestYield: unstable_requestYield,
unstable_runWithPriority: unstable_runWithPriority, unstable_runWithPriority: unstable_runWithPriority,
unstable_next: unstable_next, unstable_next: unstable_next,
unstable_wrapCallback: unstable_wrapCallback, unstable_wrapCallback: unstable_wrapCallback,

View File

@ -18,7 +18,6 @@ let performance;
let cancelCallback; let cancelCallback;
let scheduleCallback; let scheduleCallback;
let requestPaint; let requestPaint;
let requestYield;
let shouldYield; let shouldYield;
let NormalPriority; let NormalPriority;
@ -44,7 +43,6 @@ describe('SchedulerBrowser', () => {
scheduleCallback = Scheduler.unstable_scheduleCallback; scheduleCallback = Scheduler.unstable_scheduleCallback;
NormalPriority = Scheduler.unstable_NormalPriority; NormalPriority = Scheduler.unstable_NormalPriority;
requestPaint = Scheduler.unstable_requestPaint; requestPaint = Scheduler.unstable_requestPaint;
requestYield = Scheduler.unstable_requestYield;
shouldYield = Scheduler.unstable_shouldYield; shouldYield = Scheduler.unstable_shouldYield;
}); });
@ -480,19 +478,13 @@ describe('SchedulerBrowser', () => {
]); ]);
}); });
it('requestYield forces a yield immediately', () => { it('yielding continues in a new task regardless of how much time is remaining', () => {
scheduleCallback(NormalPriority, () => { scheduleCallback(NormalPriority, () => {
runtime.log('Original Task'); runtime.log('Original Task');
runtime.log('shouldYield: ' + shouldYield()); runtime.log('shouldYield: ' + shouldYield());
runtime.log('requestYield'); runtime.log('Return a continuation');
requestYield();
runtime.log('shouldYield: ' + shouldYield());
return () => { return () => {
runtime.log('Continuation Task'); runtime.log('Continuation Task');
runtime.log('shouldYield: ' + shouldYield());
runtime.log('Advance time past frame deadline');
runtime.advanceTime(10000);
runtime.log('shouldYield: ' + shouldYield());
}; };
}); });
runtime.assertLog(['Post Message']); runtime.assertLog(['Post Message']);
@ -501,27 +493,20 @@ describe('SchedulerBrowser', () => {
runtime.assertLog([ runtime.assertLog([
'Message Event', 'Message Event',
'Original Task', 'Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
'shouldYield: false', 'shouldYield: false',
'requestYield', 'Return a continuation',
// Immediately after calling requestYield, shouldYield starts
// returning true, even though no time has elapsed in the frame
'shouldYield: true',
// The continuation should be scheduled in a separate macrotask. // The continuation should be scheduled in a separate macrotask even
// though there's time remaining.
'Post Message', 'Post Message',
]); ]);
// No time has elapsed // No time has elapsed
expect(performance.now()).toBe(0); expect(performance.now()).toBe(0);
// Subsequent tasks work as normal
runtime.fireMessageEvent(); runtime.fireMessageEvent();
runtime.assertLog([ runtime.assertLog(['Message Event', 'Continuation Task']);
'Message Event',
'Continuation Task',
'shouldYield: false',
'Advance time past frame deadline',
'shouldYield: true',
]);
}); });
}); });

View File

@ -726,37 +726,53 @@ describe('Scheduler', () => {
expect(Scheduler).toFlushWithoutYielding(); expect(Scheduler).toFlushWithoutYielding();
}); });
it('requestYield forces a yield immediately', () => { it('toFlushUntilNextPaint stops if a continuation is returned', () => {
scheduleCallback(NormalPriority, () => { scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Original Task'); Scheduler.unstable_yieldValue('Original Task');
Scheduler.unstable_yieldValue( Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
'shouldYield: ' + Scheduler.unstable_shouldYield(), Scheduler.unstable_yieldValue('Return a continuation');
);
Scheduler.unstable_yieldValue('requestYield');
Scheduler.unstable_requestYield();
Scheduler.unstable_yieldValue(
'shouldYield: ' + Scheduler.unstable_shouldYield(),
);
return () => { return () => {
Scheduler.unstable_yieldValue('Continuation Task'); Scheduler.unstable_yieldValue('Continuation Task');
Scheduler.unstable_yieldValue(
'shouldYield: ' + Scheduler.unstable_shouldYield(),
);
Scheduler.unstable_yieldValue('Advance time past frame deadline');
Scheduler.unstable_yieldValue(
'shouldYield: ' + Scheduler.unstable_shouldYield(),
);
}; };
}); });
// The continuation should be scheduled in a separate macrotask.
expect(Scheduler).toFlushUntilNextPaint([ expect(Scheduler).toFlushUntilNextPaint([
'Original Task', 'Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
'shouldYield: false', 'shouldYield: false',
'requestYield', 'Return a continuation',
// Immediately after calling requestYield, shouldYield starts
// returning true // The continuation should not flush yet.
'shouldYield: true', ]);
// No time has elapsed
expect(Scheduler.unstable_now()).toBe(0);
// Continue the task
expect(Scheduler).toFlushAndYield(['Continuation Task']);
});
it("toFlushAndYield keeps flushing even if there's a continuation", () => {
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('Original Task');
Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
Scheduler.unstable_yieldValue('Return a continuation');
return () => {
Scheduler.unstable_yieldValue('Continuation Task');
};
});
expect(Scheduler).toFlushAndYield([
'Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
'shouldYield: false',
'Return a continuation',
// The continuation should flush immediately, even though the task
// yielded a continuation.
'Continuation Task',
]); ]);
}); });
}); });

View File

@ -23,7 +23,6 @@ let UserBlockingPriority;
let LowPriority; let LowPriority;
let IdlePriority; let IdlePriority;
let shouldYield; let shouldYield;
let requestYield;
// The Scheduler postTask implementation uses a new postTask browser API to // The Scheduler postTask implementation uses a new postTask browser API to
// schedule work on the main thread. This test suite mocks all browser methods // schedule work on the main thread. This test suite mocks all browser methods
@ -47,7 +46,6 @@ describe('SchedulerPostTask', () => {
LowPriority = Scheduler.unstable_LowPriority; LowPriority = Scheduler.unstable_LowPriority;
IdlePriority = Scheduler.unstable_IdlePriority; IdlePriority = Scheduler.unstable_IdlePriority;
shouldYield = Scheduler.unstable_shouldYield; shouldYield = Scheduler.unstable_shouldYield;
requestYield = Scheduler.unstable_requestYield;
}); });
afterEach(() => { afterEach(() => {
@ -301,19 +299,13 @@ describe('SchedulerPostTask', () => {
]); ]);
}); });
it('requestYield forces a yield immediately', () => { it('yielding continues in a new task regardless of how much time is remaining', () => {
scheduleCallback(NormalPriority, () => { scheduleCallback(NormalPriority, () => {
runtime.log('Original Task'); runtime.log('Original Task');
runtime.log('shouldYield: ' + shouldYield()); runtime.log('shouldYield: ' + shouldYield());
runtime.log('requestYield'); runtime.log('Return a continuation');
requestYield();
runtime.log('shouldYield: ' + shouldYield());
return () => { return () => {
runtime.log('Continuation Task'); runtime.log('Continuation Task');
runtime.log('shouldYield: ' + shouldYield());
runtime.log('Advance time past frame deadline');
runtime.advanceTime(10000);
runtime.log('shouldYield: ' + shouldYield());
}; };
}); });
runtime.assertLog(['Post Task 0 [user-visible]']); runtime.assertLog(['Post Task 0 [user-visible]']);
@ -322,27 +314,20 @@ describe('SchedulerPostTask', () => {
runtime.assertLog([ runtime.assertLog([
'Task 0 Fired', 'Task 0 Fired',
'Original Task', 'Original Task',
// Immediately before returning a continuation, `shouldYield` returns
// false, which means there must be time remaining in the frame.
'shouldYield: false', 'shouldYield: false',
'requestYield', 'Return a continuation',
// Immediately after calling requestYield, shouldYield starts
// returning true, even though no time has elapsed in the frame
'shouldYield: true',
// The continuation should be scheduled in a separate macrotask. // The continuation should be scheduled in a separate macrotask even
// though there's time remaining.
'Post Task 1 [user-visible]', 'Post Task 1 [user-visible]',
]); ]);
// No time has elapsed // No time has elapsed
expect(performance.now()).toBe(0); expect(performance.now()).toBe(0);
// Subsequent tasks work as normal
runtime.flushTasks(); runtime.flushTasks();
runtime.assertLog([ runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
'Task 1 Fired',
'Continuation Task',
'shouldYield: false',
'Advance time past frame deadline',
'shouldYield: true',
]);
}); });
}); });

View File

@ -212,10 +212,14 @@ function workLoop(hasTimeRemaining, initialTime) {
const continuationCallback = callback(didUserCallbackTimeout); const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime(); currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') { if (typeof continuationCallback === 'function') {
// If a continuation is returned, immediately yield to the main thread
// regardless of how much time is left in the current time slice.
currentTask.callback = continuationCallback; currentTask.callback = continuationCallback;
if (enableProfiling) { if (enableProfiling) {
markTaskYield(currentTask, currentTime); markTaskYield(currentTask, currentTime);
} }
advanceTimers(currentTime);
return true;
} else { } else {
if (enableProfiling) { if (enableProfiling) {
markTaskCompleted(currentTask, currentTime); markTaskCompleted(currentTask, currentTime);
@ -224,8 +228,8 @@ function workLoop(hasTimeRemaining, initialTime) {
if (currentTask === peek(taskQueue)) { if (currentTask === peek(taskQueue)) {
pop(taskQueue); pop(taskQueue);
} }
advanceTimers(currentTime);
} }
advanceTimers(currentTime);
} else { } else {
pop(taskQueue); pop(taskQueue);
} }
@ -495,11 +499,6 @@ function requestPaint() {
// Since we yield every frame regardless, `requestPaint` has no effect. // Since we yield every frame regardless, `requestPaint` has no effect.
} }
function requestYield() {
// Force a yield at the next opportunity.
startTime = -99999;
}
function forceFrameRate(fps) { function forceFrameRate(fps) {
if (fps < 0 || fps > 125) { if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint // Using console['error'] to evade Babel and ESLint
@ -617,7 +616,6 @@ export {
unstable_getCurrentPriorityLevel, unstable_getCurrentPriorityLevel,
shouldYieldToHost as unstable_shouldYield, shouldYieldToHost as unstable_shouldYield,
requestPaint as unstable_requestPaint, requestPaint as unstable_requestPaint,
requestYield as unstable_requestYield,
unstable_continueExecution, unstable_continueExecution,
unstable_pauseExecution, unstable_pauseExecution,
unstable_getFirstCallbackNode, unstable_getFirstCallbackNode,

View File

@ -195,10 +195,22 @@ function workLoop(hasTimeRemaining, initialTime) {
const continuationCallback = callback(didUserCallbackTimeout); const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime(); currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') { if (typeof continuationCallback === 'function') {
// If a continuation is returned, immediately yield to the main thread
// regardless of how much time is left in the current time slice.
currentTask.callback = continuationCallback; currentTask.callback = continuationCallback;
if (enableProfiling) { if (enableProfiling) {
markTaskYield(currentTask, currentTime); markTaskYield(currentTask, currentTime);
} }
advanceTimers(currentTime);
if (shouldYieldForPaint) {
needsPaint = true;
return true;
} else {
// If `shouldYieldForPaint` is false, we keep flushing synchronously
// without yielding to the main thread. This is the behavior of the
// `toFlushAndYield` and `toFlushAndYieldThrough` testing helpers .
}
} else { } else {
if (enableProfiling) { if (enableProfiling) {
markTaskCompleted(currentTask, currentTime); markTaskCompleted(currentTask, currentTime);
@ -207,8 +219,8 @@ function workLoop(hasTimeRemaining, initialTime) {
if (currentTask === peek(taskQueue)) { if (currentTask === peek(taskQueue)) {
pop(taskQueue); pop(taskQueue);
} }
advanceTimers(currentTime);
} }
advanceTimers(currentTime);
} else { } else {
pop(taskQueue); pop(taskQueue);
} }
@ -608,11 +620,6 @@ function requestPaint() {
needsPaint = true; needsPaint = true;
} }
function requestYield() {
// Force a yield at the next opportunity.
shouldYieldForPaint = needsPaint = true;
}
export { export {
ImmediatePriority as unstable_ImmediatePriority, ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority, UserBlockingPriority as unstable_UserBlockingPriority,
@ -627,7 +634,6 @@ export {
unstable_getCurrentPriorityLevel, unstable_getCurrentPriorityLevel,
shouldYieldToHost as unstable_shouldYield, shouldYieldToHost as unstable_shouldYield,
requestPaint as unstable_requestPaint, requestPaint as unstable_requestPaint,
requestYield as unstable_requestYield,
unstable_continueExecution, unstable_continueExecution,
unstable_pauseExecution, unstable_pauseExecution,
unstable_getFirstCallbackNode, unstable_getFirstCallbackNode,

View File

@ -67,11 +67,6 @@ export function unstable_requestPaint() {
// Since we yield every frame regardless, `requestPaint` has no effect. // Since we yield every frame regardless, `requestPaint` has no effect.
} }
export function unstable_requestYield() {
// Force a yield at the next opportunity.
deadline = -99999;
}
type SchedulerCallback<T> = ( type SchedulerCallback<T> = (
didTimeout_DEPRECATED: boolean, didTimeout_DEPRECATED: boolean,
) => ) =>