Skip to content

Commit b23ea02

Browse files
authored
Track event times per lane on the root (#19387)
* Pass event time to markRootUpdated Some minor rearranging so that eventTime gets threaded through. No change in behavior. * Track event times per lane on the root Previous strategy was to store the event time on the update object and accumulate the most recent one during the render phase. Among other advantages, by tracking them on the root, we can read the event time before the render phase has finished. I haven't removed the `eventTime` field from the update object yet, because it's still used to compute the timeout. Tracking the timeout on the root is my next step.
1 parent faa697f commit b23ea02

File tree

6 files changed

+166
-141
lines changed

6 files changed

+166
-141
lines changed

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,25 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
374374
return nextLanes;
375375
}
376376

377+
export function getMostRecentEventTime(root: FiberRoot, lanes: Lanes): number {
378+
const eventTimes = root.eventTimes;
379+
380+
let mostRecentEventTime = NoTimestamp;
381+
while (lanes > 0) {
382+
const index = pickArbitraryLaneIndex(lanes);
383+
const lane = 1 << index;
384+
385+
const eventTime = eventTimes[index];
386+
if (eventTime > mostRecentEventTime) {
387+
mostRecentEventTime = eventTime;
388+
}
389+
390+
lanes &= ~lane;
391+
}
392+
393+
return mostRecentEventTime;
394+
}
395+
377396
function computeExpirationTime(lane: Lane, currentTime: number) {
378397
// TODO: Expiration heuristic is constant per lane, so could use a map.
379398
getHighestPriorityLanes(lane);
@@ -606,10 +625,14 @@ export function pickArbitraryLane(lanes: Lanes): Lane {
606625
return getHighestPriorityLane(lanes);
607626
}
608627

609-
function pickArbitraryLaneIndex(lanes: Lane | Lanes) {
628+
function pickArbitraryLaneIndex(lanes: Lanes) {
610629
return 31 - clz32(lanes);
611630
}
612631

632+
function laneToIndex(lane: Lane) {
633+
return pickArbitraryLaneIndex(lane);
634+
}
635+
613636
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
614637
return (a & b) !== NoLanes;
615638
}
@@ -648,7 +671,11 @@ export function createLaneMap<T>(initial: T): LaneMap<T> {
648671
return new Array(TotalLanes).fill(initial);
649672
}
650673

651-
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
674+
export function markRootUpdated(
675+
root: FiberRoot,
676+
updateLane: Lane,
677+
eventTime: number,
678+
) {
652679
root.pendingLanes |= updateLane;
653680

654681
// TODO: Theoretically, any update to any lane can unblock any other lane. But
@@ -666,6 +693,12 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
666693

667694
root.suspendedLanes &= higherPriorityLanes;
668695
root.pingedLanes &= higherPriorityLanes;
696+
697+
const eventTimes = root.eventTimes;
698+
const index = laneToIndex(updateLane);
699+
// We can always overwrite an existing timestamp because we prefer the most
700+
// recent event, and we assume time is monotonically increasing.
701+
eventTimes[index] = eventTime;
669702
}
670703

671704
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
@@ -723,13 +756,18 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
723756

724757
root.entangledLanes &= remainingLanes;
725758

759+
const entanglements = root.entanglements;
760+
const eventTimes = root.eventTimes;
726761
const expirationTimes = root.expirationTimes;
762+
763+
// Clear the lanes that no longer have pending work
727764
let lanes = noLongerPendingLanes;
728765
while (lanes > 0) {
729766
const index = pickArbitraryLaneIndex(lanes);
730767
const lane = 1 << index;
731768

732-
// Clear the expiration time
769+
entanglements[index] = NoLanes;
770+
eventTimes[index] = NoTimestamp;
733771
expirationTimes[index] = NoTimestamp;
734772

735773
lanes &= ~lane;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
4040
this.callbackNode = null;
4141
this.callbackId = NoLanes;
4242
this.callbackPriority = NoLanePriority;
43+
this.eventTimes = createLaneMap(NoLanes);
4344
this.expirationTimes = createLaneMap(NoTimestamp);
4445

4546
this.pendingLanes = NoLanes;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
4040
this.callbackNode = null;
4141
this.callbackId = NoLanes;
4242
this.callbackPriority = NoLanePriority;
43+
this.eventTimes = createLaneMap(NoLanes);
4344
this.expirationTimes = createLaneMap(NoTimestamp);
4445

4546
this.pendingLanes = NoLanes;

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

Lines changed: 61 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ import {
171171
getCurrentUpdateLanePriority,
172172
markStarvedLanesAsExpired,
173173
getLanesToRetrySynchronouslyOnError,
174+
getMostRecentEventTime,
174175
markRootUpdated,
175176
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
176177
markRootPinged,
@@ -294,8 +295,6 @@ const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
294295
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
295296
// A fatal error, if one is thrown
296297
let workInProgressRootFatalError: mixed = null;
297-
// Most recent event time among processed updates during this render.
298-
let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
299298
let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
300299
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
301300
// "Included" lanes refer to lanes that were worked on during this render. It's
@@ -540,6 +539,35 @@ export function scheduleUpdateOnFiber(
540539
return null;
541540
}
542541

542+
// Mark that the root has a pending update.
543+
markRootUpdated(root, lane, eventTime);
544+
545+
if (root === workInProgressRoot) {
546+
// Received an update to a tree that's in the middle of rendering. Mark
547+
// that there was an interleaved update work on this root. Unless the
548+
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
549+
// phase update. In that case, we don't treat render phase updates as if
550+
// they were interleaved, for backwards compat reasons.
551+
if (
552+
deferRenderPhaseUpdateToNextBatch ||
553+
(executionContext & RenderContext) === NoContext
554+
) {
555+
workInProgressRootUpdatedLanes = mergeLanes(
556+
workInProgressRootUpdatedLanes,
557+
lane,
558+
);
559+
}
560+
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
561+
// The root already suspended with a delay, which means this render
562+
// definitely won't finish. Since we have a new update, let's mark it as
563+
// suspended now, right before marking the incoming update. This has the
564+
// effect of interrupting the current render and switching to the update.
565+
// TODO: Make sure this doesn't override pings that happen while we've
566+
// already started rendering.
567+
markRootSuspended(root, workInProgressRootRenderLanes);
568+
}
569+
}
570+
543571
// TODO: requestUpdateLanePriority also reads the priority. Pass the
544572
// priority as an argument to that function and this one.
545573
const priorityLevel = getCurrentPriorityLevel();
@@ -605,82 +633,47 @@ export function scheduleUpdateOnFiber(
605633
// e.g. retrying a Suspense boundary isn't an update, but it does schedule work
606634
// on a fiber.
607635
function markUpdateLaneFromFiberToRoot(
608-
fiber: Fiber,
636+
sourceFiber: Fiber,
609637
lane: Lane,
610638
): FiberRoot | null {
611639
// Update the source fiber's lanes
612-
fiber.lanes = mergeLanes(fiber.lanes, lane);
613-
let alternate = fiber.alternate;
640+
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
641+
let alternate = sourceFiber.alternate;
614642
if (alternate !== null) {
615643
alternate.lanes = mergeLanes(alternate.lanes, lane);
616644
}
617645
if (__DEV__) {
618646
if (
619647
alternate === null &&
620-
(fiber.effectTag & (Placement | Hydrating)) !== NoEffect
648+
(sourceFiber.effectTag & (Placement | Hydrating)) !== NoEffect
621649
) {
622-
warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
650+
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
623651
}
624652
}
625653
// Walk the parent path to the root and update the child expiration time.
626-
let node = fiber.return;
627-
let root = null;
628-
if (node === null && fiber.tag === HostRoot) {
629-
root = fiber.stateNode;
630-
} else {
631-
while (node !== null) {
632-
alternate = node.alternate;
654+
let node = sourceFiber;
655+
let parent = sourceFiber.return;
656+
while (parent !== null) {
657+
parent.childLanes = mergeLanes(parent.childLanes, lane);
658+
alternate = parent.alternate;
659+
if (alternate !== null) {
660+
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
661+
} else {
633662
if (__DEV__) {
634-
if (
635-
alternate === null &&
636-
(node.effectTag & (Placement | Hydrating)) !== NoEffect
637-
) {
638-
warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
663+
if ((parent.effectTag & (Placement | Hydrating)) !== NoEffect) {
664+
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
639665
}
640666
}
641-
node.childLanes = mergeLanes(node.childLanes, lane);
642-
if (alternate !== null) {
643-
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
644-
}
645-
if (node.return === null && node.tag === HostRoot) {
646-
root = node.stateNode;
647-
break;
648-
}
649-
node = node.return;
650667
}
668+
node = parent;
669+
parent = parent.return;
651670
}
652-
653-
if (root !== null) {
654-
// Mark that the root has a pending update.
655-
markRootUpdated(root, lane);
656-
if (workInProgressRoot === root) {
657-
// Received an update to a tree that's in the middle of rendering. Mark
658-
// that there was an interleaved update work on this root. Unless the
659-
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
660-
// phase update. In that case, we don't treat render phase updates as if
661-
// they were interleaved, for backwards compat reasons.
662-
if (
663-
deferRenderPhaseUpdateToNextBatch ||
664-
(executionContext & RenderContext) === NoContext
665-
) {
666-
workInProgressRootUpdatedLanes = mergeLanes(
667-
workInProgressRootUpdatedLanes,
668-
lane,
669-
);
670-
}
671-
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
672-
// The root already suspended with a delay, which means this render
673-
// definitely won't finish. Since we have a new update, let's mark it as
674-
// suspended now, right before marking the incoming update. This has the
675-
// effect of interrupting the current render and switching to the update.
676-
// TODO: Make sure this doesn't override pings that happen while we've
677-
// already started rendering.
678-
markRootSuspended(root, workInProgressRootRenderLanes);
679-
}
680-
}
671+
if (node.tag === HostRoot) {
672+
const root: FiberRoot = node.stateNode;
673+
return root;
674+
} else {
675+
return null;
681676
}
682-
683-
return root;
684677
}
685678

686679
// Use this function to schedule a task for a root. There's only one task per
@@ -944,20 +937,21 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
944937
break;
945938
}
946939

940+
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
947941
let msUntilTimeout;
948942
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
949943
// We have processed a suspense config whose expiration time we
950944
// can use as the timeout.
951945
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
952-
} else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
946+
} else if (mostRecentEventTime === NoTimestamp) {
953947
// This should never normally happen because only new updates
954948
// cause delayed states, so we should have processed something.
955949
// However, this could also happen in an offscreen tree.
956950
msUntilTimeout = 0;
957951
} else {
958952
// If we didn't process a suspense config, compute a JND based on
959953
// the amount of time elapsed since the most recent event time.
960-
const eventTimeMs = workInProgressRootLatestProcessedEventTime;
954+
const eventTimeMs = mostRecentEventTime;
961955
const timeElapsedMs = now() - eventTimeMs;
962956
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
963957
}
@@ -980,18 +974,19 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
980974
}
981975
case RootCompleted: {
982976
// The work completed. Ready to commit.
977+
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
983978
if (
984979
// do not delay if we're inside an act() scope
985980
!shouldForceFlushFallbacksInDEV() &&
986-
workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
981+
mostRecentEventTime !== NoTimestamp &&
987982
workInProgressRootCanSuspendUsingConfig !== null
988983
) {
989984
// If we have exceeded the minimum loading delay, which probably
990985
// means we have shown a spinner already, we might have to suspend
991986
// a bit longer to ensure that the spinner is shown for
992987
// enough time.
993988
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
994-
workInProgressRootLatestProcessedEventTime,
989+
mostRecentEventTime,
995990
workInProgressRootCanSuspendUsingConfig,
996991
);
997992
if (msUntilTimeout > 10) {
@@ -1329,7 +1324,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13291324
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
13301325
workInProgressRootExitStatus = RootIncomplete;
13311326
workInProgressRootFatalError = null;
1332-
workInProgressRootLatestProcessedEventTime = NoTimestamp;
13331327
workInProgressRootLatestSuspenseTimeout = NoTimestamp;
13341328
workInProgressRootCanSuspendUsingConfig = null;
13351329
workInProgressRootSkippedLanes = NoLanes;
@@ -1447,11 +1441,6 @@ export function markRenderEventTimeAndConfig(
14471441
eventTime: number,
14481442
suspenseConfig: null | SuspenseConfig,
14491443
): void {
1450-
// Track the most recent event time of all updates processed in this batch.
1451-
if (workInProgressRootLatestProcessedEventTime < eventTime) {
1452-
workInProgressRootLatestProcessedEventTime = eventTime;
1453-
}
1454-
14551444
// Track the largest/latest timeout deadline in this batch.
14561445
// TODO: If there are two transitions in the same batch, shouldn't we
14571446
// choose the smaller one? Maybe this is because when an intermediate
@@ -2908,6 +2897,7 @@ function captureCommitPhaseErrorOnRoot(
29082897
const eventTime = requestEventTime();
29092898
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
29102899
if (root !== null) {
2900+
markRootUpdated(root, SyncLane, eventTime);
29112901
ensureRootIsScheduled(root, eventTime);
29122902
schedulePendingInteractions(root, SyncLane);
29132903
}
@@ -2944,6 +2934,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
29442934
const eventTime = requestEventTime();
29452935
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
29462936
if (root !== null) {
2937+
markRootUpdated(root, SyncLane, eventTime);
29472938
ensureRootIsScheduled(root, eventTime);
29482939
schedulePendingInteractions(root, SyncLane);
29492940
}
@@ -3016,6 +3007,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
30163007
const eventTime = requestEventTime();
30173008
const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
30183009
if (root !== null) {
3010+
markRootUpdated(root, retryLane, eventTime);
30193011
ensureRootIsScheduled(root, eventTime);
30203012
schedulePendingInteractions(root, retryLane);
30213013
}

0 commit comments

Comments
 (0)