@samitouri / QOS-React-1 / commits / c0f08ae74a

Fix Bugs Measuring Performance Track for Effects (#32815)

This fixes two bugs with commit phase effect tracking. I missed, or messed up the rebase for, deletion effects when a subtree was deleted and for passive disconnects when a subtree was hidden. The other bug is that when I started using self time (componentEffectDuration) for color and for determining whether to bother logging an entry, I didn't consider that the component with effects can have children which end up resetting this duration before we log. Which lead to most effects not having their components logged since they almost always have children. We don't necessarily have to push/pop but we have to store at least one thing on the stack unfortunately. That's because we have to do the actual log after the children to get the right end time. So might as well use the push/pop strategy like the rest of them.

Sebastian Markbåge committed Apr 3, 2025 at 23:33 UTC c0f08ae74a46686f5718e9e6c511d27419fd632c
2 files changed +138 -38
packages/react-reconciler/src/ReactFiberCommitWork.js
+118 -37
@@ -119,6 +119,8 @@ import {
119 resetComponentEffectTimers,
120 pushComponentEffectStart,
121 popComponentEffectStart,
122 + pushComponentEffectDuration,
123 + popComponentEffectDuration,
124 pushComponentEffectErrors,
125 popComponentEffectErrors,
126 componentEffectStartTime,
@@ -543,6 +545,7 @@ function commitLayoutEffectOnFiber(
545 committedLanes: Lanes,
546 ): void {
547 const prevEffectStart = pushComponentEffectStart();
548 + const prevEffectDuration = pushComponentEffectDuration();
549 const prevEffectErrors = pushComponentEffectErrors();
550 // When updating this function, also update reappearLayoutEffects, which does
551 // most of the same things when an offscreen tree goes from hidden -> visible.
@@ -581,7 +584,7 @@ function commitLayoutEffectOnFiber(
584 break;
585 }
586 case HostRoot: {
584 - const prevEffectDuration = pushNestedEffectDurations();
587 + const prevProfilerEffectDuration = pushNestedEffectDurations();
588 recursivelyTraverseLayoutEffects(
589 finishedRoot,
590 finishedWork,
@@ -591,8 +594,9 @@ function commitLayoutEffectOnFiber(
594 commitRootCallbacks(finishedWork);
595 }
596 if (enableProfilerTimer && enableProfilerCommitHooks) {
594 - finishedRoot.effectDuration +=
595 - popNestedEffectDurations(prevEffectDuration);
597 + finishedRoot.effectDuration += popNestedEffectDurations(
598 + prevProfilerEffectDuration,
599 + );
600 }
601 break;
602 }
@@ -638,7 +642,7 @@ function commitLayoutEffectOnFiber(
642 // TODO: Should this fire inside an offscreen tree? Or should it wait to
643 // fire when the tree becomes visible again.
644 if (flags & Update) {
641 - const prevEffectDuration = pushNestedEffectDurations();
645 + const prevProfilerEffectDuration = pushNestedEffectDurations();
646
647 recursivelyTraverseLayoutEffects(
648 finishedRoot,
@@ -651,8 +655,9 @@ function commitLayoutEffectOnFiber(
655 if (enableProfilerTimer && enableProfilerCommitHooks) {
656 // Propagate layout effect durations to the next nearest Profiler ancestor.
657 // Do not reset these values until the next render so DevTools has a chance to read them first.
654 - profilerInstance.effectDuration +=
655 - bubbleNestedEffectDurations(prevEffectDuration);
658 + profilerInstance.effectDuration += bubbleNestedEffectDurations(
659 + prevProfilerEffectDuration,
660 + );
661 }
662
663 commitProfilerUpdate(
@@ -804,6 +809,7 @@ function commitLayoutEffectOnFiber(
809 }
810
811 popComponentEffectStart(prevEffectStart);
812 + popComponentEffectDuration(prevEffectDuration);
813 popComponentEffectErrors(prevEffectErrors);
814 }
815
@@ -1300,6 +1306,10 @@ function commitDeletionEffectsOnFiber(
1306 // TODO: Delete this Hook once new DevTools ships everywhere. No longer needed.
1307 onCommitUnmount(deletedFiber);
1308
1309 + const prevEffectStart = pushComponentEffectStart();
1310 + const prevEffectDuration = pushComponentEffectDuration();
1311 + const prevEffectErrors = pushComponentEffectErrors();
1312 +
1313 // The cases in this outer switch modify the stack before they traverse
1314 // into their subtree. There are simpler cases in the inner switch
1315 // that don't modify the stack.
@@ -1319,7 +1329,7 @@ function commitDeletionEffectsOnFiber(
1329 } else if (deletedFiber.stateNode) {
1330 unmountHoistable(deletedFiber.stateNode);
1331 }
1322 - return;
1332 + break;
1333 }
1334 // Fall through
1335 }
@@ -1351,7 +1361,7 @@ function commitDeletionEffectsOnFiber(
1361 hostParent = prevHostParent;
1362 hostParentIsContainer = prevHostParentIsContainer;
1363
1354 - return;
1364 + break;
1365 }
1366 // Fall through
1367 }
@@ -1406,7 +1416,7 @@ function commitDeletionEffectsOnFiber(
1416 deletedFiber,
1417 );
1418 }
1409 - return;
1419 + break;
1420 }
1421 case DehydratedFragment: {
1422 if (enableSuspenseCallback) {
@@ -1445,7 +1455,7 @@ function commitDeletionEffectsOnFiber(
1455 }
1456 }
1457 }
1448 - return;
1458 + break;
1459 }
1460 case HostPortal: {
1461 if (supportsMutation) {
@@ -1476,7 +1486,7 @@ function commitDeletionEffectsOnFiber(
1486 deletedFiber,
1487 );
1488 }
1479 - return;
1489 + break;
1490 }
1491 case FunctionComponent:
1492 case ForwardRef:
@@ -1505,7 +1515,7 @@ function commitDeletionEffectsOnFiber(
1515 nearestMountedAncestor,
1516 deletedFiber,
1517 );
1508 - return;
1518 + break;
1519 }
1520 case ClassComponent: {
1521 if (!offscreenSubtreeWasHidden) {
@@ -1524,7 +1534,7 @@ function commitDeletionEffectsOnFiber(
1534 nearestMountedAncestor,
1535 deletedFiber,
1536 );
1527 - return;
1537 + break;
1538 }
1539 case ScopeComponent: {
1540 if (enableScopeAPI) {
@@ -1537,7 +1547,7 @@ function commitDeletionEffectsOnFiber(
1547 nearestMountedAncestor,
1548 deletedFiber,
1549 );
1540 - return;
1550 + break;
1551 }
1552 case OffscreenComponent: {
1553 if (disableLegacyMode || deletedFiber.mode & ConcurrentMode) {
@@ -1582,7 +1592,7 @@ function commitDeletionEffectsOnFiber(
1592 nearestMountedAncestor,
1593 deletedFiber,
1594 );
1585 - return;
1595 + break;
1596 }
1597 // Fallthrough
1598 }
@@ -1596,7 +1606,7 @@ function commitDeletionEffectsOnFiber(
1606 nearestMountedAncestor,
1607 deletedFiber,
1608 );
1599 - return;
1609 + break;
1610 }
1611 // Fallthrough
1612 }
@@ -1606,10 +1616,33 @@ function commitDeletionEffectsOnFiber(
1616 nearestMountedAncestor,
1617 deletedFiber,
1618 );
1609 - return;
1619 + break;
1620 }
1621 }
1622 +
1623 + if (
1624 + enableProfilerTimer &&
1625 + enableProfilerCommitHooks &&
1626 + enableComponentPerformanceTrack &&
1627 + (deletedFiber.mode & ProfileMode) !== NoMode &&
1628 + componentEffectStartTime >= 0 &&
1629 + componentEffectEndTime >= 0 &&
1630 + componentEffectDuration > 0.05
1631 + ) {
1632 + logComponentEffect(
1633 + deletedFiber,
1634 + componentEffectStartTime,
1635 + componentEffectEndTime,
1636 + componentEffectDuration,
1637 + componentEffectErrors,
1638 + );
1639 + }
1640 +
1641 + popComponentEffectStart(prevEffectStart);
1642 + popComponentEffectDuration(prevEffectDuration);
1643 + popComponentEffectErrors(prevEffectErrors);
1644 }
1645 +
1646 function commitSuspenseCallback(finishedWork: Fiber) {
1647 // TODO: Delete this feature. It's not properly covered by DEV features.
1648 const newState: SuspenseState | null = finishedWork.memoizedState;
@@ -1796,6 +1829,7 @@ function commitMutationEffectsOnFiber(
1829 lanes: Lanes,
1830 ) {
1831 const prevEffectStart = pushComponentEffectStart();
1832 + const prevEffectDuration = pushComponentEffectDuration();
1833 const prevEffectErrors = pushComponentEffectErrors();
1834 const current = finishedWork.alternate;
1835 const flags = finishedWork.flags;
@@ -2019,7 +2053,7 @@ function commitMutationEffectsOnFiber(
2053 break;
2054 }
2055 case HostRoot: {
2022 - const prevEffectDuration = pushNestedEffectDurations();
2056 + const prevProfilerEffectDuration = pushNestedEffectDurations();
2057
2058 if (supportsResources) {
2059 prepareToCommitHoistables();
@@ -2065,7 +2099,9 @@ function commitMutationEffectsOnFiber(
2099 }
2100
2101 if (enableProfilerTimer && enableProfilerCommitHooks) {
2068 - root.effectDuration += popNestedEffectDurations(prevEffectDuration);
2102 + root.effectDuration += popNestedEffectDurations(
2103 + prevProfilerEffectDuration,
2104 + );
2105 }
2106
2107 break;
@@ -2105,7 +2141,7 @@ function commitMutationEffectsOnFiber(
2141 break;
2142 }
2143 case Profiler: {
2108 - const prevEffectDuration = pushNestedEffectDurations();
2144 + const prevProfilerEffectDuration = pushNestedEffectDurations();
2145
2146 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2147 commitReconciliationEffects(finishedWork, lanes);
@@ -2114,8 +2150,9 @@ function commitMutationEffectsOnFiber(
2150 const profilerInstance = finishedWork.stateNode;
2151 // Propagate layout effect durations to the next nearest Profiler ancestor.
2152 // Do not reset these values until the next render so DevTools has a chance to read them first.
2117 - profilerInstance.effectDuration +=
2118 - bubbleNestedEffectDurations(prevEffectDuration);
2153 + profilerInstance.effectDuration += bubbleNestedEffectDurations(
2154 + prevProfilerEffectDuration,
2155 + );
2156 }
2157 break;
2158 }
@@ -2347,6 +2384,7 @@ function commitMutationEffectsOnFiber(
2384 }
2385
2386 popComponentEffectStart(prevEffectStart);
2387 + popComponentEffectDuration(prevEffectDuration);
2388 popComponentEffectErrors(prevEffectErrors);
2389 }
2390
@@ -2598,6 +2636,7 @@ function recursivelyTraverseLayoutEffects(
2636
2637 export function disappearLayoutEffects(finishedWork: Fiber) {
2638 const prevEffectStart = pushComponentEffectStart();
2639 + const prevEffectDuration = pushComponentEffectDuration();
2640 const prevEffectErrors = pushComponentEffectErrors();
2641 switch (finishedWork.tag) {
2642 case FunctionComponent:
@@ -2701,6 +2740,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2740 }
2741
2742 popComponentEffectStart(prevEffectStart);
2743 + popComponentEffectDuration(prevEffectDuration);
2744 popComponentEffectErrors(prevEffectErrors);
2745 }
2746
@@ -2723,6 +2763,7 @@ export function reappearLayoutEffects(
2763 includeWorkInProgressEffects: boolean,
2764 ) {
2765 const prevEffectStart = pushComponentEffectStart();
2766 + const prevEffectDuration = pushComponentEffectDuration();
2767 const prevEffectErrors = pushComponentEffectErrors();
2768 // Turn on layout effects in a tree that previously disappeared.
2769 const flags = finishedWork.flags;
@@ -2806,7 +2847,7 @@ export function reappearLayoutEffects(
2847 case Profiler: {
2848 // TODO: Figure out how Profiler updates should work with Offscreen
2849 if (includeWorkInProgressEffects && flags & Update) {
2809 - const prevEffectDuration = pushNestedEffectDurations();
2850 + const prevProfilerEffectDuration = pushNestedEffectDurations();
2851
2852 recursivelyTraverseReappearLayoutEffects(
2853 finishedRoot,
@@ -2819,8 +2860,9 @@ export function reappearLayoutEffects(
2860 if (enableProfilerTimer && enableProfilerCommitHooks) {
2861 // Propagate layout effect durations to the next nearest Profiler ancestor.
2862 // Do not reset these values until the next render so DevTools has a chance to read them first.
2822 - profilerInstance.effectDuration +=
2823 - bubbleNestedEffectDurations(prevEffectDuration);
2863 + profilerInstance.effectDuration += bubbleNestedEffectDurations(
2864 + prevProfilerEffectDuration,
2865 + );
2866 }
2867
2868 commitProfilerUpdate(
@@ -2919,6 +2961,7 @@ export function reappearLayoutEffects(
2961 }
2962
2963 popComponentEffectStart(prevEffectStart);
2964 + popComponentEffectDuration(prevEffectDuration);
2965 popComponentEffectErrors(prevEffectErrors);
2966 }
2967
@@ -3169,6 +3212,7 @@ function commitPassiveMountOnFiber(
3212 endTime: number, // Profiling-only. The start time of the next Fiber or root completion.
3213 ): void {
3214 const prevEffectStart = pushComponentEffectStart();
3215 + const prevEffectDuration = pushComponentEffectDuration();
3216 const prevEffectErrors = pushComponentEffectErrors();
3217
3218 const isViewTransitionEligible = enableViewTransition
@@ -3272,7 +3316,7 @@ function commitPassiveMountOnFiber(
3316 break;
3317 }
3318 case HostRoot: {
3275 - const prevEffectDuration = pushNestedEffectDurations();
3319 + const prevProfilerEffectDuration = pushNestedEffectDurations();
3320
3321 const wasInHydratedSubtree = inHydratedSubtree;
3322 if (enableProfilerTimer && enableComponentPerformanceTrack) {
@@ -3349,15 +3393,16 @@ function commitPassiveMountOnFiber(
3393 }
3394 }
3395 if (enableProfilerTimer && enableProfilerCommitHooks) {
3352 - finishedRoot.passiveEffectDuration +=
3353 - popNestedEffectDurations(prevEffectDuration);
3396 + finishedRoot.passiveEffectDuration += popNestedEffectDurations(
3397 + prevProfilerEffectDuration,
3398 + );
3399 }
3400 break;
3401 }
3402 case Profiler: {
3403 // Only Profilers with work in their subtree will have a Passive effect scheduled.
3404 if (flags & Passive) {
3360 - const prevEffectDuration = pushNestedEffectDurations();
3405 + const prevProfilerEffectDuration = pushNestedEffectDurations();
3406
3407 recursivelyTraversePassiveMountEffects(
3408 finishedRoot,
@@ -3372,8 +3417,9 @@ function commitPassiveMountOnFiber(
3417 if (enableProfilerTimer && enableProfilerCommitHooks) {
3418 // Bubble times to the next nearest ancestor Profiler.
3419 // After we process that Profiler, we'll bubble further up.
3375 - profilerInstance.passiveEffectDuration +=
3376 - bubbleNestedEffectDurations(prevEffectDuration);
3420 + profilerInstance.passiveEffectDuration += bubbleNestedEffectDurations(
3421 + prevProfilerEffectDuration,
3422 + );
3423 }
3424
3425 commitProfilerPostCommit(
@@ -3657,6 +3703,7 @@ function commitPassiveMountOnFiber(
3703 }
3704
3705 popComponentEffectStart(prevEffectStart);
3706 + popComponentEffectDuration(prevEffectDuration);
3707 popComponentEffectErrors(prevEffectErrors);
3708 }
3709
@@ -3717,6 +3764,7 @@ export function reconnectPassiveEffects(
3764 endTime: number, // Profiling-only. The start time of the next Fiber or root completion.
3765 ) {
3766 const prevEffectStart = pushComponentEffectStart();
3767 + const prevEffectDuration = pushComponentEffectDuration();
3768 const prevEffectErrors = pushComponentEffectErrors();
3769 // If this component rendered in Profiling mode (DEV or in Profiler component) then log its
3770 // render time. We do this after the fact in the passive effect to avoid the overhead of this
@@ -3916,6 +3964,7 @@ export function reconnectPassiveEffects(
3964 }
3965
3966 popComponentEffectStart(prevEffectStart);
3967 + popComponentEffectDuration(prevEffectDuration);
3968 popComponentEffectErrors(prevEffectErrors);
3969 }
3970
@@ -4218,6 +4267,7 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
4267
4268 function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4269 const prevEffectStart = pushComponentEffectStart();
4270 + const prevEffectDuration = pushComponentEffectDuration();
4271 const prevEffectErrors = pushComponentEffectErrors();
4272 switch (finishedWork.tag) {
4273 case FunctionComponent:
@@ -4234,17 +4284,18 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4284 break;
4285 }
4286 case HostRoot: {
4237 - const prevEffectDuration = pushNestedEffectDurations();
4287 + const prevProfilerEffectDuration = pushNestedEffectDurations();
4288 recursivelyTraversePassiveUnmountEffects(finishedWork);
4289 if (enableProfilerTimer && enableProfilerCommitHooks) {
4290 const finishedRoot: FiberRoot = finishedWork.stateNode;
4241 - finishedRoot.passiveEffectDuration +=
4242 - popNestedEffectDurations(prevEffectDuration);
4291 + finishedRoot.passiveEffectDuration += popNestedEffectDurations(
4292 + prevProfilerEffectDuration,
4293 + );
4294 }
4295 break;
4296 }
4297 case Profiler: {
4247 - const prevEffectDuration = pushNestedEffectDurations();
4298 + const prevProfilerEffectDuration = pushNestedEffectDurations();
4299
4300 recursivelyTraversePassiveUnmountEffects(finishedWork);
4301
@@ -4252,8 +4303,9 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4303 const profilerInstance = finishedWork.stateNode;
4304 // Propagate layout effect durations to the next nearest Profiler ancestor.
4305 // Do not reset these values until the next render so DevTools has a chance to read them first.
4255 - profilerInstance.passiveEffectDuration +=
4256 - bubbleNestedEffectDurations(prevEffectDuration);
4306 + profilerInstance.passiveEffectDuration += bubbleNestedEffectDurations(
4307 + prevProfilerEffectDuration,
4308 + );
4309 }
4310 break;
4311 }
@@ -4308,6 +4360,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4360 }
4361
4362 popComponentEffectStart(prevEffectStart);
4363 + popComponentEffectDuration(prevEffectDuration);
4364 popComponentEffectErrors(prevEffectErrors);
4365 }
4366
@@ -4340,6 +4393,10 @@ function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
4393 }
4394
4395 export function disconnectPassiveEffect(finishedWork: Fiber): void {
4396 + const prevEffectStart = pushComponentEffectStart();
4397 + const prevEffectDuration = pushComponentEffectDuration();
4398 + const prevEffectErrors = pushComponentEffectErrors();
4399 +
4400 switch (finishedWork.tag) {
4401 case FunctionComponent:
4402 case ForwardRef:
@@ -4370,6 +4427,28 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void {
4427 break;
4428 }
4429 }
4430 +
4431 + if (
4432 + enableProfilerTimer &&
4433 + enableProfilerCommitHooks &&
4434 + enableComponentPerformanceTrack &&
4435 + (finishedWork.mode & ProfileMode) !== NoMode &&
4436 + componentEffectStartTime >= 0 &&
4437 + componentEffectEndTime >= 0 &&
4438 + componentEffectDuration > 0.05
4439 + ) {
4440 + logComponentEffect(
4441 + finishedWork,
4442 + componentEffectStartTime,
4443 + componentEffectEndTime,
4444 + componentEffectDuration,
4445 + componentEffectErrors,
4446 + );
4447 + }
4448 +
4449 + popComponentEffectStart(prevEffectStart);
4450 + popComponentEffectDuration(prevEffectDuration);
4451 + popComponentEffectErrors(prevEffectErrors);
4452 }
4453
4454 function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
@@ -4428,6 +4507,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4507 nearestMountedAncestor: Fiber | null,
4508 ): void {
4509 const prevEffectStart = pushComponentEffectStart();
4510 + const prevEffectDuration = pushComponentEffectDuration();
4511 const prevEffectErrors = pushComponentEffectErrors();
4512 switch (current.tag) {
4513 case FunctionComponent:
@@ -4560,6 +4640,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4640 }
4641
4642 popComponentEffectStart(prevEffectStart);
4643 + popComponentEffectDuration(prevEffectDuration);
4644 popComponentEffectErrors(prevEffectErrors);
4645 }
4646
packages/react-reconciler/src/ReactProfilerTimer.js
+20 -1
@@ -272,7 +272,6 @@ export function pushComponentEffectStart(): number {
272 }
273 const prevEffectStart = componentEffectStartTime;
274 componentEffectStartTime = -1.1; // Track the next start.
275 - componentEffectDuration = -0; // Reset component level duration.
275 return prevEffectStart;
276 }
277
@@ -287,6 +286,26 @@ export function popComponentEffectStart(prevEffectStart: number): void {
286 }
287 }
288
289 +export function pushComponentEffectDuration(): number {
290 + if (!enableProfilerTimer || !enableProfilerCommitHooks) {
291 + return 0;
292 + }
293 + const prevEffectDuration = componentEffectDuration;
294 + componentEffectDuration = -0; // Reset component level duration.
295 + return prevEffectDuration;
296 +}
297 +
298 +export function popComponentEffectDuration(prevEffectDuration: number): void {
299 + if (!enableProfilerTimer || !enableProfilerCommitHooks) {
300 + return;
301 + }
302 + // If the parent component didn't have a start time, we let this current time persist.
303 + if (prevEffectDuration >= 0) {
304 + // Otherwise, we restore the previous parent's start time.
305 + componentEffectDuration = prevEffectDuration;
306 + }
307 +}
308 +
309 export function pushComponentEffectErrors(): null | Array<
310 CapturedValue<mixed>,
311 > {