From e69babad86fa6628df3a6abedaea58762ea25d0a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 20:19:23 -0400 Subject: [PATCH 01/10] Clamp start times to simplify logic Also give a name to anonymous entries to help debugging --- .../src/ReactFiberPerformanceTrack.js | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 86f96d5d079..8fe2f2a8a95 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -636,11 +636,25 @@ export function logBlockingStart( ): void { if (supportsUserTiming) { currentTrack = 'Blocking'; + // Clamp start times + if (updateTime > 0) { + if (updateTime > renderStartTime) { + updateTime = renderStartTime; + } + } else { + updateTime = renderStartTime; + } + if (eventTime > 0) { + if (eventTime > updateTime) { + eventTime = updateTime; + } + } else { + eventTime = updateTime; + } // If a blocking update was spawned within render or an effect, that's considered a cascading render. // If you have a second blocking update within the same event, that suggests multiple flushSync or // setState in a microtask which is also considered a cascade. - const eventEndTime = updateTime > 0 ? updateTime : renderStartTime; - if (eventTime > 0 && eventType !== null && eventEndTime > eventTime) { + if (eventType !== null && updateTime > eventTime) { // Log the time from the event timeStamp until we called setState. const color = eventIsRepeat ? 'secondary-light' : 'warning'; if (__DEV__ && debugTask) { @@ -648,9 +662,9 @@ export function logBlockingStart( // $FlowFixMe[method-unbinding] console.timeStamp.bind( console, - eventIsRepeat ? '' : 'Event: ' + eventType, + eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - eventEndTime, + updateTime, currentTrack, LANES_TRACK_GROUP, color, @@ -658,16 +672,16 @@ export function logBlockingStart( ); } else { console.timeStamp( - eventIsRepeat ? '' : 'Event: ' + eventType, + eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - eventEndTime, + updateTime, currentTrack, LANES_TRACK_GROUP, color, ); } } - if (updateTime > 0 && renderStartTime > updateTime) { + if (renderStartTime > updateTime) { // Log the time from when we called setState until we started rendering. const color = isSpawnedUpdate ? 'error' @@ -739,18 +753,39 @@ export function logTransitionStart( ): void { if (supportsUserTiming) { currentTrack = 'Transition'; - const eventEndTime = - startTime > 0 ? startTime : updateTime > 0 ? updateTime : renderStartTime; - if (eventTime > 0 && eventEndTime > eventTime && eventType !== null) { + // Clamp start times + if (updateTime > 0) { + if (updateTime > renderStartTime) { + updateTime = renderStartTime; + } + } else { + updateTime = renderStartTime; + } + if (startTime > 0) { + if (startTime > updateTime) { + startTime = updateTime; + } + } else { + startTime = updateTime; + } + if (eventTime > 0) { + if (eventTime > startTime) { + eventTime = startTime; + } + } else { + eventTime = startTime; + } + + if (startTime > eventTime && eventType !== null) { // Log the time from the event timeStamp until we started a transition. const color = eventIsRepeat ? 'secondary-light' : 'warning'; if (__DEV__ && debugTask) { debugTask.run( console.timeStamp.bind( console, - eventIsRepeat ? '' : 'Event: ' + eventType, + eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - eventEndTime, + startTime, currentTrack, LANES_TRACK_GROUP, color, @@ -758,17 +793,16 @@ export function logTransitionStart( ); } else { console.timeStamp( - eventIsRepeat ? '' : 'Event: ' + eventType, + eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType, eventTime, - eventEndTime, + startTime, currentTrack, LANES_TRACK_GROUP, color, ); } } - const startEndTime = updateTime > 0 ? updateTime : renderStartTime; - if (startTime > 0 && startEndTime > startTime) { + if (updateTime > startTime) { // Log the time from when we started an async transition until we called setState or started rendering. // TODO: Ideally this would use the debugTask of the startTransition call perhaps. if (__DEV__ && debugTask) { @@ -778,7 +812,7 @@ export function logTransitionStart( console, 'Action', startTime, - startEndTime, + updateTime, currentTrack, LANES_TRACK_GROUP, 'primary-dark', @@ -788,14 +822,14 @@ export function logTransitionStart( console.timeStamp( 'Action', startTime, - startEndTime, + updateTime, currentTrack, LANES_TRACK_GROUP, 'primary-dark', ); } } - if (updateTime > 0 && renderStartTime > updateTime) { + if (renderStartTime > updateTime) { // Log the time from when we called setState until we started rendering. const label = isPingedUpdate ? 'Promise Resolved' @@ -1337,7 +1371,7 @@ export function logPaintYieldPhase( // $FlowFixMe[method-unbinding] console.timeStamp.bind( console, - delayedUntilPaint ? 'Waiting for Paint' : '', + delayedUntilPaint ? 'Waiting for Paint' : 'Waiting', startTime, endTime, currentTrack, @@ -1347,7 +1381,7 @@ export function logPaintYieldPhase( ); } else { console.timeStamp( - delayedUntilPaint ? 'Waiting for Paint' : '', + delayedUntilPaint ? 'Waiting for Paint' : 'Waiting', startTime, endTime, currentTrack, From 13d7460d74d042490d5d1b73c48521586c1913a0 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 21:25:58 -0400 Subject: [PATCH 02/10] Update clamp time when logging the event start time This ensures that we can't log another consecutive event on top of the first event which would overlap. Normally we do this in finalizeRender but it's currently possible to prepare a fresh stack that doesn't lead to a render which doesn't finalize. --- packages/react-reconciler/src/ReactProfilerTimer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index a1779235fc0..62b76ee6a40 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -246,6 +246,7 @@ export function clearBlockingTimers(): void { blockingUpdateComponentName = null; blockingSuspendedTime = -1.1; blockingEventIsRepeat = true; + blockingClampTime = now(); } export function startAsyncTransitionTimer(): void { @@ -282,6 +283,7 @@ export function clearTransitionTimers(): void { transitionUpdateType = 0; transitionSuspendedTime = -1.1; transitionEventIsRepeat = true; + transitionClampTime = now(); } export function clampBlockingTimers(finalTime: number): void { From 747deef71581b9f1b6dd608204372f57c19f017a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 21:39:42 -0400 Subject: [PATCH 03/10] Update test --- packages/react/src/__tests__/ReactProfiler-test.internal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index a8c00b2b562..ee1ec514e4d 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -179,6 +179,7 @@ describe(`onRender`, () => { 'read current time', 'read current time', 'read current time', + 'read current time', ]); } else { assertLog([ From 8e2591945ee91347ab8425214f25cc3f23274c8f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 15:56:17 -0400 Subject: [PATCH 04/10] Move commit phase logging to end of layout effects After mutation and flush spawned work will be part of "Starting Animation" or "Waiting for Paint". --- .../src/ReactFiberWorkLoop.js | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 79909cc25f1..e77eedfbbb3 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -3736,6 +3736,22 @@ function flushLayoutEffects(): void { ReactSharedInternals.T = prevTransition; } } + + const completedRenderEndTime = pendingEffectsRenderEndTime; + const suspendedCommitReason = pendingSuspendedCommitReason; + + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordCommitEndTime(); + logCommitPhase( + suspendedCommitReason === IMMEDIATE_COMMIT + ? completedRenderEndTime + : commitStartTime, + commitEndTime, + commitErrors, + workInProgressUpdateTask, + ); + } + pendingEffectsStatus = PENDING_AFTER_MUTATION_PHASE; } @@ -3759,22 +3775,8 @@ function flushSpawnedWork(): void { const root = pendingEffectsRoot; const finishedWork = pendingFinishedWork; const lanes = pendingEffectsLanes; - const completedRenderEndTime = pendingEffectsRenderEndTime; const recoverableErrors = pendingRecoverableErrors; const didIncludeRenderPhaseUpdate = pendingDidIncludeRenderPhaseUpdate; - const suspendedCommitReason = pendingSuspendedCommitReason; - - if (enableProfilerTimer && enableComponentPerformanceTrack) { - recordCommitEndTime(); - logCommitPhase( - suspendedCommitReason === IMMEDIATE_COMMIT - ? completedRenderEndTime - : commitStartTime, - commitEndTime, - commitErrors, - workInProgressUpdateTask, - ); - } const passiveSubtreeMask = enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes) From a10c239bde70dee0ee8575c654cd7e35866041d4 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 17:17:25 -0400 Subject: [PATCH 05/10] Log the time from when we commit to when the startViewTransition is ready Skip animation start time --- .../src/ReactFiberPerformanceTrack.js | 35 +++++++++++++++++++ .../src/ReactFiberWorkLoop.js | 17 +++++++++ 2 files changed, 52 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 8fe2f2a8a95..c70d4e2263a 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -1392,6 +1392,41 @@ export function logPaintYieldPhase( } } +export function logStartViewTransitionYieldPhase( + 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, + 'Starting Animation', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary-light', + ), + ); + } else { + console.timeStamp( + 'Starting Animation', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary-light', + ); + } + } +} + export function logPassiveCommitPhase( startTime: number, endTime: number, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index e77eedfbbb3..ff7a0676602 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -83,6 +83,7 @@ import { logSuspendedCommitPhase, logCommitPhase, logPaintYieldPhase, + logStartViewTransitionYieldPhase, logPassiveCommitPhase, logYieldTime, logActionYieldTime, @@ -3764,6 +3765,22 @@ function flushSpawnedWork(): void { ) { return; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // If we didn't skip the after mutation phase, when is means we started an animation. + const startedAnimation = pendingEffectsStatus === PENDING_SPAWNED_WORK; + if (startedAnimation) { + const startViewTransitionStartTime = commitEndTime; + // Update the new commitEndTime to when we started the animation. + recordCommitEndTime(); + logStartViewTransitionYieldPhase( + startViewTransitionStartTime, + commitEndTime, + pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, + workInProgressUpdateTask, // TODO: Use a ViewTransition Task. + ); + } + } + pendingEffectsStatus = NO_PENDING_EFFECTS; pendingViewTransition = null; // The view transition has now fully started. From 7f6330d1e4262b97be8bc8ffa22bbf2a4717beee Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 17:40:30 -0400 Subject: [PATCH 06/10] Track state for whether a flush was due to aborting a view transition early or due to a delayed passive effect flush --- .../src/ReactFiberRootScheduler.js | 3 ++- .../src/ReactFiberWorkLoop.js | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index 3ed7ad7e280..26b62552276 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -41,6 +41,7 @@ import { NoContext, RenderContext, flushPendingEffects, + flushPendingEffectsDelayed, getExecutionContext, getWorkInProgressRoot, getWorkInProgressRootRenderLanes, @@ -542,7 +543,7 @@ function performWorkOnRootViaSchedulerTask( // Flush any pending passive effects before deciding which lanes to work on, // in case they schedule additional work. const originalCallbackNode = root.callbackNode; - const didFlushPassiveEffects = flushPendingEffects(true); + const didFlushPassiveEffects = flushPendingEffectsDelayed(); if (didFlushPassiveEffects) { // Something in the passive effect phase may have canceled the current task. // Check if the task node for this root was changed. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index ff7a0676602..822940d1fde 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -675,6 +675,10 @@ const IMMEDIATE_COMMIT = 0; const SUSPENDED_COMMIT = 1; const THROTTLED_COMMIT = 2; +type DelayedCommitReason = 0 | 1 | 2; +const ABORTED_VIEW_TRANSITION_COMMIT = 1; +const DELAYED_PASSIVE_COMMIT = 2; + const NO_PENDING_EFFECTS = 0; const PENDING_MUTATION_PHASE = 1; const PENDING_LAYOUT_PHASE = 2; @@ -697,6 +701,7 @@ let pendingViewTransitionEvents: Array<(types: Array) => void> | null = let pendingTransitionTypes: null | TransitionTypes = null; let pendingDidIncludeRenderPhaseUpdate: boolean = false; let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only +let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only // Use these to prevent an infinite loop of nested updates const NESTED_UPDATE_LIMIT = 50; @@ -3437,6 +3442,7 @@ function commitRoot( if (enableProfilerTimer) { pendingEffectsRenderEndTime = completedRenderEndTime; pendingSuspendedCommitReason = suspendedCommitReason; + pendingDelayedCommitReason = IMMEDIATE_COMMIT; } if (enableGestureTransition && isGestureRender(lanes)) { @@ -3496,7 +3502,8 @@ function commitRoot( // event when logging events. trackSchedulerEvent(); } - flushPassiveEffects(true); + pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT; + flushPassiveEffects(); // This render triggered passive effects: release the root cache pool // *after* passive effects fire to avoid freeing a cache pool that may // be referenced by a node in the tree (HostRoot, Cache boundary etc) @@ -4160,7 +4167,12 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) { let didWarnAboutInterruptedViewTransitions = false; -export function flushPendingEffects(wasDelayedCommit?: boolean): boolean { +export function flushPendingEffectsDelayed(): boolean { + pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT; + return flushPendingEffects(); +} + +export function flushPendingEffects(): boolean { // Returns whether passive effects were flushed. if (enableViewTransition && pendingViewTransition !== null) { // If we forced a flush before the View Transition full started then we skip it. @@ -4178,6 +4190,7 @@ export function flushPendingEffects(wasDelayedCommit?: boolean): boolean { } } pendingViewTransition = null; + pendingDelayedCommitReason = ABORTED_VIEW_TRANSITION_COMMIT; } flushGestureMutations(); flushGestureAnimations(); @@ -4185,10 +4198,10 @@ export function flushPendingEffects(wasDelayedCommit?: boolean): boolean { flushLayoutEffects(); // Skip flushAfterMutation if we're forcing this early. flushSpawnedWork(); - return flushPassiveEffects(wasDelayedCommit); + return flushPassiveEffects(); } -function flushPassiveEffects(wasDelayedCommit?: boolean): boolean { +function flushPassiveEffects(): boolean { if (pendingEffectsStatus !== PENDING_PASSIVE_PHASE) { return false; } @@ -4213,7 +4226,7 @@ function flushPassiveEffects(wasDelayedCommit?: boolean): boolean { try { setCurrentUpdatePriority(priority); ReactSharedInternals.T = null; - return flushPassiveEffectsImpl(wasDelayedCommit); + return flushPassiveEffectsImpl(); } finally { setCurrentUpdatePriority(previousPriority); ReactSharedInternals.T = prevTransition; @@ -4225,7 +4238,7 @@ function flushPassiveEffects(wasDelayedCommit?: boolean): boolean { } } -function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { +function flushPassiveEffectsImpl() { // Cache and clear the transitions flag const transitions = pendingPassiveTransitions; pendingPassiveTransitions = null; @@ -4268,7 +4281,7 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { logPaintYieldPhase( commitEndTime, passiveEffectStartTime, - !!wasDelayedCommit, + pendingDelayedCommitReason === DELAYED_PASSIVE_COMMIT, workInProgressUpdateTask, ); } From c89d004438df986d656a998bba04a1df3925dc65 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 17:25:25 -0400 Subject: [PATCH 07/10] Log interrupted commit when we haven't reached the flushSpawnedWork before forcing a sync update --- .../src/ReactFiberPerformanceTrack.js | 24 ++++++++++++------- .../src/ReactFiberWorkLoop.js | 1 + 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index c70d4e2263a..57651b87ffb 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -1320,6 +1320,7 @@ export function logCommitPhase( startTime: number, endTime: number, errors: null | Array>, + abortedViewTransition: boolean, debugTask: null | ConsoleTask, ): void { if (errors !== null) { @@ -1335,22 +1336,24 @@ export function logCommitPhase( // $FlowFixMe[method-unbinding] console.timeStamp.bind( console, - 'Commit', + abortedViewTransition + ? 'Commit Interrupted View Transition' + : 'Commit', startTime, endTime, currentTrack, LANES_TRACK_GROUP, - 'secondary-dark', + abortedViewTransition ? 'error' : 'secondary-dark', ), ); } else { console.timeStamp( - 'Commit', + abortedViewTransition ? 'Commit Interrupted View Transition' : 'Commit', startTime, endTime, currentTrack, LANES_TRACK_GROUP, - 'secondary-dark', + abortedViewTransition ? 'error' : 'secondary-dark', ); } } @@ -1395,6 +1398,7 @@ export function logPaintYieldPhase( export function logStartViewTransitionYieldPhase( startTime: number, endTime: number, + abortedViewTransition: boolean, debugTask: null | ConsoleTask, ): void { if (supportsUserTiming) { @@ -1406,22 +1410,26 @@ export function logStartViewTransitionYieldPhase( // $FlowFixMe[method-unbinding] console.timeStamp.bind( console, - 'Starting Animation', + abortedViewTransition + ? 'Interrupted View Transition' + : 'Starting Animation', startTime, endTime, currentTrack, LANES_TRACK_GROUP, - 'secondary-light', + abortedViewTransition ? 'error' : 'secondary-light', ), ); } else { console.timeStamp( - 'Starting Animation', + abortedViewTransition + ? 'Interrupted View Transition' + : 'Starting Animation', startTime, endTime, currentTrack, LANES_TRACK_GROUP, - 'secondary-light', + abortedViewTransition ? ' error' : 'secondary-light', ); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 822940d1fde..00a4da26d7d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -3756,6 +3756,7 @@ function flushLayoutEffects(): void { : commitStartTime, commitEndTime, commitErrors, + pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, workInProgressUpdateTask, ); } From c9a7932d523567ced4f14d7f9d2110aff1d30fa5 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 19:37:04 -0400 Subject: [PATCH 08/10] Stash --- packages/react-reconciler/src/ReactFiberWorkLoop.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 00a4da26d7d..de7de66896d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -675,9 +675,10 @@ const IMMEDIATE_COMMIT = 0; const SUSPENDED_COMMIT = 1; const THROTTLED_COMMIT = 2; -type DelayedCommitReason = 0 | 1 | 2; +type DelayedCommitReason = 0 | 1 | 2 | 3; const ABORTED_VIEW_TRANSITION_COMMIT = 1; const DELAYED_PASSIVE_COMMIT = 2; +const ANIMATION_STARTED_COMMIT = 3; const NO_PENDING_EFFECTS = 0; const PENDING_MUTATION_PHASE = 1; @@ -3786,6 +3787,9 @@ function flushSpawnedWork(): void { pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, workInProgressUpdateTask, // TODO: Use a ViewTransition Task. ); + if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) { + pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT; + } } } From 9eb48426e31f07a7d0910bc62547a3316bf3cb9d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 19:51:11 -0400 Subject: [PATCH 09/10] Log the gap before the passive effects as animating --- .../src/ReactFiberPerformanceTrack.js | 35 +++++++++++++++++++ .../src/ReactFiberWorkLoop.js | 24 +++++++++---- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 57651b87ffb..92ca7e00e26 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -1435,6 +1435,41 @@ export function logStartViewTransitionYieldPhase( } } +export function logAnimatingPhase( + 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, + 'Animating', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary', + ), + ); + } else { + console.timeStamp( + 'Animating', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary', + ); + } + } +} + export function logPassiveCommitPhase( startTime: number, endTime: number, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index de7de66896d..6e384a0a609 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -84,6 +84,7 @@ import { logCommitPhase, logPaintYieldPhase, logStartViewTransitionYieldPhase, + logAnimatingPhase, logPassiveCommitPhase, logYieldTime, logActionYieldTime, @@ -3787,7 +3788,7 @@ function flushSpawnedWork(): void { pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, workInProgressUpdateTask, // TODO: Use a ViewTransition Task. ); - if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) { + if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) { pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT; } } @@ -4283,12 +4284,21 @@ function flushPassiveEffectsImpl() { if (enableProfilerTimer && enableComponentPerformanceTrack) { resetCommitErrors(); passiveEffectStartTime = now(); - logPaintYieldPhase( - commitEndTime, - passiveEffectStartTime, - pendingDelayedCommitReason === DELAYED_PASSIVE_COMMIT, - workInProgressUpdateTask, - ); + if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) { + // The animation was started, so we've been animating since that happened. + logAnimatingPhase( + commitEndTime, + passiveEffectStartTime, + workInProgressUpdateTask, // TODO: Use a ViewTransition Task + ); + } else { + logPaintYieldPhase( + commitEndTime, + passiveEffectStartTime, + pendingDelayedCommitReason === DELAYED_PASSIVE_COMMIT, + workInProgressUpdateTask, + ); + } } if (enableSchedulingProfiler) { From 7cc3919971b93647a92c34c40462deee45ef5b55 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 16 Sep 2025 21:32:45 -0400 Subject: [PATCH 10/10] Don't mark as delayed if we started or aborted an animation --- packages/react-reconciler/src/ReactFiberWorkLoop.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 6e384a0a609..d141c2855f6 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -3504,7 +3504,9 @@ function commitRoot( // event when logging events. trackSchedulerEvent(); } - pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT; + if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) { + pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT; + } flushPassiveEffects(); // This render triggered passive effects: release the root cache pool // *after* passive effects fire to avoid freeing a cache pool that may @@ -4174,7 +4176,9 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) { let didWarnAboutInterruptedViewTransitions = false; export function flushPendingEffectsDelayed(): boolean { - pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT; + if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) { + pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT; + } return flushPendingEffects(); }