@samitouri / QOS-React-2 / commits / 11ed7010c6

[Transition Tracing] onMarkerIncomplete - Tracing Marker/Suspense Boundary Deletions (#24885)

This PR adds the `onMarkerIncomplete` callback for tracing marker name changes. Specifically, this PR: * Adds the `onMarkerIncomplete` callback * When a tracing marker is deleted, call `onMarkerIncomplete` with the `name` of the tracing marker for the tracing marker. * When a tracing marker/suspense boundary is deleted, call `onMarkerIncomplete` for every parent tracing marker with the `name` of the tracing marker that caused the transition to be incomplete. * Don't call `onTransitionComplete` or `onMarkerComplete` when `onMarkerIncomplete` is called for all tracing markers with the same transitions, but continue to call `onTransitionProgress`

Luna Ruan committed Aug 25, 2022 at 19:03 UTC 11ed7010c6d34c40342e8b829a48fec138215ae0
14 files changed +1571 -106
packages/react-reconciler/src/ReactFiber.new.js
+2
@@ -774,6 +774,8 @@ export function createFiberFromTracingMarker(
774 tag: TransitionTracingMarker,
775 transitions: null,
776 pendingBoundaries: null,
777 + aborts: null,
778 + name: pendingProps.name,
779 };
780 fiber.stateNode = tracingMarkerInstance;
781 return fiber;
packages/react-reconciler/src/ReactFiber.old.js
+2
@@ -774,6 +774,8 @@ export function createFiberFromTracingMarker(
774 tag: TransitionTracingMarker,
775 transitions: null,
776 pendingBoundaries: null,
777 + aborts: null,
778 + name: pendingProps.name,
779 };
780 fiber.stateNode = tracingMarkerInstance;
781 return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+9 -1
@@ -89,6 +89,7 @@ import {
89 StaticMask,
90 ShouldCapture,
91 ForceClientRender,
92 + Passive,
93 } from './ReactFiberFlags';
94 import ReactSharedInternals from 'shared/ReactSharedInternals';
95 import {
@@ -979,10 +980,17 @@ function updateTracingMarkerComponent(
980 const markerInstance: TracingMarkerInstance = {
981 tag: TransitionTracingMarker,
982 transitions: new Set(currentTransitions),
982 - pendingBoundaries: new Map(),
983 + pendingBoundaries: null,
984 name: workInProgress.pendingProps.name,
985 + aborts: null,
986 };
987 workInProgress.stateNode = markerInstance;
988 +
989 + // We call the marker complete callback when all child suspense boundaries resolve.
990 + // We do this in the commit phase on Offscreen. If the marker has no child suspense
991 + // boundaries, we need to schedule a passive effect to make sure we call the marker
992 + // complete callback.
993 + workInProgress.flags |= Passive;
994 }
995 } else {
996 if (__DEV__) {
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+9 -1
@@ -89,6 +89,7 @@ import {
89 StaticMask,
90 ShouldCapture,
91 ForceClientRender,
92 + Passive,
93 } from './ReactFiberFlags';
94 import ReactSharedInternals from 'shared/ReactSharedInternals';
95 import {
@@ -979,10 +980,17 @@ function updateTracingMarkerComponent(
980 const markerInstance: TracingMarkerInstance = {
981 tag: TransitionTracingMarker,
982 transitions: new Set(currentTransitions),
982 - pendingBoundaries: new Map(),
983 + pendingBoundaries: null,
984 name: workInProgress.pendingProps.name,
985 + aborts: null,
986 };
987 workInProgress.stateNode = markerInstance;
988 +
989 + // We call the marker complete callback when all child suspense boundaries resolve.
990 + // We do this in the commit phase on Offscreen. If the marker has no child suspense
991 + // boundaries, we need to schedule a passive effect to make sure we call the marker
992 + // complete callback.
993 + workInProgress.flags |= Passive;
994 }
995 } else {
996 if (__DEV__) {
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+250 -33
@@ -30,7 +30,11 @@ import type {
30 import type {HookFlags} from './ReactHookEffectTags';
31 import type {Cache} from './ReactFiberCacheComponent.new';
32 import type {RootState} from './ReactFiberRoot.new';
33 -import type {Transition} from './ReactFiberTracingMarkerComponent.new';
33 +import type {
34 + Transition,
35 + TracingMarkerInstance,
36 + TransitionAbort,
37 +} from './ReactFiberTracingMarkerComponent.new';
38
39 import {
40 enableCreateEventHandleAPI,
@@ -145,6 +149,7 @@ import {
149 addTransitionProgressCallbackToPendingTransition,
150 addTransitionCompleteCallbackToPendingTransition,
151 addMarkerProgressCallbackToPendingTransition,
152 + addMarkerIncompleteCallbackToPendingTransition,
153 addMarkerCompleteCallbackToPendingTransition,
154 setIsRunningInsertionEffect,
155 getExecutionContext,
@@ -1130,6 +1135,141 @@ function commitLayoutEffectOnFiber(
1135 }
1136 }
1137
1138 +function abortRootTransitions(
1139 + root: FiberRoot,
1140 + abort: TransitionAbort,
1141 + deletedTransitions: Set<Transition>,
1142 + deletedOffscreenInstance: OffscreenInstance | null,
1143 + isInDeletedTree: boolean,
1144 +) {
1145 + if (enableTransitionTracing) {
1146 + const rootTransitions = root.incompleteTransitions;
1147 + deletedTransitions.forEach(transition => {
1148 + if (rootTransitions.has(transition)) {
1149 + const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
1150 + transition,
1151 + ): any);
1152 + if (transitionInstance.aborts === null) {
1153 + transitionInstance.aborts = [];
1154 + }
1155 + transitionInstance.aborts.push(abort);
1156 +
1157 + if (deletedOffscreenInstance !== null) {
1158 + if (
1159 + transitionInstance.pendingBoundaries !== null &&
1160 + transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
1161 + ) {
1162 + transitionInstance.pendingBoundaries.delete(
1163 + deletedOffscreenInstance,
1164 + );
1165 + }
1166 + }
1167 + }
1168 + });
1169 + }
1170 +}
1171 +
1172 +function abortTracingMarkerTransitions(
1173 + abortedFiber: Fiber,
1174 + abort: TransitionAbort,
1175 + deletedTransitions: Set<Transition>,
1176 + deletedOffscreenInstance: OffscreenInstance | null,
1177 + isInDeletedTree: boolean,
1178 +) {
1179 + if (enableTransitionTracing) {
1180 + const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
1181 + const markerTransitions = markerInstance.transitions;
1182 + const pendingBoundaries = markerInstance.pendingBoundaries;
1183 + if (markerTransitions !== null) {
1184 + // TODO: Refactor this code. Is there a way to move this code to
1185 + // the deletions phase instead of calculating it here while making sure
1186 + // complete is called appropriately?
1187 + deletedTransitions.forEach(transition => {
1188 + // If one of the transitions on the tracing marker is a transition
1189 + // that was in an aborted subtree, we will abort that tracing marker
1190 + if (
1191 + abortedFiber !== null &&
1192 + markerTransitions.has(transition) &&
1193 + (markerInstance.aborts === null ||
1194 + !markerInstance.aborts.includes(abort))
1195 + ) {
1196 + if (markerInstance.transitions !== null) {
1197 + if (markerInstance.aborts === null) {
1198 + markerInstance.aborts = [abort];
1199 + addMarkerIncompleteCallbackToPendingTransition(
1200 + abortedFiber.memoizedProps.name,
1201 + markerInstance.transitions,
1202 + markerInstance.aborts,
1203 + );
1204 + } else {
1205 + markerInstance.aborts.push(abort);
1206 + }
1207 +
1208 + // We only want to call onTransitionProgress when the marker hasn't been
1209 + // deleted
1210 + if (
1211 + deletedOffscreenInstance !== null &&
1212 + !isInDeletedTree &&
1213 + pendingBoundaries !== null &&
1214 + pendingBoundaries.has(deletedOffscreenInstance)
1215 + ) {
1216 + pendingBoundaries.delete(deletedOffscreenInstance);
1217 +
1218 + addMarkerProgressCallbackToPendingTransition(
1219 + abortedFiber.memoizedProps.name,
1220 + deletedTransitions,
1221 + pendingBoundaries,
1222 + );
1223 + }
1224 + }
1225 + }
1226 + });
1227 + }
1228 + }
1229 +}
1230 +
1231 +function abortParentMarkerTransitionsForDeletedFiber(
1232 + abortedFiber: Fiber,
1233 + abort: TransitionAbort,
1234 + deletedTransitions: Set<Transition>,
1235 + deletedOffscreenInstance: OffscreenInstance | null,
1236 + isInDeletedTree: boolean,
1237 +) {
1238 + if (enableTransitionTracing) {
1239 + // Find all pending markers that are waiting on child suspense boundaries in the
1240 + // aborted subtree and cancels them
1241 + let fiber = abortedFiber;
1242 + while (fiber !== null) {
1243 + switch (fiber.tag) {
1244 + case TracingMarkerComponent:
1245 + abortTracingMarkerTransitions(
1246 + fiber,
1247 + abort,
1248 + deletedTransitions,
1249 + deletedOffscreenInstance,
1250 + isInDeletedTree,
1251 + );
1252 + break;
1253 + case HostRoot:
1254 + const root = fiber.stateNode;
1255 + abortRootTransitions(
1256 + root,
1257 + abort,
1258 + deletedTransitions,
1259 + deletedOffscreenInstance,
1260 + isInDeletedTree,
1261 + );
1262 +
1263 + break;
1264 + default:
1265 + break;
1266 + }
1267 +
1268 + fiber = fiber.return;
1269 + }
1270 + }
1271 +}
1272 +
1273 function commitTransitionProgress(offscreenFiber: Fiber) {
1274 if (enableTransitionTracing) {
1275 // This function adds suspense boundaries to the root
@@ -1175,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1315 pendingMarkers.forEach(markerInstance => {
1316 const pendingBoundaries = markerInstance.pendingBoundaries;
1317 const transitions = markerInstance.transitions;
1318 + const markerName = markerInstance.name;
1319 if (
1320 pendingBoundaries !== null &&
1321 !pendingBoundaries.has(offscreenInstance)
@@ -1185,10 +1326,10 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1326 if (transitions !== null) {
1327 if (
1328 markerInstance.tag === TransitionTracingMarker &&
1188 - markerInstance.name !== undefined
1329 + markerName !== null
1330 ) {
1331 addMarkerProgressCallbackToPendingTransition(
1191 - markerInstance.name,
1332 + markerName,
1333 transitions,
1334 pendingBoundaries,
1335 );
@@ -1212,6 +1353,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1353 pendingMarkers.forEach(markerInstance => {
1354 const pendingBoundaries = markerInstance.pendingBoundaries;
1355 const transitions = markerInstance.transitions;
1356 + const markerName = markerInstance.name;
1357 if (
1358 pendingBoundaries !== null &&
1359 pendingBoundaries.has(offscreenInstance)
@@ -1220,13 +1362,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1362 if (transitions !== null) {
1363 if (
1364 markerInstance.tag === TransitionTracingMarker &&
1223 - markerInstance.name !== undefined
1365 + markerName !== null
1366 ) {
1367 addMarkerProgressCallbackToPendingTransition(
1226 - markerInstance.name,
1368 + markerName,
1369 transitions,
1370 pendingBoundaries,
1371 );
1372 +
1373 + // If there are no more unresolved suspense boundaries, the interaction
1374 + // is considered finished
1375 + if (pendingBoundaries.size === 0) {
1376 + if (markerInstance.aborts === null) {
1377 + addMarkerCompleteCallbackToPendingTransition(
1378 + markerName,
1379 + transitions,
1380 + );
1381 + }
1382 + markerInstance.transitions = null;
1383 + markerInstance.pendingBoundaries = null;
1384 + markerInstance.aborts = null;
1385 + }
1386 } else if (markerInstance.tag === TransitionRoot) {
1387 transitions.forEach(transition => {
1388 addTransitionProgressCallbackToPendingTransition(
@@ -1737,6 +1893,7 @@ function commitDeletionEffects(
1893 'a bug in React. Please file an issue.',
1894 );
1895 }
1896 +
1897 commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1898 hostParent = null;
1899 hostParentIsContainer = false;
@@ -1979,6 +2136,7 @@ function commitDeletionEffectsOnFiber(
2136 const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
2137 offscreenSubtreeWasHidden =
2138 prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
2139 +
2140 recursivelyTraverseDeletionEffects(
2141 finishedRoot,
2142 nearestMountedAncestor,
@@ -2957,6 +3115,12 @@ function commitOffscreenPassiveMountEffects(
3115 }
3116
3117 commitTransitionProgress(finishedWork);
3118 +
3119 + // TODO: Refactor this into an if/else branch
3120 + if (!isHidden) {
3121 + instance.transitions = null;
3122 + instance.pendingMarkers = null;
3123 + }
3124 }
3125 }
3126
@@ -2987,20 +3151,18 @@ function commitCachePassiveMountEffect(
3151 function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
3152 // Get the transitions that were initiatized during the render
3153 // and add a start transition callback for each of them
3154 + // We will only call this on initial mount of the tracing marker
3155 + // only if there are no suspense children
3156 const instance = finishedWork.stateNode;
2991 - if (
2992 - instance.transitions !== null &&
2993 - (instance.pendingBoundaries === null ||
2994 - instance.pendingBoundaries.size === 0)
2995 - ) {
2996 - instance.transitions.forEach(transition => {
2997 - addMarkerCompleteCallbackToPendingTransition(
2998 - finishedWork.memoizedProps.name,
2999 - instance.transitions,
3000 - );
3001 - });
3157 + if (instance.transitions !== null && instance.pendingBoundaries === null) {
3158 + addMarkerCompleteCallbackToPendingTransition(
3159 + finishedWork.memoizedProps.name,
3160 + instance.transitions,
3161 + );
3162 instance.transitions = null;
3163 instance.pendingBoundaries = null;
3164 + instance.aborts = null;
3165 + instance.name = null;
3166 }
3167 }
3168
@@ -3102,7 +3264,7 @@ function commitPassiveMountOnFiber(
3264 if (enableTransitionTracing) {
3265 // Get the transitions that were initiatized during the render
3266 // and add a start transition callback for each of them
3105 - const root = finishedWork.stateNode;
3267 + const root: FiberRoot = finishedWork.stateNode;
3268 const incompleteTransitions = root.incompleteTransitions;
3269 // Initial render
3270 if (committedTransitions !== null) {
@@ -3116,7 +3278,9 @@ function commitPassiveMountOnFiber(
3278 incompleteTransitions.forEach((markerInstance, transition) => {
3279 const pendingBoundaries = markerInstance.pendingBoundaries;
3280 if (pendingBoundaries === null || pendingBoundaries.size === 0) {
3119 - addTransitionCompleteCallbackToPendingTransition(transition);
3281 + if (markerInstance.aborts === null) {
3282 + addTransitionCompleteCallbackToPendingTransition(transition);
3283 + }
3284 incompleteTransitions.delete(transition);
3285 }
3286 });
@@ -3489,21 +3653,6 @@ function commitAtomicPassiveEffects(
3653 }
3654 break;
3655 }
3492 - case TracingMarkerComponent: {
3493 - if (enableTransitionTracing) {
3494 - recursivelyTraverseAtomicPassiveEffects(
3495 - finishedRoot,
3496 - finishedWork,
3497 - committedLanes,
3498 - committedTransitions,
3499 - );
3500 - if (flags & Passive) {
3501 - commitTracingMarkerPassiveMountEffect(finishedWork);
3502 - }
3503 - break;
3504 - }
3505 - // Intentional fallthrough to next branch
3506 - }
3656 // eslint-disable-next-line-no-fallthrough
3657 default: {
3658 recursivelyTraverseAtomicPassiveEffects(
@@ -3827,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
3976 }
3977 break;
3978 }
3979 + case SuspenseComponent: {
3980 + if (enableTransitionTracing) {
3981 + // We need to mark this fiber's parents as deleted
3982 + const offscreenFiber: Fiber = (current.child: any);
3983 + const instance: OffscreenInstance = offscreenFiber.stateNode;
3984 + const transitions = instance.transitions;
3985 + if (transitions !== null) {
3986 + const abortReason = {
3987 + reason: 'suspense',
3988 + name: current.memoizedProps.unstable_name || null,
3989 + };
3990 + if (
3991 + current.memoizedState === null ||
3992 + current.memoizedState.dehydrated === null
3993 + ) {
3994 + abortParentMarkerTransitionsForDeletedFiber(
3995 + offscreenFiber,
3996 + abortReason,
3997 + transitions,
3998 + instance,
3999 + true,
4000 + );
4001 +
4002 + if (nearestMountedAncestor !== null) {
4003 + abortParentMarkerTransitionsForDeletedFiber(
4004 + nearestMountedAncestor,
4005 + abortReason,
4006 + transitions,
4007 + instance,
4008 + false,
4009 + );
4010 + }
4011 + }
4012 + }
4013 + }
4014 + break;
4015 + }
4016 case CacheComponent: {
4017 if (enableCache) {
4018 const cache = current.memoizedState.cache;
@@ -3834,6 +4020,37 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4020 }
4021 break;
4022 }
4023 + case TracingMarkerComponent: {
4024 + if (enableTransitionTracing) {
4025 + // We need to mark this fiber's parents as deleted
4026 + const instance: TracingMarkerInstance = current.stateNode;
4027 + const transitions = instance.transitions;
4028 + if (transitions !== null) {
4029 + const abortReason = {
4030 + reason: 'marker',
4031 + name: current.memoizedProps.name,
4032 + };
4033 + abortParentMarkerTransitionsForDeletedFiber(
4034 + current,
4035 + abortReason,
4036 + transitions,
4037 + null,
4038 + true,
4039 + );
4040 +
4041 + if (nearestMountedAncestor !== null) {
4042 + abortParentMarkerTransitionsForDeletedFiber(
4043 + nearestMountedAncestor,
4044 + abortReason,
4045 + transitions,
4046 + null,
4047 + false,
4048 + );
4049 + }
4050 + }
4051 + }
4052 + break;
4053 + }
4054 }
4055 }
4056
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+250 -33
@@ -30,7 +30,11 @@ import type {
30 import type {HookFlags} from './ReactHookEffectTags';
31 import type {Cache} from './ReactFiberCacheComponent.old';
32 import type {RootState} from './ReactFiberRoot.old';
33 -import type {Transition} from './ReactFiberTracingMarkerComponent.old';
33 +import type {
34 + Transition,
35 + TracingMarkerInstance,
36 + TransitionAbort,
37 +} from './ReactFiberTracingMarkerComponent.old';
38
39 import {
40 enableCreateEventHandleAPI,
@@ -145,6 +149,7 @@ import {
149 addTransitionProgressCallbackToPendingTransition,
150 addTransitionCompleteCallbackToPendingTransition,
151 addMarkerProgressCallbackToPendingTransition,
152 + addMarkerIncompleteCallbackToPendingTransition,
153 addMarkerCompleteCallbackToPendingTransition,
154 setIsRunningInsertionEffect,
155 getExecutionContext,
@@ -1130,6 +1135,141 @@ function commitLayoutEffectOnFiber(
1135 }
1136 }
1137
1138 +function abortRootTransitions(
1139 + root: FiberRoot,
1140 + abort: TransitionAbort,
1141 + deletedTransitions: Set<Transition>,
1142 + deletedOffscreenInstance: OffscreenInstance | null,
1143 + isInDeletedTree: boolean,
1144 +) {
1145 + if (enableTransitionTracing) {
1146 + const rootTransitions = root.incompleteTransitions;
1147 + deletedTransitions.forEach(transition => {
1148 + if (rootTransitions.has(transition)) {
1149 + const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
1150 + transition,
1151 + ): any);
1152 + if (transitionInstance.aborts === null) {
1153 + transitionInstance.aborts = [];
1154 + }
1155 + transitionInstance.aborts.push(abort);
1156 +
1157 + if (deletedOffscreenInstance !== null) {
1158 + if (
1159 + transitionInstance.pendingBoundaries !== null &&
1160 + transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
1161 + ) {
1162 + transitionInstance.pendingBoundaries.delete(
1163 + deletedOffscreenInstance,
1164 + );
1165 + }
1166 + }
1167 + }
1168 + });
1169 + }
1170 +}
1171 +
1172 +function abortTracingMarkerTransitions(
1173 + abortedFiber: Fiber,
1174 + abort: TransitionAbort,
1175 + deletedTransitions: Set<Transition>,
1176 + deletedOffscreenInstance: OffscreenInstance | null,
1177 + isInDeletedTree: boolean,
1178 +) {
1179 + if (enableTransitionTracing) {
1180 + const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
1181 + const markerTransitions = markerInstance.transitions;
1182 + const pendingBoundaries = markerInstance.pendingBoundaries;
1183 + if (markerTransitions !== null) {
1184 + // TODO: Refactor this code. Is there a way to move this code to
1185 + // the deletions phase instead of calculating it here while making sure
1186 + // complete is called appropriately?
1187 + deletedTransitions.forEach(transition => {
1188 + // If one of the transitions on the tracing marker is a transition
1189 + // that was in an aborted subtree, we will abort that tracing marker
1190 + if (
1191 + abortedFiber !== null &&
1192 + markerTransitions.has(transition) &&
1193 + (markerInstance.aborts === null ||
1194 + !markerInstance.aborts.includes(abort))
1195 + ) {
1196 + if (markerInstance.transitions !== null) {
1197 + if (markerInstance.aborts === null) {
1198 + markerInstance.aborts = [abort];
1199 + addMarkerIncompleteCallbackToPendingTransition(
1200 + abortedFiber.memoizedProps.name,
1201 + markerInstance.transitions,
1202 + markerInstance.aborts,
1203 + );
1204 + } else {
1205 + markerInstance.aborts.push(abort);
1206 + }
1207 +
1208 + // We only want to call onTransitionProgress when the marker hasn't been
1209 + // deleted
1210 + if (
1211 + deletedOffscreenInstance !== null &&
1212 + !isInDeletedTree &&
1213 + pendingBoundaries !== null &&
1214 + pendingBoundaries.has(deletedOffscreenInstance)
1215 + ) {
1216 + pendingBoundaries.delete(deletedOffscreenInstance);
1217 +
1218 + addMarkerProgressCallbackToPendingTransition(
1219 + abortedFiber.memoizedProps.name,
1220 + deletedTransitions,
1221 + pendingBoundaries,
1222 + );
1223 + }
1224 + }
1225 + }
1226 + });
1227 + }
1228 + }
1229 +}
1230 +
1231 +function abortParentMarkerTransitionsForDeletedFiber(
1232 + abortedFiber: Fiber,
1233 + abort: TransitionAbort,
1234 + deletedTransitions: Set<Transition>,
1235 + deletedOffscreenInstance: OffscreenInstance | null,
1236 + isInDeletedTree: boolean,
1237 +) {
1238 + if (enableTransitionTracing) {
1239 + // Find all pending markers that are waiting on child suspense boundaries in the
1240 + // aborted subtree and cancels them
1241 + let fiber = abortedFiber;
1242 + while (fiber !== null) {
1243 + switch (fiber.tag) {
1244 + case TracingMarkerComponent:
1245 + abortTracingMarkerTransitions(
1246 + fiber,
1247 + abort,
1248 + deletedTransitions,
1249 + deletedOffscreenInstance,
1250 + isInDeletedTree,
1251 + );
1252 + break;
1253 + case HostRoot:
1254 + const root = fiber.stateNode;
1255 + abortRootTransitions(
1256 + root,
1257 + abort,
1258 + deletedTransitions,
1259 + deletedOffscreenInstance,
1260 + isInDeletedTree,
1261 + );
1262 +
1263 + break;
1264 + default:
1265 + break;
1266 + }
1267 +
1268 + fiber = fiber.return;
1269 + }
1270 + }
1271 +}
1272 +
1273 function commitTransitionProgress(offscreenFiber: Fiber) {
1274 if (enableTransitionTracing) {
1275 // This function adds suspense boundaries to the root
@@ -1175,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1315 pendingMarkers.forEach(markerInstance => {
1316 const pendingBoundaries = markerInstance.pendingBoundaries;
1317 const transitions = markerInstance.transitions;
1318 + const markerName = markerInstance.name;
1319 if (
1320 pendingBoundaries !== null &&
1321 !pendingBoundaries.has(offscreenInstance)
@@ -1185,10 +1326,10 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1326 if (transitions !== null) {
1327 if (
1328 markerInstance.tag === TransitionTracingMarker &&
1188 - markerInstance.name !== undefined
1329 + markerName !== null
1330 ) {
1331 addMarkerProgressCallbackToPendingTransition(
1191 - markerInstance.name,
1332 + markerName,
1333 transitions,
1334 pendingBoundaries,
1335 );
@@ -1212,6 +1353,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1353 pendingMarkers.forEach(markerInstance => {
1354 const pendingBoundaries = markerInstance.pendingBoundaries;
1355 const transitions = markerInstance.transitions;
1356 + const markerName = markerInstance.name;
1357 if (
1358 pendingBoundaries !== null &&
1359 pendingBoundaries.has(offscreenInstance)
@@ -1220,13 +1362,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1362 if (transitions !== null) {
1363 if (
1364 markerInstance.tag === TransitionTracingMarker &&
1223 - markerInstance.name !== undefined
1365 + markerName !== null
1366 ) {
1367 addMarkerProgressCallbackToPendingTransition(
1226 - markerInstance.name,
1368 + markerName,
1369 transitions,
1370 pendingBoundaries,
1371 );
1372 +
1373 + // If there are no more unresolved suspense boundaries, the interaction
1374 + // is considered finished
1375 + if (pendingBoundaries.size === 0) {
1376 + if (markerInstance.aborts === null) {
1377 + addMarkerCompleteCallbackToPendingTransition(
1378 + markerName,
1379 + transitions,
1380 + );
1381 + }
1382 + markerInstance.transitions = null;
1383 + markerInstance.pendingBoundaries = null;
1384 + markerInstance.aborts = null;
1385 + }
1386 } else if (markerInstance.tag === TransitionRoot) {
1387 transitions.forEach(transition => {
1388 addTransitionProgressCallbackToPendingTransition(
@@ -1737,6 +1893,7 @@ function commitDeletionEffects(
1893 'a bug in React. Please file an issue.',
1894 );
1895 }
1896 +
1897 commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1898 hostParent = null;
1899 hostParentIsContainer = false;
@@ -1979,6 +2136,7 @@ function commitDeletionEffectsOnFiber(
2136 const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
2137 offscreenSubtreeWasHidden =
2138 prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
2139 +
2140 recursivelyTraverseDeletionEffects(
2141 finishedRoot,
2142 nearestMountedAncestor,
@@ -2957,6 +3115,12 @@ function commitOffscreenPassiveMountEffects(
3115 }
3116
3117 commitTransitionProgress(finishedWork);
3118 +
3119 + // TODO: Refactor this into an if/else branch
3120 + if (!isHidden) {
3121 + instance.transitions = null;
3122 + instance.pendingMarkers = null;
3123 + }
3124 }
3125 }
3126
@@ -2987,20 +3151,18 @@ function commitCachePassiveMountEffect(
3151 function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
3152 // Get the transitions that were initiatized during the render
3153 // and add a start transition callback for each of them
3154 + // We will only call this on initial mount of the tracing marker
3155 + // only if there are no suspense children
3156 const instance = finishedWork.stateNode;
2991 - if (
2992 - instance.transitions !== null &&
2993 - (instance.pendingBoundaries === null ||
2994 - instance.pendingBoundaries.size === 0)
2995 - ) {
2996 - instance.transitions.forEach(transition => {
2997 - addMarkerCompleteCallbackToPendingTransition(
2998 - finishedWork.memoizedProps.name,
2999 - instance.transitions,
3000 - );
3001 - });
3157 + if (instance.transitions !== null && instance.pendingBoundaries === null) {
3158 + addMarkerCompleteCallbackToPendingTransition(
3159 + finishedWork.memoizedProps.name,
3160 + instance.transitions,
3161 + );
3162 instance.transitions = null;
3163 instance.pendingBoundaries = null;
3164 + instance.aborts = null;
3165 + instance.name = null;
3166 }
3167 }
3168
@@ -3102,7 +3264,7 @@ function commitPassiveMountOnFiber(
3264 if (enableTransitionTracing) {
3265 // Get the transitions that were initiatized during the render
3266 // and add a start transition callback for each of them
3105 - const root = finishedWork.stateNode;
3267 + const root: FiberRoot = finishedWork.stateNode;
3268 const incompleteTransitions = root.incompleteTransitions;
3269 // Initial render
3270 if (committedTransitions !== null) {
@@ -3116,7 +3278,9 @@ function commitPassiveMountOnFiber(
3278 incompleteTransitions.forEach((markerInstance, transition) => {
3279 const pendingBoundaries = markerInstance.pendingBoundaries;
3280 if (pendingBoundaries === null || pendingBoundaries.size === 0) {
3119 - addTransitionCompleteCallbackToPendingTransition(transition);
3281 + if (markerInstance.aborts === null) {
3282 + addTransitionCompleteCallbackToPendingTransition(transition);
3283 + }
3284 incompleteTransitions.delete(transition);
3285 }
3286 });
@@ -3489,21 +3653,6 @@ function commitAtomicPassiveEffects(
3653 }
3654 break;
3655 }
3492 - case TracingMarkerComponent: {
3493 - if (enableTransitionTracing) {
3494 - recursivelyTraverseAtomicPassiveEffects(
3495 - finishedRoot,
3496 - finishedWork,
3497 - committedLanes,
3498 - committedTransitions,
3499 - );
3500 - if (flags & Passive) {
3501 - commitTracingMarkerPassiveMountEffect(finishedWork);
3502 - }
3503 - break;
3504 - }
3505 - // Intentional fallthrough to next branch
3506 - }
3656 // eslint-disable-next-line-no-fallthrough
3657 default: {
3658 recursivelyTraverseAtomicPassiveEffects(
@@ -3827,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
3976 }
3977 break;
3978 }
3979 + case SuspenseComponent: {
3980 + if (enableTransitionTracing) {
3981 + // We need to mark this fiber's parents as deleted
3982 + const offscreenFiber: Fiber = (current.child: any);
3983 + const instance: OffscreenInstance = offscreenFiber.stateNode;
3984 + const transitions = instance.transitions;
3985 + if (transitions !== null) {
3986 + const abortReason = {
3987 + reason: 'suspense',
3988 + name: current.memoizedProps.unstable_name || null,
3989 + };
3990 + if (
3991 + current.memoizedState === null ||
3992 + current.memoizedState.dehydrated === null
3993 + ) {
3994 + abortParentMarkerTransitionsForDeletedFiber(
3995 + offscreenFiber,
3996 + abortReason,
3997 + transitions,
3998 + instance,
3999 + true,
4000 + );
4001 +
4002 + if (nearestMountedAncestor !== null) {
4003 + abortParentMarkerTransitionsForDeletedFiber(
4004 + nearestMountedAncestor,
4005 + abortReason,
4006 + transitions,
4007 + instance,
4008 + false,
4009 + );
4010 + }
4011 + }
4012 + }
4013 + }
4014 + break;
4015 + }
4016 case CacheComponent: {
4017 if (enableCache) {
4018 const cache = current.memoizedState.cache;
@@ -3834,6 +4020,37 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4020 }
4021 break;
4022 }
4023 + case TracingMarkerComponent: {
4024 + if (enableTransitionTracing) {
4025 + // We need to mark this fiber's parents as deleted
4026 + const instance: TracingMarkerInstance = current.stateNode;
4027 + const transitions = instance.transitions;
4028 + if (transitions !== null) {
4029 + const abortReason = {
4030 + reason: 'marker',
4031 + name: current.memoizedProps.name,
4032 + };
4033 + abortParentMarkerTransitionsForDeletedFiber(
4034 + current,
4035 + abortReason,
4036 + transitions,
4037 + null,
4038 + true,
4039 + );
4040 +
4041 + if (nearestMountedAncestor !== null) {
4042 + abortParentMarkerTransitionsForDeletedFiber(
4043 + nearestMountedAncestor,
4044 + abortReason,
4045 + transitions,
4046 + null,
4047 + false,
4048 + );
4049 + }
4050 + }
4051 + }
4052 + break;
4053 + }
4054 }
4055 }
4056
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
-10
@@ -1589,16 +1589,6 @@ function completeWork(
1589 popMarkerInstance(workInProgress);
1590 }
1591 bubbleProperties(workInProgress);
1592 -
1593 - if (
1594 - current === null ||
1595 - (workInProgress.subtreeFlags & Visibility) !== NoFlags
1596 - ) {
1597 - // If any of our suspense children toggle visibility, this means that
1598 - // the pending boundaries array needs to be updated, which we only
1599 - // do in the passive phase.
1600 - workInProgress.flags |= Passive;
1601 - }
1592 }
1593 return null;
1594 }
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
-10
@@ -1589,16 +1589,6 @@ function completeWork(
1589 popMarkerInstance(workInProgress);
1590 }
1591 bubbleProperties(workInProgress);
1592 -
1593 - if (
1594 - current === null ||
1595 - (workInProgress.subtreeFlags & Visibility) !== NoFlags
1596 - ) {
1597 - // If any of our suspense children toggle visibility, this means that
1598 - // the pending boundaries array needs to be updated, which we only
1599 - // do in the passive phase.
1600 - workInProgress.flags |= Passive;
1601 - }
1592 }
1593 return null;
1594 }
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
+68 -5
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes';
10 +import type {
11 + TransitionTracingCallbacks,
12 + Fiber,
13 + FiberRoot,
14 +} from './ReactInternalTypes';
15 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
16 import type {StackCursor} from './ReactFiberStack.new';
17
@@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = {
25 transitionStart: Array<Transition> | null,
26 transitionProgress: Map<Transition, PendingBoundaries> | null,
27 transitionComplete: Array<Transition> | null,
24 - markerProgress: Map<string, TracingMarkerInstance> | null,
28 + markerProgress: Map<
29 + string,
30 + {pendingBoundaries: PendingBoundaries, transitions: Set<Transition>},
31 + > | null,
32 + markerIncomplete: Map<
33 + string,
34 + {aborts: Array<TransitionAbort>, transitions: Set<Transition>},
35 + > | null,
36 markerComplete: Map<string, Set<Transition>> | null,
37 };
38
@@ -36,11 +47,18 @@ export type BatchConfigTransition = {
47 _updatedFibers?: Set<Fiber>,
48 };
49
50 +// TODO: Is there a way to not include the tag or name here?
51 export type TracingMarkerInstance = {|
52 tag?: TracingMarkerTag,
41 - pendingBoundaries: PendingBoundaries | null,
53 transitions: Set<Transition> | null,
43 - name?: string,
54 + pendingBoundaries: PendingBoundaries | null,
55 + aborts: Array<TransitionAbort> | null,
56 + name: string | null,
57 +|};
58 +
59 +export type TransitionAbort = {|
60 + reason: 'error' | 'unknown' | 'marker' | 'suspense',
61 + name?: string | null,
62 |};
63
64 export const TransitionRoot = 0;
@@ -69,6 +87,7 @@ export function processTransitionCallbacks(
87 if (onMarkerProgress != null && markerProgress !== null) {
88 markerProgress.forEach((markerInstance, markerName) => {
89 if (markerInstance.transitions !== null) {
90 + // TODO: Clone the suspense object so users can't modify it
91 const pending =
92 markerInstance.pendingBoundaries !== null
93 ? Array.from(markerInstance.pendingBoundaries.values())
@@ -101,6 +120,48 @@ export function processTransitionCallbacks(
120 });
121 }
122
123 + const markerIncomplete = pendingTransitions.markerIncomplete;
124 + const onMarkerIncomplete = callbacks.onMarkerIncomplete;
125 + if (onMarkerIncomplete != null && markerIncomplete !== null) {
126 + markerIncomplete.forEach(({transitions, aborts}, markerName) => {
127 + transitions.forEach(transition => {
128 + const filteredAborts = [];
129 + aborts.forEach(abort => {
130 + switch (abort.reason) {
131 + case 'marker': {
132 + filteredAborts.push({
133 + type: 'marker',
134 + name: abort.name,
135 + endTime,
136 + });
137 + break;
138 + }
139 + case 'suspense': {
140 + filteredAborts.push({
141 + type: 'suspense',
142 + name: abort.name,
143 + endTime,
144 + });
145 + break;
146 + }
147 + default: {
148 + break;
149 + }
150 + }
151 + });
152 +
153 + if (filteredAborts.length > 0) {
154 + onMarkerIncomplete(
155 + transition.name,
156 + markerName,
157 + transition.startTime,
158 + filteredAborts,
159 + );
160 + }
161 + });
162 + });
163 + }
164 +
165 const transitionProgress = pendingTransitions.transitionProgress;
166 const onTransitionProgress = callbacks.onTransitionProgress;
167 if (onTransitionProgress != null && transitionProgress !== null) {
@@ -145,7 +206,7 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
206 // transitions map. Each entry in this map functions like a tracing
207 // marker does, so we can push it onto the marker instance stack
208 const transitions = getWorkInProgressTransitions();
148 - const root = workInProgress.stateNode;
209 + const root: FiberRoot = workInProgress.stateNode;
210
211 if (transitions !== null) {
212 transitions.forEach(transition => {
@@ -154,6 +215,8 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
215 tag: TransitionRoot,
216 transitions: new Set([transition]),
217 pendingBoundaries: null,
218 + aborts: null,
219 + name: null,
220 };
221 root.incompleteTransitions.set(transition, markerInstance);
222 }
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js
+68 -5
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes';
10 +import type {
11 + TransitionTracingCallbacks,
12 + Fiber,
13 + FiberRoot,
14 +} from './ReactInternalTypes';
15 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
16 import type {StackCursor} from './ReactFiberStack.old';
17
@@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = {
25 transitionStart: Array<Transition> | null,
26 transitionProgress: Map<Transition, PendingBoundaries> | null,
27 transitionComplete: Array<Transition> | null,
24 - markerProgress: Map<string, TracingMarkerInstance> | null,
28 + markerProgress: Map<
29 + string,
30 + {pendingBoundaries: PendingBoundaries, transitions: Set<Transition>},
31 + > | null,
32 + markerIncomplete: Map<
33 + string,
34 + {aborts: Array<TransitionAbort>, transitions: Set<Transition>},
35 + > | null,
36 markerComplete: Map<string, Set<Transition>> | null,
37 };
38
@@ -36,11 +47,18 @@ export type BatchConfigTransition = {
47 _updatedFibers?: Set<Fiber>,
48 };
49
50 +// TODO: Is there a way to not include the tag or name here?
51 export type TracingMarkerInstance = {|
52 tag?: TracingMarkerTag,
41 - pendingBoundaries: PendingBoundaries | null,
53 transitions: Set<Transition> | null,
43 - name?: string,
54 + pendingBoundaries: PendingBoundaries | null,
55 + aborts: Array<TransitionAbort> | null,
56 + name: string | null,
57 +|};
58 +
59 +export type TransitionAbort = {|
60 + reason: 'error' | 'unknown' | 'marker' | 'suspense',
61 + name?: string | null,
62 |};
63
64 export const TransitionRoot = 0;
@@ -69,6 +87,7 @@ export function processTransitionCallbacks(
87 if (onMarkerProgress != null && markerProgress !== null) {
88 markerProgress.forEach((markerInstance, markerName) => {
89 if (markerInstance.transitions !== null) {
90 + // TODO: Clone the suspense object so users can't modify it
91 const pending =
92 markerInstance.pendingBoundaries !== null
93 ? Array.from(markerInstance.pendingBoundaries.values())
@@ -101,6 +120,48 @@ export function processTransitionCallbacks(
120 });
121 }
122
123 + const markerIncomplete = pendingTransitions.markerIncomplete;
124 + const onMarkerIncomplete = callbacks.onMarkerIncomplete;
125 + if (onMarkerIncomplete != null && markerIncomplete !== null) {
126 + markerIncomplete.forEach(({transitions, aborts}, markerName) => {
127 + transitions.forEach(transition => {
128 + const filteredAborts = [];
129 + aborts.forEach(abort => {
130 + switch (abort.reason) {
131 + case 'marker': {
132 + filteredAborts.push({
133 + type: 'marker',
134 + name: abort.name,
135 + endTime,
136 + });
137 + break;
138 + }
139 + case 'suspense': {
140 + filteredAborts.push({
141 + type: 'suspense',
142 + name: abort.name,
143 + endTime,
144 + });
145 + break;
146 + }
147 + default: {
148 + break;
149 + }
150 + }
151 + });
152 +
153 + if (filteredAborts.length > 0) {
154 + onMarkerIncomplete(
155 + transition.name,
156 + markerName,
157 + transition.startTime,
158 + filteredAborts,
159 + );
160 + }
161 + });
162 + });
163 + }
164 +
165 const transitionProgress = pendingTransitions.transitionProgress;
166 const onTransitionProgress = callbacks.onTransitionProgress;
167 if (onTransitionProgress != null && transitionProgress !== null) {
@@ -145,7 +206,7 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
206 // transitions map. Each entry in this map functions like a tracing
207 // marker does, so we can push it onto the marker instance stack
208 const transitions = getWorkInProgressTransitions();
148 - const root = workInProgress.stateNode;
209 + const root: FiberRoot = workInProgress.stateNode;
210
211 if (transitions !== null) {
212 transitions.forEach(transition => {
@@ -154,6 +215,8 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
215 tag: TransitionRoot,
216 transitions: new Set([transition]),
217 pendingBoundaries: null,
218 + aborts: null,
219 + name: null,
220 };
221 root.incompleteTransitions.set(transition, markerInstance);
222 }
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+35 -1
@@ -19,6 +19,7 @@ import type {
19 PendingTransitionCallbacks,
20 PendingBoundaries,
21 Transition,
22 + TransitionAbort,
23 } from './ReactFiberTracingMarkerComponent.new';
24 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
25
@@ -361,6 +362,7 @@ export function addTransitionStartCallbackToPendingTransition(
362 transitionProgress: null,
363 transitionComplete: null,
364 markerProgress: null,
365 + markerIncomplete: null,
366 markerComplete: null,
367 };
368 }
@@ -376,7 +378,7 @@ export function addTransitionStartCallbackToPendingTransition(
378 export function addMarkerProgressCallbackToPendingTransition(
379 markerName: string,
380 transitions: Set<Transition>,
379 - pendingBoundaries: PendingBoundaries | null,
381 + pendingBoundaries: PendingBoundaries,
382 ) {
383 if (enableTransitionTracing) {
384 if (currentPendingTransitionCallbacks === null) {
@@ -385,6 +387,7 @@ export function addMarkerProgressCallbackToPendingTransition(
387 transitionProgress: null,
388 transitionComplete: null,
389 markerProgress: new Map(),
390 + markerIncomplete: null,
391 markerComplete: null,
392 };
393 }
@@ -400,6 +403,34 @@ export function addMarkerProgressCallbackToPendingTransition(
403 }
404 }
405
406 +export function addMarkerIncompleteCallbackToPendingTransition(
407 + markerName: string,
408 + transitions: Set<Transition>,
409 + aborts: Array<TransitionAbort>,
410 +) {
411 + if (enableTransitionTracing) {
412 + if (currentPendingTransitionCallbacks === null) {
413 + currentPendingTransitionCallbacks = {
414 + transitionStart: null,
415 + transitionProgress: null,
416 + transitionComplete: null,
417 + markerProgress: null,
418 + markerIncomplete: new Map(),
419 + markerComplete: null,
420 + };
421 + }
422 +
423 + if (currentPendingTransitionCallbacks.markerIncomplete === null) {
424 + currentPendingTransitionCallbacks.markerIncomplete = new Map();
425 + }
426 +
427 + currentPendingTransitionCallbacks.markerIncomplete.set(markerName, {
428 + transitions,
429 + aborts,
430 + });
431 + }
432 +}
433 +
434 export function addMarkerCompleteCallbackToPendingTransition(
435 markerName: string,
436 transitions: Set<Transition>,
@@ -411,6 +442,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
442 transitionProgress: null,
443 transitionComplete: null,
444 markerProgress: null,
445 + markerIncomplete: null,
446 markerComplete: new Map(),
447 };
448 }
@@ -437,6 +469,7 @@ export function addTransitionProgressCallbackToPendingTransition(
469 transitionProgress: new Map(),
470 transitionComplete: null,
471 markerProgress: null,
472 + markerIncomplete: null,
473 markerComplete: null,
474 };
475 }
@@ -462,6 +495,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
495 transitionProgress: null,
496 transitionComplete: [],
497 markerProgress: null,
498 + markerIncomplete: null,
499 markerComplete: null,
500 };
501 }
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+35 -1
@@ -19,6 +19,7 @@ import type {
19 PendingTransitionCallbacks,
20 PendingBoundaries,
21 Transition,
22 + TransitionAbort,
23 } from './ReactFiberTracingMarkerComponent.old';
24 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
25
@@ -361,6 +362,7 @@ export function addTransitionStartCallbackToPendingTransition(
362 transitionProgress: null,
363 transitionComplete: null,
364 markerProgress: null,
365 + markerIncomplete: null,
366 markerComplete: null,
367 };
368 }
@@ -376,7 +378,7 @@ export function addTransitionStartCallbackToPendingTransition(
378 export function addMarkerProgressCallbackToPendingTransition(
379 markerName: string,
380 transitions: Set<Transition>,
379 - pendingBoundaries: PendingBoundaries | null,
381 + pendingBoundaries: PendingBoundaries,
382 ) {
383 if (enableTransitionTracing) {
384 if (currentPendingTransitionCallbacks === null) {
@@ -385,6 +387,7 @@ export function addMarkerProgressCallbackToPendingTransition(
387 transitionProgress: null,
388 transitionComplete: null,
389 markerProgress: new Map(),
390 + markerIncomplete: null,
391 markerComplete: null,
392 };
393 }
@@ -400,6 +403,34 @@ export function addMarkerProgressCallbackToPendingTransition(
403 }
404 }
405
406 +export function addMarkerIncompleteCallbackToPendingTransition(
407 + markerName: string,
408 + transitions: Set<Transition>,
409 + aborts: Array<TransitionAbort>,
410 +) {
411 + if (enableTransitionTracing) {
412 + if (currentPendingTransitionCallbacks === null) {
413 + currentPendingTransitionCallbacks = {
414 + transitionStart: null,
415 + transitionProgress: null,
416 + transitionComplete: null,
417 + markerProgress: null,
418 + markerIncomplete: new Map(),
419 + markerComplete: null,
420 + };
421 + }
422 +
423 + if (currentPendingTransitionCallbacks.markerIncomplete === null) {
424 + currentPendingTransitionCallbacks.markerIncomplete = new Map();
425 + }
426 +
427 + currentPendingTransitionCallbacks.markerIncomplete.set(markerName, {
428 + transitions,
429 + aborts,
430 + });
431 + }
432 +}
433 +
434 export function addMarkerCompleteCallbackToPendingTransition(
435 markerName: string,
436 transitions: Set<Transition>,
@@ -411,6 +442,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
442 transitionProgress: null,
443 transitionComplete: null,
444 markerProgress: null,
445 + markerIncomplete: null,
446 markerComplete: new Map(),
447 };
448 }
@@ -437,6 +469,7 @@ export function addTransitionProgressCallbackToPendingTransition(
469 transitionProgress: new Map(),
470 transitionComplete: null,
471 markerProgress: null,
472 + markerIncomplete: null,
473 markerComplete: null,
474 };
475 }
@@ -462,6 +495,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
495 transitionProgress: null,
496 transitionComplete: [],
497 markerProgress: null,
498 + markerIncomplete: null,
499 markerComplete: null,
500 };
501 }
packages/react-reconciler/src/ReactInternalTypes.js
+3 -5
@@ -291,8 +291,7 @@ export type TransitionTracingCallbacks = {
291 startTime: number,
292 deletions: Array<{
293 type: string,
294 - name?: string,
295 - newName?: string,
294 + name?: string | null,
295 endTime: number,
296 }>,
297 ) => void,
@@ -314,8 +313,7 @@ export type TransitionTracingCallbacks = {
313 startTime: number,
314 deletions: Array<{
315 type: string,
317 - name?: string,
318 - newName?: string,
316 + name?: string | null,
317 endTime: number,
318 }>,
319 ) => void,
@@ -337,7 +335,7 @@ type TransitionTracingOnlyFiberRootProperties = {|
335 // are considered complete when the pending suspense boundaries set is
336 // empty. We can represent this as a Map of transitions to suspense
337 // boundary sets
340 - incompleteTransitions: Map<Array<Transition>, TracingMarkerInstance>,
338 + incompleteTransitions: Map<Transition, TracingMarkerInstance>,
339 |};
340
341 // Exported FiberRoot type includes all properties,
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+840 -1
@@ -23,6 +23,17 @@ let caches;
23 let seededCache;
24
25 describe('ReactInteractionTracing', () => {
26 + function stringifyDeletions(deletions) {
27 + return deletions
28 + .map(
29 + d =>
30 + `{${Object.keys(d)
31 + .map(key => `${key}: ${d[key]}`)
32 + .sort()
33 + .join(', ')}}`,
34 + )
35 + .join(', ');
36 + }
37 beforeEach(() => {
38 jest.resetModules();
39
@@ -1284,18 +1295,846 @@ describe('ReactInteractionTracing', () => {
1295 });
1296
1297 // @gate enableTransitionTracing
1287 - it('warns when marker name changes', async () => {
1298 + it.skip('warn and calls marker incomplete if name changes before transition completes', async () => {
1299 + const transitionCallbacks = {
1300 + onTransitionStart: (name, startTime) => {
1301 + Scheduler.unstable_yieldValue(
1302 + `onTransitionStart(${name}, ${startTime})`,
1303 + );
1304 + },
1305 + onTransitionProgress: (name, startTime, endTime, pending) => {
1306 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1307 + Scheduler.unstable_yieldValue(
1308 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1309 + );
1310 + },
1311 + onTransitionComplete: (name, startTime, endTime) => {
1312 + Scheduler.unstable_yieldValue(
1313 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1314 + );
1315 + },
1316 + onMarkerProgress: (
1317 + transitioName,
1318 + markerName,
1319 + startTime,
1320 + currentTime,
1321 + pending,
1322 + ) => {
1323 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1324 + Scheduler.unstable_yieldValue(
1325 + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1326 + );
1327 + },
1328 + onMarkerIncomplete: (
1329 + transitionName,
1330 + markerName,
1331 + startTime,
1332 + deletions,
1333 + ) => {
1334 + Scheduler.unstable_yieldValue(
1335 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1336 + deletions,
1337 + )}])`,
1338 + );
1339 + },
1340 + onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1341 + Scheduler.unstable_yieldValue(
1342 + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1343 + );
1344 + },
1345 + };
1346 +
1347 + function App({navigate, markerName}) {
1348 + return (
1349 + <div>
1350 + {navigate ? (
1351 + <React.unstable_TracingMarker name={markerName}>
1352 + <Suspense fallback={<Text text="Loading..." />}>
1353 + <AsyncText text="Page Two" />
1354 + </Suspense>
1355 + </React.unstable_TracingMarker>
1356 + ) : (
1357 + <Text text="Page One" />
1358 + )}
1359 + </div>
1360 + );
1361 + }
1362 +
1363 + const root = ReactNoop.createRoot({
1364 + unstable_transitionCallbacks: transitionCallbacks,
1365 + });
1366 + await act(async () => {
1367 + root.render(<App navigate={false} markerName="marker one" />);
1368 + ReactNoop.expire(1000);
1369 + await advanceTimers(1000);
1370 + expect(Scheduler).toFlushAndYield(['Page One']);
1371 +
1372 + startTransition(
1373 + () => root.render(<App navigate={true} markerName="marker one" />),
1374 + {
1375 + name: 'transition one',
1376 + },
1377 + );
1378 + ReactNoop.expire(1000);
1379 + await advanceTimers(1000);
1380 +
1381 + expect(Scheduler).toFlushAndYield([
1382 + 'Suspend [Page Two]',
1383 + 'Loading...',
1384 + 'onTransitionStart(transition one, 1000)',
1385 + 'onMarkerProgress(transition one, marker one, 1000, 2000, [<null>])',
1386 + 'onTransitionProgress(transition one, 1000, 2000, [<null>])',
1387 + ]);
1388 +
1389 + root.render(<App navigate={true} markerName="marker two" />);
1390 + ReactNoop.expire(1000);
1391 + await advanceTimers(1000);
1392 + expect(() =>
1393 + expect(Scheduler).toFlushAndYield([
1394 + 'Suspend [Page Two]',
1395 + 'Loading...',
1396 + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])',
1397 + ]),
1398 + ).toErrorDev('');
1399 +
1400 + resolveText('Page Two');
1401 + ReactNoop.expire(1000);
1402 + await advanceTimers(1000);
1403 + expect(Scheduler).toFlushAndYield([
1404 + 'Page Two',
1405 + 'onMarkerProgress(transition one, marker one, 1000, 4000, [])',
1406 + 'onTransitionProgress(transition one, 1000, 4000, [])',
1407 + 'onTransitionComplete(transition one, 1000, 4000)',
1408 + ]);
1409 + });
1410 + });
1411 +
1412 + // @gate enableTransitionTracing
1413 + it('marker incomplete for tree with parent and sibling tracing markers', async () => {
1414 + const transitionCallbacks = {
1415 + onTransitionStart: (name, startTime) => {
1416 + Scheduler.unstable_yieldValue(
1417 + `onTransitionStart(${name}, ${startTime})`,
1418 + );
1419 + },
1420 + onTransitionProgress: (name, startTime, endTime, pending) => {
1421 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1422 + Scheduler.unstable_yieldValue(
1423 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1424 + );
1425 + },
1426 + onTransitionComplete: (name, startTime, endTime) => {
1427 + Scheduler.unstable_yieldValue(
1428 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1429 + );
1430 + },
1431 + onMarkerProgress: (
1432 + transitioName,
1433 + markerName,
1434 + startTime,
1435 + currentTime,
1436 + pending,
1437 + ) => {
1438 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1439 + Scheduler.unstable_yieldValue(
1440 + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1441 + );
1442 + },
1443 + onMarkerIncomplete: (
1444 + transitionName,
1445 + markerName,
1446 + startTime,
1447 + deletions,
1448 + ) => {
1449 + Scheduler.unstable_yieldValue(
1450 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1451 + deletions,
1452 + )}])`,
1453 + );
1454 + },
1455 + onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1456 + Scheduler.unstable_yieldValue(
1457 + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1458 + );
1459 + },
1460 + };
1461 +
1462 + function App({navigate, showMarker}) {
1463 + return (
1464 + <div>
1465 + {navigate ? (
1466 + <React.unstable_TracingMarker name="parent">
1467 + {showMarker ? (
1468 + <React.unstable_TracingMarker name="marker one">
1469 + <Suspense
1470 + unstable_name="suspense page"
1471 + fallback={<Text text="Loading..." />}>
1472 + <AsyncText text="Page Two" />
1473 + </Suspense>
1474 + </React.unstable_TracingMarker>
1475 + ) : (
1476 + <Suspense
1477 + unstable_name="suspense page"
1478 + fallback={<Text text="Loading..." />}>
1479 + <AsyncText text="Page Two" />
1480 + </Suspense>
1481 + )}
1482 + <React.unstable_TracingMarker name="sibling">
1483 + <Suspense
1484 + unstable_name="suspense sibling"
1485 + fallback={<Text text="Sibling Loading..." />}>
1486 + <AsyncText text="Sibling Text" />
1487 + </Suspense>
1488 + </React.unstable_TracingMarker>
1489 + </React.unstable_TracingMarker>
1490 + ) : (
1491 + <Text text="Page One" />
1492 + )}
1493 + </div>
1494 + );
1495 + }
1496 +
1497 + const root = ReactNoop.createRoot({
1498 + unstable_transitionCallbacks: transitionCallbacks,
1499 + });
1500 + await act(async () => {
1501 + root.render(<App navigate={false} showMarker={true} />);
1502 + ReactNoop.expire(1000);
1503 + await advanceTimers(1000);
1504 + expect(Scheduler).toFlushAndYield(['Page One']);
1505 +
1506 + startTransition(
1507 + () => root.render(<App navigate={true} showMarker={true} />),
1508 + {
1509 + name: 'transition one',
1510 + },
1511 + );
1512 + ReactNoop.expire(1000);
1513 + await advanceTimers(1000);
1514 + expect(Scheduler).toFlushAndYield([
1515 + 'Suspend [Page Two]',
1516 + 'Loading...',
1517 + 'Suspend [Sibling Text]',
1518 + 'Sibling Loading...',
1519 + 'onTransitionStart(transition one, 1000)',
1520 + 'onMarkerProgress(transition one, parent, 1000, 2000, [suspense page, suspense sibling])',
1521 + 'onMarkerProgress(transition one, marker one, 1000, 2000, [suspense page])',
1522 + 'onMarkerProgress(transition one, sibling, 1000, 2000, [suspense sibling])',
1523 + 'onTransitionProgress(transition one, 1000, 2000, [suspense page, suspense sibling])',
1524 + ]);
1525 + root.render(<App navigate={true} showMarker={false} />);
1526 +
1527 + ReactNoop.expire(1000);
1528 + await advanceTimers(1000);
1529 + expect(Scheduler).toFlushAndYield([
1530 + 'Suspend [Page Two]',
1531 + 'Loading...',
1532 + 'Suspend [Sibling Text]',
1533 + 'Sibling Loading...',
1534 + 'onMarkerProgress(transition one, parent, 1000, 3000, [suspense sibling])',
1535 + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
1536 + 'onMarkerIncomplete(transition one, parent, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
1537 + ]);
1538 +
1539 + root.render(<App navigate={true} showMarker={true} />);
1540 + ReactNoop.expire(1000);
1541 + await advanceTimers(1000);
1542 + expect(Scheduler).toFlushAndYield([
1543 + 'Suspend [Page Two]',
1544 + 'Loading...',
1545 + 'Suspend [Sibling Text]',
1546 + 'Sibling Loading...',
1547 + ]);
1548 + });
1549 +
1550 + resolveText('Page Two');
1551 + ReactNoop.expire(1000);
1552 + await advanceTimers(1000);
1553 + expect(Scheduler).toFlushAndYield(['Page Two']);
1554 +
1555 + resolveText('Sibling Text');
1556 + ReactNoop.expire(1000);
1557 + await advanceTimers(1000);
1558 + expect(Scheduler).toFlushAndYield([
1559 + 'Sibling Text',
1560 + 'onMarkerProgress(transition one, parent, 1000, 6000, [])',
1561 + 'onMarkerProgress(transition one, sibling, 1000, 6000, [])',
1562 + // Calls markerComplete and transitionComplete for all parents
1563 + 'onMarkerComplete(transition one, sibling, 1000, 6000)',
1564 + 'onTransitionProgress(transition one, 1000, 6000, [])',
1565 + ]);
1566 + });
1567 +
1568 + // @gate enableTransitionTracing
1569 + it('marker gets deleted', async () => {
1570 const transitionCallbacks = {
1571 onTransitionStart: (name, startTime) => {
1572 Scheduler.unstable_yieldValue(
1573 `onTransitionStart(${name}, ${startTime})`,
1574 );
1575 },
1576 + onTransitionProgress: (name, startTime, endTime, pending) => {
1577 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1578 + Scheduler.unstable_yieldValue(
1579 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1580 + );
1581 + },
1582 onTransitionComplete: (name, startTime, endTime) => {
1583 Scheduler.unstable_yieldValue(
1584 `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1585 );
1586 },
1587 + onMarkerProgress: (
1588 + transitioName,
1589 + markerName,
1590 + startTime,
1591 + currentTime,
1592 + pending,
1593 + ) => {
1594 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1595 + Scheduler.unstable_yieldValue(
1596 + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1597 + );
1598 + },
1599 + onMarkerIncomplete: (
1600 + transitionName,
1601 + markerName,
1602 + startTime,
1603 + deletions,
1604 + ) => {
1605 + Scheduler.unstable_yieldValue(
1606 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1607 + deletions,
1608 + )}])`,
1609 + );
1610 + },
1611 + onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1612 + Scheduler.unstable_yieldValue(
1613 + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1614 + );
1615 + },
1616 + };
1617 +
1618 + function App({navigate, deleteOne}) {
1619 + return (
1620 + <div>
1621 + {navigate ? (
1622 + <React.unstable_TracingMarker name="parent">
1623 + {!deleteOne ? (
1624 + <div>
1625 + <React.unstable_TracingMarker name="one">
1626 + <Suspense
1627 + unstable_name="suspense one"
1628 + fallback={<Text text="Loading One..." />}>
1629 + <AsyncText text="Page One" />
1630 + </Suspense>
1631 + </React.unstable_TracingMarker>
1632 + </div>
1633 + ) : null}
1634 + <React.unstable_TracingMarker name="two">
1635 + <Suspense
1636 + unstable_name="suspense two"
1637 + fallback={<Text text="Loading Two..." />}>
1638 + <AsyncText text="Page Two" />
1639 + </Suspense>
1640 + </React.unstable_TracingMarker>
1641 + </React.unstable_TracingMarker>
1642 + ) : (
1643 + <Text text="Page One" />
1644 + )}
1645 + </div>
1646 + );
1647 + }
1648 + const root = ReactNoop.createRoot({
1649 + unstable_transitionCallbacks: transitionCallbacks,
1650 + });
1651 + await act(async () => {
1652 + root.render(<App navigate={false} deleteOne={false} />);
1653 + ReactNoop.expire(1000);
1654 + await advanceTimers(1000);
1655 + expect(Scheduler).toFlushAndYield(['Page One']);
1656 +
1657 + startTransition(
1658 + () => root.render(<App navigate={true} deleteOne={false} />),
1659 + {
1660 + name: 'transition',
1661 + },
1662 + );
1663 + ReactNoop.expire(1000);
1664 + await advanceTimers(1000);
1665 + expect(Scheduler).toFlushAndYield([
1666 + 'Suspend [Page One]',
1667 + 'Loading One...',
1668 + 'Suspend [Page Two]',
1669 + 'Loading Two...',
1670 + 'onTransitionStart(transition, 1000)',
1671 + 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
1672 + 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
1673 + 'onMarkerProgress(transition, two, 1000, 2000, [suspense two])',
1674 + 'onTransitionProgress(transition, 1000, 2000, [suspense one, suspense two])',
1675 + ]);
1676 +
1677 + root.render(<App navigate={true} deleteOne={true} />);
1678 + ReactNoop.expire(1000);
1679 + await advanceTimers(1000);
1680 + expect(Scheduler).toFlushAndYield([
1681 + 'Suspend [Page Two]',
1682 + 'Loading Two...',
1683 + 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two])',
1684 + 'onMarkerIncomplete(transition, one, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
1685 + 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
1686 + ]);
1687 +
1688 + await resolveText('Page Two');
1689 + ReactNoop.expire(1000);
1690 + await advanceTimers(1000);
1691 + expect(Scheduler).toFlushAndYield([
1692 + 'Page Two',
1693 + // Marker progress will still get called after incomplete but not marker complete
1694 + 'onMarkerProgress(transition, parent, 1000, 4000, [])',
1695 + 'onMarkerProgress(transition, two, 1000, 4000, [])',
1696 + 'onMarkerComplete(transition, two, 1000, 4000)',
1697 + // Transition progress will still get called after incomplete but not transition complete
1698 + 'onTransitionProgress(transition, 1000, 4000, [])',
1699 + ]);
1700 + });
1701 + });
1702 +
1703 + // @gate enableTransitionTracing
1704 + it('Suspense boundary added by the transition is deleted', async () => {
1705 + const transitionCallbacks = {
1706 + onTransitionStart: (name, startTime) => {
1707 + Scheduler.unstable_yieldValue(
1708 + `onTransitionStart(${name}, ${startTime})`,
1709 + );
1710 + },
1711 + onTransitionProgress: (name, startTime, endTime, pending) => {
1712 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1713 + Scheduler.unstable_yieldValue(
1714 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1715 + );
1716 + },
1717 + onTransitionComplete: (name, startTime, endTime) => {
1718 + Scheduler.unstable_yieldValue(
1719 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1720 + );
1721 + },
1722 + onMarkerProgress: (
1723 + transitioName,
1724 + markerName,
1725 + startTime,
1726 + currentTime,
1727 + pending,
1728 + ) => {
1729 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1730 + Scheduler.unstable_yieldValue(
1731 + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1732 + );
1733 + },
1734 + onMarkerIncomplete: (
1735 + transitionName,
1736 + markerName,
1737 + startTime,
1738 + deletions,
1739 + ) => {
1740 + Scheduler.unstable_yieldValue(
1741 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1742 + deletions,
1743 + )}])`,
1744 + );
1745 + },
1746 + onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1747 + Scheduler.unstable_yieldValue(
1748 + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1749 + );
1750 + },
1751 + };
1752 +
1753 + function App({navigate, deleteOne}) {
1754 + return (
1755 + <div>
1756 + {navigate ? (
1757 + <React.unstable_TracingMarker name="parent">
1758 + <React.unstable_TracingMarker name="one">
1759 + {!deleteOne ? (
1760 + <Suspense
1761 + unstable_name="suspense one"
1762 + fallback={<Text text="Loading One..." />}>
1763 + <AsyncText text="Page One" />
1764 + <React.unstable_TracingMarker name="page one" />
1765 + <Suspense
1766 + unstable_name="suspense child"
1767 + fallback={<Text text="Loading Child..." />}>
1768 + <React.unstable_TracingMarker name="child" />
1769 + <AsyncText text="Child" />
1770 + </Suspense>
1771 + </Suspense>
1772 + ) : null}
1773 + </React.unstable_TracingMarker>
1774 + <React.unstable_TracingMarker name="two">
1775 + <Suspense
1776 + unstable_name="suspense two"
1777 + fallback={<Text text="Loading Two..." />}>
1778 + <AsyncText text="Page Two" />
1779 + </Suspense>
1780 + </React.unstable_TracingMarker>
1781 + </React.unstable_TracingMarker>
1782 + ) : (
1783 + <Text text="Page One" />
1784 + )}
1785 + </div>
1786 + );
1787 + }
1788 + const root = ReactNoop.createRoot({
1789 + unstable_transitionCallbacks: transitionCallbacks,
1790 + });
1791 + await act(async () => {
1792 + root.render(<App navigate={false} deleteOne={false} />);
1793 +
1794 + ReactNoop.expire(1000);
1795 + await advanceTimers(1000);
1796 + expect(Scheduler).toFlushAndYield(['Page One']);
1797 +
1798 + startTransition(
1799 + () => root.render(<App navigate={true} deleteOne={false} />),
1800 + {
1801 + name: 'transition',
1802 + },
1803 + );
1804 + ReactNoop.expire(1000);
1805 + await advanceTimers(1000);
1806 + expect(Scheduler).toFlushAndYield([
1807 + 'Suspend [Page One]',
1808 + 'Suspend [Child]',
1809 + 'Loading Child...',
1810 + 'Loading One...',
1811 + 'Suspend [Page Two]',
1812 + 'Loading Two...',
1813 + 'onTransitionStart(transition, 1000)',
1814 + 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
1815 + 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
1816 + 'onMarkerProgress(transition, two, 1000, 2000, [suspense two])',
1817 + 'onTransitionProgress(transition, 1000, 2000, [suspense one, suspense two])',
1818 + ]);
1819 +
1820 + await resolveText('Page One');
1821 + ReactNoop.expire(1000);
1822 + await advanceTimers(1000);
1823 + expect(Scheduler).toFlushAndYield([
1824 + 'Page One',
1825 + 'Suspend [Child]',
1826 + 'Loading Child...',
1827 + 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two, suspense child])',
1828 + 'onMarkerProgress(transition, one, 1000, 3000, [suspense child])',
1829 + 'onMarkerComplete(transition, page one, 1000, 3000)',
1830 + 'onTransitionProgress(transition, 1000, 3000, [suspense two, suspense child])',
1831 + ]);
1832 +
1833 + root.render(<App navigate={true} deleteOne={true} />);
1834 + ReactNoop.expire(1000);
1835 + await advanceTimers(1000);
1836 + expect(Scheduler).toFlushAndYield([
1837 + 'Suspend [Page Two]',
1838 + 'Loading Two...',
1839 + // "suspense one" has unsuspended so shouldn't be included
1840 + // tracing marker "page one" has completed so shouldn't be included
1841 + // all children of "suspense child" haven't yet been rendered so shouldn't be included
1842 + 'onMarkerProgress(transition, one, 1000, 4000, [])',
1843 + 'onMarkerProgress(transition, parent, 1000, 4000, [suspense two])',
1844 + 'onMarkerIncomplete(transition, one, 1000, [{endTime: 4000, name: suspense child, type: suspense}])',
1845 + 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 4000, name: suspense child, type: suspense}])',
1846 + ]);
1847 +
1848 + await resolveText('Page Two');
1849 + ReactNoop.expire(1000);
1850 + await advanceTimers(1000);
1851 + expect(Scheduler).toFlushAndYield([
1852 + 'Page Two',
1853 + 'onMarkerProgress(transition, parent, 1000, 5000, [])',
1854 + 'onMarkerProgress(transition, two, 1000, 5000, [])',
1855 + 'onMarkerComplete(transition, two, 1000, 5000)',
1856 + 'onTransitionProgress(transition, 1000, 5000, [])',
1857 + ]);
1858 + });
1859 + });
1860 +
1861 + // @gate enableTransitionTracing
1862 + it('Suspense boundary not added by the transition is deleted ', async () => {
1863 + const transitionCallbacks = {
1864 + onTransitionStart: (name, startTime) => {
1865 + Scheduler.unstable_yieldValue(
1866 + `onTransitionStart(${name}, ${startTime})`,
1867 + );
1868 + },
1869 + onTransitionProgress: (name, startTime, endTime, pending) => {
1870 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1871 + Scheduler.unstable_yieldValue(
1872 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1873 + );
1874 + },
1875 + onTransitionComplete: (name, startTime, endTime) => {
1876 + Scheduler.unstable_yieldValue(
1877 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1878 + );
1879 + },
1880 + onMarkerProgress: (
1881 + transitioName,
1882 + markerName,
1883 + startTime,
1884 + currentTime,
1885 + pending,
1886 + ) => {
1887 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1888 + Scheduler.unstable_yieldValue(
1889 + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1890 + );
1891 + },
1892 + onMarkerIncomplete: (
1893 + transitionName,
1894 + markerName,
1895 + startTime,
1896 + deletions,
1897 + ) => {
1898 + Scheduler.unstable_yieldValue(
1899 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1900 + deletions,
1901 + )}])`,
1902 + );
1903 + },
1904 + onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1905 + Scheduler.unstable_yieldValue(
1906 + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1907 + );
1908 + },
1909 + };
1910 +
1911 + function App({show}) {
1912 + return (
1913 + <React.unstable_TracingMarker name="parent">
1914 + {show ? (
1915 + <Suspense unstable_name="appended child">
1916 + <AsyncText text="Appended child" />
1917 + </Suspense>
1918 + ) : null}
1919 + <Suspense unstable_name="child">
1920 + <AsyncText text="Child" />
1921 + </Suspense>
1922 + </React.unstable_TracingMarker>
1923 + );
1924 + }
1925 +
1926 + const root = ReactNoop.createRoot({
1927 + unstable_transitionCallbacks: transitionCallbacks,
1928 + });
1929 + await act(async () => {
1930 + startTransition(() => root.render(<App show={false} />), {
1931 + name: 'transition',
1932 + });
1933 + ReactNoop.expire(1000);
1934 + await advanceTimers(1000);
1935 +
1936 + expect(Scheduler).toFlushAndYield([
1937 + 'Suspend [Child]',
1938 + 'onTransitionStart(transition, 0)',
1939 + 'onMarkerProgress(transition, parent, 0, 1000, [child])',
1940 + 'onTransitionProgress(transition, 0, 1000, [child])',
1941 + ]);
1942 +
1943 + root.render(<App show={true} />);
1944 + ReactNoop.expire(1000);
1945 + await advanceTimers(1000);
1946 + // This appended child isn't part of the transition so we
1947 + // don't call any callback
1948 + expect(Scheduler).toFlushAndYield([
1949 + 'Suspend [Appended child]',
1950 + 'Suspend [Child]',
1951 + ]);
1952 +
1953 + // This deleted child isn't part of the transition so we
1954 + // don't call any callbacks
1955 + root.render(<App show={false} />);
1956 + ReactNoop.expire(1000);
1957 + await advanceTimers(1000);
1958 + expect(Scheduler).toFlushAndYield(['Suspend [Child]']);
1959 +
1960 + await resolveText('Child');
1961 + ReactNoop.expire(1000);
1962 + await advanceTimers(1000);
1963 +
1964 + expect(Scheduler).toFlushAndYield([
1965 + 'Child',
1966 + 'onMarkerProgress(transition, parent, 0, 4000, [])',
1967 + 'onMarkerComplete(transition, parent, 0, 4000)',
1968 + 'onTransitionProgress(transition, 0, 4000, [])',
1969 + 'onTransitionComplete(transition, 0, 4000)',
1970 + ]);
1971 + });
1972 + });
1973 +
1974 + // @gate enableTransitionTracing
1975 + it('marker incomplete gets called properly if child suspense marker is not part of it', async () => {
1976 + const transitionCallbacks = {
1977 + onTransitionStart: (name, startTime) => {
1978 + Scheduler.unstable_yieldValue(
1979 + `onTransitionStart(${name}, ${startTime})`,
1980 + );
1981 + },
1982 + onTransitionProgress: (name, startTime, endTime, pending) => {
1983 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1984 + Scheduler.unstable_yieldValue(
1985 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1986 + );
1987 + },
1988 + onTransitionComplete: (name, startTime, endTime) => {
1989 + Scheduler.unstable_yieldValue(
1990 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1991 + );
1992 + },
1993 + onMarkerProgress: (
1994 + transitioName,
1995 + markerName,
1996 + startTime,
1997 + currentTime,
1998 + pending,
1999 + ) => {
2000 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
2001 + Scheduler.unstable_yieldValue(
2002 + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
2003 + );
2004 + },
2005 + onMarkerIncomplete: (
2006 + transitionName,
2007 + markerName,
2008 + startTime,
2009 + deletions,
2010 + ) => {
2011 + Scheduler.unstable_yieldValue(
2012 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
2013 + deletions,
2014 + )}])`,
2015 + );
2016 + },
2017 + onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
2018 + Scheduler.unstable_yieldValue(
2019 + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
2020 + );
2021 + },
2022 + };
2023 +
2024 + function App({show, showSuspense}) {
2025 + return (
2026 + <React.unstable_TracingMarker name="parent">
2027 + {show ? (
2028 + <React.unstable_TracingMarker name="appended child">
2029 + {showSuspense ? (
2030 + <Suspense unstable_name="appended child">
2031 + <AsyncText text="Appended child" />
2032 + </Suspense>
2033 + ) : null}
2034 + </React.unstable_TracingMarker>
2035 + ) : null}
2036 + <Suspense unstable_name="child">
2037 + <AsyncText text="Child" />
2038 + </Suspense>
2039 + </React.unstable_TracingMarker>
2040 + );
2041 + }
2042 +
2043 + const root = ReactNoop.createRoot({
2044 + unstable_transitionCallbacks: transitionCallbacks,
2045 + });
2046 +
2047 + await act(async () => {
2048 + startTransition(
2049 + () => root.render(<App show={false} showSuspense={false} />),
2050 + {
2051 + name: 'transition one',
2052 + },
2053 + );
2054 +
2055 + ReactNoop.expire(1000);
2056 + await advanceTimers(1000);
2057 + });
2058 +
2059 + expect(Scheduler).toHaveYielded([
2060 + 'Suspend [Child]',
2061 + 'onTransitionStart(transition one, 0)',
2062 + 'onMarkerProgress(transition one, parent, 0, 1000, [child])',
2063 + 'onTransitionProgress(transition one, 0, 1000, [child])',
2064 + ]);
2065 +
2066 + await act(async () => {
2067 + startTransition(
2068 + () => root.render(<App show={true} showSuspense={true} />),
2069 + {
2070 + name: 'transition two',
2071 + },
2072 + );
2073 +
2074 + ReactNoop.expire(1000);
2075 + await advanceTimers(1000);
2076 + });
2077 +
2078 + expect(Scheduler).toHaveYielded([
2079 + 'Suspend [Appended child]',
2080 + 'Suspend [Child]',
2081 + 'onTransitionStart(transition two, 1000)',
2082 + 'onMarkerProgress(transition two, appended child, 1000, 2000, [appended child])',
2083 + 'onTransitionProgress(transition two, 1000, 2000, [appended child])',
2084 + ]);
2085 +
2086 + await act(async () => {
2087 + root.render(<App show={true} showSuspense={false} />);
2088 + ReactNoop.expire(1000);
2089 + await advanceTimers(1000);
2090 + });
2091 +
2092 + expect(Scheduler).toHaveYielded([
2093 + 'Suspend [Child]',
2094 + 'onMarkerProgress(transition two, appended child, 1000, 3000, [])',
2095 + 'onMarkerIncomplete(transition two, appended child, 1000, [{endTime: 3000, name: appended child, type: suspense}])',
2096 + ]);
2097 +
2098 + await act(async () => {
2099 + resolveText('Child');
2100 + ReactNoop.expire(1000);
2101 + await advanceTimers(1000);
2102 + });
2103 +
2104 + expect(Scheduler).toHaveYielded([
2105 + 'Child',
2106 + 'onMarkerProgress(transition one, parent, 0, 4000, [])',
2107 + 'onMarkerComplete(transition one, parent, 0, 4000)',
2108 + 'onTransitionProgress(transition one, 0, 4000, [])',
2109 + 'onTransitionComplete(transition one, 0, 4000)',
2110 + ]);
2111 + });
2112 +
2113 + // @gate enableTransitionTracing
2114 + it('warns when marker name changes', async () => {
2115 + const transitionCallbacks = {
2116 + onTransitionStart: (name, startTime) => {
2117 + Scheduler.unstable_yieldValue(
2118 + `onTransitionStart(${name}, ${startTime})`,
2119 + );
2120 + },
2121 + onTransitionComplete: (name, startTime, endTime) => {
2122 + Scheduler.unstable_yieldValue(
2123 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
2124 + );
2125 + },
2126 + onMarkerIncomplete: (
2127 + transitionName,
2128 + markerName,
2129 + startTime,
2130 + deletions,
2131 + ) => {
2132 + Scheduler.unstable_yieldValue(
2133 + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
2134 + deletions,
2135 + )}])`,
2136 + );
2137 + },
2138 onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
2139 Scheduler.unstable_yieldValue(
2140 `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,