Skip to content

Commit 84af908

Browse files
authored
Log Performance Track Entries for View Transitions (#34510)
Stacked on #34509. View Transitions introduces a bunch of new types of gaps in the commit phase which needs to be logged differently in the performance track. One thing that can happen is that a `flushSync` update forces the View Transition to abort before it has started if it happens in the gap before the transition is ready. In that case we log "Interrupted View Transition". Otherwise, when we're done in `startViewTransition` there's some work to finalize the animations before the `ready` calllback. This is logged as "Starting Animation". Then there's a gap before the passive effects fire which we log as "Animating". This can be long unless they're forced to flush early e.g. due to another lane updating. The "Animating" track should then pick up which doesn't do yet. This one is tricky because this is after the actual commit phase and needs to be interrupted by new renders which themselves can be suspended on the animation finshing. This PR is just a subset of all the cases. Will need a lot more work. <img width="679" height="161" alt="Screenshot 2025-09-16 at 10 19 06 PM" src="https://github.com/user-attachments/assets/0407372d-aaed-41f5-a262-059b2686ae87" />
1 parent 128abcf commit 84af908

File tree

3 files changed

+161
-31
lines changed

3 files changed

+161
-31
lines changed

packages/react-reconciler/src/ReactFiberPerformanceTrack.js

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,7 @@ export function logCommitPhase(
13201320
startTime: number,
13211321
endTime: number,
13221322
errors: null | Array<CapturedValue<mixed>>,
1323+
abortedViewTransition: boolean,
13231324
debugTask: null | ConsoleTask,
13241325
): void {
13251326
if (errors !== null) {
@@ -1335,22 +1336,24 @@ export function logCommitPhase(
13351336
// $FlowFixMe[method-unbinding]
13361337
console.timeStamp.bind(
13371338
console,
1338-
'Commit',
1339+
abortedViewTransition
1340+
? 'Commit Interrupted View Transition'
1341+
: 'Commit',
13391342
startTime,
13401343
endTime,
13411344
currentTrack,
13421345
LANES_TRACK_GROUP,
1343-
'secondary-dark',
1346+
abortedViewTransition ? 'error' : 'secondary-dark',
13441347
),
13451348
);
13461349
} else {
13471350
console.timeStamp(
1348-
'Commit',
1351+
abortedViewTransition ? 'Commit Interrupted View Transition' : 'Commit',
13491352
startTime,
13501353
endTime,
13511354
currentTrack,
13521355
LANES_TRACK_GROUP,
1353-
'secondary-dark',
1356+
abortedViewTransition ? 'error' : 'secondary-dark',
13541357
);
13551358
}
13561359
}
@@ -1392,6 +1395,81 @@ export function logPaintYieldPhase(
13921395
}
13931396
}
13941397

1398+
export function logStartViewTransitionYieldPhase(
1399+
startTime: number,
1400+
endTime: number,
1401+
abortedViewTransition: boolean,
1402+
debugTask: null | ConsoleTask,
1403+
): void {
1404+
if (supportsUserTiming) {
1405+
if (endTime <= startTime) {
1406+
return;
1407+
}
1408+
if (__DEV__ && debugTask) {
1409+
debugTask.run(
1410+
// $FlowFixMe[method-unbinding]
1411+
console.timeStamp.bind(
1412+
console,
1413+
abortedViewTransition
1414+
? 'Interrupted View Transition'
1415+
: 'Starting Animation',
1416+
startTime,
1417+
endTime,
1418+
currentTrack,
1419+
LANES_TRACK_GROUP,
1420+
abortedViewTransition ? 'error' : 'secondary-light',
1421+
),
1422+
);
1423+
} else {
1424+
console.timeStamp(
1425+
abortedViewTransition
1426+
? 'Interrupted View Transition'
1427+
: 'Starting Animation',
1428+
startTime,
1429+
endTime,
1430+
currentTrack,
1431+
LANES_TRACK_GROUP,
1432+
abortedViewTransition ? ' error' : 'secondary-light',
1433+
);
1434+
}
1435+
}
1436+
}
1437+
1438+
export function logAnimatingPhase(
1439+
startTime: number,
1440+
endTime: number,
1441+
debugTask: null | ConsoleTask,
1442+
): void {
1443+
if (supportsUserTiming) {
1444+
if (endTime <= startTime) {
1445+
return;
1446+
}
1447+
if (__DEV__ && debugTask) {
1448+
debugTask.run(
1449+
// $FlowFixMe[method-unbinding]
1450+
console.timeStamp.bind(
1451+
console,
1452+
'Animating',
1453+
startTime,
1454+
endTime,
1455+
currentTrack,
1456+
LANES_TRACK_GROUP,
1457+
'secondary',
1458+
),
1459+
);
1460+
} else {
1461+
console.timeStamp(
1462+
'Animating',
1463+
startTime,
1464+
endTime,
1465+
currentTrack,
1466+
LANES_TRACK_GROUP,
1467+
'secondary',
1468+
);
1469+
}
1470+
}
1471+
}
1472+
13951473
export function logPassiveCommitPhase(
13961474
startTime: number,
13971475
endTime: number,

packages/react-reconciler/src/ReactFiberRootScheduler.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
NoContext,
4242
RenderContext,
4343
flushPendingEffects,
44+
flushPendingEffectsDelayed,
4445
getExecutionContext,
4546
getWorkInProgressRoot,
4647
getWorkInProgressRootRenderLanes,
@@ -542,7 +543,7 @@ function performWorkOnRootViaSchedulerTask(
542543
// Flush any pending passive effects before deciding which lanes to work on,
543544
// in case they schedule additional work.
544545
const originalCallbackNode = root.callbackNode;
545-
const didFlushPassiveEffects = flushPendingEffects(true);
546+
const didFlushPassiveEffects = flushPendingEffectsDelayed();
546547
if (didFlushPassiveEffects) {
547548
// Something in the passive effect phase may have canceled the current task.
548549
// Check if the task node for this root was changed.

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ import {
8383
logSuspendedCommitPhase,
8484
logCommitPhase,
8585
logPaintYieldPhase,
86+
logStartViewTransitionYieldPhase,
87+
logAnimatingPhase,
8688
logPassiveCommitPhase,
8789
logYieldTime,
8890
logActionYieldTime,
@@ -674,6 +676,11 @@ const IMMEDIATE_COMMIT = 0;
674676
const SUSPENDED_COMMIT = 1;
675677
const THROTTLED_COMMIT = 2;
676678

679+
type DelayedCommitReason = 0 | 1 | 2 | 3;
680+
const ABORTED_VIEW_TRANSITION_COMMIT = 1;
681+
const DELAYED_PASSIVE_COMMIT = 2;
682+
const ANIMATION_STARTED_COMMIT = 3;
683+
677684
const NO_PENDING_EFFECTS = 0;
678685
const PENDING_MUTATION_PHASE = 1;
679686
const PENDING_LAYOUT_PHASE = 2;
@@ -696,6 +703,7 @@ let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
696703
let pendingTransitionTypes: null | TransitionTypes = null;
697704
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
698705
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
706+
let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
699707

700708
// Use these to prevent an infinite loop of nested updates
701709
const NESTED_UPDATE_LIMIT = 50;
@@ -3436,6 +3444,7 @@ function commitRoot(
34363444
if (enableProfilerTimer) {
34373445
pendingEffectsRenderEndTime = completedRenderEndTime;
34383446
pendingSuspendedCommitReason = suspendedCommitReason;
3447+
pendingDelayedCommitReason = IMMEDIATE_COMMIT;
34393448
}
34403449

34413450
if (enableGestureTransition && isGestureRender(lanes)) {
@@ -3495,7 +3504,10 @@ function commitRoot(
34953504
// event when logging events.
34963505
trackSchedulerEvent();
34973506
}
3498-
flushPassiveEffects(true);
3507+
if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) {
3508+
pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT;
3509+
}
3510+
flushPassiveEffects();
34993511
// This render triggered passive effects: release the root cache pool
35003512
// *after* passive effects fire to avoid freeing a cache pool that may
35013513
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
@@ -3736,6 +3748,23 @@ function flushLayoutEffects(): void {
37363748
ReactSharedInternals.T = prevTransition;
37373749
}
37383750
}
3751+
3752+
const completedRenderEndTime = pendingEffectsRenderEndTime;
3753+
const suspendedCommitReason = pendingSuspendedCommitReason;
3754+
3755+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3756+
recordCommitEndTime();
3757+
logCommitPhase(
3758+
suspendedCommitReason === IMMEDIATE_COMMIT
3759+
? completedRenderEndTime
3760+
: commitStartTime,
3761+
commitEndTime,
3762+
commitErrors,
3763+
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
3764+
workInProgressUpdateTask,
3765+
);
3766+
}
3767+
37393768
pendingEffectsStatus = PENDING_AFTER_MUTATION_PHASE;
37403769
}
37413770

@@ -3748,6 +3777,25 @@ function flushSpawnedWork(): void {
37483777
) {
37493778
return;
37503779
}
3780+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3781+
// If we didn't skip the after mutation phase, when is means we started an animation.
3782+
const startedAnimation = pendingEffectsStatus === PENDING_SPAWNED_WORK;
3783+
if (startedAnimation) {
3784+
const startViewTransitionStartTime = commitEndTime;
3785+
// Update the new commitEndTime to when we started the animation.
3786+
recordCommitEndTime();
3787+
logStartViewTransitionYieldPhase(
3788+
startViewTransitionStartTime,
3789+
commitEndTime,
3790+
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
3791+
workInProgressUpdateTask, // TODO: Use a ViewTransition Task.
3792+
);
3793+
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
3794+
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
3795+
}
3796+
}
3797+
}
3798+
37513799
pendingEffectsStatus = NO_PENDING_EFFECTS;
37523800

37533801
pendingViewTransition = null; // The view transition has now fully started.
@@ -3759,22 +3807,8 @@ function flushSpawnedWork(): void {
37593807
const root = pendingEffectsRoot;
37603808
const finishedWork = pendingFinishedWork;
37613809
const lanes = pendingEffectsLanes;
3762-
const completedRenderEndTime = pendingEffectsRenderEndTime;
37633810
const recoverableErrors = pendingRecoverableErrors;
37643811
const didIncludeRenderPhaseUpdate = pendingDidIncludeRenderPhaseUpdate;
3765-
const suspendedCommitReason = pendingSuspendedCommitReason;
3766-
3767-
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3768-
recordCommitEndTime();
3769-
logCommitPhase(
3770-
suspendedCommitReason === IMMEDIATE_COMMIT
3771-
? completedRenderEndTime
3772-
: commitStartTime,
3773-
commitEndTime,
3774-
commitErrors,
3775-
workInProgressUpdateTask,
3776-
);
3777-
}
37783812

37793813
const passiveSubtreeMask =
37803814
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes)
@@ -4141,7 +4175,14 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
41414175

41424176
let didWarnAboutInterruptedViewTransitions = false;
41434177

4144-
export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
4178+
export function flushPendingEffectsDelayed(): boolean {
4179+
if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) {
4180+
pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT;
4181+
}
4182+
return flushPendingEffects();
4183+
}
4184+
4185+
export function flushPendingEffects(): boolean {
41454186
// Returns whether passive effects were flushed.
41464187
if (enableViewTransition && pendingViewTransition !== null) {
41474188
// If we forced a flush before the View Transition full started then we skip it.
@@ -4159,17 +4200,18 @@ export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
41594200
}
41604201
}
41614202
pendingViewTransition = null;
4203+
pendingDelayedCommitReason = ABORTED_VIEW_TRANSITION_COMMIT;
41624204
}
41634205
flushGestureMutations();
41644206
flushGestureAnimations();
41654207
flushMutationEffects();
41664208
flushLayoutEffects();
41674209
// Skip flushAfterMutation if we're forcing this early.
41684210
flushSpawnedWork();
4169-
return flushPassiveEffects(wasDelayedCommit);
4211+
return flushPassiveEffects();
41704212
}
41714213

4172-
function flushPassiveEffects(wasDelayedCommit?: boolean): boolean {
4214+
function flushPassiveEffects(): boolean {
41734215
if (pendingEffectsStatus !== PENDING_PASSIVE_PHASE) {
41744216
return false;
41754217
}
@@ -4194,7 +4236,7 @@ function flushPassiveEffects(wasDelayedCommit?: boolean): boolean {
41944236
try {
41954237
setCurrentUpdatePriority(priority);
41964238
ReactSharedInternals.T = null;
4197-
return flushPassiveEffectsImpl(wasDelayedCommit);
4239+
return flushPassiveEffectsImpl();
41984240
} finally {
41994241
setCurrentUpdatePriority(previousPriority);
42004242
ReactSharedInternals.T = prevTransition;
@@ -4206,7 +4248,7 @@ function flushPassiveEffects(wasDelayedCommit?: boolean): boolean {
42064248
}
42074249
}
42084250

4209-
function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
4251+
function flushPassiveEffectsImpl() {
42104252
// Cache and clear the transitions flag
42114253
const transitions = pendingPassiveTransitions;
42124254
pendingPassiveTransitions = null;
@@ -4246,12 +4288,21 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
42464288
if (enableProfilerTimer && enableComponentPerformanceTrack) {
42474289
resetCommitErrors();
42484290
passiveEffectStartTime = now();
4249-
logPaintYieldPhase(
4250-
commitEndTime,
4251-
passiveEffectStartTime,
4252-
!!wasDelayedCommit,
4253-
workInProgressUpdateTask,
4254-
);
4291+
if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) {
4292+
// The animation was started, so we've been animating since that happened.
4293+
logAnimatingPhase(
4294+
commitEndTime,
4295+
passiveEffectStartTime,
4296+
workInProgressUpdateTask, // TODO: Use a ViewTransition Task
4297+
);
4298+
} else {
4299+
logPaintYieldPhase(
4300+
commitEndTime,
4301+
passiveEffectStartTime,
4302+
pendingDelayedCommitReason === DELAYED_PASSIVE_COMMIT,
4303+
workInProgressUpdateTask,
4304+
);
4305+
}
42554306
}
42564307

42574308
if (enableSchedulingProfiler) {

0 commit comments

Comments
 (0)