Log the time until the Animation finishes as "Animating" (#34538)

Stacked on #34522.

<img width="1025" height="200" alt="Screenshot 2025-09-19 at 6 37 28 PM"
src="https://github.com/user-attachments/assets/f25900f6-6503-48b1-876d-bd6697a29c6f"
/>

We already cover the time between "Starting Animation" and "Remaining
Effects" as "Animating". However, if the effects are forced then we can
still be animating after that. This fills in that gap.

This also fills in the gap if another render starts before the animation
finishes on the same track. It'll mark the blank space between the
previous render finishing and the next render starting as "Animating".

This should correspond roughly to the native "Animations" track.
This commit is contained in:
Sebastian Markbåge
2025-09-20 11:10:42 -04:00
committed by GitHub
parent b204edda3a
commit b4fe1e6c7e
8 changed files with 193 additions and 19 deletions
@@ -2100,6 +2100,7 @@ export function startViewTransition(
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
@@ -2302,6 +2303,9 @@ export function startViewTransition(
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = null;
}
if (enableProfilerTimer) {
finishedAnimation();
}
passiveCallback();
});
return transition;
@@ -674,6 +674,7 @@ export function startViewTransition(
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
+2
View File
@@ -860,6 +860,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
+18
View File
@@ -73,6 +73,8 @@ const TransitionLane12: Lane = /* */ 0b0000000000010000000
const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
export const SomeTransitionLane: Lane = TransitionLane1;
const TransitionUpdateLanes =
TransitionLane1 |
TransitionLane2 |
@@ -633,6 +635,22 @@ export function includesTransitionLane(lanes: Lanes): boolean {
return (lanes & TransitionLanes) !== NoLanes;
}
export function includesRetryLane(lanes: Lanes): boolean {
return (lanes & RetryLanes) !== NoLanes;
}
export function includesIdleGroupLanes(lanes: Lanes): boolean {
return (
(lanes &
(SelectiveHydrationLane |
IdleHydrationLane |
IdleLane |
OffscreenLane |
DeferredLane)) !==
NoLanes
);
}
export function includesOnlyHydrationLanes(lanes: Lanes): boolean {
return (lanes & HydrationLanes) === lanes;
}
@@ -1458,7 +1458,7 @@ export function logAnimatingPhase(
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary',
'secondary-dark',
),
);
} else {
@@ -1468,7 +1468,7 @@ export function logAnimatingPhase(
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary',
'secondary-dark',
);
}
}
+137 -17
View File
@@ -179,6 +179,8 @@ import {
includesOnlyTransitions,
includesBlockingLane,
includesTransitionLane,
includesRetryLane,
includesIdleGroupLanes,
includesExpiredLane,
getNextLanes,
getEntangledLanes,
@@ -201,6 +203,9 @@ import {
includesOnlyViewTransitionEligibleLanes,
isGestureRender,
GestureLane,
SomeTransitionLane,
SomeRetryLane,
IdleLane,
} from './ReactFiberLane';
import {
DiscreteEventPriority,
@@ -292,6 +297,8 @@ import {
clearTransitionTimers,
clampBlockingTimers,
clampTransitionTimers,
clampRetryTimers,
clampIdleTimers,
markNestedUpdateScheduled,
renderStartTime,
commitStartTime,
@@ -312,6 +319,11 @@ import {
resetCommitErrors,
PINGED_UPDATE,
SPAWNED_UPDATE,
startAnimating,
stopAnimating,
animatingLanes,
retryClampTime,
idleClampTime,
} from './ReactProfilerTimer';
// DEV stuff
@@ -1426,6 +1438,7 @@ function finishConcurrentRender(
// immediately, wait for more data to arrive.
// TODO: Combine retry throttling with Suspensey commits. Right now they
// run one after the other.
pendingEffectsLanes = lanes;
root.timeoutHandle = scheduleTimeout(
commitRootWhenReady.bind(
null,
@@ -1539,6 +1552,7 @@ function commitRootWhenReady(
// Not yet ready to commit. Delay the commit until the renderer notifies
// us that it's ready. This will be canceled if we start work on the
// root again.
pendingEffectsLanes = lanes;
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(
null,
@@ -1889,6 +1903,12 @@ function finalizeRender(lanes: Lanes, finalizationTime: number): void {
if (includesTransitionLane(lanes)) {
clampTransitionTimers(finalizationTime);
}
if (includesRetryLane(lanes)) {
clampRetryTimers(finalizationTime);
}
if (includesIdleGroupLanes(lanes)) {
clampIdleTimers(finalizationTime);
}
}
}
@@ -1939,6 +1959,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
}
finalizeRender(workInProgressRootRenderLanes, renderStartTime);
}
const previousUpdateTask = workInProgressUpdateTask;
workInProgressUpdateTask = null;
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
@@ -1951,18 +1972,30 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
blockingEventTime >= 0 && blockingEventTime < blockingClampTime
? blockingClampTime
: blockingEventTime;
const clampedRenderStartTime = // Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (blockingSuspendedTime >= 0) {
setCurrentTrackFromLanes(lanes);
setCurrentTrackFromLanes(SyncLane);
logSuspendedWithDelayPhase(
blockingSuspendedTime,
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
previousUpdateTask,
);
} else if (
includesSyncLane(animatingLanes) ||
includesBlockingLane(animatingLanes)
) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(
blockingClampTime,
clampedRenderStartTime,
previousUpdateTask,
);
}
logBlockingStart(
@@ -1994,19 +2027,29 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
transitionEventTime >= 0 && transitionEventTime < transitionClampTime
? transitionClampTime
: transitionEventTime;
const clampedRenderStartTime =
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (transitionSuspendedTime >= 0) {
setCurrentTrackFromLanes(lanes);
setCurrentTrackFromLanes(SomeTransitionLane);
logSuspendedWithDelayPhase(
transitionSuspendedTime,
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
);
} else if (includesTransitionLane(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SomeTransitionLane);
logAnimatingPhase(
transitionClampTime,
clampedRenderStartTime,
previousUpdateTask,
);
}
logTransitionStart(
clampedStartTime,
@@ -2022,6 +2065,20 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
clearTransitionTimers();
}
if (includesRetryLane(lanes)) {
if (includesRetryLane(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SomeRetryLane);
logAnimatingPhase(retryClampTime, renderStartTime, previousUpdateTask);
}
}
if (includesIdleGroupLanes(lanes)) {
if (includesIdleGroupLanes(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(IdleLane);
logAnimatingPhase(idleClampTime, renderStartTime, previousUpdateTask);
}
}
}
const timeoutHandle = root.timeoutHandle;
@@ -2038,6 +2095,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
cancelPendingCommit();
}
pendingEffectsLanes = NoLanes;
resetWorkInProgressStack();
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
@@ -3592,6 +3651,9 @@ function commitRoot(
pendingEffectsStatus = PENDING_MUTATION_PHASE;
if (enableViewTransition && willStartViewTransition) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
startAnimating(lanes);
}
pendingViewTransition = startViewTransition(
suspendedState,
root.containerInfo,
@@ -3603,6 +3665,15 @@ function commitRoot(
flushPassiveEffects,
reportViewTransitionError,
enableProfilerTimer ? suspendedViewTransition : (null: any),
enableProfilerTimer
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
finishedViewTransition.bind(
null,
lanes,
// TODO: Use a ViewTransition Task
__DEV__ ? workInProgressUpdateTask : null,
)
: (null: any),
);
} else {
// Flush synchronously.
@@ -3634,13 +3705,62 @@ function suspendedViewTransition(reason: string): void {
commitEndTime,
commitErrors,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
workInProgressUpdateTask,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
);
pendingSuspendedViewTransitionReason = reason;
pendingSuspendedCommitReason = reason;
}
}
function finishedViewTransition(
lanes: Lanes,
task: null | ConsoleTask, // DEV-only
): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if ((animatingLanes & lanes) === NoLanes) {
// Was already stopped by some other action or maybe other root.
return;
}
stopAnimating(lanes);
// If an affected track isn't in the middle of rendering or committing, log from the previous
// finished render until the end of the animation.
if (
(includesSyncLane(lanes) || includesBlockingLane(lanes)) &&
!includesSyncLane(workInProgressRootRenderLanes) &&
!includesBlockingLane(workInProgressRootRenderLanes) &&
!includesSyncLane(pendingEffectsLanes) &&
!includesBlockingLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(blockingClampTime, now(), task);
}
if (
includesTransitionLane(lanes) &&
!includesTransitionLane(workInProgressRootRenderLanes) &&
!includesTransitionLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SomeTransitionLane);
logAnimatingPhase(transitionClampTime, now(), task);
}
if (
includesRetryLane(lanes) &&
!includesRetryLane(workInProgressRootRenderLanes) &&
!includesRetryLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SomeRetryLane);
logAnimatingPhase(retryClampTime, now(), task);
}
if (
includesIdleGroupLanes(lanes) &&
!includesIdleGroupLanes(workInProgressRootRenderLanes) &&
!includesIdleGroupLanes(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(IdleLane);
logAnimatingPhase(idleClampTime, now(), task);
}
}
}
function flushAfterMutationEffects(): void {
if (pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE) {
return;
@@ -3715,7 +3835,7 @@ function flushLayoutEffects(): void {
commitEndTime, // The start is the end of the first commit part.
commitStartTime, // The end is the start of the second commit part.
suspendedViewTransitionReason,
workInProgressUpdateTask,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
);
}
}
+28
View File
@@ -22,6 +22,7 @@ import {
includesTransitionLane,
includesBlockingLane,
includesSyncLane,
NoLanes,
} from './ReactFiberLane';
import {resolveEventType, resolveEventTimeStamp} from './ReactFiberConfig';
@@ -88,6 +89,11 @@ export let transitionEventType: null | string = null; // Event type of the first
export let transitionEventIsRepeat: boolean = false;
export let transitionSuspendedTime: number = -1.1;
export let retryClampTime: number = -0;
export let idleClampTime: number = -0;
export let animatingLanes: Lanes = NoLanes;
export let yieldReason: SuspendedReason = (0: any);
export let yieldStartTime: number = -1.1; // The time when we yielded to the event loop
@@ -306,6 +312,20 @@ export function clampTransitionTimers(finalTime: number): void {
transitionClampTime = finalTime;
}
export function clampRetryTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
retryClampTime = finalTime;
}
export function clampIdleTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
idleClampTime = finalTime;
}
export function pushNestedEffectDurations(): number {
if (!enableProfilerTimer || !enableProfilerCommitHooks) {
return 0;
@@ -578,3 +598,11 @@ export function transferActualDuration(fiber: Fiber): void {
child = child.sibling;
}
}
export function startAnimating(lanes: Lanes): void {
animatingLanes |= lanes;
}
export function stopAnimating(lanes: Lanes): void {
animatingLanes &= ~lanes;
}
@@ -424,6 +424,7 @@ export function startViewTransition(
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();