diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 5967d18a59f92..d7ca1951e00aa 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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; diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 3e6ea65db3137..12b256e016fe2 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -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(); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index eeaa43627ca4b..db69232d1295a 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -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(); diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 3248556fbe077..3e4f22b854104 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -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; } diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 31d90ee6a2177..c19ef704b20b2 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -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', ); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 7ccc8ad45803d..7003d25aff63e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -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. ); } } diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index 62b76ee6a407a..0891a4f5f5ee8 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -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; +} diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 004e9ae365752..b5bddad8e6156 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -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();