Detect and prevent render starvation, per lane (#18864)

* Detect and prevent render starvation, per lane

If an update is CPU-bound for longer than expected according to its
priority, we assume it's being starved by other work on the main thread.

To detect this, we keep track of the elapsed time using a fixed-size
array where each slot corresponds to a lane. What we actually store is
the event time when the lane first became CPU-bound.

Then, when receiving a new update or yielding to the main thread, we
check how long each lane has been pending. If the time exceeds a
threshold constant corresponding to its priority, we mark it as expired
to force it to synchronously finish.

We don't want to mistake time elapsed while an update is IO-bound
(waiting for data to resolve) for time when it is CPU-bound. So when a
lane suspends, we clear its associated event time from the array. When
it receives a signal to try again, either a ping or an update, we assign
a new event time to restart the clock.

* Store as expiration time, not start time

I originally stored the start time because I thought I could use this
in the future to also measure Suspense timeouts. (Event times are
currently stored on each update object for this purpose.) But that
won't work because in the case of expiration times, we reset the clock
whenever the update becomes IO-bound. So to replace the per-update
field, I'm going to have to track those on the room separately from
expiration times.
This commit is contained in:
Andrew Clark
2020-05-08 12:47:51 -07:00
committed by GitHub
parent 6207743168
commit 6edaf6f764
16 changed files with 719 additions and 130 deletions
@@ -112,6 +112,7 @@ import {
SyncLane,
OffscreenLane,
DefaultHydrationLane,
NoTimestamp,
includesSomeLane,
laneToLanes,
removeLanes,
@@ -2301,7 +2302,9 @@ function updateDehydratedSuspenseComponent(
// is one of the very rare times where we mutate the current tree
// during the render phase.
suspenseState.retryLane = attemptHydrationAtLane;
scheduleUpdateOnFiber(current, attemptHydrationAtLane);
// TODO: Ideally this would inherit the event time of the current render
const eventTime = NoTimestamp;
scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
} else {
// We have already tried to ping at a higher priority than we're rendering with
// so if we got here, we must have failed to hydrate at those levels. We must
@@ -202,7 +202,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
},
enqueueReplaceState(inst, payload, callback) {
const fiber = getInstance(inst);
@@ -222,7 +222,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
},
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
@@ -241,7 +241,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
},
};
@@ -1720,7 +1720,7 @@ function dispatchAction<S, A>(
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
@@ -20,7 +20,7 @@ import {
} from './ReactFiberWorkLoop.new';
import {updateContainer} from './ReactFiberReconciler.new';
import {emptyContextObject} from './ReactFiberContext.new';
import {SyncLane} from './ReactFiberLane';
import {SyncLane, NoTimestamp} from './ReactFiberLane';
import {
ClassComponent,
FunctionComponent,
@@ -319,7 +319,7 @@ function scheduleFibersWithFamiliesRecursively(
fiber._debugNeedsRemount = true;
}
if (needsRemount || needsRender) {
scheduleUpdateOnFiber(fiber, SyncLane);
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}
if (child !== null && !needsRemount) {
scheduleFibersWithFamiliesRecursively(
+113 -11
View File
@@ -29,6 +29,7 @@ export opaque type LanePriority =
| 16;
export opaque type Lanes = number;
export opaque type Lane = number;
export opaque type LaneMap<T> = Array<T>;
import invariant from 'shared/invariant';
@@ -66,7 +67,7 @@ const IdleLanePriority: LanePriority = 2;
const OffscreenLanePriority: LanePriority = 1;
const NoLanePriority: LanePriority = 0;
export const NoLanePriority: LanePriority = 0;
const TotalLanes = 31;
@@ -117,6 +118,8 @@ const IdleUpdateRangeEnd = 30;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
export const NoTimestamp = -1;
// "Registers" used to "return" multiple values
// Used by getHighestPriorityLanes and getNextLanes:
let return_highestLanePriority: LanePriority = DefaultLanePriority;
@@ -365,6 +368,63 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
return nextLanes;
}
function computeExpirationTime(lane: Lane, currentTime: number) {
// TODO: Expiration heuristic is constant per lane, so could use a map.
getHighestPriorityLanes(lane);
const priority = return_highestLanePriority;
if (priority >= InputContinuousLanePriority) {
// User interactions should expire slightly more quickly.
return currentTime + 1000;
} else if (priority >= TransitionLongLanePriority) {
return currentTime + 5000;
} else {
// Anything idle priority or lower should never expire.
return NoTimestamp;
}
}
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
// TODO: This gets called every time we yield. We can optimize by storing
// the earliest expiration time on the root. Then use that to quickly bail out
// of this function.
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
// Iterate through the pending lanes and check if we've reached their
// expiration time. If so, we'll assume the update is being starved and mark
// it as expired to force it to finish.
let lanes = pendingLanes;
while (lanes > 0) {
const index = ctrz(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
// Found a pending lane with no expiration time. If it's not suspended, or
// if it's pinged, assume it's CPU-bound. Compute a new expiration time
// using the current time.
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
// Assumes timestamps are monotonically increasing.
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// This lane expired
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
}
// This returns the highest priority pending lanes regardless of whether they
// are suspended.
export function getHighestPriorityPendingLanes(root: FiberRoot) {
@@ -555,6 +615,10 @@ export function higherPriorityLane(a: Lane, b: Lane) {
return a !== NoLane && a < b ? a : b;
}
export function createLaneMap<T>(initial: T): LaneMap<T> {
return new Array(TotalLanes).fill(initial);
}
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
root.pendingLanes |= updateLane;
@@ -570,6 +634,7 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
// Unsuspend any update at equal or lower priority.
const higherPriorityLanes = updateLane - 1; // Turns 0b1000 into 0b0111
root.suspendedLanes &= higherPriorityLanes;
root.pingedLanes &= higherPriorityLanes;
}
@@ -577,9 +642,25 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;
// The suspended lanes are no longer CPU-bound. Clear their expiration times.
const expirationTimes = root.expirationTimes;
let lanes = suspendedLanes;
while (lanes > 0) {
const index = ctrz(lanes);
const lane = 1 << index;
expirationTimes[index] = NoTimestamp;
lanes &= ~lane;
}
}
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
export function markRootPinged(
root: FiberRoot,
pingedLanes: Lanes,
eventTime: number,
) {
root.pingedLanes |= root.suspendedLanes & pingedLanes;
}
@@ -600,6 +681,8 @@ export function markRootMutableRead(root: FiberRoot, updateLane: Lane) {
}
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
root.pendingLanes = remainingLanes;
// Let's try everything again
@@ -608,6 +691,18 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
root.expiredLanes &= remainingLanes;
root.mutableReadLanes &= remainingLanes;
const expirationTimes = root.expirationTimes;
let lanes = noLongerPendingLanes;
while (lanes > 0) {
const index = ctrz(lanes);
const lane = 1 << index;
// Clear the expiration time
expirationTimes[index] = -1;
lanes &= ~lane;
}
}
export function getBumpedLaneForHydration(
@@ -671,18 +766,25 @@ export function getBumpedLaneForHydration(
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Taken from:
// Count leading zeros. Only used on lanes, so assume input is an integer.
// Based on:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
const log = Math.log;
const LN2 = Math.LN2;
function clz32Fallback(x) {
// Let n be ToUint32(x).
// Let p be the number of leading zero bits in
// the 32-bit binary representation of n.
// Return p.
const asUint = x >>> 0;
if (asUint === 0) {
function clz32Fallback(lanes: Lanes | Lane) {
if (lanes === 0) {
return 32;
}
return (31 - ((log(asUint) / LN2) | 0)) | 0; // the "| 0" acts like math.floor
return (31 - ((log(lanes) / LN2) | 0)) | 0;
}
// Count trailing zeros. Only used on lanes, so assume input is an integer.
function ctrz(lanes: Lanes | Lane) {
let bits = lanes;
bits |= bits << 16;
bits |= bits << 8;
bits |= bits << 4;
bits |= bits << 2;
bits |= bits << 1;
return 32 - clz32(~bits);
}
@@ -22,6 +22,7 @@ import {
} from './ReactWorkTags';
import {
NoLanes,
NoTimestamp,
isSubsetOfLanes,
includesSomeLane,
mergeLanes,
@@ -209,7 +210,7 @@ export function propagateContextChange(
if (fiber.tag === ClassComponent) {
// Schedule a force update on the work-in-progress.
const update = createUpdate(
-1,
NoTimestamp,
pickArbitraryLane(renderLanes),
null,
);
@@ -76,6 +76,7 @@ import {
SyncLane,
InputDiscreteHydrationLane,
SelectiveHydrationLane,
NoTimestamp,
getHighestPriorityPendingLanes,
higherPriorityLane,
} from './ReactFiberLane';
@@ -307,7 +308,7 @@ export function updateContainer(
}
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current, lane);
scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}
@@ -352,7 +353,8 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
}
break;
case SuspenseComponent:
flushSync(() => scheduleUpdateOnFiber(fiber, SyncLane));
const eventTime = requestEventTime();
flushSync(() => scheduleUpdateOnFiber(fiber, SyncLane, eventTime));
// If we're still blocked after this, we need to increase
// the priority of any promises resolving within this
// boundary so that they next attempt also has higher pri.
@@ -389,8 +391,9 @@ export function attemptUserBlockingHydration(fiber: Fiber): void {
// Suspense.
return;
}
const eventTime = requestEventTime();
const lane = InputDiscreteHydrationLane;
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
markRetryLaneIfNotHydrated(fiber, lane);
}
@@ -402,8 +405,9 @@ export function attemptContinuousHydration(fiber: Fiber): void {
// Suspense.
return;
}
const eventTime = requestEventTime();
const lane = SelectiveHydrationLane;
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
markRetryLaneIfNotHydrated(fiber, lane);
}
@@ -413,8 +417,9 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
// their priority other than synchronously flush it.
return;
}
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber, null);
scheduleUpdateOnFiber(fiber, lane);
scheduleUpdateOnFiber(fiber, lane, eventTime);
markRetryLaneIfNotHydrated(fiber, lane);
}
@@ -497,7 +502,7 @@ if (__DEV__) {
// Shallow cloning props works as a workaround for now to bypass the bailout check.
fiber.memoizedProps = {...fiber.memoizedProps};
scheduleUpdateOnFiber(fiber, SyncLane);
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}
};
@@ -507,11 +512,11 @@ if (__DEV__) {
if (fiber.alternate) {
fiber.alternate.pendingProps = fiber.pendingProps;
}
scheduleUpdateOnFiber(fiber, SyncLane);
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
};
scheduleUpdate = (fiber: Fiber) => {
scheduleUpdateOnFiber(fiber, SyncLane);
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
};
setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
@@ -12,7 +12,12 @@ import type {RootTag} from './ReactRootTags';
import {noTimeout} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber.new';
import {NoLanes} from './ReactFiberLane';
import {
NoLanes,
NoLanePriority,
NoTimestamp,
createLaneMap,
} from './ReactFiberLane';
import {
enableSchedulerTracing,
enableSuspenseCallback,
@@ -33,8 +38,8 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.hydrate = hydrate;
this.callbackNode = null;
this.callbackId = NoLanes;
this.callbackIsSync = false;
this.expiresAt = -1;
this.callbackPriority_new = NoLanePriority;
this.expirationTimes = createLaneMap(NoTimestamp);
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
@@ -36,7 +36,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.pendingContext = null;
this.hydrate = hydrate;
this.callbackNode = null;
this.callbackPriority = NoPriority;
this.callbackPriority_old = NoPriority;
this.firstPendingTime = NoWork;
this.lastPendingTime = NoWork;
this.firstSuspendedTime = NoWork;
@@ -56,6 +56,7 @@ import {logCapturedError} from './ReactFiberErrorLogger';
import {
SyncLane,
NoTimestamp,
includesSomeLane,
mergeLanes,
pickArbitraryLane,
@@ -68,7 +69,7 @@ function createRootErrorUpdate(
errorInfo: CapturedValue<mixed>,
lane: Lane,
): Update<mixed> {
const update = createUpdate(-1, lane, null);
const update = createUpdate(NoTimestamp, lane, null);
// Unmount the root by rendering null.
update.tag = CaptureUpdate;
// Caution: React DevTools currently depends on this property
@@ -87,7 +88,7 @@ function createClassErrorUpdate(
errorInfo: CapturedValue<mixed>,
lane: Lane,
): Update<mixed> {
const update = createUpdate(-1, lane, null);
const update = createUpdate(NoTimestamp, lane, null);
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === 'function') {
@@ -254,7 +255,7 @@ function throwException(
// When we try rendering again, we should not reuse the current fiber,
// since it's known to be in an inconsistent state. Use a force update to
// prevent a bail out.
const update = createUpdate(-1, SyncLane, null);
const update = createUpdate(NoTimestamp, SyncLane, null);
update.tag = ForceUpdate;
enqueueUpdate(sourceFiber, update);
}
@@ -109,6 +109,7 @@ import {
HydratingAndUpdate,
} from './ReactSideEffectTags';
import {
NoLanePriority,
SyncLanePriority,
InputDiscreteLanePriority,
TransitionShortLanePriority,
@@ -118,6 +119,7 @@ import {
SyncLane,
SyncBatchedLane,
OffscreenLane,
NoTimestamp,
findUpdateLane,
findTransitionLane,
includesSomeLane,
@@ -129,6 +131,7 @@ import {
hasUpdatePriority,
getNextLanes,
returnNextLanesPriority,
markStarvedLanesAsExpired,
getLanesToRetrySynchronouslyOnError,
markRootUpdated,
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
@@ -253,8 +256,8 @@ let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
// A fatal error, if one is thrown
let workInProgressRootFatalError: mixed = null;
// Most recent event time among processed updates during this render.
let workInProgressRootLatestProcessedEventTime: number = -1;
let workInProgressRootLatestSuspenseTimeout: number = -1;
let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
// "Included" lanes refer to lanes that were worked on during this render. It's
// slightly different than `renderLanes` because `renderLanes` can change as you
@@ -310,7 +313,7 @@ let spawnedWorkDuringRender: null | Array<Lane | Lanes> = null;
// If two updates are scheduled within the same event, we should treat their
// event times as simultaneous, even if the actual clock time has advanced
// between the first and second call.
let currentEventTime: number = -1;
let currentEventTime: number = NoTimestamp;
let currentEventWipLanes: Lanes = NoLanes;
let currentEventPendingLanes: Lanes = NoLanes;
@@ -331,7 +334,7 @@ export function requestEventTime() {
return now();
}
// We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== -1) {
if (currentEventTime !== NoTimestamp) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
}
@@ -434,7 +437,11 @@ export function requestUpdateLane(
return lane;
}
export function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
@@ -463,7 +470,7 @@ export function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
@@ -492,7 +499,7 @@ export function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
}
}
// Schedule other updates after in case the callback is sync.
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
@@ -592,40 +599,39 @@ function markUpdateLaneFromFiberToRoot(
// expiration time of the existing task is the same as the expiration time of
// the next level that the root has work on. This function is called on every
// update, and right before exiting a task.
function ensureRootIsScheduled(root: FiberRoot) {
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
const newCallbackId = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// This returns the priority level computed during the `getNextLanes` call.
const newCallbackPriorityLevel = returnNextLanesPriority();
const newCallbackPriority = returnNextLanesPriority();
if (newCallbackId === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
root.expiresAt = -1;
root.callbackNode = null;
root.callbackIsSync = false;
root.callbackPriority_new = NoLanePriority;
root.callbackId = NoLanes;
}
return;
}
const newTaskIsSync = newCallbackPriorityLevel === SyncLanePriority;
// Check if there's an existing task. We may be able to reuse it.
const existingTaskId = root.callbackId;
const existingCallbackIsSync = root.callbackIsSync;
if (existingTaskId !== NoLanes) {
if (newCallbackId === existingTaskId) {
const existingCallbackId = root.callbackId;
const existingCallbackPriority = root.callbackPriority_new;
if (existingCallbackId !== NoLanes) {
if (newCallbackId === existingCallbackId) {
// This task is already scheduled. Let's check its priority.
if (
(newTaskIsSync && existingCallbackIsSync) ||
(!newTaskIsSync && !existingCallbackIsSync)
) {
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. Exit.
return;
}
@@ -637,7 +643,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
// Schedule a new callback.
let newCallbackNode;
if (newTaskIsSync) {
if (newCallbackPriority === SyncLanePriority) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
newCallbackNode = scheduleSyncCallback(
@@ -645,7 +651,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
);
} else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriorityLevel,
newCallbackPriority,
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
@@ -654,8 +660,8 @@ function ensureRootIsScheduled(root: FiberRoot) {
}
root.callbackId = newCallbackId;
root.callbackPriority_new = newCallbackPriority;
root.callbackNode = newCallbackNode;
root.callbackIsSync = newTaskIsSync;
}
// This is the entry point for every concurrent task, i.e. anything that
@@ -663,7 +669,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
function performConcurrentWorkOnRoot(root, didTimeout) {
// Since we know we're in a React event, we can clear the current
// event time. The next update will compute a new event time.
currentEventTime = -1;
currentEventTime = NoTimestamp;
currentEventWipLanes = NoLanes;
currentEventPendingLanes = NoLanes;
@@ -677,29 +683,16 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
return null;
}
// Check if any work has expired.
const rootExpiresAt = root.expiresAt;
if (rootExpiresAt !== -1 && rootExpiresAt < now()) {
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug where `shouldYield` sometimes returns `true` even if `didTimeout` is
// true, which leads to an infinite loop. Once the bug in Scheduler is
// fixed, we can remove this, since we track expiration ourselves.
if (didTimeout) {
// Something expired. Flush synchronously until there's no expired
// work left.
// TODO: Should flush only the lanes that have expired, and maybe any lanes
// that are higher priority than that.
markRootExpired(root, lanes);
// This will schedule a synchronous callback.
ensureRootIsScheduled(root);
return null;
}
// Similar branch, but for Scheduler.
// TODO: This is only here to account for a Scheduler bug where `shouldYield`
// sometimes returns `true` even if `didTimeout` is true, which leads to
// an infinite loop. Once the bug in Scheduler is fixed, we can remove this,
// since we track expiration times ourselves.
if (didTimeout) {
// The Scheduler task took too long to complete. Mark the root as expired to
// prevent yielding to other tasks until this one finishes.
markRootExpired(root, lanes);
// This will schedule a synchronous callback.
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
return null;
}
@@ -742,7 +735,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
throw fatalError;
}
@@ -754,7 +747,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
finishConcurrentRender(root, finishedWork, exitStatus, lanes);
}
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
@@ -788,7 +781,7 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
// have a new loading state ready. We want to ensure that we commit
// that as soon as possible.
const hasNotProcessedNewUpdates =
workInProgressRootLatestProcessedEventTime === -1;
workInProgressRootLatestProcessedEventTime === NoTimestamp;
if (
hasNotProcessedNewUpdates &&
// do not delay if we're inside an act() scope
@@ -815,7 +808,8 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
// suspended level. Ping the last suspended level to try
// rendering it again.
// FIXME: What if the suspended lanes are Idle? Should not restart.
markRootPinged(root, suspendedLanes);
const eventTime = requestEventTime();
markRootPinged(root, suspendedLanes, eventTime);
break;
}
@@ -853,16 +847,17 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
// suspended level. Ping the last suspended level to try
// rendering it again.
// FIXME: What if the suspended lanes are Idle? Should not restart.
markRootPinged(root, suspendedLanes);
const eventTime = requestEventTime();
markRootPinged(root, suspendedLanes, eventTime);
break;
}
let msUntilTimeout;
if (workInProgressRootLatestSuspenseTimeout !== -1) {
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
// We have processed a suspense config whose expiration time we
// can use as the timeout.
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
} else if (workInProgressRootLatestProcessedEventTime === -1) {
} else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
// This should never normally happen because only new updates
// cause delayed states, so we should have processed something.
// However, this could also happen in an offscreen tree.
@@ -896,7 +891,7 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
if (
// do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV() &&
workInProgressRootLatestProcessedEventTime !== -1 &&
workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
workInProgressRootCanSuspendUsingConfig !== null
) {
// If we have exceeded the minimum loading delay, which probably
@@ -992,7 +987,7 @@ function performSyncWorkOnRoot(root) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
throw fatalError;
}
@@ -1005,14 +1000,14 @@ function performSyncWorkOnRoot(root) {
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
return null;
}
export function flushRoot(root: FiberRoot, lanes: Lanes) {
markRootExpired(root, lanes);
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
flushSyncCallbackQueue();
}
@@ -1059,7 +1054,7 @@ function flushPendingDiscreteUpdates() {
rootsWithPendingDiscreteUpdates = null;
roots.forEach(root => {
markDiscreteUpdatesExpired(root);
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
});
}
// Now flush the immediate queue.
@@ -1214,8 +1209,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootLatestProcessedEventTime = -1;
workInProgressRootLatestSuspenseTimeout = -1;
workInProgressRootLatestProcessedEventTime = NoTimestamp;
workInProgressRootLatestSuspenseTimeout = NoTimestamp;
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
@@ -1785,11 +1780,6 @@ function commitRootImpl(root, renderPriorityLevel) {
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackId = NoLanes;
// TODO: Use LanePriority instead of SchedulerPriority
if (renderPriorityLevel < ImmediateSchedulerPriority) {
// If this was a concurrent render, we can reset the expiration time.
root.expiresAt = -1;
}
// Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
@@ -2046,7 +2036,7 @@ function commitRootImpl(root, renderPriorityLevel) {
// Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, now());
if (hasUncaughtError) {
hasUncaughtError = false;
@@ -2504,9 +2494,10 @@ function captureCommitPhaseErrorOnRoot(
const errorInfo = createCapturedValue(error, sourceFiber);
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
enqueueUpdate(rootFiber, update);
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
if (root !== null) {
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, SyncLane);
}
}
@@ -2539,9 +2530,10 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
(SyncLane: Lane),
);
enqueueUpdate(fiber, update);
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
if (root !== null) {
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, SyncLane);
}
return;
@@ -2563,7 +2555,8 @@ export function pingSuspendedRoot(
pingCache.delete(wakeable);
}
markRootPinged(root, pingedLanes);
const eventTime = requestEventTime();
markRootPinged(root, pingedLanes, eventTime);
if (
workInProgressRoot === root &&
@@ -2585,7 +2578,7 @@ export function pingSuspendedRoot(
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
workInProgressRootLatestProcessedEventTime === -1 &&
workInProgressRootLatestProcessedEventTime === NoTimestamp &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
@@ -2600,7 +2593,7 @@ export function pingSuspendedRoot(
}
}
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, pingedLanes);
}
@@ -2616,9 +2609,10 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
retryLane = requestUpdateLane(boundaryFiber, suspenseConfig);
}
// TODO: Special case idle priority?
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
if (root !== null) {
ensureRootIsScheduled(root);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, retryLane);
}
}
@@ -620,7 +620,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
if (lastExpiredTime !== NoWork) {
// Special case: Expired work should flush synchronously.
root.callbackExpirationTime = Sync;
root.callbackPriority = ImmediatePriority;
root.callbackPriority_old = ImmediatePriority;
root.callbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
@@ -634,7 +634,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
if (existingCallbackNode !== null) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
root.callbackPriority_old = NoPriority;
}
return;
}
@@ -650,7 +650,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
// If there's an existing render task, confirm it has the correct priority and
// expiration time. Otherwise, we'll cancel it and schedule a new one.
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
const existingCallbackPriority = root.callbackPriority_old;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// Callback must have the exact same expiration time.
@@ -668,7 +668,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
}
root.callbackExpirationTime = expirationTime;
root.callbackPriority = priorityLevel;
root.callbackPriority_old = priorityLevel;
let callbackNode;
if (expirationTime === Sync) {
@@ -1878,7 +1878,7 @@ function commitRootImpl(root, renderPriorityLevel) {
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = NoPriority;
root.callbackPriority_old = NoPriority;
// Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
+4 -13
View File
@@ -23,7 +23,7 @@ import type {WorkTag} from './ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {SideEffectTag} from './ReactSideEffectTags';
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
import type {Lane, Lanes} from './ReactFiberLane';
import type {Lane, LanePriority, Lanes, LaneMap} from './ReactFiberLane';
import type {HookType} from './ReactFiberHooks.old';
import type {RootTag} from './ReactRootTags';
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
@@ -226,7 +226,7 @@ type BaseFiberRootProperties = {|
// Expiration of the callback associated with this root
callbackExpirationTime: ExpirationTime,
// Priority of the callback associated with this root
callbackPriority: ReactPriorityLevel,
callbackPriority_old: ReactPriorityLevel,
finishedExpirationTime: ExpirationTime,
// The earliest pending expiration time that exists in the tree
@@ -252,17 +252,8 @@ type BaseFiberRootProperties = {|
// Represents the next task that the root should work on, or the current one
// if it's already working.
callbackId: Lanes,
// Whether the currently scheduled task for this root is synchronous or
// batched/concurrent. We have to track this because Scheduler does not
// support synchronous tasks, so we put those on a separate queue. So you
// could also think of this as "which queue is the callback scheduled with?"
callbackIsSync: boolean,
// Timestamp at which we will synchronously finish the current task to
// prevent starvation.
// TODO: There should be a separate expiration per lane.
// NOTE: This is not an "ExpirationTime" as used by the old reconciler. It's a
// timestamp, in milliseconds.
expiresAt: number,
callbackPriority_new: LanePriority,
expirationTimes: LaneMap<number>,
pendingLanes: Lanes,
suspendedLanes: Lanes,
@@ -12,6 +12,8 @@
let React;
let ReactNoop;
let Scheduler;
let readText;
let resolveText;
describe('ReactExpiration', () => {
beforeEach(() => {
@@ -20,6 +22,53 @@ describe('ReactExpiration', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const textCache = new Map();
readText = text => {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
throw record.promise;
case 'rejected':
throw Error('Failed to load: ' + text);
case 'resolved':
return text;
}
} else {
let ping;
const promise = new Promise(resolve => (ping = resolve));
const newRecord = {
status: 'pending',
ping: ping,
promise,
};
textCache.set(text, newRecord);
throw promise;
}
};
resolveText = text => {
const record = textCache.get(text);
if (record !== undefined) {
if (record.status === 'pending') {
Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
record.ping();
record.ping = null;
record.status = 'resolved';
clearTimeout(record.promise._timer);
record.promise = null;
}
} else {
const newRecord = {
ping: null,
status: 'resolved',
promise: null,
};
textCache.set(text, newRecord);
}
};
});
function Text(props) {
@@ -27,6 +76,27 @@ describe('ReactExpiration', () => {
return props.text;
}
function AsyncText(props) {
const text = props.text;
try {
readText(text);
Scheduler.unstable_yieldValue(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
if (typeof props.ms === 'number' && promise._timer === undefined) {
promise._timer = setTimeout(() => {
resolveText(text);
}, props.ms);
}
} else {
Scheduler.unstable_yieldValue(`Error! [${text}]`);
}
throw promise;
}
}
function span(prop) {
return {type: 'span', children: [], prop, hidden: false};
}
@@ -348,4 +418,408 @@ describe('ReactExpiration', () => {
expect(Scheduler).toFlushExpired([]);
expect(ReactNoop).toMatchRenderedOutput('Hi');
});
it('prevents starvation by high priority updates', async () => {
const {useState} = React;
let updateHighPri;
let updateNormalPri;
function App() {
const [highPri, setHighPri] = useState(0);
const [normalPri, setNormalPri] = useState(0);
updateHighPri = () =>
Scheduler.unstable_runWithPriority(
Scheduler.unstable_UserBlockingPriority,
() => setHighPri(n => n + 1),
);
updateNormalPri = () => setNormalPri(n => n + 1);
return (
<>
<Text text={'High pri: ' + highPri} />
{', '}
<Text text={'Normal pri: ' + normalPri} />
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['High pri: 0', 'Normal pri: 0']);
expect(root).toMatchRenderedOutput('High pri: 0, Normal pri: 0');
// First demonstrate what happens when there's no starvation
await ReactNoop.act(async () => {
updateNormalPri();
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
updateHighPri();
});
expect(Scheduler).toHaveYielded([
// Interrupt high pri update to render sync update
'High pri: 1',
'Normal pri: 0',
// Now render normal pri
'High pri: 1',
'Normal pri: 1',
]);
expect(root).toMatchRenderedOutput('High pri: 1, Normal pri: 1');
// Do the same thing, but starve the first update
await ReactNoop.act(async () => {
updateNormalPri();
expect(Scheduler).toFlushAndYieldThrough(['High pri: 1']);
// This time, a lot of time has elapsed since the normal pri update
// started rendering. (This should advance time by some number that's
// definitely bigger than the constant heuristic we use to detect
// starvation of normal priority updates.)
Scheduler.unstable_advanceTime(10000);
// So when we get a high pri update, we shouldn't interrupt
updateHighPri();
});
expect(Scheduler).toHaveYielded([
// Finish normal pri update
'Normal pri: 2',
// Then do high pri update
'High pri: 2',
'Normal pri: 2',
]);
expect(root).toMatchRenderedOutput('High pri: 2, Normal pri: 2');
});
// @gate new
it('prevents starvation by sync updates', async () => {
const {useState} = React;
let updateSyncPri;
let updateHighPri;
function App() {
const [syncPri, setSyncPri] = useState(0);
const [highPri, setHighPri] = useState(0);
updateSyncPri = () => ReactNoop.flushSync(() => setSyncPri(n => n + 1));
updateHighPri = () =>
Scheduler.unstable_runWithPriority(
Scheduler.unstable_UserBlockingPriority,
() => setHighPri(n => n + 1),
);
return (
<>
<Text text={'Sync pri: ' + syncPri} />
{', '}
<Text text={'High pri: ' + highPri} />
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Sync pri: 0', 'High pri: 0']);
expect(root).toMatchRenderedOutput('Sync pri: 0, High pri: 0');
// First demonstrate what happens when there's no starvation
await ReactNoop.act(async () => {
updateHighPri();
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 0']);
updateSyncPri();
});
expect(Scheduler).toHaveYielded([
// Interrupt high pri update to render sync update
'Sync pri: 1',
'High pri: 0',
// Now render high pri
'Sync pri: 1',
'High pri: 1',
]);
expect(root).toMatchRenderedOutput('Sync pri: 1, High pri: 1');
// Do the same thing, but starve the first update
await ReactNoop.act(async () => {
updateHighPri();
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 1']);
// This time, a lot of time has elapsed since the high pri update started
// rendering. (This should advance time by some number that's definitely
// bigger than the constant heuristic we use to detect starvation of user
// interactions, but not as high as the onse used for normal pri updates.)
Scheduler.unstable_advanceTime(1500);
// So when we get a sync update, we shouldn't interrupt
updateSyncPri();
});
expect(Scheduler).toHaveYielded([
// Finish high pri update
'High pri: 2',
// Then do sync update
'Sync pri: 2',
'High pri: 2',
]);
expect(root).toMatchRenderedOutput('Sync pri: 2, High pri: 2');
});
it('idle work never expires', async () => {
const {useState} = React;
let updateSyncPri;
let updateIdlePri;
function App() {
const [syncPri, setSyncPri] = useState(0);
const [highPri, setIdlePri] = useState(0);
updateSyncPri = () => ReactNoop.flushSync(() => setSyncPri(n => n + 1));
updateIdlePri = () =>
Scheduler.unstable_runWithPriority(
Scheduler.unstable_IdlePriority,
() => setIdlePri(n => n + 1),
);
return (
<>
<Text text={'Sync pri: ' + syncPri} />
{', '}
<Text text={'Idle pri: ' + highPri} />
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Sync pri: 0', 'Idle pri: 0']);
expect(root).toMatchRenderedOutput('Sync pri: 0, Idle pri: 0');
// First demonstrate what happens when there's no starvation
await ReactNoop.act(async () => {
updateIdlePri();
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 0']);
updateSyncPri();
});
expect(Scheduler).toHaveYielded([
// Interrupt idle update to render sync update
'Sync pri: 1',
'Idle pri: 0',
// Now render idle
'Sync pri: 1',
'Idle pri: 1',
]);
expect(root).toMatchRenderedOutput('Sync pri: 1, Idle pri: 1');
// Do the same thing, but starve the first update
await ReactNoop.act(async () => {
updateIdlePri();
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 1']);
// Advance a ridiculously large amount of time to demonstrate that the
// idle work never expires
Scheduler.unstable_advanceTime(100000);
updateSyncPri();
});
// Same thing should happen as last time
expect(Scheduler).toHaveYielded([
// Interrupt idle update to render sync update
'Sync pri: 2',
'Idle pri: 1',
// Now render idle
'Sync pri: 2',
'Idle pri: 2',
]);
expect(root).toMatchRenderedOutput('Sync pri: 2, Idle pri: 2');
});
// @gate new
it('a single update can expire without forcing all other updates to expire', async () => {
const {useState} = React;
let updateHighPri;
let updateNormalPri;
function App() {
const [highPri, setHighPri] = useState(0);
const [normalPri, setNormalPri] = useState(0);
updateHighPri = () =>
Scheduler.unstable_runWithPriority(
Scheduler.unstable_UserBlockingPriority,
() => setHighPri(n => n + 1),
);
updateNormalPri = () => setNormalPri(n => n + 1);
return (
<>
<Text text={'High pri: ' + highPri} />
{', '}
<Text text={'Normal pri: ' + normalPri} />
{', '}
<Text text="Sibling" />
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded([
'High pri: 0',
'Normal pri: 0',
'Sibling',
]);
expect(root).toMatchRenderedOutput('High pri: 0, Normal pri: 0, Sibling');
await ReactNoop.act(async () => {
// Partially render an update
updateNormalPri();
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
// Some time goes by. In an interleaved event, schedule another update.
// This will be placed into a separate batch.
Scheduler.unstable_advanceTime(4000);
updateNormalPri();
// Keep rendering the first update
expect(Scheduler).toFlushAndYieldThrough(['Normal pri: 1']);
// More time goes by. Enough to expire the first batch, but not the
// second one.
Scheduler.unstable_advanceTime(1000);
// Attempt to interrupt with a high pri update.
updateHighPri();
// The first update expired, so first will finish it without interrupting.
// But not the second update, which hasn't expired yet.
expect(Scheduler).toFlushExpired(['Sibling']);
});
expect(Scheduler).toHaveYielded([
// Then render the high pri update
'High pri: 1',
'Normal pri: 1',
'Sibling',
// Then the second normal pri update
'High pri: 1',
'Normal pri: 2',
'Sibling',
]);
});
it('detects starvation in multiple batches', async () => {
const {useState} = React;
let updateHighPri;
let updateNormalPri;
function App() {
const [highPri, setHighPri] = useState(0);
const [normalPri, setNormalPri] = useState(0);
updateHighPri = () =>
Scheduler.unstable_runWithPriority(
Scheduler.unstable_UserBlockingPriority,
() => setHighPri(n => n + 1),
);
updateNormalPri = () => setNormalPri(n => n + 1);
return (
<>
<Text text={'High pri: ' + highPri} />
{', '}
<Text text={'Normal pri: ' + normalPri} />
{', '}
<Text text="Sibling" />
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded([
'High pri: 0',
'Normal pri: 0',
'Sibling',
]);
expect(root).toMatchRenderedOutput('High pri: 0, Normal pri: 0, Sibling');
await ReactNoop.act(async () => {
// Partially render an update
updateNormalPri();
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
// Some time goes by. In an interleaved event, schedule another update.
// This will be placed into a separate batch.
Scheduler.unstable_advanceTime(4000);
updateNormalPri();
// Keep rendering the first update
expect(Scheduler).toFlushAndYieldThrough(['Normal pri: 1']);
// More time goes by. This expires both of the updates just scheduled.
Scheduler.unstable_advanceTime(10000);
// Attempt to interrupt with a high pri update.
updateHighPri();
// Both normal pri updates should have expired.
expect(Scheduler).toFlushExpired([
'Sibling',
// Note: we also flushed the high pri update here, because in the
// current implementation, once we pick the next lanes to work on, we
// entangle it with all pending at equal or higher priority. We could
// feasibly change this heuristic so that the high pri update doesn't
// render until after the expired updates have finished. But the
// important thing in this test is that the normal updates expired.
'High pri: 1',
'Normal pri: 2',
'Sibling',
]);
});
});
// @gate new
it('updates do not expire while they are IO-bound', async () => {
const {Suspense} = React;
function App({text}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={text} />
{', '}
<Text text="Sibling" />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App text="A" />);
});
expect(Scheduler).toHaveYielded(['A', 'Sibling']);
expect(root).toMatchRenderedOutput('A, Sibling');
await ReactNoop.act(async () => {
root.render(<App text="B" />);
expect(Scheduler).toFlushAndYield([
'Suspend! [B]',
'Sibling',
'Loading...',
]);
// Lots of time elapses before the promise resolves
Scheduler.unstable_advanceTime(10000);
await resolveText('B');
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
// But the update doesn't expire, because it was IO bound. So we can
// partially rendering without finishing.
expect(Scheduler).toFlushAndYieldThrough(['B']);
expect(root).toMatchRenderedOutput('A, Sibling');
// Lots more time elapses. We're CPU-bound now, so we should treat this
// as starvation.
Scheduler.unstable_advanceTime(10000);
// Attempt to interrupt with a sync update.
ReactNoop.flushSync(() => root.render(<App text="A" />));
expect(Scheduler).toHaveYielded([
// Because the previous update had already expired, we don't interrupt
// it. Finish rendering it first.
'Sibling',
// Then do the sync update.
'A',
'Sibling',
]);
});
});
});
@@ -488,8 +488,8 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop.getChildren()).toEqual([span('derived state')]);
});
// Note: This test doesn't really make sense in the new model, but we might
// want to port it once lanes are implemented.
// Note: This test doesn't really make sense in the new model. The
// corresponding concept is tested in ReactExpiration-test.
// @gate old
it('flushes all expired updates in a single batch', () => {
const {useEffect} = React;
@@ -295,13 +295,26 @@ describe('Profiler', () => {
);
// Should be called two times:
// 2. To compute the update expiration time
// 3. To record the commit time
// 1. To compute the update expiration time
// 2. To record the commit time
// No additional calls from ProfilerTimer are expected.
expect(Scheduler).toHaveYielded([
'read current time',
'read current time',
]);
expect(Scheduler).toHaveYielded(
gate(flags =>
flags.new
? [
// The new reconciler reads the current time in more places,
// to detect starvation. This is unrelated to the profiler,
// which happens to use the same Scheduler method that we
// mocked above. We should rewrite this test so that it's
// less fragile.
'read current time',
'read current time',
'read current time',
'read current time',
]
: ['read current time', 'read current time'],
),
);
// Restore original mock
jest.mock('scheduler', () =>