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"
/>
This commit is contained in:
Sebastian Markbåge 2025-09-20 11:01:52 -04:00 committed by GitHub
parent 115e3ec15f
commit b204edda3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 80 additions and 61 deletions

View File

@ -621,6 +621,10 @@ export function waitForCommitToBeReady(timeoutOffset) {
return null; return null;
} }
export function getSuspendedCommitReason(state, rootContainer) {
return null;
}
export const NotPendingTransition = null; export const NotPendingTransition = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = { export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE, $$typeof: REACT_CONTEXT_TYPE,

View File

@ -5965,6 +5965,7 @@ export opaque type SuspendedState = {
imgBytes: number, // number of bytes we estimate needing to download imgBytes: number, // number of bytes we estimate needing to download
suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not) suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
waitingForImages: boolean, // false when we're no longer blocking on images waitingForImages: boolean, // false when we're no longer blocking on images
waitingForViewTransition: boolean,
unsuspend: null | (() => void), unsuspend: null | (() => void),
}; };
@ -5976,6 +5977,7 @@ export function startSuspendingCommit(): SuspendedState {
imgBytes: 0, imgBytes: 0,
suspenseyImages: [], suspenseyImages: [],
waitingForImages: true, waitingForImages: true,
waitingForViewTransition: false,
// We use a noop function when we begin suspending because if possible we want the // We use a noop function when we begin suspending because if possible we want the
// waitfor step to finish synchronously. If it doesn't we'll return a function to // waitfor step to finish synchronously. If it doesn't we'll return a function to
// provide the actual unsuspend function and that will get completed when the count // provide the actual unsuspend function and that will get completed when the count
@ -6123,6 +6125,7 @@ export function suspendOnActiveViewTransition(
return; return;
} }
state.count++; state.count++;
state.waitingForViewTransition = true;
const ping = onUnsuspend.bind(state); const ping = onUnsuspend.bind(state);
activeViewTransition.finished.then(ping, ping); activeViewTransition.finished.then(ping, ping);
} }
@ -6206,6 +6209,28 @@ export function waitForCommitToBeReady(
return null; return null;
} }
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
if (state.waitingForViewTransition) {
return 'Waiting for the previous Animation';
}
if (state.count > 0) {
if (state.imgCount > 0) {
return 'Suspended on CSS and Images';
}
return 'Suspended on CSS';
}
if (state.imgCount === 1) {
return 'Suspended on an Image';
}
if (state.imgCount > 0) {
return 'Suspended on Images';
}
return null;
}
function checkIfFullyUnsuspended(state: SuspendedState) { function checkIfFullyUnsuspended(state: SuspendedState) {
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) { if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
if (state.stylesheets) { if (state.stylesheets) {

View File

@ -627,6 +627,13 @@ export function waitForCommitToBeReady(
return null; return null;
} }
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export type FragmentInstanceType = { export type FragmentInstanceType = {
_fragmentFiber: Fiber, _fragmentFiber: Fiber,
_observers: null | Set<IntersectionObserver>, _observers: null | Set<IntersectionObserver>,

View File

@ -806,6 +806,13 @@ export function waitForCommitToBeReady(
return null; return null;
} }
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export const NotPendingTransition: TransitionStatus = null; export const NotPendingTransition: TransitionStatus = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = { export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE, $$typeof: REACT_CONTEXT_TYPE,

View File

@ -702,6 +702,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
waitForCommitToBeReady, waitForCommitToBeReady,
getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
},
NotPendingTransition: (null: TransitionStatus), NotPendingTransition: (null: TransitionStatus),
resetFormInstance(form: Instance) {}, resetFormInstance(form: Instance) {},

View File

@ -1180,45 +1180,10 @@ export function logInconsistentRender(
} }
} }
export function logSuspenseThrottlePhase(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
// This was inside a throttled Suspense boundary commit.
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
export function logSuspendedCommitPhase( export function logSuspendedCommitPhase(
startTime: number, startTime: number,
endTime: number, endTime: number,
reason: string,
debugTask: null | ConsoleTask, debugTask: null | ConsoleTask,
): void { ): void {
// This means the commit was suspended on CSS or images. // This means the commit was suspended on CSS or images.
@ -1233,7 +1198,7 @@ export function logSuspendedCommitPhase(
// $FlowFixMe[method-unbinding] // $FlowFixMe[method-unbinding]
console.timeStamp.bind( console.timeStamp.bind(
console, console,
'Suspended on CSS or Images', reason,
startTime, startTime,
endTime, endTime,
currentTrack, currentTrack,
@ -1243,7 +1208,7 @@ export function logSuspendedCommitPhase(
); );
} else { } else {
console.timeStamp( console.timeStamp(
'Suspended on CSS or Images', reason,
startTime, startTime,
endTime, endTime,
currentTrack, currentTrack,

View File

@ -79,7 +79,6 @@ import {
logErroredRenderPhase, logErroredRenderPhase,
logInconsistentRender, logInconsistentRender,
logSuspendedWithDelayPhase, logSuspendedWithDelayPhase,
logSuspenseThrottlePhase,
logSuspendedCommitPhase, logSuspendedCommitPhase,
logSuspendedViewTransitionPhase, logSuspendedViewTransitionPhase,
logCommitPhase, logCommitPhase,
@ -103,6 +102,7 @@ import {
startSuspendingCommit, startSuspendingCommit,
suspendOnActiveViewTransition, suspendOnActiveViewTransition,
waitForCommitToBeReady, waitForCommitToBeReady,
getSuspendedCommitReason,
preloadInstance, preloadInstance,
preloadResource, preloadResource,
supportsHydration, supportsHydration,
@ -672,12 +672,10 @@ export function getRenderTargetTime(): number {
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null; let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
type SuspendedCommitReason = 0 | 1 | 2; type SuspendedCommitReason = null | string;
const IMMEDIATE_COMMIT = 0;
const SUSPENDED_COMMIT = 1;
const THROTTLED_COMMIT = 2;
type DelayedCommitReason = 0 | 1 | 2 | 3; type DelayedCommitReason = 0 | 1 | 2 | 3;
const IMMEDIATE_COMMIT = 0;
const ABORTED_VIEW_TRANSITION_COMMIT = 1; const ABORTED_VIEW_TRANSITION_COMMIT = 1;
const DELAYED_PASSIVE_COMMIT = 2; const DELAYED_PASSIVE_COMMIT = 2;
const ANIMATION_STARTED_COMMIT = 3; const ANIMATION_STARTED_COMMIT = 3;
@ -703,7 +701,7 @@ let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
null; null;
let pendingTransitionTypes: null | TransitionTypes = null; let pendingTransitionTypes: null | TransitionTypes = null;
let pendingDidIncludeRenderPhaseUpdate: boolean = false; let pendingDidIncludeRenderPhaseUpdate: boolean = false;
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only let pendingSuspendedCommitReason: SuspendedCommitReason = null; // Profiling-only
let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only
@ -1391,7 +1389,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes, workInProgressSuspendedRetryLanes,
exitStatus, exitStatus,
null, null,
IMMEDIATE_COMMIT, null,
renderStartTime, renderStartTime,
renderEndTime, renderEndTime,
); );
@ -1442,7 +1440,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes, workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings, workInProgressRootDidSkipSuspendedSiblings,
exitStatus, exitStatus,
THROTTLED_COMMIT, 'Throttled',
renderStartTime, renderStartTime,
renderEndTime, renderEndTime,
), ),
@ -1463,7 +1461,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes, workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings, workInProgressRootDidSkipSuspendedSiblings,
exitStatus, exitStatus,
IMMEDIATE_COMMIT, null,
renderStartTime, renderStartTime,
renderEndTime, renderEndTime,
); );
@ -1555,7 +1553,9 @@ function commitRootWhenReady(
suspendedRetryLanes, suspendedRetryLanes,
exitStatus, exitStatus,
suspendedState, suspendedState,
SUSPENDED_COMMIT, enableProfilerTimer
? getSuspendedCommitReason(suspendedState, root.containerInfo)
: null,
completedRenderStartTime, completedRenderStartTime,
completedRenderEndTime, completedRenderEndTime,
), ),
@ -3458,7 +3458,7 @@ function commitRoot(
recoverableErrors, recoverableErrors,
suspendedState, suspendedState,
enableProfilerTimer enableProfilerTimer
? suspendedCommitReason === IMMEDIATE_COMMIT ? suspendedCommitReason === null
? completedRenderEndTime ? completedRenderEndTime
: commitStartTime : commitStartTime
: 0, : 0,
@ -3530,16 +3530,11 @@ function commitRoot(
resetCommitErrors(); resetCommitErrors();
recordCommitTime(); recordCommitTime();
if (enableComponentPerformanceTrack) { if (enableComponentPerformanceTrack) {
if (suspendedCommitReason === SUSPENDED_COMMIT) { if (suspendedCommitReason !== null) {
logSuspendedCommitPhase( logSuspendedCommitPhase(
completedRenderEndTime, completedRenderEndTime,
commitStartTime, commitStartTime,
workInProgressUpdateTask, suspendedCommitReason,
);
} else if (suspendedCommitReason === THROTTLED_COMMIT) {
logSuspenseThrottlePhase(
completedRenderEndTime,
commitStartTime,
workInProgressUpdateTask, workInProgressUpdateTask,
); );
} }
@ -3633,7 +3628,7 @@ function suspendedViewTransition(reason: string): void {
// We'll split the commit into two phases, because we're suspended in the middle. // We'll split the commit into two phases, because we're suspended in the middle.
recordCommitEndTime(); recordCommitEndTime();
logCommitPhase( logCommitPhase(
pendingSuspendedCommitReason === IMMEDIATE_COMMIT pendingSuspendedCommitReason === null
? pendingEffectsRenderEndTime ? pendingEffectsRenderEndTime
: commitStartTime, : commitStartTime,
commitEndTime, commitEndTime,
@ -3642,7 +3637,7 @@ function suspendedViewTransition(reason: string): void {
workInProgressUpdateTask, workInProgressUpdateTask,
); );
pendingSuspendedViewTransitionReason = reason; pendingSuspendedViewTransitionReason = reason;
pendingSuspendedCommitReason = SUSPENDED_COMMIT; pendingSuspendedCommitReason = reason;
} }
} }
@ -3792,9 +3787,7 @@ function flushLayoutEffects(): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) { if (enableProfilerTimer && enableComponentPerformanceTrack) {
recordCommitEndTime(); recordCommitEndTime();
logCommitPhase( logCommitPhase(
suspendedCommitReason === IMMEDIATE_COMMIT suspendedCommitReason === null ? completedRenderEndTime : commitStartTime,
? completedRenderEndTime
: commitStartTime,
commitEndTime, commitEndTime,
commitErrors, commitErrors,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,

View File

@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
waitForCommitToBeReady(state, timeoutOffset) { waitForCommitToBeReady(state, timeoutOffset) {
return null; return null;
}, },
getSuspendedCommitReason(state, rootContainer) {
return null;
},
supportsMutation: true, supportsMutation: true,
}); });

View File

@ -99,6 +99,7 @@ export const suspendInstance = $$$config.suspendInstance;
export const suspendOnActiveViewTransition = export const suspendOnActiveViewTransition =
$$$config.suspendOnActiveViewTransition; $$$config.suspendOnActiveViewTransition;
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady; export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason;
export const NotPendingTransition = $$$config.NotPendingTransition; export const NotPendingTransition = $$$config.NotPendingTransition;
export const HostTransitionContext = $$$config.HostTransitionContext; export const HostTransitionContext = $$$config.HostTransitionContext;
export const resetFormInstance = $$$config.resetFormInstance; export const resetFormInstance = $$$config.resetFormInstance;

View File

@ -589,6 +589,13 @@ export function waitForCommitToBeReady(
return null; return null;
} }
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export const NotPendingTransition: TransitionStatus = null; export const NotPendingTransition: TransitionStatus = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = { export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE, $$typeof: REACT_CONTEXT_TYPE,