Skip to content

Commit a478883

Browse files
committed
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 66413e4 commit a478883

File tree

6 files changed

+53
-26
lines changed

6 files changed

+53
-26
lines changed

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 36 additions & 2 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
}
@@ -670,6 +693,12 @@ export function markRootUpdated(
670693

671694
root.suspendedLanes &= higherPriorityLanes;
672695
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;
673702
}
674703

675704
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
@@ -727,13 +756,18 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
727756

728757
root.entangledLanes &= remainingLanes;
729758

759+
const entanglements = root.entanglements;
760+
const eventTimes = root.eventTimes;
730761
const expirationTimes = root.expirationTimes;
762+
763+
// Clear the lanes that no longer have pending work
731764
let lanes = noLongerPendingLanes;
732765
while (lanes > 0) {
733766
const index = pickArbitraryLaneIndex(lanes);
734767
const lane = 1 << index;
735768

736-
// Clear the expiration time
769+
entanglements[index] = NoTimestamp;
770+
eventTimes[index] = NoTimestamp;
737771
expirationTimes[index] = NoTimestamp;
738772

739773
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(NoTimestamp);
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(NoTimestamp);
4344
this.expirationTimes = createLaneMap(NoTimestamp);
4445

4546
this.pendingLanes = NoLanes;

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ import {
148148
getCurrentUpdateLanePriority,
149149
markStarvedLanesAsExpired,
150150
getLanesToRetrySynchronouslyOnError,
151+
getMostRecentEventTime,
151152
markRootUpdated,
152153
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
153154
markRootPinged,
@@ -271,8 +272,6 @@ const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
271272
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
272273
// A fatal error, if one is thrown
273274
let workInProgressRootFatalError: mixed = null;
274-
// Most recent event time among processed updates during this render.
275-
let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
276275
let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
277276
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
278277
// "Included" lanes refer to lanes that were worked on during this render. It's
@@ -915,20 +914,21 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
915914
break;
916915
}
917916

917+
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
918918
let msUntilTimeout;
919919
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
920920
// We have processed a suspense config whose expiration time we
921921
// can use as the timeout.
922922
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
923-
} else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
923+
} else if (mostRecentEventTime === NoTimestamp) {
924924
// This should never normally happen because only new updates
925925
// cause delayed states, so we should have processed something.
926926
// However, this could also happen in an offscreen tree.
927927
msUntilTimeout = 0;
928928
} else {
929929
// If we didn't process a suspense config, compute a JND based on
930930
// the amount of time elapsed since the most recent event time.
931-
const eventTimeMs = workInProgressRootLatestProcessedEventTime;
931+
const eventTimeMs = mostRecentEventTime;
932932
const timeElapsedMs = now() - eventTimeMs;
933933
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
934934
}
@@ -951,18 +951,19 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
951951
}
952952
case RootCompleted: {
953953
// The work completed. Ready to commit.
954+
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
954955
if (
955956
// do not delay if we're inside an act() scope
956957
!shouldForceFlushFallbacksInDEV() &&
957-
workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
958+
mostRecentEventTime !== NoTimestamp &&
958959
workInProgressRootCanSuspendUsingConfig !== null
959960
) {
960961
// If we have exceeded the minimum loading delay, which probably
961962
// means we have shown a spinner already, we might have to suspend
962963
// a bit longer to ensure that the spinner is shown for
963964
// enough time.
964965
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
965-
workInProgressRootLatestProcessedEventTime,
966+
mostRecentEventTime,
966967
workInProgressRootCanSuspendUsingConfig,
967968
);
968969
if (msUntilTimeout > 10) {
@@ -1300,7 +1301,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13001301
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
13011302
workInProgressRootExitStatus = RootIncomplete;
13021303
workInProgressRootFatalError = null;
1303-
workInProgressRootLatestProcessedEventTime = NoTimestamp;
13041304
workInProgressRootLatestSuspenseTimeout = NoTimestamp;
13051305
workInProgressRootCanSuspendUsingConfig = null;
13061306
workInProgressRootSkippedLanes = NoLanes;
@@ -1418,11 +1418,6 @@ export function markRenderEventTimeAndConfig(
14181418
eventTime: number,
14191419
suspenseConfig: null | SuspenseConfig,
14201420
): void {
1421-
// Track the most recent event time of all updates processed in this batch.
1422-
if (workInProgressRootLatestProcessedEventTime < eventTime) {
1423-
workInProgressRootLatestProcessedEventTime = eventTime;
1424-
}
1425-
14261421
// Track the largest/latest timeout deadline in this batch.
14271422
// TODO: If there are two transitions in the same batch, shouldn't we
14281423
// choose the smaller one? Maybe this is because when an intermediate

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ import {
163163
getCurrentUpdateLanePriority,
164164
markStarvedLanesAsExpired,
165165
getLanesToRetrySynchronouslyOnError,
166+
getMostRecentEventTime,
166167
markRootUpdated,
167168
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
168169
markRootPinged,
@@ -286,8 +287,6 @@ const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
286287
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
287288
// A fatal error, if one is thrown
288289
let workInProgressRootFatalError: mixed = null;
289-
// Most recent event time among processed updates during this render.
290-
let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
291290
let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
292291
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
293292
// "Included" lanes refer to lanes that were worked on during this render. It's
@@ -931,20 +930,21 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
931930
break;
932931
}
933932

933+
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
934934
let msUntilTimeout;
935935
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
936936
// We have processed a suspense config whose expiration time we
937937
// can use as the timeout.
938938
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
939-
} else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
939+
} else if (mostRecentEventTime === NoTimestamp) {
940940
// This should never normally happen because only new updates
941941
// cause delayed states, so we should have processed something.
942942
// However, this could also happen in an offscreen tree.
943943
msUntilTimeout = 0;
944944
} else {
945945
// If we didn't process a suspense config, compute a JND based on
946946
// the amount of time elapsed since the most recent event time.
947-
const eventTimeMs = workInProgressRootLatestProcessedEventTime;
947+
const eventTimeMs = mostRecentEventTime;
948948
const timeElapsedMs = now() - eventTimeMs;
949949
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
950950
}
@@ -967,18 +967,19 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
967967
}
968968
case RootCompleted: {
969969
// The work completed. Ready to commit.
970+
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
970971
if (
971972
// do not delay if we're inside an act() scope
972973
!shouldForceFlushFallbacksInDEV() &&
973-
workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
974+
mostRecentEventTime !== NoTimestamp &&
974975
workInProgressRootCanSuspendUsingConfig !== null
975976
) {
976977
// If we have exceeded the minimum loading delay, which probably
977978
// means we have shown a spinner already, we might have to suspend
978979
// a bit longer to ensure that the spinner is shown for
979980
// enough time.
980981
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
981-
workInProgressRootLatestProcessedEventTime,
982+
mostRecentEventTime,
982983
workInProgressRootCanSuspendUsingConfig,
983984
);
984985
if (msUntilTimeout > 10) {
@@ -1316,7 +1317,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13161317
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
13171318
workInProgressRootExitStatus = RootIncomplete;
13181319
workInProgressRootFatalError = null;
1319-
workInProgressRootLatestProcessedEventTime = NoTimestamp;
13201320
workInProgressRootLatestSuspenseTimeout = NoTimestamp;
13211321
workInProgressRootCanSuspendUsingConfig = null;
13221322
workInProgressRootSkippedLanes = NoLanes;
@@ -1434,11 +1434,6 @@ export function markRenderEventTimeAndConfig(
14341434
eventTime: number,
14351435
suspenseConfig: null | SuspenseConfig,
14361436
): void {
1437-
// Track the most recent event time of all updates processed in this batch.
1438-
if (workInProgressRootLatestProcessedEventTime < eventTime) {
1439-
workInProgressRootLatestProcessedEventTime = eventTime;
1440-
}
1441-
14421437
// Track the largest/latest timeout deadline in this batch.
14431438
// TODO: If there are two transitions in the same batch, shouldn't we
14441439
// choose the smaller one? Maybe this is because when an intermediate

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ type BaseFiberRootProperties = {|
217217
// if it's already working.
218218
callbackId: Lanes,
219219
callbackPriority: LanePriority,
220+
eventTimes: LaneMap<number>,
220221
expirationTimes: LaneMap<number>,
221222

222223
pendingLanes: Lanes,

0 commit comments

Comments
 (0)