Skip to content

Commit 4874042

Browse files
authored
Expiration: Do nothing except disable time slicing (#21345)
We have a feature called "expiration" whose purpose is to prevent a concurrent update from being starved by higher priority events. If a lane is CPU-bound for too long, we finish the rest of the work synchronously without allowing further interruptions. In the current implementation, we do this in sort of a roundabout way: once a lane is determined to have expired, we entangle it with SyncLane and switch to the synchronous work loop. There are a few flaws with the approach. One is that SyncLane has a particular semantic meaning besides its non-yieldiness. For example, `flushSync` will force remaining Sync work to finish; currently, that also includes expired work, which isn't an intended behavior, but rather an artifact of the implementation. An event worse example is that passive effects triggered by a Sync update are flushed synchronously, before paint, so that its result is guaranteed to be observed by the next discrete event. But expired work has no such requirement: we're flushing expired effects before paint unnecessarily. Aside from the behaviorial implications, the current implementation has proven to be fragile: more than once, we've accidentally regressed performance due to a subtle change in how expiration is handled. This PR aims to radically simplify how we model starvation protection by scaling back the implementation as much as possible. In this new model, if a lane is expired, we disable time slicing. That's it. We don't entangle it with SyncLane. The only thing we do is skip the call to `shouldYield` in between each time slice. This is identical to how we model synchronous-by-default updates in React 18.
1 parent 0f5ebf3 commit 4874042

10 files changed

+213
-294
lines changed

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

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,6 @@ export function markStarvedLanesAsExpired(
401401
// expiration time. If so, we'll assume the update is being starved and mark
402402
// it as expired to force it to finish.
403403
let lanes = pendingLanes;
404-
let expiredLanes = 0;
405404
while (lanes > 0) {
406405
const index = pickArbitraryLaneIndex(lanes);
407406
const lane = 1 << index;
@@ -420,15 +419,11 @@ export function markStarvedLanesAsExpired(
420419
}
421420
} else if (expirationTime <= currentTime) {
422421
// This lane expired
423-
expiredLanes |= lane;
422+
root.expiredLanes |= lane;
424423
}
425424

426425
lanes &= ~lane;
427426
}
428-
429-
if (expiredLanes !== 0) {
430-
markRootExpired(root, expiredLanes);
431-
}
432427
}
433428

434429
// This returns the highest priority pending lanes regardless of whether they
@@ -459,16 +454,22 @@ export function includesOnlyTransitions(lanes: Lanes) {
459454
}
460455

461456
export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
462-
if (!enableSyncDefaultUpdates) {
457+
if ((lanes & root.expiredLanes) !== NoLanes) {
458+
// At least one of these lanes expired. To prevent additional starvation,
459+
// finish rendering without yielding execution.
460+
return false;
461+
}
462+
if (enableSyncDefaultUpdates) {
463+
const SyncDefaultLanes =
464+
InputContinuousHydrationLane |
465+
InputContinuousLane |
466+
DefaultHydrationLane |
467+
DefaultLane;
468+
// TODO: Check for root override, once that lands
469+
return (lanes & SyncDefaultLanes) === NoLanes;
470+
} else {
463471
return true;
464472
}
465-
const SyncDefaultLanes =
466-
InputContinuousHydrationLane |
467-
InputContinuousLane |
468-
DefaultHydrationLane |
469-
DefaultLane;
470-
// TODO: Check for root override, once that lands
471-
return (lanes & SyncDefaultLanes) === NoLanes;
472473
}
473474

474475
export function isTransitionLane(lane: Lane) {
@@ -613,14 +614,6 @@ export function markRootPinged(
613614
root.pingedLanes |= root.suspendedLanes & pingedLanes;
614615
}
615616

616-
export function markRootExpired(root: FiberRoot, expiredLanes: Lanes) {
617-
const entanglements = root.entanglements;
618-
const SyncLaneIndex = 0;
619-
entanglements[SyncLaneIndex] |= expiredLanes;
620-
root.entangledLanes |= SyncLane;
621-
root.pendingLanes |= SyncLane;
622-
}
623-
624617
export function markRootMutableRead(root: FiberRoot, updateLane: Lane) {
625618
root.mutableReadLanes |= updateLane & root.pendingLanes;
626619
}
@@ -634,6 +627,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
634627
root.suspendedLanes = 0;
635628
root.pingedLanes = 0;
636629

630+
root.expiredLanes &= remainingLanes;
637631
root.mutableReadLanes &= remainingLanes;
638632

639633
root.entangledLanes &= remainingLanes;

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

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,6 @@ export function markStarvedLanesAsExpired(
401401
// expiration time. If so, we'll assume the update is being starved and mark
402402
// it as expired to force it to finish.
403403
let lanes = pendingLanes;
404-
let expiredLanes = 0;
405404
while (lanes > 0) {
406405
const index = pickArbitraryLaneIndex(lanes);
407406
const lane = 1 << index;
@@ -420,15 +419,11 @@ export function markStarvedLanesAsExpired(
420419
}
421420
} else if (expirationTime <= currentTime) {
422421
// This lane expired
423-
expiredLanes |= lane;
422+
root.expiredLanes |= lane;
424423
}
425424

426425
lanes &= ~lane;
427426
}
428-
429-
if (expiredLanes !== 0) {
430-
markRootExpired(root, expiredLanes);
431-
}
432427
}
433428

434429
// This returns the highest priority pending lanes regardless of whether they
@@ -459,16 +454,22 @@ export function includesOnlyTransitions(lanes: Lanes) {
459454
}
460455

461456
export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
462-
if (!enableSyncDefaultUpdates) {
457+
if ((lanes & root.expiredLanes) !== NoLanes) {
458+
// At least one of these lanes expired. To prevent additional starvation,
459+
// finish rendering without yielding execution.
460+
return false;
461+
}
462+
if (enableSyncDefaultUpdates) {
463+
const SyncDefaultLanes =
464+
InputContinuousHydrationLane |
465+
InputContinuousLane |
466+
DefaultHydrationLane |
467+
DefaultLane;
468+
// TODO: Check for root override, once that lands
469+
return (lanes & SyncDefaultLanes) === NoLanes;
470+
} else {
463471
return true;
464472
}
465-
const SyncDefaultLanes =
466-
InputContinuousHydrationLane |
467-
InputContinuousLane |
468-
DefaultHydrationLane |
469-
DefaultLane;
470-
// TODO: Check for root override, once that lands
471-
return (lanes & SyncDefaultLanes) === NoLanes;
472473
}
473474

474475
export function isTransitionLane(lane: Lane) {
@@ -613,14 +614,6 @@ export function markRootPinged(
613614
root.pingedLanes |= root.suspendedLanes & pingedLanes;
614615
}
615616

616-
export function markRootExpired(root: FiberRoot, expiredLanes: Lanes) {
617-
const entanglements = root.entanglements;
618-
const SyncLaneIndex = 0;
619-
entanglements[SyncLaneIndex] |= expiredLanes;
620-
root.entangledLanes |= SyncLane;
621-
root.pendingLanes |= SyncLane;
622-
}
623-
624617
export function markRootMutableRead(root: FiberRoot, updateLane: Lane) {
625618
root.mutableReadLanes |= updateLane & root.pendingLanes;
626619
}
@@ -634,6 +627,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
634627
root.suspendedLanes = 0;
635628
root.pingedLanes = 0;
636629

630+
root.expiredLanes &= remainingLanes;
637631
root.mutableReadLanes &= remainingLanes;
638632

639633
root.entangledLanes &= remainingLanes;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
5050
this.pendingLanes = NoLanes;
5151
this.suspendedLanes = NoLanes;
5252
this.pingedLanes = NoLanes;
53+
this.expiredLanes = NoLanes;
5354
this.mutableReadLanes = NoLanes;
5455
this.finishedLanes = NoLanes;
5556

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
5050
this.pendingLanes = NoLanes;
5151
this.suspendedLanes = NoLanes;
5252
this.pingedLanes = NoLanes;
53+
this.expiredLanes = NoLanes;
5354
this.mutableReadLanes = NoLanes;
5455
this.finishedLanes = NoLanes;
5556

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

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ import {
159159
markRootUpdated,
160160
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
161161
markRootPinged,
162-
markRootExpired,
162+
markRootEntangled,
163163
markRootFinished,
164164
getHighestPriorityLane,
165165
addFiberToLanesMap,
@@ -787,22 +787,17 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
787787
return null;
788788
}
789789

790+
// We disable time-slicing in some cases: if the work has been CPU-bound
791+
// for too long ("expired" work, to prevent starvation), or we're in
792+
// sync-updates-by-default mode.
790793
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
791794
// bug we're still investigating. Once the bug in Scheduler is fixed,
792795
// we can remove this, since we track expiration ourselves.
793-
if (!disableSchedulerTimeoutInWorkLoop && didTimeout) {
794-
// Something expired. Flush synchronously until there's no expired
795-
// work left.
796-
markRootExpired(root, lanes);
797-
// This will schedule a synchronous callback.
798-
ensureRootIsScheduled(root, now());
799-
return null;
800-
}
801-
802-
let exitStatus = shouldTimeSlice(root, lanes)
803-
? renderRootConcurrent(root, lanes)
804-
: // Time slicing is disabled for default updates in this root.
805-
renderRootSync(root, lanes);
796+
let exitStatus =
797+
shouldTimeSlice(root, lanes) &&
798+
(disableSchedulerTimeoutInWorkLoop || !didTimeout)
799+
? renderRootConcurrent(root, lanes)
800+
: renderRootSync(root, lanes);
806801
if (exitStatus !== RootIncomplete) {
807802
if (exitStatus === RootErrored) {
808803
executionContext |= RetryAfterError;
@@ -990,16 +985,7 @@ function performSyncWorkOnRoot(root) {
990985
flushPassiveEffects();
991986

992987
let lanes = getNextLanes(root, NoLanes);
993-
if (includesSomeLane(lanes, SyncLane)) {
994-
if (
995-
root === workInProgressRoot &&
996-
includesSomeLane(lanes, workInProgressRootRenderLanes)
997-
) {
998-
// There's a partial tree, and at least one of its lanes has expired. Finish
999-
// rendering it before rendering the rest of the expired work.
1000-
lanes = workInProgressRootRenderLanes;
1001-
}
1002-
} else {
988+
if (!includesSomeLane(lanes, SyncLane)) {
1003989
// There's no remaining sync work left.
1004990
ensureRootIsScheduled(root, now());
1005991
return null;
@@ -1052,11 +1038,9 @@ function performSyncWorkOnRoot(root) {
10521038
return null;
10531039
}
10541040

1055-
// TODO: Do we still need this API? I think we can delete it. Was only used
1056-
// internally.
10571041
export function flushRoot(root: FiberRoot, lanes: Lanes) {
10581042
if (lanes !== NoLanes) {
1059-
markRootExpired(root, lanes);
1043+
markRootEntangled(root, mergeLanes(lanes, SyncLane));
10601044
ensureRootIsScheduled(root, now());
10611045
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
10621046
resetRenderTimer();

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

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ import {
159159
markRootUpdated,
160160
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
161161
markRootPinged,
162-
markRootExpired,
162+
markRootEntangled,
163163
markRootFinished,
164164
getHighestPriorityLane,
165165
addFiberToLanesMap,
@@ -787,22 +787,17 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
787787
return null;
788788
}
789789

790+
// We disable time-slicing in some cases: if the work has been CPU-bound
791+
// for too long ("expired" work, to prevent starvation), or we're in
792+
// sync-updates-by-default mode.
790793
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
791794
// bug we're still investigating. Once the bug in Scheduler is fixed,
792795
// we can remove this, since we track expiration ourselves.
793-
if (!disableSchedulerTimeoutInWorkLoop && didTimeout) {
794-
// Something expired. Flush synchronously until there's no expired
795-
// work left.
796-
markRootExpired(root, lanes);
797-
// This will schedule a synchronous callback.
798-
ensureRootIsScheduled(root, now());
799-
return null;
800-
}
801-
802-
let exitStatus = shouldTimeSlice(root, lanes)
803-
? renderRootConcurrent(root, lanes)
804-
: // Time slicing is disabled for default updates in this root.
805-
renderRootSync(root, lanes);
796+
let exitStatus =
797+
shouldTimeSlice(root, lanes) &&
798+
(disableSchedulerTimeoutInWorkLoop || !didTimeout)
799+
? renderRootConcurrent(root, lanes)
800+
: renderRootSync(root, lanes);
806801
if (exitStatus !== RootIncomplete) {
807802
if (exitStatus === RootErrored) {
808803
executionContext |= RetryAfterError;
@@ -990,16 +985,7 @@ function performSyncWorkOnRoot(root) {
990985
flushPassiveEffects();
991986

992987
let lanes = getNextLanes(root, NoLanes);
993-
if (includesSomeLane(lanes, SyncLane)) {
994-
if (
995-
root === workInProgressRoot &&
996-
includesSomeLane(lanes, workInProgressRootRenderLanes)
997-
) {
998-
// There's a partial tree, and at least one of its lanes has expired. Finish
999-
// rendering it before rendering the rest of the expired work.
1000-
lanes = workInProgressRootRenderLanes;
1001-
}
1002-
} else {
988+
if (!includesSomeLane(lanes, SyncLane)) {
1003989
// There's no remaining sync work left.
1004990
ensureRootIsScheduled(root, now());
1005991
return null;
@@ -1052,11 +1038,9 @@ function performSyncWorkOnRoot(root) {
10521038
return null;
10531039
}
10541040

1055-
// TODO: Do we still need this API? I think we can delete it. Was only used
1056-
// internally.
10571041
export function flushRoot(root: FiberRoot, lanes: Lanes) {
10581042
if (lanes !== NoLanes) {
1059-
markRootExpired(root, lanes);
1043+
markRootEntangled(root, mergeLanes(lanes, SyncLane));
10601044
ensureRootIsScheduled(root, now());
10611045
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
10621046
resetRenderTimer();

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ type BaseFiberRootProperties = {|
228228
pendingLanes: Lanes,
229229
suspendedLanes: Lanes,
230230
pingedLanes: Lanes,
231+
expiredLanes: Lanes,
231232
mutableReadLanes: Lanes,
232233

233234
finishedLanes: Lanes,

0 commit comments

Comments
 (0)