diff --git a/fixtures/view-transition/src/components/SwipeRecognizer.js b/fixtures/view-transition/src/components/SwipeRecognizer.js index df4d743e1ba..6d628110237 100644 --- a/fixtures/view-transition/src/components/SwipeRecognizer.js +++ b/fixtures/view-transition/src/components/SwipeRecognizer.js @@ -114,16 +114,17 @@ export default function SwipeRecognizer({ ); } function onGestureEnd(changed) { - // Reset scroll - if (changed) { - // Trigger side-effects - startTransition(action); - } + // We cancel the gesture before invoking side-effects to allow the gesture lane to fully commit + // before scheduling new updates. if (activeGesture.current !== null) { const cancelGesture = activeGesture.current; activeGesture.current = null; cancelGesture(); } + if (changed) { + // Trigger side-effects + startTransition(action); + } } function onScrollEnd() { if (touchTimeline.current) { diff --git a/packages/react-reconciler/src/ReactFiberGestureScheduler.js b/packages/react-reconciler/src/ReactFiberGestureScheduler.js index 144d5f3aa5f..373e8167cab 100644 --- a/packages/react-reconciler/src/ReactFiberGestureScheduler.js +++ b/packages/react-reconciler/src/ReactFiberGestureScheduler.js @@ -35,6 +35,7 @@ export type ScheduledGesture = { rangeEnd: number, // The percentage along the timeline where the "destination" state is reached. types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition. running: null | RunningViewTransition, // Used to cancel the running transition after we're done. + commit: null | (() => void), // Callback to run to commit if there's a pending commit. committing: boolean, // If the gesture was released in a committed state and should actually commit. revertLane: Lane, // The Lane that we'll use to schedule the revert. prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root. @@ -64,6 +65,7 @@ export function scheduleGesture( rangeEnd: 100, // Uninitialized types: null, running: null, + commit: null, committing: false, revertLane: NoLane, // Starts uninitialized. prev: prev, @@ -164,9 +166,17 @@ export function cancelScheduledGesture( // lane to actually commit it. gesture.committing = true; if (root.pendingGestures === gesture) { - // Ping the root given the new state. This is similar to pingSuspendedRoot. - // This will either schedule the gesture lane to be committed possibly from its current state. - pingGestureRoot(root); + const commitCallback = gesture.commit; + if (commitCallback !== null) { + gesture.commit = null; + // If we already have a commit prepared we can immediately commit the tree + // without rerendering. + // TODO: Consider scheduling this in a task instead of synchronously inside the last cancellation.s + commitCallback(); + } else { + // Ping the root given the new state. This is similar to pingSuspendedRoot. + pingGestureRoot(root); + } } } else { // If we're not going to commit this gesture we can stop the View Transition @@ -235,3 +245,13 @@ export function stopCommittedGesture(root: FiberRoot) { } } } + +export function scheduleGestureCommit( + gesture: ScheduledGesture, + callback: () => void, +): () => void { + gesture.commit = callback; + return function () { + gesture.commit = null; + }; +} diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index f926bd6085b..a1a2d76cecc 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -1569,6 +1569,41 @@ export function logPaintYieldPhase( } } +export function logApplyGesturePhase( + startTime: number, + endTime: number, + debugTask: null | ConsoleTask, +): void { + if (supportsUserTiming) { + if (endTime <= startTime) { + return; + } + if (__DEV__ && debugTask) { + debugTask.run( + // $FlowFixMe[method-unbinding] + console.timeStamp.bind( + console, + 'Create Ghost Tree', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary-dark', + ), + ); + } else { + console.timeStamp( + 'Create Ghost Tree', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary-dark', + ); + } + } +} + export function logStartViewTransitionYieldPhase( startTime: number, endTime: number, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 1cc9d426ae1..5bc334364ad 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -92,6 +92,7 @@ import { logSuspendedYieldTime, setCurrentTrackFromLanes, markAllLanesInOrder, + logApplyGesturePhase, } from './ReactFiberPerformanceTrack'; import { @@ -398,7 +399,10 @@ import { } from './ReactFiberRootScheduler'; import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext'; import {logUncaughtError} from './ReactFiberErrorLogger'; -import {stopCommittedGesture} from './ReactFiberGestureScheduler'; +import { + scheduleGestureCommit, + stopCommittedGesture, +} from './ReactFiberGestureScheduler'; import {claimQueuedTransitionTypes} from './ReactFiberTransitionTypes'; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; @@ -1542,11 +1546,10 @@ function completeRootWhenReady( isViewTransitionEligible || (isGestureTransition && root.pendingGestures !== null && - // If we're committing this gesture and it already has a View Transition - // running, then we don't have to wait for that gesture. We'll stop it - // when we commit. - (root.pendingGestures.running === null || - !root.pendingGestures.committing)) + // If this gesture already has a View Transition running then we don't + // have to wait on that one before proceeding. We may hold the commit + // on the gesture committing later on in completeRoot. + root.pendingGestures.running === null) ) { // Wait for any pending View Transition (including gestures) to finish. suspendOnActiveViewTransition(suspendedState, root.containerInfo); @@ -3463,6 +3466,16 @@ function completeRoot( if (enableProfilerTimer && enableComponentPerformanceTrack) { // Log the previous render phase once we commit. I.e. we weren't interrupted. setCurrentTrackFromLanes(lanes); + if (isGestureRender(lanes)) { + // Clamp the render start time in case if something else on this lane was committed + // (such as this same tree before). + if (completedRenderStartTime < gestureClampTime) { + completedRenderStartTime = gestureClampTime; + } + if (completedRenderEndTime < gestureClampTime) { + completedRenderEndTime = gestureClampTime; + } + } if (exitStatus === RootErrored) { logErroredRenderPhase( completedRenderStartTime, @@ -3580,7 +3593,38 @@ function completeRoot( } else { // If we already have a gesture running, we don't update it in place // even if we have a new tree. Instead we wait until we can commit. + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Clamp at the render time since we're not going to finish the rest + // of this commit or apply yet. + finalizeRender(lanes, completedRenderEndTime); + } + // We are no longer committing. + pendingEffectsRoot = (null: any); // Clear for GC purposes. + pendingFinishedWork = (null: any); // Clear for GC purposes. + pendingEffectsLanes = NoLanes; } + // Schedule the root to be committed when the gesture completes. + root.cancelPendingCommit = scheduleGestureCommit( + committingGesture, + completeRoot.bind( + null, + root, + finishedWork, + lanes, + recoverableErrors, + transitions, + didIncludeRenderPhaseUpdate, + spawnedLane, + updatedLanes, + suspendedRetryLanes, + didSkipSuspendedSiblings, + exitStatus, + suspendedState, + 'Waiting for the Gesture to finish' /* suspendedCommitReason */, + completedRenderStartTime, + completedRenderEndTime, + ), + ); return; } } @@ -4368,6 +4412,15 @@ function flushGestureMutations(): void { ReactSharedInternals.T = prevTransition; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordCommitEndTime(); + logApplyGesturePhase( + pendingEffectsRenderEndTime, + commitEndTime, + animatingTask, + ); + } + pendingEffectsStatus = PENDING_GESTURE_ANIMATION_PHASE; } @@ -4385,10 +4438,11 @@ function flushGestureAnimations(): void { const lanes = pendingEffectsLanes; if (enableProfilerTimer && enableComponentPerformanceTrack) { + const startViewTransitionStartTime = commitEndTime; // Update the new commitEndTime to when we started the animation. recordCommitEndTime(); logStartViewTransitionYieldPhase( - pendingEffectsRenderEndTime, + startViewTransitionStartTime, commitEndTime, pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, animatingTask, @@ -4904,18 +4958,6 @@ export function pingGestureRoot(root: FiberRoot): void { if (gesture === null) { return; } - if ( - root.cancelPendingCommit !== null && - isGestureRender(pendingEffectsLanes) - ) { - // We have a suspended commit which we'll discard and rerender. - // TODO: Just use this commit since it's ready to go. - const cancelPendingCommit = root.cancelPendingCommit; - if (cancelPendingCommit !== null) { - root.cancelPendingCommit = null; - cancelPendingCommit(); - } - } // Ping it for rerender and commit. markRootPinged(root, GestureLane); ensureRootIsScheduled(root);