-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
[DevTools][Transition Tracing] Added support for Suspense Boundaries #23365
Conversation
Comparing: c5e039d...bc535a8 Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: (No significant changes) |
addTransitionStartCallbackToPendingTransition({ | ||
transitionName: transition.name, | ||
startTime: transition.startTime, | ||
if (flags & SuspenseToggle) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to move all this to the passive phase, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we can't because the end time might be incorrect because the passive phase could be called after or before paint and it's not guaranteed when, which is why it lives in the sync commit phase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing you have to do in the commit phase is read the time, which you can then access in the passive phase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But how would you know which time you should use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the question. You would read the current time in the mutation phase, then pass that timestamp to the passive phase. Then all the logic after that is the same.
5fe7487
to
c7dd02c
Compare
88377a0
to
87dba57
Compare
const currentTransitions = getSuspendedTransitions(); | ||
if (currentTransitions !== null) { | ||
const primaryChildUpdateQueue: OffscreenQueue = { | ||
transitions: currentTransitions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add pendingSuspenseBoundaries here and use fiberRoot.current
) { | ||
if (enableTransitionTracing) { | ||
const rootTransitions = getWorkInProgressTransitions(); | ||
push(transitionStack, [rootTransitions], workInProgress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If rootTransitions is null
it should push null
, no need for the extra array
@@ -28,18 +29,17 @@ export type Transition = { | |||
startTime: number, | |||
}; | |||
|
|||
export type Transitions = Array<Transition> | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need these type aliases?
} | ||
|
||
if (enableTransitionTracing) { | ||
if (current !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: We shouldn't check current
twice. Combine with check above.
@@ -746,14 +750,14 @@ function updateOffscreenComponent( | |||
|
|||
subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes); | |||
|
|||
if (enableCache) { | |||
if (enableCache || enableTransitionTracing) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor to make two separate branches
cloneUpdateQueue(current, workInProgress); | ||
processUpdateQueue(workInProgress, nextProps, null, renderLanes); | ||
|
||
const nextState: RootState = workInProgress.memoizedState; | ||
const root: FiberRoot = workInProgress.stateNode; | ||
|
||
if (enableCache || enableTransitionTracing) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
@@ -655,13 +657,14 @@ function updateOffscreenComponent( | |||
const nextState: OffscreenState = { | |||
baseLanes: NoLanes, | |||
cachePool: null, | |||
transitions: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a TODO here to consider how Offscreen should work with transitions?
function addOrRemovePendingBoundariesOnRoot( | ||
finishedWork: Fiber, | ||
name: string | null, | ||
stateNode: OffscreenInstance, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Maybe call this offscreen instance instead
case HostRoot: { | ||
// Get the transitions that were initiatized during the render | ||
// and add a start transition callback for each of them | ||
const currentTransitions = getWorkInProgressTransitions(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass as argument to commitRoot
|
||
addOrRemovePendingBoundariesOnRoot( | ||
fiber, | ||
props.name || null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Suspense name
5c65303
to
f560514
Compare
@@ -3572,14 +3637,13 @@ function attemptEarlyBailoutIfNoScheduledUpdate( | |||
case HostRoot: | |||
pushHostRootContext(workInProgress); | |||
const root: FiberRoot = workInProgress.stateNode; | |||
pushRootTransition(workInProgress, root, renderLanes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add if (enableTransitionTracing)
// once we add tracing markers | ||
setRootPendingSuspenseBoundaries(pendingSuspenseBoundaries); | ||
} | ||
const pendingSuspenseBoundaries = nextState.pendingSuspenseBoundaries || null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need this variable anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used below in the override states
if (currentTransitions !== null) { | ||
const primaryChildUpdateQueue: OffscreenQueue = { | ||
transitions: currentTransitions, | ||
rootMemoizedState: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pass the whole root
if (currentTransitions !== null) { | ||
const primaryChildUpdateQueue: OffscreenQueue = { | ||
transitions: currentTransitions, | ||
rootMemoizedState: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can put the whole root object in the queue
@@ -31,11 +31,12 @@ export type OffscreenState = {| | |||
// order to unhide the component. | |||
baseLanes: Lanes, | |||
cachePool: SpawnedCachePool | null, | |||
transitions: LazyTransitions | null, | |||
transitions: Array<Array<Transition> | null> | null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a Set instead
// comparing the arrays of transitions when we combine them and storing them | ||
// and filtering out the duplicates, we will instead store the unprocessed transitions | ||
// in an array of arrays and actually filter them in the passive phase. | ||
const transitionStack: StackCursor<Array<Array<Transition> | null> | null> = createCursor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this just an Array and make the transition field a Set
@@ -867,7 +867,8 @@ function performConcurrentWorkOnRoot(root, didTimeout) { | |||
// Flush any pending passive effects before deciding which lanes to work on, | |||
// in case they schedule additional work. | |||
const originalCallbackNode = root.callbackNode; | |||
const didFlushPassiveEffects = flushPassiveEffects(); | |||
// workInProgressTransitions should be what it in the previous render | |||
const didFlushPassiveEffects = flushPassiveEffects(workInProgressTransitions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of doing this make it a work loop variable (pendingPassiveEffectsRemainingLanes
)
@@ -2089,7 +2104,7 @@ function commitRootImpl( | |||
rootDoesHavePassiveEffects = true; | |||
pendingPassiveEffectsRemainingLanes = remainingLanes; | |||
scheduleCallback(NormalSchedulerPriority, () => { | |||
flushPassiveEffects(); | |||
flushPassiveEffects(transitions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Use a work loop variable instead, just for consistency's sake
eed8fa3
to
dd5768c
Compare
800c1d9
to
64e441f
Compare
// that doesnt change from render to render. This way we can | ||
// distinguish between different Offscreen instances (vs. the same | ||
// Offscreen instance with different fibers) | ||
const offscreenInstance = finishedWork.stateNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const offscreenInstance = finishedWork.stateNode; | |
const offscreenInstance: OffscreenInstance = finishedWork.stateNode; |
|
||
let prevState: SuspenseState | null = null; | ||
if ( | ||
finishedWork.alternate !== null && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Assign the alternate to a variable, perhaps called previous
or something. We try to minimize direct access to alternate
since it's going to be refactored away in the future.
} | ||
|
||
if (rootPendingBoundaries !== null) { | ||
if (finishedWork.alternate === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing as above
@@ -1067,6 +1069,77 @@ function reappearLayoutEffectsOnFiber(node: Fiber) { | |||
} | |||
} | |||
|
|||
function addOrRemovePendingBoundariesOnRoot( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rename this so it's clearer it's related to transitions? Maybe commitTransitionProgress
or something
@@ -1067,6 +1069,77 @@ function reappearLayoutEffectsOnFiber(node: Fiber) { | |||
} | |||
} | |||
|
|||
function addOrRemovePendingBoundariesOnRoot( | |||
finishedRoot: FiberRoot, | |||
finishedWork: Fiber, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also rename this so it's more obvious it's an offscreen fiber
const isHidden = nextState !== null; | ||
|
||
const rootPendingBoundaries = | ||
finishedRoot.current.memoizedState.pendingSuspenseBoundaries; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, whenever you access an untyped field like memoizedState
make sure you cast it to the correct type before using it
finishedRoot.current.memoizedState.pendingSuspenseBoundaries; | |
const rootState: RootState = finishedRoot.current.memoizedState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this can be in a follow up, but we should consider moving the pending suspense boundaries to the FiberRoot instead of the HostRoot fiber. Just for consistency with the other mutable ref-like fields. Could add a TODO for this.
// Get the transitions that were initiatized during the render | ||
// and add a start transition callback for each of them | ||
const state = finishedWork.memoizedState; | ||
if (state.transitions === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's a mutable field, this should probably live on the FiberRoot, too. Can do in a follow up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Could you add a TODO for this so we don't forget?
} | ||
|
||
const pendingSuspenseBoundaries = state.pendingSuspenseBoundaries; | ||
const processedTransitions = new Set(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this extra set necessary? Couldn't you check if it's already in pendingSuspenseBoundaries
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was to avoid adding duplicate transitionComplete calls if the same transition is in pendingTransitions
twice
if (!processedTransitions.has(transition)) { | ||
if ( | ||
pendingSuspenseBoundaries === null || | ||
pendingSuspenseBoundaries.size === 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the other way this could work is to call addTransitionCompleteCallbackToPendingTransition
whenever the last pending Suspense boundary is removed from the set, in addOrRemovePendingBoundariesOnRoot
. Did you try that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could do that, but we'd have to know and get all the pendingTransitions
and processedTransitions
on the root, so it doesn't really save us any work. We can do that instead if you prefer. I don't feel strongly either way. Keeping this code in the Host Root seems more intuitive to me though.
64e441f
to
65643b0
Compare
c17ca59
to
a634f55
Compare
} | ||
} | ||
|
||
if (rootPendingBoundaries.size === 0 && rootTransitions !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Move this right after the .delete
call above since logically that's the only case where the count could have reached 0.
const pendingSuspenseBoundaries = state.pendingSuspenseBoundaries; | ||
|
||
// Initial render | ||
if (committedTransitions != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (committedTransitions != null) { | |
if (committedTransitions !== null) { |
// Add all the transitions saved in the update queue during | ||
// the render phase (ie the transitions associated with this boundary) | ||
// into the transitions set. | ||
if (transitions != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (transitions != null) { | |
if (transitions !== null) { |
function commitTransitionProgress( | ||
finishedRoot: FiberRoot, | ||
offscreenFiber: Fiber, | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Wrap this in a feature flag
a634f55
to
bc535a8
Compare
Summary: This sync includes the following changes: - **[dd4950c90](facebook/react@dd4950c90 )**: [Flight] Implement useId hook ([#24172](facebook/react#24172)) //<Josh Story>// - **[26a5b3c7f](facebook/react@26a5b3c7f )**: Explicitly set `highWaterMark` to 0 for `ReadableStream` ([#24641](facebook/react#24641)) //<Josh Larson>// - **[aec575914](facebook/react@aec575914 )**: [Fizz] Send errors down to client ([#24551](facebook/react#24551)) //<Josh Story>// - **[a2766387e](facebook/react@a2766387e )**: [Fizz] Improve text separator byte efficiency ([#24630](facebook/react#24630)) //<Josh Story>// - **[f7860538a](facebook/react@f7860538a )**: Fix typo in useSyncExternalStore main entry point error ([#24631](facebook/react#24631)) //<François Chalifour>// - **[1bed20731](facebook/react@1bed20731 )**: Add a module map option to the Webpack Flight Client ([#24629](facebook/react#24629)) //<Sebastian Markbåge>// - **[b2763d3ea](facebook/react@b2763d3ea )**: Move hydration code out of normal Suspense path ([#24532](facebook/react#24532)) //<Andrew Clark>// - **[357a61324](facebook/react@357a61324 )**: [DevTools][Transition Tracing] Added support for Suspense Boundaries ([#23365](facebook/react#23365)) //<Luna Ruan>// - **[2c8a1452b](facebook/react@2c8a1452b )**: Fix ignored setState in Safari when iframe is touched ([#24459](facebook/react#24459)) //<dan>// - **[62662633d](facebook/react@62662633d )**: Remove enableFlipOffscreenUnhideOrder ([#24545](facebook/react#24545)) //<Ricky>// - **[34da5aa69](facebook/react@34da5aa69 )**: Only treat updates to lazy as a new mount in legacy mode ([#24530](facebook/react#24530)) //<Ricky>// - **[46a6d77e3](facebook/react@46a6d77e3 )**: Unify JSResourceReference Interfaces ([#24507](facebook/react#24507)) //<Timothy Yung>// - **[6cbf0f7fa](facebook/react@6cbf0f7fa )**: Fork ReactSymbols ([#24484](facebook/react#24484)) //<Ricky>// - **[a10a9a6b5](facebook/react@a10a9a6b5 )**: Add test for hiding children after layout destroy ([#24483](facebook/react#24483)) //<Ricky>// - **[b4eb0ad71](facebook/react@b4eb0ad71 )**: Do not replay erroring beginWork with invokeGuardedCallback when suspended or previously errored ([#24480](facebook/react#24480)) //<Josh Story>// - **[99eef9e2d](facebook/react@99eef9e2d )**: Hide children of Offscreen after destroy effects ([#24446](facebook/react#24446)) //<Ricky>// - **[ce1386028](facebook/react@ce1386028 )**: Remove enablePersistentOffscreenHostContainer flag ([#24460](facebook/react#24460)) //<Andrew Clark>// - **[72b7462fe](facebook/react@72b7462fe )**: Bump local package.json versions for 18.1 release ([#24447](facebook/react#24447)) //<Andrew Clark>// - **[22edb9f77](facebook/react@22edb9f77 )**: React `version` field should match package.json ([#24445](facebook/react#24445)) //<Andrew Clark>// - **[6bf3deef5](facebook/react@6bf3deef5 )**: Upgrade react-shallow-renderer to support react 18 ([#24442](facebook/react#24442)) //<Michael サイトー 中村 Bashurov>// Changelog: [General][Changed] - React Native sync for revisions bd4784c...d300ceb jest_e2e[run_all_tests] Reviewed By: cortinico, kacieb Differential Revision: D36874368 fbshipit-source-id: c0ee015f4ef2fa56e57f7a1f6bc37dd05c949877
This PR: