Skip to content

[Transition Tracing] Add Tracing Markers #24686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiber.new.js
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import type {
OffscreenProps,
OffscreenInstance,
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';

import {
createRootStrictEffectsByDefault,
@@ -757,6 +758,11 @@ export function createFiberFromTracingMarker(
const fiber = createFiber(TracingMarkerComponent, pendingProps, key, mode);
fiber.elementType = REACT_TRACING_MARKER_TYPE;
fiber.lanes = lanes;
const tracingMarkerInstance: TracingMarkerInstance = {
transitions: null,
pendingSuspenseBoundaries: null,
};
fiber.stateNode = tracingMarkerInstance;
return fiber;
}

6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiber.old.js
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import type {
OffscreenProps,
OffscreenInstance,
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';

import {
createRootStrictEffectsByDefault,
@@ -757,6 +758,11 @@ export function createFiberFromTracingMarker(
const fiber = createFiber(TracingMarkerComponent, pendingProps, key, mode);
fiber.elementType = REACT_TRACING_MARKER_TYPE;
fiber.lanes = lanes;
const tracingMarkerInstance: TracingMarkerInstance = {
transitions: null,
pendingSuspenseBoundaries: null,
};
fiber.stateNode = tracingMarkerInstance;
return fiber;
}

31 changes: 27 additions & 4 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ import type {
} from './ReactFiberCacheComponent.new';
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
import type {RootState} from './ReactFiberRoot.new';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
@@ -255,9 +256,12 @@ import {
getSuspendedCache,
pushTransition,
getOffscreenDeferredCache,
getSuspendedTransitions,
getPendingTransitions,
} from './ReactFiberTransition.new';
import {pushTracingMarker} from './ReactFiberTracingMarkerComponent.new';
import {
getTracingMarkers,
pushTracingMarker,
} from './ReactFiberTracingMarkerComponent.new';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

@@ -891,6 +895,20 @@ function updateTracingMarkerComponent(
return null;
}

// TODO: (luna) Only update the tracing marker if it's newly rendered or it's name changed.
// A tracing marker is only associated with the transitions that rendered
// or updated it, so we can create a new set of transitions each time
if (current === null) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const markerInstance: TracingMarkerInstance = {
transitions: new Set(currentTransitions),
pendingSuspenseBoundaries: new Map(),
};
workInProgress.stateNode = markerInstance;
}
}

pushTracingMarker(workInProgress);
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -2094,10 +2112,13 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
);
workInProgress.memoizedState = SUSPENDED_MARKER;
if (enableTransitionTracing) {
const currentTransitions = getSuspendedTransitions();
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
// If there are no transitions, we don't need to keep track of tracing markers
const currentTracingMarkers = getTracingMarkers();
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
tracingMarkers: currentTracingMarkers,
};
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
}
@@ -2178,10 +2199,12 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
? mountSuspenseOffscreenState(renderLanes)
: updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
if (enableTransitionTracing) {
const currentTransitions = getSuspendedTransitions();
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const currentTracingMarkers = getTracingMarkers();
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
tracingMarkers: currentTracingMarkers,
};
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
}
31 changes: 27 additions & 4 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ import type {
} from './ReactFiberCacheComponent.old';
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
import type {RootState} from './ReactFiberRoot.old';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
@@ -255,9 +256,12 @@ import {
getSuspendedCache,
pushTransition,
getOffscreenDeferredCache,
getSuspendedTransitions,
getPendingTransitions,
} from './ReactFiberTransition.old';
import {pushTracingMarker} from './ReactFiberTracingMarkerComponent.old';
import {
getTracingMarkers,
pushTracingMarker,
} from './ReactFiberTracingMarkerComponent.old';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

@@ -891,6 +895,20 @@ function updateTracingMarkerComponent(
return null;
}

// TODO: (luna) Only update the tracing marker if it's newly rendered or it's name changed.
// A tracing marker is only associated with the transitions that rendered
// or updated it, so we can create a new set of transitions each time
if (current === null) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const markerInstance: TracingMarkerInstance = {
transitions: new Set(currentTransitions),
pendingSuspenseBoundaries: new Map(),
};
workInProgress.stateNode = markerInstance;
}
}

pushTracingMarker(workInProgress);
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -2094,10 +2112,13 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
);
workInProgress.memoizedState = SUSPENDED_MARKER;
if (enableTransitionTracing) {
const currentTransitions = getSuspendedTransitions();
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
// If there are no transitions, we don't need to keep track of tracing markers
const currentTracingMarkers = getTracingMarkers();
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
tracingMarkers: currentTracingMarkers,
};
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
}
@@ -2178,10 +2199,12 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
? mountSuspenseOffscreenState(renderLanes)
: updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
if (enableTransitionTracing) {
const currentTransitions = getSuspendedTransitions();
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const currentTracingMarkers = getTracingMarkers();
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
tracingMarkers: currentTracingMarkers,
};
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
}
41 changes: 41 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
@@ -138,6 +138,7 @@ import {
restorePendingUpdaters,
addTransitionStartCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
} from './ReactFiberWorkLoop.new';
import {
@@ -2913,6 +2914,7 @@ function commitPassiveMountOnFiber(
instance.transitions = prevTransitions = new Set();
}

// TODO(luna): Combine the root code with the tracing marker code
if (transitions !== null) {
transitions.forEach(transition => {
// Add all the transitions saved in the update queue during
@@ -2932,6 +2934,23 @@ function commitPassiveMountOnFiber(
);
});
}

const tracingMarkers = queue.tracingMarkers;
if (tracingMarkers !== null) {
tracingMarkers.forEach(marker => {
const markerInstance = marker.stateNode;
// There should only be a few tracing marker transitions because
// they should be only associated with the transition that
// caused them
markerInstance.transitions.forEach(transition => {
if (instance.transitions.has(transition)) {
instance.pendingMarkers.add(
markerInstance.pendingSuspenseBoundaries,
);
}
});
});
}
}

commitTransitionProgress(finishedWork);
@@ -2968,6 +2987,28 @@ function commitPassiveMountOnFiber(
}
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
const instance = finishedWork.stateNode;
if (
instance.pendingSuspenseBoundaries === null ||
instance.pendingSuspenseBoundaries.size === 0
) {
instance.transitions.forEach(transition => {
addMarkerCompleteCallbackToPendingTransition({
transitionName: transition.name,
startTime: transition.startTime,
markerName: finishedWork.memoizedProps.name,
});
});
instance.transitions = null;
instance.pendingSuspenseBoundaries = null;
}
}
break;
}
}
}

41 changes: 41 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Original file line number Diff line number Diff line change
@@ -138,6 +138,7 @@ import {
restorePendingUpdaters,
addTransitionStartCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
} from './ReactFiberWorkLoop.old';
import {
@@ -2913,6 +2914,7 @@ function commitPassiveMountOnFiber(
instance.transitions = prevTransitions = new Set();
}

// TODO(luna): Combine the root code with the tracing marker code
if (transitions !== null) {
transitions.forEach(transition => {
// Add all the transitions saved in the update queue during
@@ -2932,6 +2934,23 @@ function commitPassiveMountOnFiber(
);
});
}

const tracingMarkers = queue.tracingMarkers;
if (tracingMarkers !== null) {
tracingMarkers.forEach(marker => {
const markerInstance = marker.stateNode;
// There should only be a few tracing marker transitions because
// they should be only associated with the transition that
// caused them
markerInstance.transitions.forEach(transition => {
if (instance.transitions.has(transition)) {
instance.pendingMarkers.add(
markerInstance.pendingSuspenseBoundaries,
);
}
});
});
}
}

commitTransitionProgress(finishedWork);
@@ -2968,6 +2987,28 @@ function commitPassiveMountOnFiber(
}
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
const instance = finishedWork.stateNode;
if (
instance.pendingSuspenseBoundaries === null ||
instance.pendingSuspenseBoundaries.size === 0
) {
instance.transitions.forEach(transition => {
addMarkerCompleteCallbackToPendingTransition({
transitionName: transition.name,
startTime: transition.startTime,
markerName: finishedWork.memoizedProps.name,
});
});
instance.transitions = null;
instance.pendingSuspenseBoundaries = null;
}
}
break;
}
}
}

11 changes: 10 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
@@ -1581,9 +1581,18 @@ function completeWork(
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
// Bubble subtree flags before so we can set the flag property
popTracingMarker(workInProgress);
bubbleProperties(workInProgress);

if (
current === null ||
(workInProgress.subtreeFlags & Visibility) !== NoFlags
) {
// If any of our suspense children toggle visibility, this means that
// the pending boundaries array needs to be updated, which we only
// do in the passive phase.
workInProgress.flags |= Passive;
}
}
return null;
}
11 changes: 10 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.old.js
Original file line number Diff line number Diff line change
@@ -1581,9 +1581,18 @@ function completeWork(
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
// Bubble subtree flags before so we can set the flag property
popTracingMarker(workInProgress);
bubbleProperties(workInProgress);

if (
current === null ||
(workInProgress.subtreeFlags & Visibility) !== NoFlags
) {
// If any of our suspense children toggle visibility, this means that
// the pending boundaries array needs to be updated, which we only
// do in the passive phase.
workInProgress.flags |= Passive;
}
}
return null;
}
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberOffscreenComponent.js
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
*/

import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
import type {
@@ -38,6 +39,7 @@ export type OffscreenState = {|

export type OffscreenQueue = {|
transitions: Array<Transition> | null,
tracingMarkers: Array<Fiber> | null,
|} | null;

export type OffscreenInstance = {|
Original file line number Diff line number Diff line change
@@ -39,6 +39,11 @@ export type BatchConfigTransition = {
_updatedFibers?: Set<Fiber>,
};

export type TracingMarkerInstance = {|
pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
transitions: Set<Transition> | null,
|} | null;

export type PendingSuspenseBoundaries = Map<OffscreenInstance, SuspenseInfo>;

export function processTransitionCallbacks(
Original file line number Diff line number Diff line change
@@ -39,6 +39,11 @@ export type BatchConfigTransition = {
_updatedFibers?: Set<Fiber>,
};

export type TracingMarkerInstance = {|
pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
transitions: Set<Transition> | null,
|} | null;

export type PendingSuspenseBoundaries = Map<OffscreenInstance, SuspenseInfo>;

export function processTransitionCallbacks(
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactFiberTransition.new.js
Original file line number Diff line number Diff line change
@@ -149,7 +149,7 @@ export function popTransition(workInProgress: Fiber, current: Fiber | null) {
}
}

export function getSuspendedTransitions(): Array<Transition> | null {
export function getPendingTransitions(): Array<Transition> | null {
if (!enableTransitionTracing) {
return null;
}
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactFiberTransition.old.js
Original file line number Diff line number Diff line change
@@ -149,7 +149,7 @@ export function popTransition(workInProgress: Fiber, current: Fiber | null) {
}
}

export function getSuspendedTransitions(): Array<Transition> | null {
export function getPendingTransitions(): Array<Transition> | null {
if (!enableTransitionTracing) {
return null;
}
361 changes: 361 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
Original file line number Diff line number Diff line change
@@ -477,4 +477,365 @@ describe('ReactInteractionTracing', () => {
]);
});
});

// @gate enableTransitionTracing
it('should correctly trace interactions for tracing markers complete', async () => {
const transitionCallbacks = {
onTransitionStart: (name, startTime) => {
Scheduler.unstable_yieldValue(
`onTransitionStart(${name}, ${startTime})`,
);
},
onTransitionComplete: (name, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
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 ? (
<Suspense
fallback={<Text text="Loading..." />}
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">
<AsyncText text="Marker Text" />
</Suspense>
</React.unstable_TracingMarker>
</Suspense>
) : (
<Text text="Page One" />
)}
</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([
'Suspend [Page Two]',
'Suspend [Marker Text]',
'Loading...',
'Loading...',
'onTransitionStart(page transition, 1000)',
]);

ReactNoop.expire(1000);
await advanceTimers(1000);
await resolveText('Page Two');

expect(Scheduler).toFlushAndYield([
'Page Two',
'Suspend [Marker Text]',
'Loading...',
'onMarkerComplete(page transition, sync marker, 1000, 3000)',
]);

ReactNoop.expire(1000);
await advanceTimers(1000);
await resolveText('Marker Text');

expect(Scheduler).toFlushAndYield([
'Marker Text',
'onMarkerComplete(page transition, async marker, 1000, 4000)',
'onTransitionComplete(page transition, 1000, 4000)',
]);
});
});

// @gate enableTransitionTracing
it('trace interaction with multiple tracing markers', async () => {
const transitionCallbacks = {
onTransitionStart: (name, startTime) => {
Scheduler.unstable_yieldValue(
`onTransitionStart(${name}, ${startTime})`,
);
},
onTransitionComplete: (name, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
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="outer marker">
<Suspense fallback={<Text text="Outer..." />}>
<AsyncText text="Outer Text" />
<Suspense fallback={<Text text="Inner One..." />}>
<React.unstable_TracingMarker name="marker one">
<AsyncText text="Inner Text One" />
</React.unstable_TracingMarker>
</Suspense>
<Suspense fallback={<Text text="Inner Two..." />}>
<React.unstable_TracingMarker name="marker two">
<AsyncText text="Inner Text Two" />
</React.unstable_TracingMarker>
</Suspense>
</Suspense>
</React.unstable_TracingMarker>
) : (
<Text text="Page One" />
)}
</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([
'Suspend [Outer Text]',
'Suspend [Inner Text One]',
'Inner One...',
'Suspend [Inner Text Two]',
'Inner Two...',
'Outer...',
'onTransitionStart(page transition, 1000)',
]);

ReactNoop.expire(1000);
await advanceTimers(1000);
await resolveText('Inner Text Two');
expect(Scheduler).toFlushAndYield([]);

ReactNoop.expire(1000);
await advanceTimers(1000);
await resolveText('Outer Text');
expect(Scheduler).toFlushAndYield([
'Outer Text',
'Suspend [Inner Text One]',
'Inner One...',
'Inner Text Two',
'onMarkerComplete(page transition, marker two, 1000, 4000)',
]);

ReactNoop.expire(1000);
await advanceTimers(1000);
await resolveText('Inner Text One');
expect(Scheduler).toFlushAndYield([
'Inner Text One',
'onMarkerComplete(page transition, marker one, 1000, 5000)',
'onMarkerComplete(page transition, outer marker, 1000, 5000)',
'onTransitionComplete(page transition, 1000, 5000)',
]);
});
});

// @gate enableTransitionTracing
it.skip('marker interaction cancelled when name changes', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this one meant to be skipped?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes! Skipping because we need to talk about what to do when the marker's name changes. Leaving this test here for now as a reminder (and also cause I wrote it already)

const transitionCallbacks = {
onTransitionStart: (name, startTime) => {
Scheduler.unstable_yieldValue(
`onTransitionStart(${name}, ${startTime})`,
);
},
onTransitionComplete: (name, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
);
},
};

let navigateToPageTwo;
let setMarkerNameFn;
function App() {
const [navigate, setNavigate] = useState(false);
navigateToPageTwo = () => {
setNavigate(true);
};

const [markerName, setMarkerName] = useState('old marker');
setMarkerNameFn = () => setMarkerName('new marker');

return (
<div>
{navigate ? (
<React.unstable_TracingMarker name={markerName}>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Page Two" />
</Suspense>
</React.unstable_TracingMarker>
) : (
<Text text="Page One" />
)}
</div>
);
}

const root = ReactNoop.createRoot({transitionCallbacks});
await act(async () => {
root.render(<App />);
ReactNoop.expire(1000);
await advanceTimers(1000);

expect(Scheduler).toFlushAndYield(['Page One']);

startTransition(() => navigateToPageTwo(), {name: 'page transition'});
expect(Scheduler).toFlushAndYield([
'Suspend [Page Two]',
'Loading...',
'onTransitionStart(page transition, 1000)',
]);

ReactNoop.expire(1000);
await advanceTimers(1000);
setMarkerNameFn();

expect(Scheduler).toFlushAndYield(['Suspend [Page Two]', 'Loading...']);
ReactNoop.expire(1000);
await advanceTimers(1000);
resolveText('Page Two');

// Marker complete is not called because the marker name changed
expect(Scheduler).toFlushAndYield([
'Page Two',
'onTransitionComplete(page transition, 1000, 3000)',
]);
});
});

// @gate enableTransitionTracing
it.skip('marker changes to new interaction when name changes', async () => {
const transitionCallbacks = {
onTransitionStart: (name, startTime) => {
Scheduler.unstable_yieldValue(
`onTransitionStart(${name}, ${startTime})`,
);
},
onTransitionComplete: (name, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
);
},
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
Scheduler.unstable_yieldValue(
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
);
},
};

let navigateToPageTwo;
let setMarkerNameFn;
function App() {
const [navigate, setNavigate] = useState(false);
navigateToPageTwo = () => {
setNavigate(true);
};

const [markerName, setMarkerName] = useState('old marker');
setMarkerNameFn = () => setMarkerName('new marker');

return (
<div>
{navigate ? (
<React.unstable_TracingMarker name={markerName}>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Page Two" />
</Suspense>
</React.unstable_TracingMarker>
) : (
<Text text="Page One" />
)}
</div>
);
}

const root = ReactNoop.createRoot({transitionCallbacks});
await act(async () => {
root.render(<App />);
ReactNoop.expire(1000);
await advanceTimers(1000);

expect(Scheduler).toFlushAndYield(['Page One']);

startTransition(() => navigateToPageTwo(), {name: 'page transition'});
expect(Scheduler).toFlushAndYield([
'Suspend [Page Two]',
'Loading...',
'onTransitionStart(page transition, 1000)',
]);

ReactNoop.expire(1000);
await advanceTimers(1000);
startTransition(() => setMarkerNameFn(), {name: 'marker transition'});

expect(Scheduler).toFlushAndYield([
'Suspend [Page Two]',
'Loading...',
'onTransitionStart(marker transition, 2000)',
]);
ReactNoop.expire(1000);
await advanceTimers(1000);
resolveText('Page Two');

// Marker complete is not called because the marker name changed
expect(Scheduler).toFlushAndYield([
'Page Two',
'onMarkerComplete(new marker, 2000, 3000)',
'onTransitionComplete(page transition, 1000, 3000)',
]);
});
});
});