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;
}
export function getSuspendedCommitReason(state, rootContainer) {
return null;
}
export const NotPendingTransition = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@ -5965,6 +5965,7 @@ export opaque type SuspendedState = {
imgBytes: number, // number of bytes we estimate needing to download
suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
waitingForImages: boolean, // false when we're no longer blocking on images
waitingForViewTransition: boolean,
unsuspend: null | (() => void),
};
@ -5976,6 +5977,7 @@ export function startSuspendingCommit(): SuspendedState {
imgBytes: 0,
suspenseyImages: [],
waitingForImages: true,
waitingForViewTransition: false,
// 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
// provide the actual unsuspend function and that will get completed when the count
@ -6123,6 +6125,7 @@ export function suspendOnActiveViewTransition(
return;
}
state.count++;
state.waitingForViewTransition = true;
const ping = onUnsuspend.bind(state);
activeViewTransition.finished.then(ping, ping);
}
@ -6206,6 +6209,28 @@ export function waitForCommitToBeReady(
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) {
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
if (state.stylesheets) {

View File

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

View File

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

View File

@ -702,6 +702,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
waitForCommitToBeReady,
getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
},
NotPendingTransition: (null: TransitionStatus),
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(
startTime: number,
endTime: number,
reason: string,
debugTask: null | ConsoleTask,
): void {
// This means the commit was suspended on CSS or images.
@ -1233,7 +1198,7 @@ export function logSuspendedCommitPhase(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Suspended on CSS or Images',
reason,
startTime,
endTime,
currentTrack,
@ -1243,7 +1208,7 @@ export function logSuspendedCommitPhase(
);
} else {
console.timeStamp(
'Suspended on CSS or Images',
reason,
startTime,
endTime,
currentTrack,

View File

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

View File

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

View File

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

View File

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