[Transition Tracing] onMarkerProgress (#24861)

This PR adds support for `onMarkerProgress` (`onTransitionProgress(transitionName: string, markerName: string, startTime: number, currentTime: number, pending: Array<{name: null | string}>)`)

We call this callback when:
    * When **a child suspense boundary of the marker commits in a fallback state**. Only the suspense boundaries that are triggered and commit in a fallback state when the transition first occurs (and all subsequent suspense boundaries in the initial suspense boundary's subtree) are considered a part of the transition
    * **A child suspense boundary of the marker resolves**
   
When we call `onMarkerProgress`, we call the function with a `pending` array. This array contains the names of the transition's suspense boundaries that are still in a fallback state
This commit is contained in:
Luna Ruan 2022-07-12 14:59:54 -04:00 committed by GitHub
parent b641d02097
commit 5e8c1961c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 344 additions and 48 deletions

View File

@ -978,6 +978,7 @@ function updateTracingMarkerComponent(
const markerInstance: TracingMarkerInstance = {
transitions: new Set(currentTransitions),
pendingSuspenseBoundaries: new Map(),
name: workInProgress.pendingProps.name,
};
workInProgress.stateNode = markerInstance;
}

View File

@ -978,6 +978,7 @@ function updateTracingMarkerComponent(
const markerInstance: TracingMarkerInstance = {
transitions: new Set(currentTransitions),
pendingSuspenseBoundaries: new Map(),
name: workInProgress.pendingProps.name,
};
workInProgress.stateNode = markerInstance;
}

View File

@ -145,6 +145,7 @@ import {
addTransitionStartCallbackToPendingTransition,
addTransitionProgressCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerProgressCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
} from './ReactFiberWorkLoop.new';
@ -1250,6 +1251,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
if (pendingMarkers !== null) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingSuspenseBoundaries;
const transitions = markerInstance.transitions;
if (
pendingBoundaries !== null &&
!pendingBoundaries.has(offscreenInstance)
@ -1257,13 +1259,21 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
pendingBoundaries.set(offscreenInstance, {
name,
});
if (markerInstance.transitions !== null) {
markerInstance.transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
if (transitions !== null) {
if (markerInstance.name) {
addMarkerProgressCallbackToPendingTransition(
markerInstance.name,
transitions,
pendingBoundaries,
);
});
} else {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
pendingBoundaries,
);
});
}
}
}
});
@ -1275,18 +1285,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
if (pendingMarkers !== null) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingSuspenseBoundaries;
const transitions = markerInstance.transitions;
if (
pendingBoundaries !== null &&
pendingBoundaries.has(offscreenInstance)
) {
pendingBoundaries.delete(offscreenInstance);
if (markerInstance.transitions !== null) {
markerInstance.transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
if (transitions !== null) {
if (markerInstance.name) {
addMarkerProgressCallbackToPendingTransition(
markerInstance.name,
transitions,
pendingBoundaries,
);
});
} else {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
pendingBoundaries,
);
});
}
}
}
});
@ -3083,19 +3102,18 @@ function commitPassiveMountOnFiber(
// and add a start transition callback for each of them
const instance = finishedWork.stateNode;
if (
instance.pendingSuspenseBoundaries === null ||
instance.pendingSuspenseBoundaries.size === 0
instance.transitions !== null &&
(instance.pendingSuspenseBoundaries === null ||
instance.pendingSuspenseBoundaries.size === 0)
) {
if (instance.transitions !== null) {
instance.transitions.forEach(transition => {
addMarkerCompleteCallbackToPendingTransition({
transition,
name: finishedWork.memoizedProps.name,
});
instance.transitions.forEach(transition => {
addMarkerCompleteCallbackToPendingTransition({
transition,
name: finishedWork.memoizedProps.name,
});
instance.transitions = null;
instance.pendingSuspenseBoundaries = null;
}
});
instance.transitions = null;
instance.pendingSuspenseBoundaries = null;
}
}
break;

View File

@ -145,6 +145,7 @@ import {
addTransitionStartCallbackToPendingTransition,
addTransitionProgressCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerProgressCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
} from './ReactFiberWorkLoop.old';
@ -1250,6 +1251,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
if (pendingMarkers !== null) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingSuspenseBoundaries;
const transitions = markerInstance.transitions;
if (
pendingBoundaries !== null &&
!pendingBoundaries.has(offscreenInstance)
@ -1257,13 +1259,21 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
pendingBoundaries.set(offscreenInstance, {
name,
});
if (markerInstance.transitions !== null) {
markerInstance.transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
if (transitions !== null) {
if (markerInstance.name) {
addMarkerProgressCallbackToPendingTransition(
markerInstance.name,
transitions,
pendingBoundaries,
);
});
} else {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
pendingBoundaries,
);
});
}
}
}
});
@ -1275,18 +1285,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
if (pendingMarkers !== null) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingSuspenseBoundaries;
const transitions = markerInstance.transitions;
if (
pendingBoundaries !== null &&
pendingBoundaries.has(offscreenInstance)
) {
pendingBoundaries.delete(offscreenInstance);
if (markerInstance.transitions !== null) {
markerInstance.transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
if (transitions !== null) {
if (markerInstance.name) {
addMarkerProgressCallbackToPendingTransition(
markerInstance.name,
transitions,
pendingBoundaries,
);
});
} else {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
pendingBoundaries,
);
});
}
}
}
});
@ -3083,19 +3102,18 @@ function commitPassiveMountOnFiber(
// and add a start transition callback for each of them
const instance = finishedWork.stateNode;
if (
instance.pendingSuspenseBoundaries === null ||
instance.pendingSuspenseBoundaries.size === 0
instance.transitions !== null &&
(instance.pendingSuspenseBoundaries === null ||
instance.pendingSuspenseBoundaries.size === 0)
) {
if (instance.transitions !== null) {
instance.transitions.forEach(transition => {
addMarkerCompleteCallbackToPendingTransition({
transition,
name: finishedWork.memoizedProps.name,
});
instance.transitions.forEach(transition => {
addMarkerCompleteCallbackToPendingTransition({
transition,
name: finishedWork.memoizedProps.name,
});
instance.transitions = null;
instance.pendingSuspenseBoundaries = null;
}
});
instance.transitions = null;
instance.pendingSuspenseBoundaries = null;
}
}
break;

View File

@ -26,6 +26,7 @@ export type PendingTransitionCallbacks = {
transitionStart: Array<Transition> | null,
transitionProgress: Map<Transition, PendingSuspenseBoundaries> | null,
transitionComplete: Array<Transition> | null,
markerProgress: Map<string, TracingMarkerInstance> | null,
markerComplete: Array<MarkerTransition> | null,
};
@ -43,6 +44,7 @@ export type BatchConfigTransition = {
export type TracingMarkerInstance = {|
pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
transitions: Set<Transition> | null,
name?: string,
|};
export type PendingSuspenseBoundaries = Map<OffscreenInstance, SuspenseInfo>;
@ -63,6 +65,28 @@ export function processTransitionCallbacks(
});
}
const markerProgress = pendingTransitions.markerProgress;
const onMarkerProgress = callbacks.onMarkerProgress;
if (onMarkerProgress != null && markerProgress !== null) {
markerProgress.forEach((markerInstance, markerName) => {
if (markerInstance.transitions !== null) {
const pending =
markerInstance.pendingSuspenseBoundaries !== null
? Array.from(markerInstance.pendingSuspenseBoundaries.values())
: [];
markerInstance.transitions.forEach(transition => {
onMarkerProgress(
transition.name,
markerName,
transition.startTime,
endTime,
pending,
);
});
}
});
}
const markerComplete = pendingTransitions.markerComplete;
if (markerComplete !== null) {
markerComplete.forEach(marker => {

View File

@ -26,6 +26,7 @@ export type PendingTransitionCallbacks = {
transitionStart: Array<Transition> | null,
transitionProgress: Map<Transition, PendingSuspenseBoundaries> | null,
transitionComplete: Array<Transition> | null,
markerProgress: Map<string, TracingMarkerInstance> | null,
markerComplete: Array<MarkerTransition> | null,
};
@ -43,6 +44,7 @@ export type BatchConfigTransition = {
export type TracingMarkerInstance = {|
pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
transitions: Set<Transition> | null,
name?: string,
|};
export type PendingSuspenseBoundaries = Map<OffscreenInstance, SuspenseInfo>;
@ -63,6 +65,28 @@ export function processTransitionCallbacks(
});
}
const markerProgress = pendingTransitions.markerProgress;
const onMarkerProgress = callbacks.onMarkerProgress;
if (onMarkerProgress != null && markerProgress !== null) {
markerProgress.forEach((markerInstance, markerName) => {
if (markerInstance.transitions !== null) {
const pending =
markerInstance.pendingSuspenseBoundaries !== null
? Array.from(markerInstance.pendingSuspenseBoundaries.values())
: [];
markerInstance.transitions.forEach(transition => {
onMarkerProgress(
transition.name,
markerName,
transition.startTime,
endTime,
pending,
);
});
}
});
}
const markerComplete = pendingTransitions.markerComplete;
if (markerComplete !== null) {
markerComplete.forEach(marker => {

View File

@ -342,6 +342,7 @@ export function addTransitionStartCallbackToPendingTransition(
transitionStart: [],
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
markerComplete: null,
};
}
@ -354,6 +355,33 @@ export function addTransitionStartCallbackToPendingTransition(
}
}
export function addMarkerProgressCallbackToPendingTransition(
markerName: string,
transitions: Set<Transition>,
pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
) {
if (enableTransitionTracing) {
if (currentPendingTransitionCallbacks === null) {
currentPendingTransitionCallbacks = {
transitionStart: null,
transitionProgress: null,
transitionComplete: null,
markerProgress: new Map(),
markerComplete: null,
};
}
if (currentPendingTransitionCallbacks.markerProgress === null) {
currentPendingTransitionCallbacks.markerProgress = new Map();
}
currentPendingTransitionCallbacks.markerProgress.set(markerName, {
pendingSuspenseBoundaries,
transitions,
});
}
}
export function addMarkerCompleteCallbackToPendingTransition(
transition: MarkerTransition,
) {
@ -363,6 +391,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
transitionStart: null,
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
markerComplete: [],
};
}
@ -385,6 +414,7 @@ export function addTransitionProgressCallbackToPendingTransition(
transitionStart: null,
transitionProgress: new Map(),
transitionComplete: null,
markerProgress: null,
markerComplete: null,
};
}
@ -409,6 +439,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
transitionStart: null,
transitionProgress: null,
transitionComplete: [],
markerProgress: null,
markerComplete: null,
};
}

View File

@ -342,6 +342,7 @@ export function addTransitionStartCallbackToPendingTransition(
transitionStart: [],
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
markerComplete: null,
};
}
@ -354,6 +355,33 @@ export function addTransitionStartCallbackToPendingTransition(
}
}
export function addMarkerProgressCallbackToPendingTransition(
markerName: string,
transitions: Set<Transition>,
pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
) {
if (enableTransitionTracing) {
if (currentPendingTransitionCallbacks === null) {
currentPendingTransitionCallbacks = {
transitionStart: null,
transitionProgress: null,
transitionComplete: null,
markerProgress: new Map(),
markerComplete: null,
};
}
if (currentPendingTransitionCallbacks.markerProgress === null) {
currentPendingTransitionCallbacks.markerProgress = new Map();
}
currentPendingTransitionCallbacks.markerProgress.set(markerName, {
pendingSuspenseBoundaries,
transitions,
});
}
}
export function addMarkerCompleteCallbackToPendingTransition(
transition: MarkerTransition,
) {
@ -363,6 +391,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
transitionStart: null,
transitionProgress: null,
transitionComplete: null,
markerProgress: null,
markerComplete: [],
};
}
@ -385,6 +414,7 @@ export function addTransitionProgressCallbackToPendingTransition(
transitionStart: null,
transitionProgress: new Map(),
transitionComplete: null,
markerProgress: null,
markerComplete: null,
};
}
@ -409,6 +439,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
transitionStart: null,
transitionProgress: null,
transitionComplete: [],
markerProgress: null,
markerComplete: null,
};
}

View File

@ -941,7 +941,91 @@ describe('ReactInteractionTracing', () => {
});
// @gate enableTransitionTracing
it('should correctly trace interactions for tracing markers complete', async () => {
it('should correctly trace basic interaction with tracing markers', async () => {
const transitionCallbacks = {
onTransitionStart: (name, startTime) => {
Scheduler.unstable_yieldValue(
`onTransitionStart(${name}, ${startTime})`,
);
},
onTransitionProgress: (name, startTime, endTime, pending) => {
const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
Scheduler.unstable_yieldValue(
`onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
);
},
onTransitionComplete: (name, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerProgress: (
transitioName,
markerName,
startTime,
currentTime,
pending,
) => {
const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
Scheduler.unstable_yieldValue(
`onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
);
},
};
let navigateToPageTwo;
function App() {
const [navigate, setNavigate] = useState(false);
navigateToPageTwo = () => {
setNavigate(true);
};
return (
<div>
{navigate ? (
<React.unstable_TracingMarker name="marker two" key="marker two">
<Text text="Page Two" />
</React.unstable_TracingMarker>
) : (
<React.unstable_TracingMarker name="marker one">
<Text text="Page One" />
</React.unstable_TracingMarker>
)}
</div>
);
}
const root = ReactNoop.createRoot({transitionCallbacks});
await act(async () => {
root.render(<App />);
ReactNoop.expire(1000);
await advanceTimers(1000);
expect(Scheduler).toFlushAndYield(['Page One']);
await act(async () => {
startTransition(() => navigateToPageTwo(), {name: 'page transition'});
ReactNoop.expire(1000);
await advanceTimers(1000);
expect(Scheduler).toFlushAndYield([
'Page Two',
'onTransitionStart(page transition, 1000)',
'onMarkerComplete(page transition, marker two, 1000, 2000)',
'onTransitionComplete(page transition, 1000, 2000)',
]);
});
});
});
// @gate enableTransitionTracing
it('should correctly trace interactions for tracing markers', async () => {
const transitionCallbacks = {
onTransitionStart: (name, startTime) => {
Scheduler.unstable_yieldValue(
@ -953,6 +1037,18 @@ describe('ReactInteractionTracing', () => {
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerProgress: (
transitioName,
markerName,
startTime,
currentTime,
pending,
) => {
const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
Scheduler.unstable_yieldValue(
`onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
@ -971,13 +1067,13 @@ describe('ReactInteractionTracing', () => {
{navigate ? (
<Suspense
fallback={<Text text="Loading..." />}
name="suspense page">
unstable_name="suspense page">
<AsyncText text="Page Two" />
<React.unstable_TracingMarker name="sync marker" />
<React.unstable_TracingMarker name="async marker">
<Suspense
fallback={<Text text="Loading..." />}
name="marker suspense">
unstable_name="marker suspense">
<AsyncText text="Marker Text" />
</Suspense>
</React.unstable_TracingMarker>
@ -1020,6 +1116,7 @@ describe('ReactInteractionTracing', () => {
'Page Two',
'Suspend [Marker Text]',
'Loading...',
'onMarkerProgress(page transition, async marker, 1000, 3000, [marker suspense])',
'onMarkerComplete(page transition, sync marker, 1000, 3000)',
]);
@ -1029,6 +1126,7 @@ describe('ReactInteractionTracing', () => {
expect(Scheduler).toFlushAndYield([
'Marker Text',
'onMarkerProgress(page transition, async marker, 1000, 4000, [])',
'onMarkerComplete(page transition, async marker, 1000, 4000)',
'onTransitionComplete(page transition, 1000, 4000)',
]);
@ -1048,6 +1146,18 @@ describe('ReactInteractionTracing', () => {
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerProgress: (
transitioName,
markerName,
startTime,
currentTime,
pending,
) => {
const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
Scheduler.unstable_yieldValue(
`onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
@ -1066,14 +1176,20 @@ describe('ReactInteractionTracing', () => {
<div>
{navigate ? (
<React.unstable_TracingMarker name="outer marker">
<Suspense fallback={<Text text="Outer..." />}>
<Suspense
fallback={<Text text="Outer..." />}
unstable_name="outer">
<AsyncText text="Outer Text" />
<Suspense fallback={<Text text="Inner One..." />}>
<Suspense
fallback={<Text text="Inner One..." />}
unstable_name="inner one">
<React.unstable_TracingMarker name="marker one">
<AsyncText text="Inner Text One" />
</React.unstable_TracingMarker>
</Suspense>
<Suspense fallback={<Text text="Inner Two..." />}>
<Suspense
fallback={<Text text="Inner Two..." />}
unstable_name="inner two">
<React.unstable_TracingMarker name="marker two">
<AsyncText text="Inner Text Two" />
</React.unstable_TracingMarker>
@ -1110,6 +1226,7 @@ describe('ReactInteractionTracing', () => {
'Inner Two...',
'Outer...',
'onTransitionStart(page transition, 1000)',
'onMarkerProgress(page transition, outer marker, 1000, 2000, [outer])',
]);
ReactNoop.expire(1000);
@ -1125,6 +1242,7 @@ describe('ReactInteractionTracing', () => {
'Suspend [Inner Text One]',
'Inner One...',
'Inner Text Two',
'onMarkerProgress(page transition, outer marker, 1000, 4000, [inner one])',
'onMarkerComplete(page transition, marker two, 1000, 4000)',
]);
@ -1133,6 +1251,7 @@ describe('ReactInteractionTracing', () => {
await resolveText('Inner Text One');
expect(Scheduler).toFlushAndYield([
'Inner Text One',
'onMarkerProgress(page transition, outer marker, 1000, 5000, [])',
'onMarkerComplete(page transition, marker one, 1000, 5000)',
'onMarkerComplete(page transition, outer marker, 1000, 5000)',
'onTransitionComplete(page transition, 1000, 5000)',
@ -1233,6 +1352,18 @@ describe('ReactInteractionTracing', () => {
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerProgress: (
transitioName,
markerName,
startTime,
currentTime,
pending,
) => {
const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
Scheduler.unstable_yieldValue(
`onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
@ -1279,6 +1410,7 @@ describe('ReactInteractionTracing', () => {
'Suspend [Page Two]',
'Loading...',
'onTransitionStart(page transition, 1000)',
'onMarkerProgress(page transition, old marker, 1000, 1000, [<null>])',
]);
ReactNoop.expire(1000);
@ -1311,6 +1443,18 @@ describe('ReactInteractionTracing', () => {
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerProgress: (
transitioName,
markerName,
startTime,
currentTime,
pending,
) => {
const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
Scheduler.unstable_yieldValue(
`onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
@ -1357,6 +1501,7 @@ describe('ReactInteractionTracing', () => {
'Suspend [Page Two]',
'Loading...',
'onTransitionStart(page transition, 1000)',
'onMarkerProgress(page transition, old marker, 1000, 2000, [<null>])',
]);
ReactNoop.expire(1000);
@ -1367,6 +1512,9 @@ describe('ReactInteractionTracing', () => {
'Suspend [Page Two]',
'Loading...',
'onTransitionStart(marker transition, 2000)',
'onMarkerProgress(marker transition, new marker, 2000, 3000, [])',
'onMarkerComplete(marker transition, new marker, 2000, 3000)',
'onTransitionComplete(marker transition, 2000, 3000)',
]);
ReactNoop.expire(1000);
await advanceTimers(1000);