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/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/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 331708fb195d4..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, @@ -307,6 +306,7 @@ import { transitionSuspendedTime, clearBlockingTimers, clearGestureTimers, + clearGestureUpdates, clearTransitionTimers, clampBlockingTimers, clampGestureTimers, @@ -1981,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 @@ -2018,7 +2014,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { ); } logGestureStart( - clampedStartTime, clampedUpdateTime, clampedEventTime, gestureEventType, @@ -2054,7 +2049,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( @@ -3528,6 +3526,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; } @@ -4251,6 +4254,10 @@ function commitGestureOnRoot( } deleteScheduledGesture(root, finishedGesture); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + startAnimating(pendingEffectsLanes); + } + const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); @@ -4278,6 +4285,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 +4331,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 +4372,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-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index acaf540c6a6ca..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; @@ -371,6 +344,15 @@ 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. + gestureUpdateTime = -1.1; + gestureUpdateType = 0; + gestureSuspendedTime = -1.1; + gestureEventIsRepeat = true; +} + export function clampBlockingTimers(finalTime: number): void { if (!enableProfilerTimer || !enableComponentPerformanceTrack) { return; 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; }