-
Notifications
You must be signed in to change notification settings - Fork 48.8k
Track "pending" and "suspended" ranges #16663
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,8 +73,15 @@ type BaseFiberRootProperties = {| | |
firstPendingTime: ExpirationTime, | ||
// The latest pending expiration time that exists in the tree | ||
lastPendingTime: ExpirationTime, | ||
// The time at which a suspended component pinged the root to render again | ||
pingTime: ExpirationTime, | ||
// The earliest suspended expiration time that exists in the tree | ||
firstSuspendedTime: ExpirationTime, | ||
// The latest suspended expiration time that exists in the tree | ||
lastSuspendedTime: ExpirationTime, | ||
// The next known expiration time after the suspended range | ||
nextAfterSuspendedTime: ExpirationTime, | ||
// The latest time at which a suspended component pinged the root to | ||
// render again | ||
lastPingedTime: ExpirationTime, | ||
|}; | ||
|
||
// The following attributes are only used by interaction tracing builds. | ||
|
@@ -120,7 +127,10 @@ function FiberRootNode(containerInfo, tag, hydrate) { | |
this.callbackExpirationTime = NoWork; | ||
this.firstPendingTime = NoWork; | ||
this.lastPendingTime = NoWork; | ||
this.pingTime = NoWork; | ||
this.firstSuspendedTime = NoWork; | ||
this.lastSuspendedTime = NoWork; | ||
this.nextAfterSuspendedTime = NoWork; | ||
this.lastPingedTime = NoWork; | ||
|
||
if (enableSchedulerTracing) { | ||
this.interactionThreadID = unstable_getThreadID(); | ||
|
@@ -151,3 +161,53 @@ export function createFiberRoot( | |
|
||
return root; | ||
} | ||
|
||
export function isRootSuspendedAtTime( | ||
root: FiberRoot, | ||
expirationTime: ExpirationTime, | ||
): boolean { | ||
const firstSuspendedTime = root.firstSuspendedTime; | ||
const lastSuspendedTime = root.lastSuspendedTime; | ||
return ( | ||
firstSuspendedTime !== NoWork && | ||
(firstSuspendedTime >= expirationTime && | ||
lastSuspendedTime <= expirationTime) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, totally I think it's time to rename the name 'expirationTime' because it's been talked/discussed a few months, but for now, since it's a 'time' could we compare them through the 'time order'? I.e. from small to large ( |
||
); | ||
} | ||
|
||
export function markRootSuspendedAtTime( | ||
root: FiberRoot, | ||
expirationTime: ExpirationTime, | ||
): void { | ||
const firstSuspendedTime = root.firstSuspendedTime; | ||
const lastSuspendedTime = root.lastSuspendedTime; | ||
acdlite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (firstSuspendedTime < expirationTime) { | ||
acdlite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
root.firstSuspendedTime = expirationTime; | ||
} | ||
if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) { | ||
root.lastSuspendedTime = expirationTime; | ||
} | ||
|
||
if (expirationTime <= root.lastPingedTime) { | ||
root.lastPingedTime = NoWork; | ||
} | ||
} | ||
|
||
export function markRootUnsuspendedAtTime( | ||
root: FiberRoot, | ||
expirationTime: ExpirationTime, | ||
): void { | ||
if (expirationTime <= root.lastSuspendedTime) { | ||
// The entire suspended range is now unsuspended. | ||
root.firstSuspendedTime = root.lastSuspendedTime = root.nextAfterSuspendedTime = NoWork; | ||
} else if (expirationTime <= root.firstSuspendedTime) { | ||
// Part of the suspended range is now unsuspended. Narrow the range to | ||
// include everything between the unsuspended time (non-inclusive) and the | ||
// last suspended time. | ||
root.firstSuspendedTime = expirationTime - 1; | ||
} | ||
|
||
if (expirationTime <= root.lastPingedTime) { | ||
root.lastPingedTime = NoWork; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,11 @@ import { | |
} from './ReactFiberHostConfig'; | ||
|
||
import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber'; | ||
import { | ||
isRootSuspendedAtTime, | ||
markRootSuspendedAtTime, | ||
markRootUnsuspendedAtTime, | ||
} from './ReactFiberRoot'; | ||
import { | ||
NoMode, | ||
StrictMode, | ||
|
@@ -377,8 +382,6 @@ export function scheduleUpdateOnFiber( | |
return; | ||
} | ||
|
||
root.pingTime = NoWork; | ||
|
||
checkForInterruption(fiber, expirationTime); | ||
recordScheduleUpdate(); | ||
|
||
|
@@ -492,6 +495,9 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { | |
if (lastPendingTime === NoWork || expirationTime < lastPendingTime) { | ||
root.lastPendingTime = expirationTime; | ||
} | ||
|
||
// Mark that the root is no longer suspended at this time. | ||
markRootUnsuspendedAtTime(root, expirationTime); | ||
} | ||
|
||
return root; | ||
|
@@ -807,13 +813,6 @@ function renderRoot( | |
'Should not already be working.', | ||
); | ||
|
||
if (root.firstPendingTime < expirationTime) { | ||
// If there's no work left at this expiration time, exit immediately. This | ||
// happens when multiple callbacks are scheduled for a single root, but an | ||
// earlier callback flushes the work of a later one. | ||
return null; | ||
} | ||
|
||
if (isSync && root.finishedExpirationTime === expirationTime) { | ||
// There's already a pending commit at this expiration time. | ||
// TODO: This is poorly factored. This case only exists for the | ||
|
@@ -831,21 +830,25 @@ function renderRoot( | |
} else if (workInProgressRootExitStatus === RootSuspendedWithDelay) { | ||
// We could've received an update at a lower priority while we yielded. | ||
// We're suspended in a delayed state. Once we complete this render we're | ||
// just going to try to recover at the last pending time anyway so we might | ||
// as well start doing that eagerly. | ||
// just going to try to recover at the pending time anyway so we might as | ||
// well start doing that eagerly. | ||
// | ||
// Ideally we should be able to do this even for retries but we don't yet | ||
// know if we're going to process an update which wants to commit earlier, | ||
// and this path happens very early so it would happen too often. Instead, | ||
// for that case, we'll wait until we complete. | ||
if (workInProgressRootHasPendingPing) { | ||
// We have a ping at this expiration. Let's restart to see if we get unblocked. | ||
prepareFreshStack(root, expirationTime); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be removed (maybe) because there exists one in the ping function already. |
||
} else { | ||
const lastPendingTime = root.lastPendingTime; | ||
if (lastPendingTime < expirationTime) { | ||
// There's lower priority work. It might be unsuspended. Try rendering | ||
// at that level immediately, while preserving the position in the queue. | ||
return renderRoot.bind(null, root, lastPendingTime); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're discussing this. Seems like this refactoring changed things. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be replaced by a prepareFreshStack in an update that happens during a yield. If the current tree has already determined that it has remaining work at lower pri, we can use that info to eagerly throw to terminate the render and try at lower pri. |
||
} else if (!isSync) { | ||
// Check if there's work that isn't in the suspended range | ||
const firstPendingTime = root.firstPendingTime; | ||
if (!isRootSuspendedAtTime(root, firstPendingTime)) { | ||
// There's a pending update that falls outside the range of | ||
// suspended work. | ||
if (firstPendingTime > expirationTime) { | ||
return renderRoot.bind(null, root, firstPendingTime); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -958,7 +961,8 @@ function renderRoot( | |
// something suspended, wait to commit it after a timeout. | ||
stopFinishedWorkLoopTimer(); | ||
|
||
root.finishedWork = root.current.alternate; | ||
const finishedWork: Fiber = ((root.finishedWork = | ||
root.current.alternate): any); | ||
root.finishedExpirationTime = expirationTime; | ||
|
||
const isLocked = resolveLocksOnRoot(root, expirationTime); | ||
|
@@ -1002,6 +1006,11 @@ function renderRoot( | |
return commitRoot.bind(null, root); | ||
} | ||
case RootSuspended: { | ||
markRootSuspendedAtTime(root, expirationTime); | ||
const lastSuspendedTime = root.lastSuspendedTime; | ||
if (expirationTime === lastSuspendedTime) { | ||
root.nextAfterSuspendedTime = getRemainingExpirationTime(finishedWork); | ||
} | ||
flushSuspensePriorityWarningInDEV(); | ||
|
||
// We have an acceptable loading state. We need to figure out if we should | ||
|
@@ -1038,11 +1047,20 @@ function renderRoot( | |
prepareFreshStack(root, expirationTime); | ||
return renderRoot.bind(null, root, expirationTime); | ||
} | ||
const lastPendingTime = root.lastPendingTime; | ||
if (lastPendingTime < expirationTime) { | ||
|
||
const nextAfterSuspendedTime = root.nextAfterSuspendedTime; | ||
acdlite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (nextAfterSuspendedTime !== NoWork) { | ||
// There's lower priority work. It might be unsuspended. Try rendering | ||
// at that level. | ||
return renderRoot.bind(null, root, lastPendingTime); | ||
return renderRoot.bind(null, root, nextAfterSuspendedTime); | ||
} | ||
if ( | ||
lastSuspendedTime !== NoWork && | ||
lastSuspendedTime !== expirationTime | ||
) { | ||
// We should prefer to render the fallback of at the last | ||
// suspended level. | ||
return renderRoot.bind(null, root, lastSuspendedTime); | ||
} | ||
// The render is suspended, it hasn't timed out, and there's no lower | ||
// priority work to do. Instead of committing the fallback | ||
|
@@ -1058,6 +1076,11 @@ function renderRoot( | |
return commitRoot.bind(null, root); | ||
} | ||
case RootSuspendedWithDelay: { | ||
markRootSuspendedAtTime(root, expirationTime); | ||
const lastSuspendedTime = root.lastSuspendedTime; | ||
acdlite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (expirationTime === lastSuspendedTime) { | ||
root.nextAfterSuspendedTime = getRemainingExpirationTime(finishedWork); | ||
} | ||
flushSuspensePriorityWarningInDEV(); | ||
|
||
if ( | ||
|
@@ -1077,11 +1100,20 @@ function renderRoot( | |
prepareFreshStack(root, expirationTime); | ||
return renderRoot.bind(null, root, expirationTime); | ||
} | ||
const lastPendingTime = root.lastPendingTime; | ||
if (lastPendingTime < expirationTime) { | ||
|
||
const nextAfterSuspendedTime = root.nextAfterSuspendedTime; | ||
acdlite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (nextAfterSuspendedTime !== NoWork) { | ||
// There's lower priority work. It might be unsuspended. Try rendering | ||
// at that level immediately. | ||
return renderRoot.bind(null, root, lastPendingTime); | ||
// at that level. | ||
return renderRoot.bind(null, root, nextAfterSuspendedTime); | ||
} | ||
if ( | ||
lastSuspendedTime !== NoWork && | ||
lastSuspendedTime !== expirationTime | ||
) { | ||
// We should prefer to render the fallback of at the last | ||
// suspended level. | ||
return renderRoot.bind(null, root, lastSuspendedTime); | ||
} | ||
|
||
let msUntilTimeout; | ||
|
@@ -1425,6 +1457,14 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { | |
return null; | ||
} | ||
|
||
function getRemainingExpirationTime(fiber: Fiber) { | ||
const updateExpirationTime = fiber.expirationTime; | ||
const childExpirationTime = fiber.childExpirationTime; | ||
return updateExpirationTime > childExpirationTime | ||
? updateExpirationTime | ||
: childExpirationTime; | ||
acdlite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function resetChildExpirationTime(completedWork: Fiber) { | ||
if ( | ||
renderExpirationTime !== Never && | ||
|
@@ -1540,19 +1580,19 @@ function commitRootImpl(root, renderPriorityLevel) { | |
|
||
// Update the first and last pending times on this root. The new first | ||
// pending time is whatever is left on the root fiber. | ||
const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; | ||
const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; | ||
const firstPendingTimeBeforeCommit = | ||
childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit | ||
? childExpirationTimeBeforeCommit | ||
: updateExpirationTimeBeforeCommit; | ||
root.firstPendingTime = firstPendingTimeBeforeCommit; | ||
if (firstPendingTimeBeforeCommit < root.lastPendingTime) { | ||
const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime( | ||
finishedWork, | ||
); | ||
root.firstPendingTime = remainingExpirationTimeBeforeCommit; | ||
if (remainingExpirationTimeBeforeCommit < root.lastPendingTime) { | ||
// This usually means we've finished all the work, but it can also happen | ||
// when something gets downprioritized during render, like a hidden tree. | ||
root.lastPendingTime = firstPendingTimeBeforeCommit; | ||
root.lastPendingTime = remainingExpirationTimeBeforeCommit; | ||
} | ||
|
||
// Mark that the root is no longer suspended at the finished time | ||
markRootUnsuspendedAtTime(root, expirationTime); | ||
|
||
if (root === workInProgressRoot) { | ||
// We can reset these now that they are finished. | ||
workInProgressRoot = null; | ||
|
@@ -2148,20 +2188,19 @@ export function pingSuspendedRoot( | |
return; | ||
} | ||
|
||
const lastPendingTime = root.lastPendingTime; | ||
if (lastPendingTime < suspendedTime) { | ||
if (!isRootSuspendedAtTime(root, suspendedTime)) { | ||
// The root is no longer suspended at this time. | ||
return; | ||
} | ||
|
||
const pingTime = root.pingTime; | ||
if (pingTime !== NoWork && pingTime < suspendedTime) { | ||
const lastPingedTime = root.lastPingedTime; | ||
if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) { | ||
// There's already a lower priority ping scheduled. | ||
return; | ||
} | ||
|
||
// Mark the time at which this ping was scheduled. | ||
root.pingTime = suspendedTime; | ||
root.lastPingedTime = suspendedTime; | ||
|
||
if (root.finishedExpirationTime === suspendedTime) { | ||
// If there's a pending fallback waiting to commit, throw it away. | ||
|
Uh oh!
There was an error while loading. Please reload this page.