Skip to content
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

Merged
merged 3 commits into from
May 16, 2022

Conversation

lunaruan
Copy link
Contributor

@lunaruan lunaruan commented Feb 25, 2022

This PR:

  • During the passive effect complete phase for Offscreen, we add all the transitions that were added to the update queue in the render phase to the transitions set on the memoziedState. We also add the stateNode for the Offscreen component to the root pendingSuspenseBoundaries map if the suspense boundary has gone from shown to fallback. We remove the stateNode if the boundary goes from fallback to shown.
  • During the passive effect complete phase for the HostRoot, for each transition that was initiated during this commit, we add a pending transitionStart callback. We also add them to the transition set on the memoizedState for the HostRoot. If the root pendingSuspenseBoundaries is empty, we add a pending transitionComplete callback.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Feb 25, 2022
@sizebot
Copy link

sizebot commented Feb 25, 2022

Comparing: c5e039d...bc535a8

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 131.48 kB 131.49 kB = 42.14 kB 42.14 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 136.72 kB 136.73 kB = 43.69 kB 43.69 kB
facebook-www/ReactDOM-prod.classic.js = 441.11 kB 441.13 kB = 80.41 kB 80.41 kB
facebook-www/ReactDOM-prod.modern.js = 426.32 kB 426.34 kB = 78.23 kB 78.23 kB
facebook-www/ReactDOMForked-prod.classic.js = 441.11 kB 441.13 kB = 80.41 kB 80.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against bc535a8

addTransitionStartCallbackToPendingTransition({
transitionName: transition.name,
startTime: transition.startTime,
if (flags & SuspenseToggle) {
Copy link
Collaborator

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?

Copy link
Contributor Author

@lunaruan lunaruan Feb 28, 2022

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

Copy link
Collaborator

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

Copy link
Contributor Author

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?

Copy link
Collaborator

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.

@lunaruan lunaruan force-pushed the suspense_boundary branch 4 times, most recently from 5fe7487 to c7dd02c Compare March 1, 2022 21:37
@lunaruan lunaruan force-pushed the suspense_boundary branch 6 times, most recently from 88377a0 to 87dba57 Compare March 22, 2022 16:51
const currentTransitions = getSuspendedTransitions();
if (currentTransitions !== null) {
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
Copy link
Contributor Author

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);
Copy link
Collaborator

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;
Copy link
Collaborator

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) {
Copy link
Collaborator

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) {
Copy link
Collaborator

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) {
Copy link
Collaborator

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,
Copy link
Collaborator

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,
Copy link
Collaborator

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();
Copy link
Collaborator

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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use Suspense name

@lunaruan lunaruan force-pushed the suspense_boundary branch from 5c65303 to f560514 Compare April 5, 2022 19:49
@@ -3572,14 +3637,13 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
case HostRoot:
pushHostRootContext(workInProgress);
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
Copy link
Contributor Author

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;
Copy link
Collaborator

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

Copy link
Contributor Author

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:
Copy link
Contributor Author

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:
Copy link
Collaborator

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,
Copy link
Collaborator

@acdlite acdlite Apr 7, 2022

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(
Copy link
Contributor Author

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);
Copy link
Contributor Author

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);
Copy link
Collaborator

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

@lunaruan lunaruan force-pushed the suspense_boundary branch 5 times, most recently from eed8fa3 to dd5768c Compare April 11, 2022 22:44
@lunaruan lunaruan force-pushed the suspense_boundary branch 2 times, most recently from 800c1d9 to 64e441f Compare April 12, 2022 16:35
// 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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const offscreenInstance = finishedWork.stateNode;
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;


let prevState: SuspenseState | null = null;
if (
finishedWork.alternate !== null &&
Copy link
Collaborator

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) {
Copy link
Collaborator

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(
Copy link
Collaborator

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,
Copy link
Collaborator

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;
Copy link
Collaborator

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

Suggested change
finishedRoot.current.memoizedState.pendingSuspenseBoundaries;
const rootState: RootState = finishedRoot.current.memoizedState;

Copy link
Collaborator

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) {
Copy link
Collaborator

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.

Copy link
Collaborator

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();
Copy link
Collaborator

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?

Copy link
Contributor Author

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
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

@lunaruan lunaruan force-pushed the suspense_boundary branch from 64e441f to 65643b0 Compare April 14, 2022 20:47
@lunaruan lunaruan force-pushed the suspense_boundary branch from c17ca59 to a634f55 Compare May 12, 2022 21:31
}
}

if (rootPendingBoundaries.size === 0 && rootTransitions !== null) {
Copy link
Collaborator

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (transitions != null) {
if (transitions !== null) {

function commitTransitionProgress(
finishedRoot: FiberRoot,
offscreenFiber: Fiber,
) {
Copy link
Collaborator

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

@lunaruan lunaruan force-pushed the suspense_boundary branch from a634f55 to bc535a8 Compare May 16, 2022 17:08
@lunaruan lunaruan merged commit 357a613 into facebook:main May 16, 2022
facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request Jun 6, 2022
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants