Skip to content

Commit e233218

Browse files
authored
Track "Animating" Entry for Gestures while the Gesture is Still On-going (#34548)
Stacked on #34546. Same as #34538 but for gestures. Includes various fixes. This shows how it ends with a Transition when you release in the committed state. Note how the Animation of the Gesture continues until the Transition is done so that the handoff is seamless. <img width="853" height="134" alt="Screenshot 2025-09-20 at 7 37 29 PM" src="https://github.com/user-attachments/assets/6192a033-4bec-43b9-884b-77e3a6f00da6" />
1 parent 05b61f8 commit e233218

File tree

7 files changed

+110
-91
lines changed

7 files changed

+110
-91
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,6 +2320,9 @@ export function startViewTransition(
23202320
mutationCallback();
23212321
layoutCallback();
23222322
// Skip afterMutationCallback(). We don't need it since we're not animating.
2323+
if (enableProfilerTimer) {
2324+
finishedAnimation();
2325+
}
23232326
spawnedWorkCallback();
23242327
// Skip passiveCallback(). Spawned work will schedule a task.
23252328
return null;
@@ -2509,6 +2512,7 @@ export function startGestureTransition(
25092512
mutationCallback: () => void,
25102513
animateCallback: () => void,
25112514
errorCallback: mixed => void,
2515+
finishedAnimation: () => void, // Profiling-only
25122516
): null | RunningViewTransition {
25132517
const ownerDocument: Document =
25142518
rootContainer.nodeType === DOCUMENT_NODE
@@ -2723,6 +2727,12 @@ export function startGestureTransition(
27232727
// $FlowFixMe[prop-missing]
27242728
ownerDocument.__reactViewTransition = null;
27252729
}
2730+
if (enableProfilerTimer) {
2731+
// Signal that the Transition was unable to continue. We do that here
2732+
// instead of when we stop the running View Transition to ensure that
2733+
// we cover cases when something else stops it early.
2734+
finishedAnimation();
2735+
}
27262736
});
27272737
return transition;
27282738
} catch (x) {
@@ -2735,6 +2745,9 @@ export function startGestureTransition(
27352745
// Run through the sequence to put state back into a consistent state.
27362746
mutationCallback();
27372747
animateCallback();
2748+
if (enableProfilerTimer) {
2749+
finishedAnimation();
2750+
}
27382751
return null;
27392752
}
27402753
}

packages/react-native-renderer/src/ReactFiberConfigNative.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
} from 'react-reconciler/src/ReactEventPriorities';
3636
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
3737

38+
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
39+
3840
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
3941
import type {ReactContext} from 'shared/ReactTypes';
4042

@@ -680,6 +682,9 @@ export function startViewTransition(
680682
layoutCallback();
681683
// Skip afterMutationCallback(). We don't need it since we're not animating.
682684
spawnedWorkCallback();
685+
if (enableProfilerTimer) {
686+
finishedAnimation();
687+
}
683688
// Skip passiveCallback(). Spawned work will schedule a task.
684689
return null;
685690
}
@@ -696,9 +701,13 @@ export function startGestureTransition(
696701
mutationCallback: () => void,
697702
animateCallback: () => void,
698703
errorCallback: mixed => void,
704+
finishedAnimation: () => void, // Profiling-only
699705
): null | RunningViewTransition {
700706
mutationCallback();
701707
animateCallback();
708+
if (enableProfilerTimer) {
709+
finishedAnimation();
710+
}
702711
return null;
703712
}
704713

packages/react-reconciler/src/ReactFiberApplyGesture.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ import {
7777
getViewTransitionClassName,
7878
} from './ReactFiberViewTransitionComponent';
7979

80+
import {
81+
enableProfilerTimer,
82+
enableComponentPerformanceTrack,
83+
} from 'shared/ReactFeatureFlags';
84+
import {trackAnimatingTask} from './ReactProfilerTimer';
85+
8086
let didWarnForRootClone = false;
8187

8288
// Used during the apply phase to track whether a parent ViewTransition component
@@ -101,6 +107,7 @@ function applyViewTransitionToClones(
101107
name: string,
102108
className: ?string,
103109
clones: Array<Instance>,
110+
fiber: Fiber,
104111
): void {
105112
// This gets called when we have found a pair, but after the clone in created. The clone is
106113
// created by the insertion side. If the insertion side if found before the deletion side
@@ -117,6 +124,11 @@ function applyViewTransitionToClones(
117124
className,
118125
);
119126
}
127+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
128+
if (fiber._debugTask != null) {
129+
trackAnimatingTask(fiber._debugTask);
130+
}
131+
}
120132
}
121133

122134
function trackDeletedPairViewTransitions(deletion: Fiber): void {
@@ -171,7 +183,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
171183
// If we have clones that means that we've already visited this
172184
// ViewTransition boundary before and we can now apply the name
173185
// to those clones. Otherwise, we have to wait until we clone it.
174-
applyViewTransitionToClones(name, className, clones);
186+
applyViewTransitionToClones(name, className, clones, child);
175187
}
176188
}
177189
if (pairs.size === 0) {
@@ -221,7 +233,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
221233
// If we have clones that means that we've already visited this
222234
// ViewTransition boundary before and we can now apply the name
223235
// to those clones. Otherwise, we have to wait until we clone it.
224-
applyViewTransitionToClones(name, className, clones);
236+
applyViewTransitionToClones(name, className, clones, deletion);
225237
}
226238
}
227239
}
@@ -266,7 +278,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
266278
// If there are no clones at this point, that should mean that there are no
267279
// HostComponent children in this ViewTransition.
268280
if (clones !== null) {
269-
applyViewTransitionToClones(name, className, clones);
281+
applyViewTransitionToClones(name, className, clones, child);
270282
}
271283
}
272284
}
@@ -296,7 +308,7 @@ function applyExitViewTransition(placement: Fiber): void {
296308
// If there are no clones at this point, that should mean that there are no
297309
// HostComponent children in this ViewTransition.
298310
if (clones !== null) {
299-
applyViewTransitionToClones(name, className, clones);
311+
applyViewTransitionToClones(name, className, clones, placement);
300312
}
301313
}
302314
}
@@ -314,7 +326,7 @@ function applyNestedViewTransition(child: Fiber): void {
314326
// If there are no clones at this point, that should mean that there are no
315327
// HostComponent children in this ViewTransition.
316328
if (clones !== null) {
317-
applyViewTransitionToClones(name, className, clones);
329+
applyViewTransitionToClones(name, className, clones, child);
318330
}
319331
}
320332
}
@@ -346,7 +358,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
346358
// If there are no clones at this point, that should mean that there are no
347359
// HostComponent children in this ViewTransition.
348360
if (clones !== null) {
349-
applyViewTransitionToClones(oldName, className, clones);
361+
applyViewTransitionToClones(oldName, className, clones, finishedWork);
350362
}
351363
}
352364

packages/react-reconciler/src/ReactFiberPerformanceTrack.js

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,6 @@ export function logBlockingStart(
753753
}
754754

755755
export function logGestureStart(
756-
startTime: number,
757756
updateTime: number,
758757
eventTime: number,
759758
eventType: null | string,
@@ -774,22 +773,15 @@ export function logGestureStart(
774773
} else {
775774
updateTime = renderStartTime;
776775
}
777-
if (startTime > 0) {
778-
if (startTime > updateTime) {
779-
startTime = updateTime;
780-
}
781-
} else {
782-
startTime = updateTime;
783-
}
784776
if (eventTime > 0) {
785-
if (eventTime > startTime) {
786-
eventTime = startTime;
777+
if (eventTime > updateTime) {
778+
eventTime = updateTime;
787779
}
788780
} else {
789-
eventTime = startTime;
781+
eventTime = updateTime;
790782
}
791783

792-
if (startTime > eventTime && eventType !== null) {
784+
if (updateTime > eventTime && eventType !== null) {
793785
// Log the time from the event timeStamp until we started a gesture.
794786
const color = eventIsRepeat ? 'secondary-light' : 'warning';
795787
if (__DEV__ && debugTask) {
@@ -798,7 +790,7 @@ export function logGestureStart(
798790
console,
799791
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
800792
eventTime,
801-
startTime,
793+
updateTime,
802794
currentTrack,
803795
LANES_TRACK_GROUP,
804796
color,
@@ -808,36 +800,10 @@ export function logGestureStart(
808800
console.timeStamp(
809801
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
810802
eventTime,
811-
startTime,
812-
currentTrack,
813-
LANES_TRACK_GROUP,
814-
color,
815-
);
816-
}
817-
}
818-
if (updateTime > startTime) {
819-
// Log the time from when we started a gesture until we called setState or started rendering.
820-
if (__DEV__ && debugTask) {
821-
debugTask.run(
822-
// $FlowFixMe[method-unbinding]
823-
console.timeStamp.bind(
824-
console,
825-
'Gesture',
826-
startTime,
827-
updateTime,
828-
currentTrack,
829-
LANES_TRACK_GROUP,
830-
'primary-dark',
831-
),
832-
);
833-
} else {
834-
console.timeStamp(
835-
'Gesture',
836-
startTime,
837803
updateTime,
838804
currentTrack,
839805
LANES_TRACK_GROUP,
840-
'primary-dark',
806+
color,
841807
);
842808
}
843809
}
@@ -846,8 +812,8 @@ export function logGestureStart(
846812
const label = isPingedUpdate
847813
? 'Promise Resolved'
848814
: renderStartTime - updateTime > 5
849-
? 'Update Blocked'
850-
: 'Update';
815+
? 'Gesture Blocked'
816+
: 'Gesture';
851817
if (__DEV__) {
852818
const properties = [];
853819
if (updateComponentName != null) {

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ import {
284284
blockingEventIsRepeat,
285285
blockingSuspendedTime,
286286
gestureClampTime,
287-
gestureStartTime,
288287
gestureUpdateTime,
289288
gestureUpdateTask,
290289
gestureUpdateType,
@@ -307,6 +306,7 @@ import {
307306
transitionSuspendedTime,
308307
clearBlockingTimers,
309308
clearGestureTimers,
309+
clearGestureUpdates,
310310
clearTransitionTimers,
311311
clampBlockingTimers,
312312
clampGestureTimers,
@@ -1981,10 +1981,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
19811981
workInProgressUpdateTask = null;
19821982
if (isGestureRender(lanes)) {
19831983
workInProgressUpdateTask = gestureUpdateTask;
1984-
const clampedStartTime =
1985-
gestureStartTime >= 0 && gestureStartTime < gestureClampTime
1986-
? gestureClampTime
1987-
: gestureStartTime;
19881984
const clampedUpdateTime =
19891985
gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime
19901986
? gestureClampTime
@@ -2018,7 +2014,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
20182014
);
20192015
}
20202016
logGestureStart(
2021-
clampedStartTime,
20222017
clampedUpdateTime,
20232018
clampedEventTime,
20242019
gestureEventType,
@@ -2054,7 +2049,10 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
20542049
lanes,
20552050
previousUpdateTask,
20562051
);
2057-
} else if (includesBlockingLane(animatingLanes)) {
2052+
} else if (
2053+
!isGestureRender(animatingLanes) &&
2054+
includesBlockingLane(animatingLanes)
2055+
) {
20582056
// If this lane is still animating, log the time from previous render finishing to now as animating.
20592057
setCurrentTrackFromLanes(SyncLane);
20602058
logAnimatingPhase(
@@ -3528,6 +3526,11 @@ function commitRoot(
35283526
// Gestures don't clear their lanes while the gesture is still active but it
35293527
// might not be scheduled to do any more renders and so we shouldn't schedule
35303528
// any more gesture lane work until a new gesture is scheduled.
3529+
if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) {
3530+
// We need to clear any updates scheduled so that we can treat future updates
3531+
// as the cause of the render.
3532+
clearGestureUpdates();
3533+
}
35313534
remainingLanes &= ~GestureLane;
35323535
}
35333536

@@ -4251,6 +4254,10 @@ function commitGestureOnRoot(
42514254
}
42524255
deleteScheduledGesture(root, finishedGesture);
42534256

4257+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
4258+
startAnimating(pendingEffectsLanes);
4259+
}
4260+
42544261
const prevTransition = ReactSharedInternals.T;
42554262
ReactSharedInternals.T = null;
42564263
const previousPriority = getCurrentUpdatePriority();
@@ -4278,6 +4285,10 @@ function commitGestureOnRoot(
42784285
flushGestureMutations,
42794286
flushGestureAnimations,
42804287
reportViewTransitionError,
4288+
enableProfilerTimer
4289+
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
4290+
finishedViewTransition.bind(null, pendingEffectsLanes)
4291+
: (null: any),
42814292
);
42824293
}
42834294

@@ -4320,6 +4331,23 @@ function flushGestureAnimations(): void {
43204331
if (pendingEffectsStatus !== PENDING_GESTURE_ANIMATION_PHASE) {
43214332
return;
43224333
}
4334+
4335+
const lanes = pendingEffectsLanes;
4336+
4337+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
4338+
// Update the new commitEndTime to when we started the animation.
4339+
recordCommitEndTime();
4340+
logStartViewTransitionYieldPhase(
4341+
pendingEffectsRenderEndTime,
4342+
commitEndTime,
4343+
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
4344+
animatingTask,
4345+
);
4346+
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
4347+
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
4348+
}
4349+
}
4350+
43234351
pendingEffectsStatus = NO_PENDING_EFFECTS;
43244352
const root = pendingEffectsRoot;
43254353
const finishedWork = pendingFinishedWork;
@@ -4344,6 +4372,10 @@ function flushGestureAnimations(): void {
43444372
ReactSharedInternals.T = prevTransition;
43454373
}
43464374

4375+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
4376+
finalizeRender(lanes, commitEndTime);
4377+
}
4378+
43474379
// Now that we've rendered this lane. Start working on the next lane.
43484380
ensureRootIsScheduled(root);
43494381
}

0 commit comments

Comments
 (0)