@samitouri / QOS-React / commits / b204edda3a

Log Custom Reason for the Suspended Commit Track (#34522)

Stacked on #34511. We currently log all Suspended Commit as "Suspended on Images or CSS" but it can really be other reasons too now. Like waiting on the previous View Transition. This allows the host config configure this reason. Now when one animation starts before another one finishes we log that as "Waiting for the previous Animation". <img width="592" height="257" alt="Screenshot 2025-09-17 at 11 53 45 PM" src="https://github.com/user-attachments/assets/817af8b5-37ae-46d8-bfd1-cd3fc637f3f3" />

Sebastian Markbåge committed Sep 20, 2025 at 11:01 UTC b204edda3aa397243faa267512811b778542f6a5
10 files changed +80 -61
packages/react-art/src/ReactFiberConfigART.js
+4
@@ -621,6 +621,10 @@ export function waitForCommitToBeReady(timeoutOffset) {
621 return null;
622 }
623
624 +export function getSuspendedCommitReason(state, rootContainer) {
625 + return null;
626 +}
627 +
628 export const NotPendingTransition = null;
629 export const HostTransitionContext: ReactContext<TransitionStatus> = {
630 $$typeof: REACT_CONTEXT_TYPE,
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+25
@@ -5965,6 +5965,7 @@ export opaque type SuspendedState = {
5965 imgBytes: number, // number of bytes we estimate needing to download
5966 suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
5967 waitingForImages: boolean, // false when we're no longer blocking on images
5968 + waitingForViewTransition: boolean,
5969 unsuspend: null | (() => void),
5970 };
5971
@@ -5976,6 +5977,7 @@ export function startSuspendingCommit(): SuspendedState {
5977 imgBytes: 0,
5978 suspenseyImages: [],
5979 waitingForImages: true,
5980 + waitingForViewTransition: false,
5981 // We use a noop function when we begin suspending because if possible we want the
5982 // waitfor step to finish synchronously. If it doesn't we'll return a function to
5983 // provide the actual unsuspend function and that will get completed when the count
@@ -6123,6 +6125,7 @@ export function suspendOnActiveViewTransition(
6125 return;
6126 }
6127 state.count++;
6128 + state.waitingForViewTransition = true;
6129 const ping = onUnsuspend.bind(state);
6130 activeViewTransition.finished.then(ping, ping);
6131 }
@@ -6206,6 +6209,28 @@ export function waitForCommitToBeReady(
6209 return null;
6210 }
6211
6212 +export function getSuspendedCommitReason(
6213 + state: SuspendedState,
6214 + rootContainer: Container,
6215 +): null | string {
6216 + if (state.waitingForViewTransition) {
6217 + return 'Waiting for the previous Animation';
6218 + }
6219 + if (state.count > 0) {
6220 + if (state.imgCount > 0) {
6221 + return 'Suspended on CSS and Images';
6222 + }
6223 + return 'Suspended on CSS';
6224 + }
6225 + if (state.imgCount === 1) {
6226 + return 'Suspended on an Image';
6227 + }
6228 + if (state.imgCount > 0) {
6229 + return 'Suspended on Images';
6230 + }
6231 + return null;
6232 +}
6233 +
6234 function checkIfFullyUnsuspended(state: SuspendedState) {
6235 if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
6236 if (state.stylesheets) {
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+7
@@ -627,6 +627,13 @@ export function waitForCommitToBeReady(
627 return null;
628 }
629
630 +export function getSuspendedCommitReason(
631 + state: SuspendedState,
632 + rootContainer: Container,
633 +): null | string {
634 + return null;
635 +}
636 +
637 export type FragmentInstanceType = {
638 _fragmentFiber: Fiber,
639 _observers: null | Set<IntersectionObserver>,
packages/react-native-renderer/src/ReactFiberConfigNative.js
+7
@@ -806,6 +806,13 @@ export function waitForCommitToBeReady(
806 return null;
807 }
808
809 +export function getSuspendedCommitReason(
810 + state: SuspendedState,
811 + rootContainer: Container,
812 +): null | string {
813 + return null;
814 +}
815 +
816 export const NotPendingTransition: TransitionStatus = null;
817 export const HostTransitionContext: ReactContext<TransitionStatus> = {
818 $$typeof: REACT_CONTEXT_TYPE,
packages/react-noop-renderer/src/createReactNoop.js
+7
@@ -702,6 +702,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
702
703 waitForCommitToBeReady,
704
705 + getSuspendedCommitReason(
706 + state: SuspendedState,
707 + rootContainer: Container,
708 + ): null | string {
709 + return null;
710 + },
711 +
712 NotPendingTransition: (null: TransitionStatus),
713
714 resetFormInstance(form: Instance) {},
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+3 -38
@@ -1180,45 +1180,10 @@ export function logInconsistentRender(
1180 }
1181 }
1182
1183 -export function logSuspenseThrottlePhase(
1184 - startTime: number,
1185 - endTime: number,
1186 - debugTask: null | ConsoleTask,
1187 -): void {
1188 - // This was inside a throttled Suspense boundary commit.
1189 - if (supportsUserTiming) {
1190 - if (endTime <= startTime) {
1191 - return;
1192 - }
1193 - if (__DEV__ && debugTask) {
1194 - debugTask.run(
1195 - // $FlowFixMe[method-unbinding]
1196 - console.timeStamp.bind(
1197 - console,
1198 - 'Throttled',
1199 - startTime,
1200 - endTime,
1201 - currentTrack,
1202 - LANES_TRACK_GROUP,
1203 - 'secondary-light',
1204 - ),
1205 - );
1206 - } else {
1207 - console.timeStamp(
1208 - 'Throttled',
1209 - startTime,
1210 - endTime,
1211 - currentTrack,
1212 - LANES_TRACK_GROUP,
1213 - 'secondary-light',
1214 - );
1215 - }
1216 - }
1217 -}
1218 -
1183 export function logSuspendedCommitPhase(
1184 startTime: number,
1185 endTime: number,
1186 + reason: string,
1187 debugTask: null | ConsoleTask,
1188 ): void {
1189 // This means the commit was suspended on CSS or images.
@@ -1233,7 +1198,7 @@ export function logSuspendedCommitPhase(
1198 // $FlowFixMe[method-unbinding]
1199 console.timeStamp.bind(
1200 console,
1236 - 'Suspended on CSS or Images',
1201 + reason,
1202 startTime,
1203 endTime,
1204 currentTrack,
@@ -1243,7 +1208,7 @@ export function logSuspendedCommitPhase(
1208 );
1209 } else {
1210 console.timeStamp(
1246 - 'Suspended on CSS or Images',
1211 + reason,
1212 startTime,
1213 endTime,
1214 currentTrack,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+16 -23
@@ -79,7 +79,6 @@ import {
79 logErroredRenderPhase,
80 logInconsistentRender,
81 logSuspendedWithDelayPhase,
82 - logSuspenseThrottlePhase,
82 logSuspendedCommitPhase,
83 logSuspendedViewTransitionPhase,
84 logCommitPhase,
@@ -103,6 +102,7 @@ import {
102 startSuspendingCommit,
103 suspendOnActiveViewTransition,
104 waitForCommitToBeReady,
105 + getSuspendedCommitReason,
106 preloadInstance,
107 preloadResource,
108 supportsHydration,
@@ -672,12 +672,10 @@ export function getRenderTargetTime(): number {
672
673 let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
674
675 -type SuspendedCommitReason = 0 | 1 | 2;
676 -const IMMEDIATE_COMMIT = 0;
677 -const SUSPENDED_COMMIT = 1;
678 -const THROTTLED_COMMIT = 2;
675 +type SuspendedCommitReason = null | string;
676
677 type DelayedCommitReason = 0 | 1 | 2 | 3;
678 +const IMMEDIATE_COMMIT = 0;
679 const ABORTED_VIEW_TRANSITION_COMMIT = 1;
680 const DELAYED_PASSIVE_COMMIT = 2;
681 const ANIMATION_STARTED_COMMIT = 3;
@@ -703,7 +701,7 @@ let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
701 null;
702 let pendingTransitionTypes: null | TransitionTypes = null;
703 let pendingDidIncludeRenderPhaseUpdate: boolean = false;
706 -let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
704 +let pendingSuspendedCommitReason: SuspendedCommitReason = null; // Profiling-only
705 let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
706 let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only
707
@@ -1391,7 +1389,7 @@ function finishConcurrentRender(
1389 workInProgressSuspendedRetryLanes,
1390 exitStatus,
1391 null,
1394 - IMMEDIATE_COMMIT,
1392 + null,
1393 renderStartTime,
1394 renderEndTime,
1395 );
@@ -1442,7 +1440,7 @@ function finishConcurrentRender(
1440 workInProgressSuspendedRetryLanes,
1441 workInProgressRootDidSkipSuspendedSiblings,
1442 exitStatus,
1445 - THROTTLED_COMMIT,
1443 + 'Throttled',
1444 renderStartTime,
1445 renderEndTime,
1446 ),
@@ -1463,7 +1461,7 @@ function finishConcurrentRender(
1461 workInProgressSuspendedRetryLanes,
1462 workInProgressRootDidSkipSuspendedSiblings,
1463 exitStatus,
1466 - IMMEDIATE_COMMIT,
1464 + null,
1465 renderStartTime,
1466 renderEndTime,
1467 );
@@ -1555,7 +1553,9 @@ function commitRootWhenReady(
1553 suspendedRetryLanes,
1554 exitStatus,
1555 suspendedState,
1558 - SUSPENDED_COMMIT,
1556 + enableProfilerTimer
1557 + ? getSuspendedCommitReason(suspendedState, root.containerInfo)
1558 + : null,
1559 completedRenderStartTime,
1560 completedRenderEndTime,
1561 ),
@@ -3458,7 +3458,7 @@ function commitRoot(
3458 recoverableErrors,
3459 suspendedState,
3460 enableProfilerTimer
3461 - ? suspendedCommitReason === IMMEDIATE_COMMIT
3461 + ? suspendedCommitReason === null
3462 ? completedRenderEndTime
3463 : commitStartTime
3464 : 0,
@@ -3530,16 +3530,11 @@ function commitRoot(
3530 resetCommitErrors();
3531 recordCommitTime();
3532 if (enableComponentPerformanceTrack) {
3533 - if (suspendedCommitReason === SUSPENDED_COMMIT) {
3533 + if (suspendedCommitReason !== null) {
3534 logSuspendedCommitPhase(
3535 completedRenderEndTime,
3536 commitStartTime,
3537 - workInProgressUpdateTask,
3538 - );
3539 - } else if (suspendedCommitReason === THROTTLED_COMMIT) {
3540 - logSuspenseThrottlePhase(
3541 - completedRenderEndTime,
3542 - commitStartTime,
3537 + suspendedCommitReason,
3538 workInProgressUpdateTask,
3539 );
3540 }
@@ -3633,7 +3628,7 @@ function suspendedViewTransition(reason: string): void {
3628 // We'll split the commit into two phases, because we're suspended in the middle.
3629 recordCommitEndTime();
3630 logCommitPhase(
3636 - pendingSuspendedCommitReason === IMMEDIATE_COMMIT
3631 + pendingSuspendedCommitReason === null
3632 ? pendingEffectsRenderEndTime
3633 : commitStartTime,
3634 commitEndTime,
@@ -3642,7 +3637,7 @@ function suspendedViewTransition(reason: string): void {
3637 workInProgressUpdateTask,
3638 );
3639 pendingSuspendedViewTransitionReason = reason;
3645 - pendingSuspendedCommitReason = SUSPENDED_COMMIT;
3640 + pendingSuspendedCommitReason = reason;
3641 }
3642 }
3643
@@ -3792,9 +3787,7 @@ function flushLayoutEffects(): void {
3787 if (enableProfilerTimer && enableComponentPerformanceTrack) {
3788 recordCommitEndTime();
3789 logCommitPhase(
3795 - suspendedCommitReason === IMMEDIATE_COMMIT
3796 - ? completedRenderEndTime
3797 - : commitStartTime,
3790 + suspendedCommitReason === null ? completedRenderEndTime : commitStartTime,
3791 commitEndTime,
3792 commitErrors,
3793 pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+3
@@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
114 waitForCommitToBeReady(state, timeoutOffset) {
115 return null;
116 },
117 + getSuspendedCommitReason(state, rootContainer) {
118 + return null;
119 + },
120 supportsMutation: true,
121 });
122
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+1
@@ -99,6 +99,7 @@ export const suspendInstance = $$$config.suspendInstance;
99 export const suspendOnActiveViewTransition =
100 $$$config.suspendOnActiveViewTransition;
101 export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
102 +export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason;
103 export const NotPendingTransition = $$$config.NotPendingTransition;
104 export const HostTransitionContext = $$$config.HostTransitionContext;
105 export const resetFormInstance = $$$config.resetFormInstance;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+7
@@ -589,6 +589,13 @@ export function waitForCommitToBeReady(
589 return null;
590 }
591
592 +export function getSuspendedCommitReason(
593 + state: SuspendedState,
594 + rootContainer: Container,
595 +): null | string {
596 + return null;
597 +}
598 +
599 export const NotPendingTransition: TransitionStatus = null;
600 export const HostTransitionContext: ReactContext<TransitionStatus> = {
601 $$typeof: REACT_CONTEXT_TYPE,