Skip to content

Track event times per lane on the root #19387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,25 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
return nextLanes;
}

export function getMostRecentEventTime(root: FiberRoot, lanes: Lanes): number {
const eventTimes = root.eventTimes;

let mostRecentEventTime = NoTimestamp;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;

const eventTime = eventTimes[index];
if (eventTime > mostRecentEventTime) {
mostRecentEventTime = eventTime;
}

lanes &= ~lane;
}

return mostRecentEventTime;
}

function computeExpirationTime(lane: Lane, currentTime: number) {
// TODO: Expiration heuristic is constant per lane, so could use a map.
getHighestPriorityLanes(lane);
Expand Down Expand Up @@ -606,10 +625,14 @@ export function pickArbitraryLane(lanes: Lanes): Lane {
return getHighestPriorityLane(lanes);
}

function pickArbitraryLaneIndex(lanes: Lane | Lanes) {
function pickArbitraryLaneIndex(lanes: Lanes) {
return 31 - clz32(lanes);
}

function laneToIndex(lane: Lane) {
return pickArbitraryLaneIndex(lane);
}

export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}
Expand Down Expand Up @@ -648,7 +671,11 @@ export function createLaneMap<T>(initial: T): LaneMap<T> {
return new Array(TotalLanes).fill(initial);
}

export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
export function markRootUpdated(
root: FiberRoot,
updateLane: Lane,
eventTime: number,
) {
root.pendingLanes |= updateLane;

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

root.suspendedLanes &= higherPriorityLanes;
root.pingedLanes &= higherPriorityLanes;

const eventTimes = root.eventTimes;
const index = laneToIndex(updateLane);
// We can always overwrite an existing timestamp because we prefer the most
// recent event, and we assume time is monotonically increasing.
eventTimes[index] = eventTime;
}

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

root.entangledLanes &= remainingLanes;

const entanglements = root.entanglements;
const eventTimes = root.eventTimes;
const expirationTimes = root.expirationTimes;

// Clear the lanes that no longer have pending work
let lanes = noLongerPendingLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;

// Clear the expiration time
entanglements[index] = NoLanes;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was an oversight from when I added the entanglement feature. It's theoretically observable but if I write a test it'll be super convoluted, since we currently only use entanglement for useMutableSource, and only in a particular de-opt case.

eventTimes[index] = NoTimestamp;
expirationTimes[index] = NoTimestamp;

lanes &= ~lane;
Expand Down
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberRoot.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.callbackNode = null;
this.callbackId = NoLanes;
this.callbackPriority = NoLanePriority;
this.eventTimes = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);

this.pendingLanes = NoLanes;
Expand Down
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberRoot.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.callbackNode = null;
this.callbackId = NoLanes;
this.callbackPriority = NoLanePriority;
this.eventTimes = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);

this.pendingLanes = NoLanes;
Expand Down
130 changes: 61 additions & 69 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ import {
getCurrentUpdateLanePriority,
markStarvedLanesAsExpired,
getLanesToRetrySynchronouslyOnError,
getMostRecentEventTime,
markRootUpdated,
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
markRootPinged,
Expand Down Expand Up @@ -294,8 +295,6 @@ const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
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 = NoTimestamp;
let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
// "Included" lanes refer to lanes that were worked on during this render. It's
Expand Down Expand Up @@ -540,6 +539,35 @@ export function scheduleUpdateOnFiber(
return null;
}

// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);

if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}

// TODO: requestUpdateLanePriority also reads the priority. Pass the
// priority as an argument to that function and this one.
const priorityLevel = getCurrentPriorityLevel();
Expand Down Expand Up @@ -605,82 +633,47 @@ export function scheduleUpdateOnFiber(
// e.g. retrying a Suspense boundary isn't an update, but it does schedule work
// on a fiber.
function markUpdateLaneFromFiberToRoot(
fiber: Fiber,
sourceFiber: Fiber,
lane: Lane,
): FiberRoot | null {
// Update the source fiber's lanes
fiber.lanes = mergeLanes(fiber.lanes, lane);
let alternate = fiber.alternate;
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
if (__DEV__) {
if (
alternate === null &&
(fiber.effectTag & (Placement | Hydrating)) !== NoEffect
(sourceFiber.effectTag & (Placement | Hydrating)) !== NoEffect
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return;
let root = null;
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode;
} else {
while (node !== null) {
alternate = node.alternate;
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
} else {
if (__DEV__) {
if (
alternate === null &&
(node.effectTag & (Placement | Hydrating)) !== NoEffect
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
if ((parent.effectTag & (Placement | Hydrating)) !== NoEffect) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
node.childLanes = mergeLanes(node.childLanes, lane);
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
node = node.return;
}
node = parent;
parent = parent.return;
}

if (root !== null) {
// Mark that the root has a pending update.
markRootUpdated(root, lane);
if (workInProgressRoot === root) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}

return root;
}

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

const mostRecentEventTime = getMostRecentEventTime(root, lanes);
let msUntilTimeout;
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
// We have processed a suspense config whose expiration time we
// can use as the timeout.
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
} else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
} else if (mostRecentEventTime === 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.
msUntilTimeout = 0;
} else {
// If we didn't process a suspense config, compute a JND based on
// the amount of time elapsed since the most recent event time.
const eventTimeMs = workInProgressRootLatestProcessedEventTime;
const eventTimeMs = mostRecentEventTime;
const timeElapsedMs = now() - eventTimeMs;
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
}
Expand All @@ -980,18 +974,19 @@ function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
}
case RootCompleted: {
// The work completed. Ready to commit.
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
if (
// do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV() &&
workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
mostRecentEventTime !== NoTimestamp &&
workInProgressRootCanSuspendUsingConfig !== null
) {
// If we have exceeded the minimum loading delay, which probably
// means we have shown a spinner already, we might have to suspend
// a bit longer to ensure that the spinner is shown for
// enough time.
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
workInProgressRootLatestProcessedEventTime,
mostRecentEventTime,
workInProgressRootCanSuspendUsingConfig,
);
if (msUntilTimeout > 10) {
Expand Down Expand Up @@ -1329,7 +1324,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootLatestProcessedEventTime = NoTimestamp;
workInProgressRootLatestSuspenseTimeout = NoTimestamp;
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootSkippedLanes = NoLanes;
Expand Down Expand Up @@ -1447,11 +1441,6 @@ export function markRenderEventTimeAndConfig(
eventTime: number,
suspenseConfig: null | SuspenseConfig,
): void {
// Track the most recent event time of all updates processed in this batch.
if (workInProgressRootLatestProcessedEventTime < eventTime) {
workInProgressRootLatestProcessedEventTime = eventTime;
}

// Track the largest/latest timeout deadline in this batch.
// TODO: If there are two transitions in the same batch, shouldn't we
// choose the smaller one? Maybe this is because when an intermediate
Expand Down Expand Up @@ -2908,6 +2897,7 @@ function captureCommitPhaseErrorOnRoot(
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
if (root !== null) {
markRootUpdated(root, SyncLane, eventTime);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, SyncLane);
}
Expand Down Expand Up @@ -2944,6 +2934,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
if (root !== null) {
markRootUpdated(root, SyncLane, eventTime);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, SyncLane);
}
Expand Down Expand Up @@ -3016,6 +3007,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
if (root !== null) {
markRootUpdated(root, retryLane, eventTime);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, retryLane);
}
Expand Down
Loading