From e76bc3c35f97ebe68133ca89ca60db2f309e8fcd Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 20 Sep 2025 18:40:33 -0400 Subject: [PATCH 1/5] Log "Starting Animation" and "Animating" for Gesture Track --- .../src/client/ReactFiberConfigDOM.js | 13 +++++++++ .../src/ReactFiberConfigNative.js | 9 ++++++ .../src/ReactFiberWorkLoop.js | 28 +++++++++++++++++++ .../src/ReactFiberConfigTestHost.js | 5 ++++ 4 files changed, 55 insertions(+) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index d7ca1951e00aa..7d28dc43ca569 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -2320,6 +2320,9 @@ export function startViewTransition( mutationCallback(); layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. + if (enableProfilerTimer) { + finishedAnimation(); + } spawnedWorkCallback(); // Skip passiveCallback(). Spawned work will schedule a task. return null; @@ -2509,6 +2512,7 @@ export function startGestureTransition( mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { const ownerDocument: Document = rootContainer.nodeType === DOCUMENT_NODE @@ -2723,6 +2727,12 @@ export function startGestureTransition( // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; } + if (enableProfilerTimer) { + // Signal that the Transition was unable to continue. We do that here + // instead of when we stop the running View Transition to ensure that + // we cover cases when something else stops it early. + finishedAnimation(); + } }); return transition; } catch (x) { @@ -2735,6 +2745,9 @@ export function startGestureTransition( // Run through the sequence to put state back into a consistent state. mutationCallback(); animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } return null; } } diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 12b256e016fe2..89da4108fc9b8 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -35,6 +35,8 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; @@ -680,6 +682,9 @@ export function startViewTransition( layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. spawnedWorkCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } // Skip passiveCallback(). Spawned work will schedule a task. return null; } @@ -696,9 +701,13 @@ export function startGestureTransition( mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { mutationCallback(); animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } return null; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 331708fb195d4..23f669ce1a339 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -4268,6 +4268,9 @@ function commitGestureOnRoot( pendingTransitionTypes = finishedGesture.types; pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE; + if (enableProfilerTimer && enableComponentPerformanceTrack) { + startAnimating(pendingEffectsLanes); + } pendingViewTransition = finishedGesture.running = startGestureTransition( suspendedState, root.containerInfo, @@ -4278,6 +4281,10 @@ function commitGestureOnRoot( flushGestureMutations, flushGestureAnimations, reportViewTransitionError, + enableProfilerTimer + ? // This callback fires after "pendingEffects" so we need to snapshot the arguments. + finishedViewTransition.bind(null, pendingEffectsLanes) + : (null: any), ); } @@ -4320,6 +4327,23 @@ function flushGestureAnimations(): void { if (pendingEffectsStatus !== PENDING_GESTURE_ANIMATION_PHASE) { return; } + + const lanes = pendingEffectsLanes; + + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Update the new commitEndTime to when we started the animation. + recordCommitEndTime(); + logStartViewTransitionYieldPhase( + pendingEffectsRenderEndTime, + commitEndTime, + pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, + animatingTask, + ); + if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) { + pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT; + } + } + pendingEffectsStatus = NO_PENDING_EFFECTS; const root = pendingEffectsRoot; const finishedWork = pendingFinishedWork; @@ -4344,6 +4368,10 @@ function flushGestureAnimations(): void { ReactSharedInternals.T = prevTransition; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + finalizeRender(lanes, commitEndTime); + } + // Now that we've rendered this lane. Start working on the next lane. ensureRootIsScheduled(root); } diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index b5bddad8e6156..e18523bc04e92 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -17,6 +17,7 @@ import { NoEventPriority, type EventPriority, } from 'react-reconciler/src/ReactEventPriorities'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-test-renderer'; @@ -446,9 +447,13 @@ export function startGestureTransition( mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { mutationCallback(); animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } return null; } From 262164511e82b785a2346d6217b44ae2700827a3 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 20 Sep 2025 19:02:12 -0400 Subject: [PATCH 2/5] Every time we check includesBlockingLane we need to exclude gesture render --- packages/react-reconciler/src/ReactFiberWorkLoop.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 23f669ce1a339..4186456265240 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -2054,7 +2054,10 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { lanes, previousUpdateTask, ); - } else if (includesBlockingLane(animatingLanes)) { + } else if ( + !isGestureRender(animatingLanes) && + includesBlockingLane(animatingLanes) + ) { // If this lane is still animating, log the time from previous render finishing to now as animating. setCurrentTrackFromLanes(SyncLane); logAnimatingPhase( From 0a6ad8ac23b799962b0e07ec34ac0b65fc04791e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 20 Sep 2025 19:38:26 -0400 Subject: [PATCH 3/5] Clear update tracking if we skip rendering a gesture lane We need to clear any updates scheduled so that we can treat future updates as the cause of the render. --- packages/react-reconciler/src/ReactFiberWorkLoop.js | 6 ++++++ packages/react-reconciler/src/ReactProfilerTimer.js | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 4186456265240..6aa09ce0233a7 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -307,6 +307,7 @@ import { transitionSuspendedTime, clearBlockingTimers, clearGestureTimers, + clearGestureUpdates, clearTransitionTimers, clampBlockingTimers, clampGestureTimers, @@ -3531,6 +3532,11 @@ function commitRoot( // Gestures don't clear their lanes while the gesture is still active but it // might not be scheduled to do any more renders and so we shouldn't schedule // any more gesture lane work until a new gesture is scheduled. + if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) { + // We need to clear any updates scheduled so that we can treat future updates + // as the cause of the render. + clearGestureUpdates(); + } remainingLanes &= ~GestureLane; } diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index acaf540c6a6ca..b460ceb524e9a 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -371,6 +371,16 @@ export function clearGestureTimers(): void { gestureClampTime = now(); } +export function clearGestureUpdates(): void { + // Same as clearGestureTimers but doesn't reset the clamp time because we didn't + // actually emit a render. + gestureStartTime = -1.1; + gestureUpdateTime = -1.1; + gestureUpdateType = 0; + gestureSuspendedTime = -1.1; + gestureEventIsRepeat = true; +} + export function clampBlockingTimers(finalTime: number): void { if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; From 0589eef641a61f797613e0c51414fba18b3ae8b7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 21 Sep 2025 16:41:55 -0400 Subject: [PATCH 4/5] Use the Task for "exit" in clones If you're gesturing to something new and there are no updating or disappearing view transitions, then all view transitions are "exit" (because gestures are in reverse). These use the clone model which should track the animation task the same as applyViewTransitionToHostInstances. We also need to start the tracking earlier so it covers the insertion of clones. --- .../src/ReactFiberApplyGesture.js | 24 ++++++++++++++----- .../src/ReactFiberWorkLoop.js | 7 +++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index fa75a1bdbd219..f7ec9aaeb2a8c 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -77,6 +77,12 @@ import { getViewTransitionClassName, } from './ReactFiberViewTransitionComponent'; +import { + enableProfilerTimer, + enableComponentPerformanceTrack, +} from 'shared/ReactFeatureFlags'; +import {trackAnimatingTask} from './ReactProfilerTimer'; + let didWarnForRootClone = false; // Used during the apply phase to track whether a parent ViewTransition component @@ -101,6 +107,7 @@ function applyViewTransitionToClones( name: string, className: ?string, clones: Array, + fiber: Fiber, ): void { // This gets called when we have found a pair, but after the clone in created. The clone is // created by the insertion side. If the insertion side if found before the deletion side @@ -117,6 +124,11 @@ function applyViewTransitionToClones( className, ); } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + if (fiber._debugTask != null) { + trackAnimatingTask(fiber._debugTask); + } + } } function trackDeletedPairViewTransitions(deletion: Fiber): void { @@ -171,7 +183,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void { // If we have clones that means that we've already visited this // ViewTransition boundary before and we can now apply the name // to those clones. Otherwise, we have to wait until we clone it. - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, child); } } if (pairs.size === 0) { @@ -221,7 +233,7 @@ function trackEnterViewTransitions(deletion: Fiber): void { // If we have clones that means that we've already visited this // ViewTransition boundary before and we can now apply the name // to those clones. Otherwise, we have to wait until we clone it. - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, deletion); } } } @@ -266,7 +278,7 @@ function applyAppearingPairViewTransition(child: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, child); } } } @@ -296,7 +308,7 @@ function applyExitViewTransition(placement: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, placement); } } } @@ -314,7 +326,7 @@ function applyNestedViewTransition(child: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(name, className, clones); + applyViewTransitionToClones(name, className, clones, child); } } } @@ -346,7 +358,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void { // If there are no clones at this point, that should mean that there are no // HostComponent children in this ViewTransition. if (clones !== null) { - applyViewTransitionToClones(oldName, className, clones); + applyViewTransitionToClones(oldName, className, clones, finishedWork); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 6aa09ce0233a7..97b2691b5063d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -4260,6 +4260,10 @@ function commitGestureOnRoot( } deleteScheduledGesture(root, finishedGesture); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + startAnimating(pendingEffectsLanes); + } + const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); @@ -4277,9 +4281,6 @@ function commitGestureOnRoot( pendingTransitionTypes = finishedGesture.types; pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE; - if (enableProfilerTimer && enableComponentPerformanceTrack) { - startAnimating(pendingEffectsLanes); - } pendingViewTransition = finishedGesture.running = startGestureTransition( suspendedState, root.containerInfo, From 66c6a35f5879eedf638428a06fb80cba8e6f2bfd Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 21 Sep 2025 16:50:39 -0400 Subject: [PATCH 5/5] Delete "gestureStartTime" tracking We don't actually ever use this because startGestureTransition is not allowed to be async and so we never show the delta in time from when it starts until the optimistic update happens. --- .../src/ReactFiberPerformanceTrack.js | 50 +++---------------- .../src/ReactFiberWorkLoop.js | 6 --- .../src/ReactProfilerTimer.js | 46 ++++------------- 3 files changed, 17 insertions(+), 85 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index dfc6051d957a7..65cc7f0406688 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -753,7 +753,6 @@ export function logBlockingStart( } export function logGestureStart( - startTime: number, updateTime: number, eventTime: number, eventType: null | string, @@ -774,22 +773,15 @@ export function logGestureStart( } else { updateTime = renderStartTime; } - if (startTime > 0) { - if (startTime > updateTime) { - startTime = updateTime; - } - } else { - startTime = updateTime; - } if (eventTime > 0) { - if (eventTime > startTime) { - eventTime = startTime; + if (eventTime > updateTime) { + eventTime = updateTime; } } else { - eventTime = startTime; + eventTime = updateTime; } - if (startTime > eventTime && eventType !== null) { + if (updateTime > eventTime && eventType !== null) { // Log the time from the event timeStamp until we started a gesture. const color = eventIsRepeat ? 'secondary-light' : 'warning'; if (__DEV__ && debugTask) { @@ -798,7 +790,7 @@ export function logGestureStart( console, eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - startTime, + updateTime, currentTrack, LANES_TRACK_GROUP, color, @@ -808,36 +800,10 @@ export function logGestureStart( console.timeStamp( eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - startTime, - currentTrack, - LANES_TRACK_GROUP, - color, - ); - } - } - if (updateTime > startTime) { - // Log the time from when we started a gesture until we called setState or started rendering. - if (__DEV__ && debugTask) { - debugTask.run( - // $FlowFixMe[method-unbinding] - console.timeStamp.bind( - console, - 'Gesture', - startTime, - updateTime, - currentTrack, - LANES_TRACK_GROUP, - 'primary-dark', - ), - ); - } else { - console.timeStamp( - 'Gesture', - startTime, updateTime, currentTrack, LANES_TRACK_GROUP, - 'primary-dark', + color, ); } } @@ -846,8 +812,8 @@ export function logGestureStart( const label = isPingedUpdate ? 'Promise Resolved' : renderStartTime - updateTime > 5 - ? 'Update Blocked' - : 'Update'; + ? 'Gesture Blocked' + : 'Gesture'; if (__DEV__) { const properties = []; if (updateComponentName != null) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 97b2691b5063d..cd193d04e45fa 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -284,7 +284,6 @@ import { blockingEventIsRepeat, blockingSuspendedTime, gestureClampTime, - gestureStartTime, gestureUpdateTime, gestureUpdateTask, gestureUpdateType, @@ -1982,10 +1981,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressUpdateTask = null; if (isGestureRender(lanes)) { workInProgressUpdateTask = gestureUpdateTask; - const clampedStartTime = - gestureStartTime >= 0 && gestureStartTime < gestureClampTime - ? gestureClampTime - : gestureStartTime; const clampedUpdateTime = gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime ? gestureClampTime @@ -2019,7 +2014,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { ); } logGestureStart( - clampedStartTime, clampedUpdateTime, clampedEventTime, gestureEventType, diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index b460ceb524e9a..152810f85068c 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -77,7 +77,6 @@ export let blockingEventIsRepeat: boolean = false; export let blockingSuspendedTime: number = -1.1; export let gestureClampTime: number = -0; -export let gestureStartTime: number = -1.1; // First startGestureTransition call before setOptimistic. export let gestureUpdateTime: number = -1.1; // First setOptimistic scheduled inside startGestureTransition. export let gestureUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace. export let gestureUpdateType: UpdateType = 0; @@ -134,18 +133,16 @@ export function startUpdateTimerByLane( if (__DEV__ && fiber != null) { gestureUpdateComponentName = getComponentNameFromFiber(fiber); } - if (gestureStartTime < 0) { - const newEventTime = resolveEventTimeStamp(); - const newEventType = resolveEventType(); - if ( - newEventTime !== gestureEventTime || - newEventType !== gestureEventType - ) { - gestureEventIsRepeat = false; - } - gestureEventTime = newEventTime; - gestureEventType = newEventType; + const newEventTime = resolveEventTimeStamp(); + const newEventType = resolveEventType(); + if ( + newEventTime !== gestureEventTime || + newEventType !== gestureEventType + ) { + gestureEventIsRepeat = false; } + gestureEventTime = newEventTime; + gestureEventType = newEventType; } } else if (isBlockingLane(lane)) { if (blockingUpdateTime < 0) { @@ -334,36 +331,12 @@ export function clearTransitionTimers(): void { transitionClampTime = now(); } -export function startGestureTransitionTimer(): void { - if (!enableProfilerTimer || !enableComponentPerformanceTrack) { - return; - } - if (gestureStartTime < 0 && gestureUpdateTime < 0) { - gestureStartTime = now(); - const newEventTime = resolveEventTimeStamp(); - const newEventType = resolveEventType(); - if ( - newEventTime !== gestureEventTime || - newEventType !== gestureEventType - ) { - gestureEventIsRepeat = false; - } - gestureEventTime = newEventTime; - gestureEventType = newEventType; - } -} - export function hasScheduledGestureTransitionWork(): boolean { // If we have call setOptimistic on a gesture return gestureUpdateTime > -1; } -export function clearGestureTransitionTimer(): void { - gestureStartTime = -1.1; -} - export function clearGestureTimers(): void { - gestureStartTime = -1.1; gestureUpdateTime = -1.1; gestureUpdateType = 0; gestureSuspendedTime = -1.1; @@ -374,7 +347,6 @@ export function clearGestureTimers(): void { export function clearGestureUpdates(): void { // Same as clearGestureTimers but doesn't reset the clamp time because we didn't // actually emit a render. - gestureStartTime = -1.1; gestureUpdateTime = -1.1; gestureUpdateType = 0; gestureSuspendedTime = -1.1;