[Transition Tracing] Add Tracing Markers (#24686)

This PR adds support for Tracing Markers as well as onTracingMarkerComplete
This commit is contained in:
Luna Ruan
2022-06-27 09:46:43 -07:00
committed by GitHub
parent 167853026c
commit a4bed46969
14 changed files with 543 additions and 12 deletions
@@ -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;
}
@@ -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;
}
@@ -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);
@@ -2093,10 +2111,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;
}
@@ -2177,10 +2198,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;
}
@@ -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);
@@ -2093,10 +2111,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;
}
@@ -2177,10 +2198,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;
}
@@ -138,6 +138,7 @@ import {
restorePendingUpdaters,
addTransitionStartCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
} from './ReactFiberWorkLoop.new';
import {
@@ -2910,6 +2911,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
@@ -2931,6 +2933,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);
@@ -2967,6 +2986,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;
}
}
}
@@ -138,6 +138,7 @@ import {
restorePendingUpdaters,
addTransitionStartCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
} from './ReactFiberWorkLoop.old';
import {
@@ -2910,6 +2911,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
@@ -2931,6 +2933,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);
@@ -2967,6 +2986,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;
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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 = {|
@@ -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(
@@ -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(
@@ -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;
}
@@ -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;
}
@@ -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 () => {
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)',
]);
});
});
});