@samitouri / QOS-React-2 / commits / 8ef3a7c08c

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.

Andrew Clark committed Aug 11, 2022 at 22:01 UTC 8ef3a7c08c55c13995267902859381da8b5985ac
18 files changed +539 -200
packages/react-reconciler/src/ReactFiberThrow.new.js
+7 -6
@@ -357,7 +357,7 @@ function throwException(
357 sourceFiber: Fiber,
358 value: mixed,
359 rootRenderLanes: Lanes,
360 -) {
360 +): Wakeable | null {
361 // The source fiber did not complete.
362 sourceFiber.flags |= Incomplete;
363
@@ -459,7 +459,7 @@ function throwException(
459 if (suspenseBoundary.mode & ConcurrentMode) {
460 attachPingListener(root, wakeable, rootRenderLanes);
461 }
462 - return;
462 + return wakeable;
463 } else {
464 // No boundary was found. Unless this is a sync update, this is OK.
465 // We can suspend and wait for more data to arrive.
@@ -474,7 +474,7 @@ function throwException(
474 // This case also applies to initial hydration.
475 attachPingListener(root, wakeable, rootRenderLanes);
476 renderDidSuspendDelayIfPossible();
477 - return;
477 + return wakeable;
478 }
479
480 // This is a sync/discrete update. We treat this case like an error
@@ -517,7 +517,7 @@ function throwException(
517 // Even though the user may not be affected by this error, we should
518 // still log it so it can be fixed.
519 queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
520 - return;
520 + return null;
521 }
522 } else {
523 // Otherwise, fall through to the error path.
@@ -540,7 +540,7 @@ function throwException(
540 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
541 const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
542 enqueueCapturedUpdate(workInProgress, update);
543 - return;
543 + return null;
544 }
545 case ClassComponent:
546 // Capture and retry
@@ -564,7 +564,7 @@ function throwException(
564 lane,
565 );
566 enqueueCapturedUpdate(workInProgress, update);
567 - return;
567 + return null;
568 }
569 break;
570 default:
@@ -572,6 +572,7 @@ function throwException(
572 }
573 workInProgress = workInProgress.return;
574 } while (workInProgress !== null);
575 + return null;
576 }
577
578 export {throwException, createRootErrorUpdate, createClassErrorUpdate};
packages/react-reconciler/src/ReactFiberThrow.old.js
+7 -6
@@ -357,7 +357,7 @@ function throwException(
357 sourceFiber: Fiber,
358 value: mixed,
359 rootRenderLanes: Lanes,
360 -) {
360 +): Wakeable | null {
361 // The source fiber did not complete.
362 sourceFiber.flags |= Incomplete;
363
@@ -459,7 +459,7 @@ function throwException(
459 if (suspenseBoundary.mode & ConcurrentMode) {
460 attachPingListener(root, wakeable, rootRenderLanes);
461 }
462 - return;
462 + return wakeable;
463 } else {
464 // No boundary was found. Unless this is a sync update, this is OK.
465 // We can suspend and wait for more data to arrive.
@@ -474,7 +474,7 @@ function throwException(
474 // This case also applies to initial hydration.
475 attachPingListener(root, wakeable, rootRenderLanes);
476 renderDidSuspendDelayIfPossible();
477 - return;
477 + return wakeable;
478 }
479
480 // This is a sync/discrete update. We treat this case like an error
@@ -517,7 +517,7 @@ function throwException(
517 // Even though the user may not be affected by this error, we should
518 // still log it so it can be fixed.
519 queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
520 - return;
520 + return null;
521 }
522 } else {
523 // Otherwise, fall through to the error path.
@@ -540,7 +540,7 @@ function throwException(
540 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
541 const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
542 enqueueCapturedUpdate(workInProgress, update);
543 - return;
543 + return null;
544 }
545 case ClassComponent:
546 // Capture and retry
@@ -564,7 +564,7 @@ function throwException(
564 lane,
565 );
566 enqueueCapturedUpdate(workInProgress, update);
567 - return;
567 + return null;
568 }
569 break;
570 default:
@@ -572,6 +572,7 @@ function throwException(
572 }
573 workInProgress = workInProgress.return;
574 } while (workInProgress !== null);
575 + return null;
576 }
577
578 export {throwException, createRootErrorUpdate, createClassErrorUpdate};
packages/react-reconciler/src/ReactFiberWakeable.new.js new
+50
@@ -0,0 +1,50 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Wakeable} from 'shared/ReactTypes';
11 +
12 +let suspendedWakeable: Wakeable | null = null;
13 +let wasPinged = false;
14 +let adHocSuspendCount: number = 0;
15 +
16 +const MAX_AD_HOC_SUSPEND_COUNT = 50;
17 +
18 +export function suspendedWakeableWasPinged() {
19 + return wasPinged;
20 +}
21 +
22 +export function trackSuspendedWakeable(wakeable: Wakeable) {
23 + adHocSuspendCount++;
24 + suspendedWakeable = wakeable;
25 +}
26 +
27 +export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
28 + if (wakeable === suspendedWakeable) {
29 + // This ping is from the wakeable that just suspended. Mark it as pinged.
30 + // When the work loop resumes, we'll immediately try rendering the fiber
31 + // again instead of unwinding the stack.
32 + wasPinged = true;
33 + return true;
34 + }
35 + return false;
36 +}
37 +
38 +export function resetWakeableState() {
39 + suspendedWakeable = null;
40 + wasPinged = false;
41 + adHocSuspendCount = 0;
42 +}
43 +
44 +export function throwIfInfinitePingLoopDetected() {
45 + if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
46 + // TODO: Guard against an infinite loop by throwing an error if the same
47 + // component suspends too many times in a row. This should be thrown from
48 + // the render phase so that it gets the component stack.
49 + }
50 +}
packages/react-reconciler/src/ReactFiberWakeable.old.js new
+50
@@ -0,0 +1,50 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Wakeable} from 'shared/ReactTypes';
11 +
12 +let suspendedWakeable: Wakeable | null = null;
13 +let wasPinged = false;
14 +let adHocSuspendCount: number = 0;
15 +
16 +const MAX_AD_HOC_SUSPEND_COUNT = 50;
17 +
18 +export function suspendedWakeableWasPinged() {
19 + return wasPinged;
20 +}
21 +
22 +export function trackSuspendedWakeable(wakeable: Wakeable) {
23 + adHocSuspendCount++;
24 + suspendedWakeable = wakeable;
25 +}
26 +
27 +export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
28 + if (wakeable === suspendedWakeable) {
29 + // This ping is from the wakeable that just suspended. Mark it as pinged.
30 + // When the work loop resumes, we'll immediately try rendering the fiber
31 + // again instead of unwinding the stack.
32 + wasPinged = true;
33 + return true;
34 + }
35 + return false;
36 +}
37 +
38 +export function resetWakeableState() {
39 + suspendedWakeable = null;
40 + wasPinged = false;
41 + adHocSuspendCount = 0;
42 +}
43 +
44 +export function throwIfInfinitePingLoopDetected() {
45 + if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
46 + // TODO: Guard against an infinite loop by throwing an error if the same
47 + // component suspends too many times in a row. This should be thrown from
48 + // the render phase so that it gets the component stack.
49 + }
50 +}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+123 -28
@@ -86,6 +86,7 @@ import {
86 import {
87 createWorkInProgress,
88 assignFiberPropertiesInDEV,
89 + resetWorkInProgress,
90 } from './ReactFiber.new';
91 import {isRootDehydrated} from './ReactFiberShellHydration';
92 import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.new';
@@ -245,6 +246,12 @@ import {
246 isConcurrentActEnvironment,
247 } from './ReactFiberAct.new';
248 import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
249 +import {
250 + resetWakeableState,
251 + trackSuspendedWakeable,
252 + suspendedWakeableWasPinged,
253 + attemptToPingSuspendedWakeable,
254 +} from './ReactFiberWakeable.new';
255
256 const ceil = Math.ceil;
257
@@ -280,6 +287,12 @@ let workInProgress: Fiber | null = null;
287 // The lanes we're rendering
288 let workInProgressRootRenderLanes: Lanes = NoLanes;
289
290 +// When this is true, the work-in-progress fiber just suspended (or errored) and
291 +// we've yet to unwind the stack. In some cases, we may yield to the main thread
292 +// after this happens. If the fiber is pinged before we resume, we can retry
293 +// immediately instead of unwinding the stack.
294 +let workInProgressIsSuspended: boolean = false;
295 +
296 // A contextual version of workInProgressRootRenderLanes. It is a superset of
297 // the lanes that we started working on at the root. When we enter a subtree
298 // that is currently hidden, we add the lanes that would have committed if
@@ -1543,11 +1556,13 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1556 );
1557 interruptedWork = interruptedWork.return;
1558 }
1559 + resetWakeableState();
1560 }
1561 workInProgressRoot = root;
1562 const rootWorkInProgress = createWorkInProgress(root.current, null);
1563 workInProgress = rootWorkInProgress;
1564 workInProgressRootRenderLanes = renderLanes = lanes;
1565 + workInProgressIsSuspended = false;
1566 workInProgressRootExitStatus = RootInProgress;
1567 workInProgressRootFatalError = null;
1568 workInProgressRootSkippedLanes = NoLanes;
@@ -1566,7 +1581,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1581 return rootWorkInProgress;
1582 }
1583
1569 -function handleError(root, thrownValue): void {
1584 +function handleError(root, thrownValue): Wakeable | null {
1585 do {
1586 let erroredWork = workInProgress;
1587 try {
@@ -1592,7 +1607,7 @@ function handleError(root, thrownValue): void {
1607 // intentionally not calling those, we need set it here.
1608 // TODO: Consider calling `unwindWork` to pop the contexts.
1609 workInProgress = null;
1595 - return;
1610 + return null;
1611 }
1612
1613 if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
@@ -1625,14 +1640,21 @@ function handleError(root, thrownValue): void {
1640 }
1641 }
1642
1628 - throwException(
1643 + const maybeWakeable = throwException(
1644 root,
1645 erroredWork.return,
1646 erroredWork,
1647 thrownValue,
1648 workInProgressRootRenderLanes,
1649 );
1635 - completeUnitOfWork(erroredWork);
1650 + // Setting this to `true` tells the work loop to unwind the stack instead
1651 + // of entering the begin phase. It's called "suspended" because it usually
1652 + // happens because of Suspense, but it also applies to errors. Think of it
1653 + // as suspending the execution of the work loop.
1654 + workInProgressIsSuspended = true;
1655 +
1656 + // Return to the normal work loop.
1657 + return maybeWakeable;
1658 } catch (yetAnotherThrownValue) {
1659 // Something in the return path also threw.
1660 thrownValue = yetAnotherThrownValue;
@@ -1646,8 +1668,6 @@ function handleError(root, thrownValue): void {
1668 }
1669 continue;
1670 }
1649 - // Return to the normal work loop.
1650 - return;
1671 } while (true);
1672 }
1673
@@ -1810,7 +1830,14 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1830 // The work loop is an extremely hot path. Tell Closure not to inline it.
1831 /** @noinline */
1832 function workLoopSync() {
1813 - // Already timed out, so perform work without checking if we need to yield.
1833 + // Perform work without checking if we need to yield between fiber.
1834 +
1835 + if (workInProgressIsSuspended && workInProgress !== null) {
1836 + // The current work-in-progress was already attempted. We need to unwind
1837 + // it before we continue the normal work loop.
1838 + resumeSuspendedUnitOfWork(workInProgress);
1839 + }
1840 +
1841 while (workInProgress !== null) {
1842 performUnitOfWork(workInProgress);
1843 }
@@ -1860,7 +1887,14 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1887 workLoopConcurrent();
1888 break;
1889 } catch (thrownValue) {
1863 - handleError(root, thrownValue);
1890 + const maybeWakeable = handleError(root, thrownValue);
1891 + if (maybeWakeable !== null) {
1892 + // If this fiber just suspended, it's possible the data is already
1893 + // cached. Yield to the the main thread to give it a chance to ping. If
1894 + // it does, we can retry immediately without unwinding the stack.
1895 + trackSuspendedWakeable(maybeWakeable);
1896 + break;
1897 + }
1898 }
1899 } while (true);
1900 resetContextDependencies();
@@ -1899,6 +1933,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1933 /** @noinline */
1934 function workLoopConcurrent() {
1935 // Perform work until Scheduler asks us to yield
1936 +
1937 + if (workInProgressIsSuspended && workInProgress !== null) {
1938 + // The current work-in-progress was already attempted. We need to unwind
1939 + // it before we continue the normal work loop.
1940 + resumeSuspendedUnitOfWork(workInProgress);
1941 + }
1942 +
1943 while (workInProgress !== null && !shouldYield()) {
1944 performUnitOfWork(workInProgress);
1945 }
@@ -1932,6 +1973,56 @@ function performUnitOfWork(unitOfWork: Fiber): void {
1973 ReactCurrentOwner.current = null;
1974 }
1975
1976 +function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
1977 + // This is a fork of performUnitOfWork specifcally for resuming a fiber that
1978 + // just suspended. In some cases, we may choose to retry the fiber immediately
1979 + // instead of unwinding the stack. It's a separate function to keep the
1980 + // additional logic out of the work loop's hot path.
1981 +
1982 + if (!suspendedWakeableWasPinged()) {
1983 + // The wakeable wasn't pinged. Return to the normal work loop. This will
1984 + // unwind the stack, and potentially result in showing a fallback.
1985 + workInProgressIsSuspended = false;
1986 + resetWakeableState();
1987 + completeUnitOfWork(unitOfWork);
1988 + return;
1989 + }
1990 +
1991 + // The work-in-progress was immediately pinged. Instead of unwinding the
1992 + // stack and potentially showing a fallback, reset the fiber and try rendering
1993 + // it again.
1994 + unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
1995 +
1996 + const current = unitOfWork.alternate;
1997 + setCurrentDebugFiberInDEV(unitOfWork);
1998 +
1999 + let next;
2000 + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
2001 + startProfilerTimer(unitOfWork);
2002 + next = beginWork(current, unitOfWork, renderLanes);
2003 + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2004 + } else {
2005 + next = beginWork(current, unitOfWork, renderLanes);
2006 + }
2007 +
2008 + // The begin phase finished successfully without suspending. Reset the state
2009 + // used to track the fiber while it was suspended. Then return to the normal
2010 + // work loop.
2011 + workInProgressIsSuspended = false;
2012 + resetWakeableState();
2013 +
2014 + resetCurrentDebugFiberInDEV();
2015 + unitOfWork.memoizedProps = unitOfWork.pendingProps;
2016 + if (next === null) {
2017 + // If this doesn't spawn new work, complete the current work.
2018 + completeUnitOfWork(unitOfWork);
2019 + } else {
2020 + workInProgress = next;
2021 + }
2022 +
2023 + ReactCurrentOwner.current = null;
2024 +}
2025 +
2026 function completeUnitOfWork(unitOfWork: Fiber): void {
2027 // Attempt to complete the current unit of work, then move to the next
2028 // sibling. If there are no more siblings, return to the parent fiber.
@@ -2743,27 +2834,31 @@ export function pingSuspendedRoot(
2834 // Received a ping at the same priority level at which we're currently
2835 // rendering. We might want to restart this render. This should mirror
2836 // the logic of whether or not a root suspends once it completes.
2746 -
2747 - // TODO: If we're rendering sync either due to Sync, Batched or expired,
2748 - // we should probably never restart.
2749 -
2750 - // If we're suspended with delay, or if it's a retry, we'll always suspend
2751 - // so we can always restart.
2752 - if (
2753 - workInProgressRootExitStatus === RootSuspendedWithDelay ||
2754 - (workInProgressRootExitStatus === RootSuspended &&
2755 - includesOnlyRetries(workInProgressRootRenderLanes) &&
2756 - now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2757 - ) {
2758 - // Restart from the root.
2759 - prepareFreshStack(root, NoLanes);
2837 + const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
2838 + if (didPingSuspendedWakeable) {
2839 + // Successfully pinged the in-progress fiber. Don't unwind the stack.
2840 } else {
2761 - // Even though we can't restart right now, we might get an
2762 - // opportunity later. So we mark this render as having a ping.
2763 - workInProgressRootPingedLanes = mergeLanes(
2764 - workInProgressRootPingedLanes,
2765 - pingedLanes,
2766 - );
2841 + // TODO: If we're rendering sync either due to Sync, Batched or expired,
2842 + // we should probably never restart.
2843 +
2844 + // If we're suspended with delay, or if it's a retry, we'll always suspend
2845 + // so we can always restart.
2846 + if (
2847 + workInProgressRootExitStatus === RootSuspendedWithDelay ||
2848 + (workInProgressRootExitStatus === RootSuspended &&
2849 + includesOnlyRetries(workInProgressRootRenderLanes) &&
2850 + now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2851 + ) {
2852 + // Restart from the root.
2853 + prepareFreshStack(root, NoLanes);
2854 + } else {
2855 + // Even though we can't restart right now, we might get an
2856 + // opportunity later. So we mark this render as having a ping.
2857 + workInProgressRootPingedLanes = mergeLanes(
2858 + workInProgressRootPingedLanes,
2859 + pingedLanes,
2860 + );
2861 + }
2862 }
2863 }
2864
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+123 -28
@@ -86,6 +86,7 @@ import {
86 import {
87 createWorkInProgress,
88 assignFiberPropertiesInDEV,
89 + resetWorkInProgress,
90 } from './ReactFiber.old';
91 import {isRootDehydrated} from './ReactFiberShellHydration';
92 import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.old';
@@ -245,6 +246,12 @@ import {
246 isConcurrentActEnvironment,
247 } from './ReactFiberAct.old';
248 import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
249 +import {
250 + resetWakeableState,
251 + trackSuspendedWakeable,
252 + suspendedWakeableWasPinged,
253 + attemptToPingSuspendedWakeable,
254 +} from './ReactFiberWakeable.old';
255
256 const ceil = Math.ceil;
257
@@ -280,6 +287,12 @@ let workInProgress: Fiber | null = null;
287 // The lanes we're rendering
288 let workInProgressRootRenderLanes: Lanes = NoLanes;
289
290 +// When this is true, the work-in-progress fiber just suspended (or errored) and
291 +// we've yet to unwind the stack. In some cases, we may yield to the main thread
292 +// after this happens. If the fiber is pinged before we resume, we can retry
293 +// immediately instead of unwinding the stack.
294 +let workInProgressIsSuspended: boolean = false;
295 +
296 // A contextual version of workInProgressRootRenderLanes. It is a superset of
297 // the lanes that we started working on at the root. When we enter a subtree
298 // that is currently hidden, we add the lanes that would have committed if
@@ -1543,11 +1556,13 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1556 );
1557 interruptedWork = interruptedWork.return;
1558 }
1559 + resetWakeableState();
1560 }
1561 workInProgressRoot = root;
1562 const rootWorkInProgress = createWorkInProgress(root.current, null);
1563 workInProgress = rootWorkInProgress;
1564 workInProgressRootRenderLanes = renderLanes = lanes;
1565 + workInProgressIsSuspended = false;
1566 workInProgressRootExitStatus = RootInProgress;
1567 workInProgressRootFatalError = null;
1568 workInProgressRootSkippedLanes = NoLanes;
@@ -1566,7 +1581,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1581 return rootWorkInProgress;
1582 }
1583
1569 -function handleError(root, thrownValue): void {
1584 +function handleError(root, thrownValue): Wakeable | null {
1585 do {
1586 let erroredWork = workInProgress;
1587 try {
@@ -1592,7 +1607,7 @@ function handleError(root, thrownValue): void {
1607 // intentionally not calling those, we need set it here.
1608 // TODO: Consider calling `unwindWork` to pop the contexts.
1609 workInProgress = null;
1595 - return;
1610 + return null;
1611 }
1612
1613 if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
@@ -1625,14 +1640,21 @@ function handleError(root, thrownValue): void {
1640 }
1641 }
1642
1628 - throwException(
1643 + const maybeWakeable = throwException(
1644 root,
1645 erroredWork.return,
1646 erroredWork,
1647 thrownValue,
1648 workInProgressRootRenderLanes,
1649 );
1635 - completeUnitOfWork(erroredWork);
1650 + // Setting this to `true` tells the work loop to unwind the stack instead
1651 + // of entering the begin phase. It's called "suspended" because it usually
1652 + // happens because of Suspense, but it also applies to errors. Think of it
1653 + // as suspending the execution of the work loop.
1654 + workInProgressIsSuspended = true;
1655 +
1656 + // Return to the normal work loop.
1657 + return maybeWakeable;
1658 } catch (yetAnotherThrownValue) {
1659 // Something in the return path also threw.
1660 thrownValue = yetAnotherThrownValue;
@@ -1646,8 +1668,6 @@ function handleError(root, thrownValue): void {
1668 }
1669 continue;
1670 }
1649 - // Return to the normal work loop.
1650 - return;
1671 } while (true);
1672 }
1673
@@ -1810,7 +1830,14 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1830 // The work loop is an extremely hot path. Tell Closure not to inline it.
1831 /** @noinline */
1832 function workLoopSync() {
1813 - // Already timed out, so perform work without checking if we need to yield.
1833 + // Perform work without checking if we need to yield between fiber.
1834 +
1835 + if (workInProgressIsSuspended && workInProgress !== null) {
1836 + // The current work-in-progress was already attempted. We need to unwind
1837 + // it before we continue the normal work loop.
1838 + resumeSuspendedUnitOfWork(workInProgress);
1839 + }
1840 +
1841 while (workInProgress !== null) {
1842 performUnitOfWork(workInProgress);
1843 }
@@ -1860,7 +1887,14 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1887 workLoopConcurrent();
1888 break;
1889 } catch (thrownValue) {
1863 - handleError(root, thrownValue);
1890 + const maybeWakeable = handleError(root, thrownValue);
1891 + if (maybeWakeable !== null) {
1892 + // If this fiber just suspended, it's possible the data is already
1893 + // cached. Yield to the the main thread to give it a chance to ping. If
1894 + // it does, we can retry immediately without unwinding the stack.
1895 + trackSuspendedWakeable(maybeWakeable);
1896 + break;
1897 + }
1898 }
1899 } while (true);
1900 resetContextDependencies();
@@ -1899,6 +1933,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1933 /** @noinline */
1934 function workLoopConcurrent() {
1935 // Perform work until Scheduler asks us to yield
1936 +
1937 + if (workInProgressIsSuspended && workInProgress !== null) {
1938 + // The current work-in-progress was already attempted. We need to unwind
1939 + // it before we continue the normal work loop.
1940 + resumeSuspendedUnitOfWork(workInProgress);
1941 + }
1942 +
1943 while (workInProgress !== null && !shouldYield()) {
1944 performUnitOfWork(workInProgress);
1945 }
@@ -1932,6 +1973,56 @@ function performUnitOfWork(unitOfWork: Fiber): void {
1973 ReactCurrentOwner.current = null;
1974 }
1975
1976 +function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
1977 + // This is a fork of performUnitOfWork specifcally for resuming a fiber that
1978 + // just suspended. In some cases, we may choose to retry the fiber immediately
1979 + // instead of unwinding the stack. It's a separate function to keep the
1980 + // additional logic out of the work loop's hot path.
1981 +
1982 + if (!suspendedWakeableWasPinged()) {
1983 + // The wakeable wasn't pinged. Return to the normal work loop. This will
1984 + // unwind the stack, and potentially result in showing a fallback.
1985 + workInProgressIsSuspended = false;
1986 + resetWakeableState();
1987 + completeUnitOfWork(unitOfWork);
1988 + return;
1989 + }
1990 +
1991 + // The work-in-progress was immediately pinged. Instead of unwinding the
1992 + // stack and potentially showing a fallback, reset the fiber and try rendering
1993 + // it again.
1994 + unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
1995 +
1996 + const current = unitOfWork.alternate;
1997 + setCurrentDebugFiberInDEV(unitOfWork);
1998 +
1999 + let next;
2000 + if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
2001 + startProfilerTimer(unitOfWork);
2002 + next = beginWork(current, unitOfWork, renderLanes);
2003 + stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2004 + } else {
2005 + next = beginWork(current, unitOfWork, renderLanes);
2006 + }
2007 +
2008 + // The begin phase finished successfully without suspending. Reset the state
2009 + // used to track the fiber while it was suspended. Then return to the normal
2010 + // work loop.
2011 + workInProgressIsSuspended = false;
2012 + resetWakeableState();
2013 +
2014 + resetCurrentDebugFiberInDEV();
2015 + unitOfWork.memoizedProps = unitOfWork.pendingProps;
2016 + if (next === null) {
2017 + // If this doesn't spawn new work, complete the current work.
2018 + completeUnitOfWork(unitOfWork);
2019 + } else {
2020 + workInProgress = next;
2021 + }
2022 +
2023 + ReactCurrentOwner.current = null;
2024 +}
2025 +
2026 function completeUnitOfWork(unitOfWork: Fiber): void {
2027 // Attempt to complete the current unit of work, then move to the next
2028 // sibling. If there are no more siblings, return to the parent fiber.
@@ -2743,27 +2834,31 @@ export function pingSuspendedRoot(
2834 // Received a ping at the same priority level at which we're currently
2835 // rendering. We might want to restart this render. This should mirror
2836 // the logic of whether or not a root suspends once it completes.
2746 -
2747 - // TODO: If we're rendering sync either due to Sync, Batched or expired,
2748 - // we should probably never restart.
2749 -
2750 - // If we're suspended with delay, or if it's a retry, we'll always suspend
2751 - // so we can always restart.
2752 - if (
2753 - workInProgressRootExitStatus === RootSuspendedWithDelay ||
2754 - (workInProgressRootExitStatus === RootSuspended &&
2755 - includesOnlyRetries(workInProgressRootRenderLanes) &&
2756 - now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2757 - ) {
2758 - // Restart from the root.
2759 - prepareFreshStack(root, NoLanes);
2837 + const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
2838 + if (didPingSuspendedWakeable) {
2839 + // Successfully pinged the in-progress fiber. Don't unwind the stack.
2840 } else {
2761 - // Even though we can't restart right now, we might get an
2762 - // opportunity later. So we mark this render as having a ping.
2763 - workInProgressRootPingedLanes = mergeLanes(
2764 - workInProgressRootPingedLanes,
2765 - pingedLanes,
2766 - );
2841 + // TODO: If we're rendering sync either due to Sync, Batched or expired,
2842 + // we should probably never restart.
2843 +
2844 + // If we're suspended with delay, or if it's a retry, we'll always suspend
2845 + // so we can always restart.
2846 + if (
2847 + workInProgressRootExitStatus === RootSuspendedWithDelay ||
2848 + (workInProgressRootExitStatus === RootSuspended &&
2849 + includesOnlyRetries(workInProgressRootRenderLanes) &&
2850 + now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2851 + ) {
2852 + // Restart from the root.
2853 + prepareFreshStack(root, NoLanes);
2854 + } else {
2855 + // Even though we can't restart right now, we might get an
2856 + // opportunity later. So we mark this render as having a ping.
2857 + workInProgressRootPingedLanes = mergeLanes(
2858 + workInProgressRootPingedLanes,
2859 + pingedLanes,
2860 + );
2861 + }
2862 }
2863 }
2864
packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
+26 -15
@@ -485,22 +485,33 @@ describe('ReactOffscreen', () => {
485 // In the same render, also hide the offscreen tree.
486 root.render(<App show={false} />);
487
488 - expect(Scheduler).toFlushUntilNextPaint([
489 - // The outer update will commit, but the inner update is deferred until
490 - // a later render.
491 - 'Outer: 1',
492 -
493 - // Something suspended. This means we won't commit immediately; there
494 - // will be an async gap between render and commit. In this test, we will
495 - // use this property to schedule a concurrent update. The fact that
496 - // we're using Suspense to schedule a concurrent update is not directly
497 - // relevant to the test — we could also use time slicing, but I've
498 - // chosen to use Suspense the because implementation details of time
499 - // slicing are more volatile.
500 - 'Suspend! [Async: 1]',
488 + if (gate(flags => flags.enableSyncDefaultUpdates)) {
489 + expect(Scheduler).toFlushUntilNextPaint([
490 + // The outer update will commit, but the inner update is deferred until
491 + // a later render.
492 + 'Outer: 1',
493 +
494 + // Something suspended. This means we won't commit immediately; there
495 + // will be an async gap between render and commit. In this test, we will
496 + // use this property to schedule a concurrent update. The fact that
497 + // we're using Suspense to schedule a concurrent update is not directly
498 + // relevant to the test — we could also use time slicing, but I've
499 + // chosen to use Suspense the because implementation details of time
500 + // slicing are more volatile.
501 + 'Suspend! [Async: 1]',
502 +
503 + 'Loading...',
504 + ]);
505 + } else {
506 + // When default updates are time sliced, React yields before preparing
507 + // the fallback.
508 + expect(Scheduler).toFlushUntilNextPaint([
509 + 'Outer: 1',
510 + 'Suspend! [Async: 1]',
511 + ]);
512 + expect(Scheduler).toFlushUntilNextPaint(['Loading...']);
513 + }
514
502 - 'Loading...',
503 - ]);
515 // Assert that we haven't committed quite yet
516 expect(root).toMatchRenderedOutput(
517 <>
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+15 -7
@@ -963,33 +963,36 @@ describe('ReactSuspenseWithNoopRenderer', () => {
963
964 // @gate enableCache
965 it('resolves successfully even if fallback render is pending', async () => {
966 - ReactNoop.render(
966 + const root = ReactNoop.createRoot();
967 + root.render(
968 <>
969 <Suspense fallback={<Text text="Loading..." />} />
970 </>,
971 );
972 expect(Scheduler).toFlushAndYield([]);
972 - expect(ReactNoop.getChildren()).toEqual([]);
973 + expect(root).toMatchRenderedOutput(null);
974 if (gate(flags => flags.enableSyncDefaultUpdates)) {
975 React.startTransition(() => {
975 - ReactNoop.render(
976 + root.render(
977 <>
978 <Suspense fallback={<Text text="Loading..." />}>
979 <AsyncText text="Async" />
980 + <Text text="Sibling" />
981 </Suspense>
982 </>,
983 );
984 });
985 } else {
984 - ReactNoop.render(
986 + root.render(
987 <>
988 <Suspense fallback={<Text text="Loading..." />}>
989 <AsyncText text="Async" />
990 + <Text text="Sibling" />
991 </Suspense>
992 </>,
993 );
994 }
992 - expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
995 + expect(Scheduler).toFlushAndYieldThrough(['Suspend! [Async]', 'Sibling']);
996
997 await resolveText('Async');
998 expect(Scheduler).toFlushAndYield([
@@ -998,8 +1001,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1001 'Loading...',
1002 // Once we've completed the boundary we restarted.
1003 'Async',
1004 + 'Sibling',
1005 ]);
1002 - expect(ReactNoop.getChildren()).toEqual([span('Async')]);
1006 + expect(root).toMatchRenderedOutput(
1007 + <>
1008 + <span prop="Async" />
1009 + <span prop="Sibling" />
1010 + </>,
1011 + );
1012 });
1013
1014 // @gate enableCache
@@ -3859,7 +3868,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3868 'Suspend! [A2]',
3869 'Loading...',
3870 'Suspend! [B2]',
3862 - 'Loading...',
3871 ]);
3872 expect(root).toMatchRenderedOutput(
3873 <>
packages/react-reconciler/src/__tests__/ReactWakeable-test.js new
+67
@@ -0,0 +1,67 @@
1 +'use strict';
2 +
3 +let React;
4 +let ReactNoop;
5 +let Scheduler;
6 +let act;
7 +let Suspense;
8 +let startTransition;
9 +
10 +describe('ReactWakeable', () => {
11 + beforeEach(() => {
12 + jest.resetModules();
13 +
14 + React = require('react');
15 + ReactNoop = require('react-noop-renderer');
16 + Scheduler = require('scheduler');
17 + act = require('jest-react').act;
18 + Suspense = React.Suspense;
19 + startTransition = React.startTransition;
20 + });
21 +
22 + function Text(props) {
23 + Scheduler.unstable_yieldValue(props.text);
24 + return props.text;
25 + }
26 +
27 + test('if suspended fiber is pinged in a microtask, retry immediately without unwinding the stack', async () => {
28 + let resolved = false;
29 + function Async() {
30 + if (resolved) {
31 + return <Text text="Async" />;
32 + }
33 + Scheduler.unstable_yieldValue('Suspend!');
34 + throw Promise.resolve().then(() => {
35 + Scheduler.unstable_yieldValue('Resolve in microtask');
36 + resolved = true;
37 + });
38 + }
39 +
40 + function App() {
41 + return (
42 + <Suspense fallback={<Text text="Loading..." />}>
43 + <Async />
44 + </Suspense>
45 + );
46 + }
47 +
48 + await act(async () => {
49 + startTransition(() => {
50 + ReactNoop.render(<App />);
51 + });
52 +
53 + // React will yield when the async component suspends.
54 + expect(Scheduler).toFlushUntilNextPaint(['Suspend!']);
55 +
56 + // Wait for microtasks to resolve
57 + // TODO: The async form of `act` should automatically yield to microtasks
58 + // when a continuation is returned, the way Scheduler does.
59 + await null;
60 +
61 + expect(Scheduler).toHaveYielded(['Resolve in microtask']);
62 + });
63 +
64 + // Finished rendering without unwinding the stack.
65 + expect(Scheduler).toHaveYielded(['Async']);
66 + });
67 +});
packages/scheduler/npm/umd/scheduler.development.js
-8
@@ -54,13 +54,6 @@
54 );
55 }
56
57 - function unstable_requestYield() {
58 - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
59 - this,
60 - arguments
61 - );
62 - }
63 -
57 function unstable_runWithPriority() {
58 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59 this,
@@ -123,7 +116,6 @@
116 unstable_cancelCallback: unstable_cancelCallback,
117 unstable_shouldYield: unstable_shouldYield,
118 unstable_requestPaint: unstable_requestPaint,
126 - unstable_requestYield: unstable_requestYield,
119 unstable_runWithPriority: unstable_runWithPriority,
120 unstable_next: unstable_next,
121 unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/npm/umd/scheduler.production.min.js
-8
@@ -54,13 +54,6 @@
54 );
55 }
56
57 - function unstable_requestYield() {
58 - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
59 - this,
60 - arguments
61 - );
62 - }
63 -
57 function unstable_runWithPriority() {
58 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59 this,
@@ -117,7 +110,6 @@
110 unstable_cancelCallback: unstable_cancelCallback,
111 unstable_shouldYield: unstable_shouldYield,
112 unstable_requestPaint: unstable_requestPaint,
120 - unstable_requestYield: unstable_requestYield,
113 unstable_runWithPriority: unstable_runWithPriority,
114 unstable_next: unstable_next,
115 unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/npm/umd/scheduler.profiling.min.js
-8
@@ -54,13 +54,6 @@
54 );
55 }
56
57 - function unstable_requestYield() {
58 - return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_requestYield.apply(
59 - this,
60 - arguments
61 - );
62 - }
63 -
57 function unstable_runWithPriority() {
58 return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
59 this,
@@ -117,7 +110,6 @@
110 unstable_cancelCallback: unstable_cancelCallback,
111 unstable_shouldYield: unstable_shouldYield,
112 unstable_requestPaint: unstable_requestPaint,
120 - unstable_requestYield: unstable_requestYield,
113 unstable_runWithPriority: unstable_runWithPriority,
114 unstable_next: unstable_next,
115 unstable_wrapCallback: unstable_wrapCallback,
packages/scheduler/src/__tests__/Scheduler-test.js
+8 -23
@@ -18,7 +18,6 @@ let performance;
18 let cancelCallback;
19 let scheduleCallback;
20 let requestPaint;
21 -let requestYield;
21 let shouldYield;
22 let NormalPriority;
23
@@ -44,7 +43,6 @@ describe('SchedulerBrowser', () => {
43 scheduleCallback = Scheduler.unstable_scheduleCallback;
44 NormalPriority = Scheduler.unstable_NormalPriority;
45 requestPaint = Scheduler.unstable_requestPaint;
47 - requestYield = Scheduler.unstable_requestYield;
46 shouldYield = Scheduler.unstable_shouldYield;
47 });
48
@@ -480,19 +478,13 @@ describe('SchedulerBrowser', () => {
478 ]);
479 });
480
483 - it('requestYield forces a yield immediately', () => {
481 + it('yielding continues in a new task regardless of how much time is remaining', () => {
482 scheduleCallback(NormalPriority, () => {
483 runtime.log('Original Task');
484 runtime.log('shouldYield: ' + shouldYield());
487 - runtime.log('requestYield');
488 - requestYield();
489 - runtime.log('shouldYield: ' + shouldYield());
485 + runtime.log('Return a continuation');
486 return () => {
487 runtime.log('Continuation Task');
492 - runtime.log('shouldYield: ' + shouldYield());
493 - runtime.log('Advance time past frame deadline');
494 - runtime.advanceTime(10000);
495 - runtime.log('shouldYield: ' + shouldYield());
488 };
489 });
490 runtime.assertLog(['Post Message']);
@@ -501,27 +493,20 @@ describe('SchedulerBrowser', () => {
493 runtime.assertLog([
494 'Message Event',
495 'Original Task',
496 + // Immediately before returning a continuation, `shouldYield` returns
497 + // false, which means there must be time remaining in the frame.
498 'shouldYield: false',
505 - 'requestYield',
506 - // Immediately after calling requestYield, shouldYield starts
507 - // returning true, even though no time has elapsed in the frame
508 - 'shouldYield: true',
499 + 'Return a continuation',
500
510 - // The continuation should be scheduled in a separate macrotask.
501 + // The continuation should be scheduled in a separate macrotask even
502 + // though there's time remaining.
503 'Post Message',
504 ]);
505
506 // No time has elapsed
507 expect(performance.now()).toBe(0);
508
517 - // Subsequent tasks work as normal
509 runtime.fireMessageEvent();
519 - runtime.assertLog([
520 - 'Message Event',
521 - 'Continuation Task',
522 - 'shouldYield: false',
523 - 'Advance time past frame deadline',
524 - 'shouldYield: true',
525 - ]);
510 + runtime.assertLog(['Message Event', 'Continuation Task']);
511 });
512 });
packages/scheduler/src/__tests__/SchedulerMock-test.js
+37 -21
@@ -726,37 +726,53 @@ describe('Scheduler', () => {
726 expect(Scheduler).toFlushWithoutYielding();
727 });
728
729 - it('requestYield forces a yield immediately', () => {
729 + it('toFlushUntilNextPaint stops if a continuation is returned', () => {
730 scheduleCallback(NormalPriority, () => {
731 Scheduler.unstable_yieldValue('Original Task');
732 - Scheduler.unstable_yieldValue(
733 - 'shouldYield: ' + Scheduler.unstable_shouldYield(),
734 - );
735 - Scheduler.unstable_yieldValue('requestYield');
736 - Scheduler.unstable_requestYield();
737 - Scheduler.unstable_yieldValue(
738 - 'shouldYield: ' + Scheduler.unstable_shouldYield(),
739 - );
732 + Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
733 + Scheduler.unstable_yieldValue('Return a continuation');
734 return () => {
735 Scheduler.unstable_yieldValue('Continuation Task');
742 - Scheduler.unstable_yieldValue(
743 - 'shouldYield: ' + Scheduler.unstable_shouldYield(),
744 - );
745 - Scheduler.unstable_yieldValue('Advance time past frame deadline');
746 - Scheduler.unstable_yieldValue(
747 - 'shouldYield: ' + Scheduler.unstable_shouldYield(),
748 - );
736 };
737 });
738
752 - // The continuation should be scheduled in a separate macrotask.
739 expect(Scheduler).toFlushUntilNextPaint([
740 'Original Task',
741 + // Immediately before returning a continuation, `shouldYield` returns
742 + // false, which means there must be time remaining in the frame.
743 'shouldYield: false',
756 - 'requestYield',
757 - // Immediately after calling requestYield, shouldYield starts
758 - // returning true
759 - 'shouldYield: true',
744 + 'Return a continuation',
745 +
746 + // The continuation should not flush yet.
747 + ]);
748 +
749 + // No time has elapsed
750 + expect(Scheduler.unstable_now()).toBe(0);
751 +
752 + // Continue the task
753 + expect(Scheduler).toFlushAndYield(['Continuation Task']);
754 + });
755 +
756 + it("toFlushAndYield keeps flushing even if there's a continuation", () => {
757 + scheduleCallback(NormalPriority, () => {
758 + Scheduler.unstable_yieldValue('Original Task');
759 + Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield());
760 + Scheduler.unstable_yieldValue('Return a continuation');
761 + return () => {
762 + Scheduler.unstable_yieldValue('Continuation Task');
763 + };
764 + });
765 +
766 + expect(Scheduler).toFlushAndYield([
767 + 'Original Task',
768 + // Immediately before returning a continuation, `shouldYield` returns
769 + // false, which means there must be time remaining in the frame.
770 + 'shouldYield: false',
771 + 'Return a continuation',
772 +
773 + // The continuation should flush immediately, even though the task
774 + // yielded a continuation.
775 + 'Continuation Task',
776 ]);
777 });
778 });
packages/scheduler/src/__tests__/SchedulerPostTask-test.js
+8 -23
@@ -23,7 +23,6 @@ let UserBlockingPriority;
23 let LowPriority;
24 let IdlePriority;
25 let shouldYield;
26 -let requestYield;
26
27 // The Scheduler postTask implementation uses a new postTask browser API to
28 // schedule work on the main thread. This test suite mocks all browser methods
@@ -47,7 +46,6 @@ describe('SchedulerPostTask', () => {
46 LowPriority = Scheduler.unstable_LowPriority;
47 IdlePriority = Scheduler.unstable_IdlePriority;
48 shouldYield = Scheduler.unstable_shouldYield;
50 - requestYield = Scheduler.unstable_requestYield;
49 });
50
51 afterEach(() => {
@@ -301,19 +299,13 @@ describe('SchedulerPostTask', () => {
299 ]);
300 });
301
304 - it('requestYield forces a yield immediately', () => {
302 + it('yielding continues in a new task regardless of how much time is remaining', () => {
303 scheduleCallback(NormalPriority, () => {
304 runtime.log('Original Task');
305 runtime.log('shouldYield: ' + shouldYield());
308 - runtime.log('requestYield');
309 - requestYield();
310 - runtime.log('shouldYield: ' + shouldYield());
306 + runtime.log('Return a continuation');
307 return () => {
308 runtime.log('Continuation Task');
313 - runtime.log('shouldYield: ' + shouldYield());
314 - runtime.log('Advance time past frame deadline');
315 - runtime.advanceTime(10000);
316 - runtime.log('shouldYield: ' + shouldYield());
309 };
310 });
311 runtime.assertLog(['Post Task 0 [user-visible]']);
@@ -322,27 +314,20 @@ describe('SchedulerPostTask', () => {
314 runtime.assertLog([
315 'Task 0 Fired',
316 'Original Task',
317 + // Immediately before returning a continuation, `shouldYield` returns
318 + // false, which means there must be time remaining in the frame.
319 'shouldYield: false',
326 - 'requestYield',
327 - // Immediately after calling requestYield, shouldYield starts
328 - // returning true, even though no time has elapsed in the frame
329 - 'shouldYield: true',
320 + 'Return a continuation',
321
331 - // The continuation should be scheduled in a separate macrotask.
322 + // The continuation should be scheduled in a separate macrotask even
323 + // though there's time remaining.
324 'Post Task 1 [user-visible]',
325 ]);
326
327 // No time has elapsed
328 expect(performance.now()).toBe(0);
329
338 - // Subsequent tasks work as normal
330 runtime.flushTasks();
340 - runtime.assertLog([
341 - 'Task 1 Fired',
342 - 'Continuation Task',
343 - 'shouldYield: false',
344 - 'Advance time past frame deadline',
345 - 'shouldYield: true',
346 - ]);
331 + runtime.assertLog(['Task 1 Fired', 'Continuation Task']);
332 });
333 });
packages/scheduler/src/forks/Scheduler.js
+5 -7
@@ -212,10 +212,14 @@ function workLoop(hasTimeRemaining, initialTime) {
212 const continuationCallback = callback(didUserCallbackTimeout);
213 currentTime = getCurrentTime();
214 if (typeof continuationCallback === 'function') {
215 + // If a continuation is returned, immediately yield to the main thread
216 + // regardless of how much time is left in the current time slice.
217 currentTask.callback = continuationCallback;
218 if (enableProfiling) {
219 markTaskYield(currentTask, currentTime);
220 }
221 + advanceTimers(currentTime);
222 + return true;
223 } else {
224 if (enableProfiling) {
225 markTaskCompleted(currentTask, currentTime);
@@ -224,8 +228,8 @@ function workLoop(hasTimeRemaining, initialTime) {
228 if (currentTask === peek(taskQueue)) {
229 pop(taskQueue);
230 }
231 + advanceTimers(currentTime);
232 }
228 - advanceTimers(currentTime);
233 } else {
234 pop(taskQueue);
235 }
@@ -495,11 +499,6 @@ function requestPaint() {
499 // Since we yield every frame regardless, `requestPaint` has no effect.
500 }
501
498 -function requestYield() {
499 - // Force a yield at the next opportunity.
500 - startTime = -99999;
501 -}
502 -
502 function forceFrameRate(fps) {
503 if (fps < 0 || fps > 125) {
504 // Using console['error'] to evade Babel and ESLint
@@ -617,7 +616,6 @@ export {
616 unstable_getCurrentPriorityLevel,
617 shouldYieldToHost as unstable_shouldYield,
618 requestPaint as unstable_requestPaint,
620 - requestYield as unstable_requestYield,
619 unstable_continueExecution,
620 unstable_pauseExecution,
621 unstable_getFirstCallbackNode,
packages/scheduler/src/forks/SchedulerMock.js
+13 -7
@@ -195,10 +195,22 @@ function workLoop(hasTimeRemaining, initialTime) {
195 const continuationCallback = callback(didUserCallbackTimeout);
196 currentTime = getCurrentTime();
197 if (typeof continuationCallback === 'function') {
198 + // If a continuation is returned, immediately yield to the main thread
199 + // regardless of how much time is left in the current time slice.
200 currentTask.callback = continuationCallback;
201 if (enableProfiling) {
202 markTaskYield(currentTask, currentTime);
203 }
204 + advanceTimers(currentTime);
205 +
206 + if (shouldYieldForPaint) {
207 + needsPaint = true;
208 + return true;
209 + } else {
210 + // If `shouldYieldForPaint` is false, we keep flushing synchronously
211 + // without yielding to the main thread. This is the behavior of the
212 + // `toFlushAndYield` and `toFlushAndYieldThrough` testing helpers .
213 + }
214 } else {
215 if (enableProfiling) {
216 markTaskCompleted(currentTask, currentTime);
@@ -207,8 +219,8 @@ function workLoop(hasTimeRemaining, initialTime) {
219 if (currentTask === peek(taskQueue)) {
220 pop(taskQueue);
221 }
222 + advanceTimers(currentTime);
223 }
211 - advanceTimers(currentTime);
224 } else {
225 pop(taskQueue);
226 }
@@ -608,11 +620,6 @@ function requestPaint() {
620 needsPaint = true;
621 }
622
611 -function requestYield() {
612 - // Force a yield at the next opportunity.
613 - shouldYieldForPaint = needsPaint = true;
614 -}
615 -
623 export {
624 ImmediatePriority as unstable_ImmediatePriority,
625 UserBlockingPriority as unstable_UserBlockingPriority,
@@ -627,7 +634,6 @@ export {
634 unstable_getCurrentPriorityLevel,
635 shouldYieldToHost as unstable_shouldYield,
636 requestPaint as unstable_requestPaint,
630 - requestYield as unstable_requestYield,
637 unstable_continueExecution,
638 unstable_pauseExecution,
639 unstable_getFirstCallbackNode,
packages/scheduler/src/forks/SchedulerPostTask.js
-5
@@ -67,11 +67,6 @@ export function unstable_requestPaint() {
67 // Since we yield every frame regardless, `requestPaint` has no effect.
68 }
69
70 -export function unstable_requestYield() {
71 - // Force a yield at the next opportunity.
72 - deadline = -99999;
73 -}
74 -
70 type SchedulerCallback<T> = (
71 didTimeout_DEPRECATED: boolean,
72 ) =>