@samitouri / QOS-React / commits / b23ea02be5

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.

Andrew Clark committed Jul 17, 2020 at 12:47 UTC b23ea02be507cc08747d49c1994f283514c5aeea
6 files changed +166 -141
packages/react-reconciler/src/ReactFiberLane.js
+41 -3
@@ -374,6 +374,25 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
374 return nextLanes;
375 }
376
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 +
396 function computeExpirationTime(lane: Lane, currentTime: number) {
397 // TODO: Expiration heuristic is constant per lane, so could use a map.
398 getHighestPriorityLanes(lane);
@@ -606,10 +625,14 @@ export function pickArbitraryLane(lanes: Lanes): Lane {
625 return getHighestPriorityLane(lanes);
626 }
627
609 -function pickArbitraryLaneIndex(lanes: Lane | Lanes) {
628 +function pickArbitraryLaneIndex(lanes: Lanes) {
629 return 31 - clz32(lanes);
630 }
631
632 +function laneToIndex(lane: Lane) {
633 + return pickArbitraryLaneIndex(lane);
634 +}
635 +
636 export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
637 return (a & b) !== NoLanes;
638 }
@@ -648,7 +671,11 @@ export function createLaneMap<T>(initial: T): LaneMap<T> {
671 return new Array(TotalLanes).fill(initial);
672 }
673
651 -export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
674 +export function markRootUpdated(
675 + root: FiberRoot,
676 + updateLane: Lane,
677 + eventTime: number,
678 +) {
679 root.pendingLanes |= updateLane;
680
681 // TODO: Theoretically, any update to any lane can unblock any other lane. But
@@ -666,6 +693,12 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
693
694 root.suspendedLanes &= higherPriorityLanes;
695 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;
702 }
703
704 export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
@@ -723,13 +756,18 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
756
757 root.entangledLanes &= remainingLanes;
758
759 + const entanglements = root.entanglements;
760 + const eventTimes = root.eventTimes;
761 const expirationTimes = root.expirationTimes;
762 +
763 + // Clear the lanes that no longer have pending work
764 let lanes = noLongerPendingLanes;
765 while (lanes > 0) {
766 const index = pickArbitraryLaneIndex(lanes);
767 const lane = 1 << index;
768
732 - // Clear the expiration time
769 + entanglements[index] = NoLanes;
770 + eventTimes[index] = NoTimestamp;
771 expirationTimes[index] = NoTimestamp;
772
773 lanes &= ~lane;
packages/react-reconciler/src/ReactFiberRoot.new.js
+1
@@ -40,6 +40,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
40 this.callbackNode = null;
41 this.callbackId = NoLanes;
42 this.callbackPriority = NoLanePriority;
43 + this.eventTimes = createLaneMap(NoLanes);
44 this.expirationTimes = createLaneMap(NoTimestamp);
45
46 this.pendingLanes = NoLanes;
packages/react-reconciler/src/ReactFiberRoot.old.js
+1
@@ -40,6 +40,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
40 this.callbackNode = null;
41 this.callbackId = NoLanes;
42 this.callbackPriority = NoLanePriority;
43 + this.eventTimes = createLaneMap(NoLanes);
44 this.expirationTimes = createLaneMap(NoTimestamp);
45
46 this.pendingLanes = NoLanes;
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+61 -69
@@ -171,6 +171,7 @@ import {
171 getCurrentUpdateLanePriority,
172 markStarvedLanesAsExpired,
173 getLanesToRetrySynchronouslyOnError,
174 + getMostRecentEventTime,
175 markRootUpdated,
176 markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
177 markRootPinged,
@@ -294,8 +295,6 @@ const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
295 let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
296 // A fatal error, if one is thrown
297 let workInProgressRootFatalError: mixed = null;
297 -// Most recent event time among processed updates during this render.
298 -let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
298 let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
299 let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
300 // "Included" lanes refer to lanes that were worked on during this render. It's
@@ -540,6 +539,35 @@ export function scheduleUpdateOnFiber(
539 return null;
540 }
541
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 +
571 // TODO: requestUpdateLanePriority also reads the priority. Pass the
572 // priority as an argument to that function and this one.
573 const priorityLevel = getCurrentPriorityLevel();
@@ -605,82 +633,47 @@ export function scheduleUpdateOnFiber(
633 // e.g. retrying a Suspense boundary isn't an update, but it does schedule work
634 // on a fiber.
635 function markUpdateLaneFromFiberToRoot(
608 - fiber: Fiber,
636 + sourceFiber: Fiber,
637 lane: Lane,
638 ): FiberRoot | null {
639 // 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;
642 if (alternate !== null) {
643 alternate.lanes = mergeLanes(alternate.lanes, lane);
644 }
645 if (__DEV__) {
646 if (
647 alternate === null &&
620 - (fiber.effectTag & (Placement | Hydrating)) !== NoEffect
648 + (sourceFiber.effectTag & (Placement | Hydrating)) !== NoEffect
649 ) {
622 - warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
650 + warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
651 }
652 }
653 // 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 {
662 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);
665 }
666 }
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;
667 }
668 + node = parent;
669 + parent = parent.return;
670 }
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;
676 }
682 -
683 - return root;
677 }
678
679 // Use this function to schedule a task for a root. There's only one task per
@@ -944,12 +937,13 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
937 break;
938 }
939
940 + const mostRecentEventTime = getMostRecentEventTime(root, lanes);
941 let msUntilTimeout;
942 if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
943 // We have processed a suspense config whose expiration time we
944 // can use as the timeout.
945 msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
952 - } else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
946 + } else if (mostRecentEventTime === NoTimestamp) {
947 // This should never normally happen because only new updates
948 // cause delayed states, so we should have processed something.
949 // However, this could also happen in an offscreen tree.
@@ -957,7 +951,7 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
951 } else {
952 // If we didn't process a suspense config, compute a JND based on
953 // the amount of time elapsed since the most recent event time.
960 - const eventTimeMs = workInProgressRootLatestProcessedEventTime;
954 + const eventTimeMs = mostRecentEventTime;
955 const timeElapsedMs = now() - eventTimeMs;
956 msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
957 }
@@ -980,10 +974,11 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
974 }
975 case RootCompleted: {
976 // The work completed. Ready to commit.
977 + const mostRecentEventTime = getMostRecentEventTime(root, lanes);
978 if (
979 // do not delay if we're inside an act() scope
980 !shouldForceFlushFallbacksInDEV() &&
986 - workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
981 + mostRecentEventTime !== NoTimestamp &&
982 workInProgressRootCanSuspendUsingConfig !== null
983 ) {
984 // If we have exceeded the minimum loading delay, which probably
@@ -991,7 +986,7 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
986 // a bit longer to ensure that the spinner is shown for
987 // enough time.
988 const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
994 - workInProgressRootLatestProcessedEventTime,
989 + mostRecentEventTime,
990 workInProgressRootCanSuspendUsingConfig,
991 );
992 if (msUntilTimeout > 10) {
@@ -1329,7 +1324,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1324 workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
1325 workInProgressRootExitStatus = RootIncomplete;
1326 workInProgressRootFatalError = null;
1332 - workInProgressRootLatestProcessedEventTime = NoTimestamp;
1327 workInProgressRootLatestSuspenseTimeout = NoTimestamp;
1328 workInProgressRootCanSuspendUsingConfig = null;
1329 workInProgressRootSkippedLanes = NoLanes;
@@ -1447,11 +1441,6 @@ export function markRenderEventTimeAndConfig(
1441 eventTime: number,
1442 suspenseConfig: null | SuspenseConfig,
1443 ): void {
1450 - // Track the most recent event time of all updates processed in this batch.
1451 - if (workInProgressRootLatestProcessedEventTime < eventTime) {
1452 - workInProgressRootLatestProcessedEventTime = eventTime;
1453 - }
1454 -
1444 // Track the largest/latest timeout deadline in this batch.
1445 // TODO: If there are two transitions in the same batch, shouldn't we
1446 // choose the smaller one? Maybe this is because when an intermediate
@@ -2908,6 +2897,7 @@ function captureCommitPhaseErrorOnRoot(
2897 const eventTime = requestEventTime();
2898 const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
2899 if (root !== null) {
2900 + markRootUpdated(root, SyncLane, eventTime);
2901 ensureRootIsScheduled(root, eventTime);
2902 schedulePendingInteractions(root, SyncLane);
2903 }
@@ -2944,6 +2934,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2934 const eventTime = requestEventTime();
2935 const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
2936 if (root !== null) {
2937 + markRootUpdated(root, SyncLane, eventTime);
2938 ensureRootIsScheduled(root, eventTime);
2939 schedulePendingInteractions(root, SyncLane);
2940 }
@@ -3016,6 +3007,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
3007 const eventTime = requestEventTime();
3008 const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
3009 if (root !== null) {
3010 + markRootUpdated(root, retryLane, eventTime);
3011 ensureRootIsScheduled(root, eventTime);
3012 schedulePendingInteractions(root, retryLane);
3013 }
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+61 -69
@@ -163,6 +163,7 @@ import {
163 getCurrentUpdateLanePriority,
164 markStarvedLanesAsExpired,
165 getLanesToRetrySynchronouslyOnError,
166 + getMostRecentEventTime,
167 markRootUpdated,
168 markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
169 markRootPinged,
@@ -286,8 +287,6 @@ const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
287 let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
288 // A fatal error, if one is thrown
289 let workInProgressRootFatalError: mixed = null;
289 -// Most recent event time among processed updates during this render.
290 -let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
290 let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
291 let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
292 // "Included" lanes refer to lanes that were worked on during this render. It's
@@ -533,6 +532,35 @@ export function scheduleUpdateOnFiber(
532 return null;
533 }
534
535 + // Mark that the root has a pending update.
536 + markRootUpdated(root, lane, eventTime);
537 +
538 + if (root === workInProgressRoot) {
539 + // Received an update to a tree that's in the middle of rendering. Mark
540 + // that there was an interleaved update work on this root. Unless the
541 + // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
542 + // phase update. In that case, we don't treat render phase updates as if
543 + // they were interleaved, for backwards compat reasons.
544 + if (
545 + deferRenderPhaseUpdateToNextBatch ||
546 + (executionContext & RenderContext) === NoContext
547 + ) {
548 + workInProgressRootUpdatedLanes = mergeLanes(
549 + workInProgressRootUpdatedLanes,
550 + lane,
551 + );
552 + }
553 + if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
554 + // The root already suspended with a delay, which means this render
555 + // definitely won't finish. Since we have a new update, let's mark it as
556 + // suspended now, right before marking the incoming update. This has the
557 + // effect of interrupting the current render and switching to the update.
558 + // TODO: Make sure this doesn't override pings that happen while we've
559 + // already started rendering.
560 + markRootSuspended(root, workInProgressRootRenderLanes);
561 + }
562 + }
563 +
564 // TODO: requestUpdateLanePriority also reads the priority. Pass the
565 // priority as an argument to that function and this one.
566 const priorityLevel = getCurrentPriorityLevel();
@@ -598,82 +626,47 @@ export function scheduleUpdateOnFiber(
626 // e.g. retrying a Suspense boundary isn't an update, but it does schedule work
627 // on a fiber.
628 function markUpdateLaneFromFiberToRoot(
601 - fiber: Fiber,
629 + sourceFiber: Fiber,
630 lane: Lane,
631 ): FiberRoot | null {
632 // Update the source fiber's lanes
605 - fiber.lanes = mergeLanes(fiber.lanes, lane);
606 - let alternate = fiber.alternate;
633 + sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
634 + let alternate = sourceFiber.alternate;
635 if (alternate !== null) {
636 alternate.lanes = mergeLanes(alternate.lanes, lane);
637 }
638 if (__DEV__) {
639 if (
640 alternate === null &&
613 - (fiber.effectTag & (Placement | Hydrating)) !== NoEffect
641 + (sourceFiber.effectTag & (Placement | Hydrating)) !== NoEffect
642 ) {
615 - warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
643 + warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
644 }
645 }
646 // Walk the parent path to the root and update the child expiration time.
619 - let node = fiber.return;
620 - let root = null;
621 - if (node === null && fiber.tag === HostRoot) {
622 - root = fiber.stateNode;
623 - } else {
624 - while (node !== null) {
625 - alternate = node.alternate;
647 + let node = sourceFiber;
648 + let parent = sourceFiber.return;
649 + while (parent !== null) {
650 + parent.childLanes = mergeLanes(parent.childLanes, lane);
651 + alternate = parent.alternate;
652 + if (alternate !== null) {
653 + alternate.childLanes = mergeLanes(alternate.childLanes, lane);
654 + } else {
655 if (__DEV__) {
627 - if (
628 - alternate === null &&
629 - (node.effectTag & (Placement | Hydrating)) !== NoEffect
630 - ) {
631 - warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
656 + if ((parent.effectTag & (Placement | Hydrating)) !== NoEffect) {
657 + warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
658 }
659 }
634 - node.childLanes = mergeLanes(node.childLanes, lane);
635 - if (alternate !== null) {
636 - alternate.childLanes = mergeLanes(alternate.childLanes, lane);
637 - }
638 - if (node.return === null && node.tag === HostRoot) {
639 - root = node.stateNode;
640 - break;
641 - }
642 - node = node.return;
660 }
661 + node = parent;
662 + parent = parent.return;
663 }
645 -
646 - if (root !== null) {
647 - // Mark that the root has a pending update.
648 - markRootUpdated(root, lane);
649 - if (workInProgressRoot === root) {
650 - // Received an update to a tree that's in the middle of rendering. Mark
651 - // that there was an interleaved update work on this root. Unless the
652 - // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
653 - // phase update. In that case, we don't treat render phase updates as if
654 - // they were interleaved, for backwards compat reasons.
655 - if (
656 - deferRenderPhaseUpdateToNextBatch ||
657 - (executionContext & RenderContext) === NoContext
658 - ) {
659 - workInProgressRootUpdatedLanes = mergeLanes(
660 - workInProgressRootUpdatedLanes,
661 - lane,
662 - );
663 - }
664 - if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
665 - // The root already suspended with a delay, which means this render
666 - // definitely won't finish. Since we have a new update, let's mark it as
667 - // suspended now, right before marking the incoming update. This has the
668 - // effect of interrupting the current render and switching to the update.
669 - // TODO: Make sure this doesn't override pings that happen while we've
670 - // already started rendering.
671 - markRootSuspended(root, workInProgressRootRenderLanes);
672 - }
673 - }
664 + if (node.tag === HostRoot) {
665 + const root: FiberRoot = node.stateNode;
666 + return root;
667 + } else {
668 + return null;
669 }
675 -
676 - return root;
670 }
671
672 // Use this function to schedule a task for a root. There's only one task per
@@ -937,12 +930,13 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
930 break;
931 }
932
933 + const mostRecentEventTime = getMostRecentEventTime(root, lanes);
934 let msUntilTimeout;
935 if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
936 // We have processed a suspense config whose expiration time we
937 // can use as the timeout.
938 msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
945 - } else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
939 + } else if (mostRecentEventTime === NoTimestamp) {
940 // This should never normally happen because only new updates
941 // cause delayed states, so we should have processed something.
942 // However, this could also happen in an offscreen tree.
@@ -950,7 +944,7 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
944 } else {
945 // If we didn't process a suspense config, compute a JND based on
946 // the amount of time elapsed since the most recent event time.
953 - const eventTimeMs = workInProgressRootLatestProcessedEventTime;
947 + const eventTimeMs = mostRecentEventTime;
948 const timeElapsedMs = now() - eventTimeMs;
949 msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
950 }
@@ -973,10 +967,11 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
967 }
968 case RootCompleted: {
969 // The work completed. Ready to commit.
970 + const mostRecentEventTime = getMostRecentEventTime(root, lanes);
971 if (
972 // do not delay if we're inside an act() scope
973 !shouldForceFlushFallbacksInDEV() &&
979 - workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
974 + mostRecentEventTime !== NoTimestamp &&
975 workInProgressRootCanSuspendUsingConfig !== null
976 ) {
977 // If we have exceeded the minimum loading delay, which probably
@@ -984,7 +979,7 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
979 // a bit longer to ensure that the spinner is shown for
980 // enough time.
981 const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
987 - workInProgressRootLatestProcessedEventTime,
982 + mostRecentEventTime,
983 workInProgressRootCanSuspendUsingConfig,
984 );
985 if (msUntilTimeout > 10) {
@@ -1322,7 +1317,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1317 workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
1318 workInProgressRootExitStatus = RootIncomplete;
1319 workInProgressRootFatalError = null;
1325 - workInProgressRootLatestProcessedEventTime = NoTimestamp;
1320 workInProgressRootLatestSuspenseTimeout = NoTimestamp;
1321 workInProgressRootCanSuspendUsingConfig = null;
1322 workInProgressRootSkippedLanes = NoLanes;
@@ -1440,11 +1434,6 @@ export function markRenderEventTimeAndConfig(
1434 eventTime: number,
1435 suspenseConfig: null | SuspenseConfig,
1436 ): void {
1443 - // Track the most recent event time of all updates processed in this batch.
1444 - if (workInProgressRootLatestProcessedEventTime < eventTime) {
1445 - workInProgressRootLatestProcessedEventTime = eventTime;
1446 - }
1447 -
1437 // Track the largest/latest timeout deadline in this batch.
1438 // TODO: If there are two transitions in the same batch, shouldn't we
1439 // choose the smaller one? Maybe this is because when an intermediate
@@ -2764,6 +2753,7 @@ function captureCommitPhaseErrorOnRoot(
2753 const eventTime = requestEventTime();
2754 const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
2755 if (root !== null) {
2756 + markRootUpdated(root, SyncLane, eventTime);
2757 ensureRootIsScheduled(root, eventTime);
2758 schedulePendingInteractions(root, SyncLane);
2759 }
@@ -2800,6 +2790,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2790 const eventTime = requestEventTime();
2791 const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
2792 if (root !== null) {
2793 + markRootUpdated(root, SyncLane, eventTime);
2794 ensureRootIsScheduled(root, eventTime);
2795 schedulePendingInteractions(root, SyncLane);
2796 }
@@ -2872,6 +2863,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
2863 const eventTime = requestEventTime();
2864 const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
2865 if (root !== null) {
2866 + markRootUpdated(root, retryLane, eventTime);
2867 ensureRootIsScheduled(root, eventTime);
2868 schedulePendingInteractions(root, retryLane);
2869 }
packages/react-reconciler/src/ReactInternalTypes.js
+1
@@ -217,6 +217,7 @@ type BaseFiberRootProperties = {|
217 // if it's already working.
218 callbackId: Lanes,
219 callbackPriority: LanePriority,
220 + eventTimes: LaneMap<number>,
221 expirationTimes: LaneMap<number>,
222
223 pendingLanes: Lanes,