@samitouri / QOS-React / commits / 540cd65252

Log Mount/Unmount/Reconnect/Disconnect in the Component Track (#32816)

Stacked on #32815. To be able to differentiate mounted subtrees from updated subtrees. This adds a yellow entry above the component subtree that mounted. This is added both to the render phase, mutation effect phase, layout effect phase and passive effect phase. <img width="962" alt="Screenshot 2025-04-03 at 10 41 02 PM" src="https://github.com/user-attachments/assets/13777347-07e8-458c-9127-8675ef08b54f" /> Ideally we could probably give an annotation to the component instead of adding a whole other line which is also a color that's kind of distracting. However, not all components are included and keeping track of which one is the first one below is kind of annoying. Adding a marker to all components is kind of noisy. So this is a compromise. It's only one per depth so it won't make it too deep even on larger trees. If this is an unmount, those are added to the mutation effect phase for the layout unmounts and passive unmount effect phase. Since these never have a render, they're not in the render phase. <img width="1010" alt="Screenshot 2025-04-03 at 11 05 57 PM" src="https://github.com/user-attachments/assets/ab39f27e-13be-4281-94fa-9391bb293fd2" /> For showing / hiding `<Activity>` the terminology "Reconnect" and "Disconnect" is used instead.

Sebastian Markbåge committed Apr 3, 2025 at 23:33 UTC 540cd65252ced9f970fb97d5f5b7f029bd7cac83
2 files changed +310 -29
packages/react-reconciler/src/ReactFiberCommitWork.js
+264 -29
@@ -132,6 +132,10 @@ import {
132 logComponentRender,
133 logComponentErrored,
134 logComponentEffect,
135 + logComponentMount,
136 + logComponentUnmount,
137 + logComponentReappeared,
138 + logComponentDisappeared,
139 } from './ReactFiberPerformanceTrack';
140 import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
141 import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue';
@@ -287,6 +291,25 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false;
291 let viewTransitionContextChanged: boolean = false;
292 let rootViewTransitionAffected: boolean = false;
293
294 +function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean {
295 + if (finishedWork.tag === SuspenseComponent) {
296 + const prevState: SuspenseState | null = current.memoizedState;
297 + const nextState: SuspenseState | null = finishedWork.memoizedState;
298 + return (
299 + prevState !== null &&
300 + prevState.dehydrated !== null &&
301 + (nextState === null || nextState.dehydrated === null)
302 + );
303 + } else if (finishedWork.tag === HostRoot) {
304 + return (
305 + (current.memoizedState: RootState).isDehydrated &&
306 + (finishedWork.flags & ForceClientRender) === NoFlags
307 + );
308 + } else {
309 + return false;
310 + }
311 +}
312 +
313 export function commitBeforeMutationEffects(
314 root: FiberRoot,
315 firstChild: Fiber,
@@ -735,6 +758,21 @@ function commitLayoutEffectOnFiber(
758 finishedWork,
759 includeWorkInProgressEffects,
760 );
761 + if (
762 + enableProfilerTimer &&
763 + enableProfilerCommitHooks &&
764 + enableComponentPerformanceTrack &&
765 + (finishedWork.mode & ProfileMode) !== NoMode &&
766 + componentEffectStartTime >= 0 &&
767 + componentEffectEndTime >= 0 &&
768 + componentEffectEndTime - componentEffectStartTime > 0.05
769 + ) {
770 + logComponentReappeared(
771 + finishedWork,
772 + componentEffectStartTime,
773 + componentEffectEndTime,
774 + );
775 + }
776 } else {
777 recursivelyTraverseLayoutEffects(
778 finishedRoot,
@@ -796,16 +834,36 @@ function commitLayoutEffectOnFiber(
834 enableComponentPerformanceTrack &&
835 (finishedWork.mode & ProfileMode) !== NoMode &&
836 componentEffectStartTime >= 0 &&
799 - componentEffectEndTime >= 0 &&
800 - componentEffectDuration > 0.05
837 + componentEffectEndTime >= 0
838 ) {
802 - logComponentEffect(
803 - finishedWork,
804 - componentEffectStartTime,
805 - componentEffectEndTime,
806 - componentEffectDuration,
807 - componentEffectErrors,
808 - );
839 + if (componentEffectDuration > 0.05) {
840 + logComponentEffect(
841 + finishedWork,
842 + componentEffectStartTime,
843 + componentEffectEndTime,
844 + componentEffectDuration,
845 + componentEffectErrors,
846 + );
847 + }
848 + if (
849 + // Insertion
850 + finishedWork.alternate === null &&
851 + finishedWork.return !== null &&
852 + finishedWork.return.alternate !== null &&
853 + componentEffectEndTime - componentEffectStartTime > 0.05
854 + ) {
855 + const isHydration = isHydratingParent(
856 + finishedWork.return.alternate,
857 + finishedWork.return,
858 + );
859 + if (!isHydration) {
860 + logComponentMount(
861 + finishedWork,
862 + componentEffectStartTime,
863 + componentEffectEndTime,
864 + );
865 + }
866 + }
867 }
868
869 popComponentEffectStart(prevEffectStart);
@@ -1220,6 +1278,8 @@ function commitDeletionEffects(
1278 returnFiber: Fiber,
1279 deletedFiber: Fiber,
1280 ) {
1281 + const prevEffectStart = pushComponentEffectStart();
1282 +
1283 if (supportsMutation) {
1284 // We only have the top Fiber that was deleted but we need to recurse down its
1285 // children to find all the terminal nodes.
@@ -1282,6 +1342,23 @@ function commitDeletionEffects(
1342 commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1343 }
1344
1345 + if (
1346 + enableProfilerTimer &&
1347 + enableProfilerCommitHooks &&
1348 + enableComponentPerformanceTrack &&
1349 + (deletedFiber.mode & ProfileMode) !== NoMode &&
1350 + componentEffectStartTime >= 0 &&
1351 + componentEffectEndTime >= 0 &&
1352 + componentEffectEndTime - componentEffectStartTime > 0.05
1353 + ) {
1354 + logComponentUnmount(
1355 + deletedFiber,
1356 + componentEffectStartTime,
1357 + componentEffectEndTime,
1358 + );
1359 + }
1360 + popComponentEffectStart(prevEffectStart);
1361 +
1362 detachFiberMutation(deletedFiber);
1363 }
1364
@@ -2224,6 +2301,27 @@ function commitMutationEffectsOnFiber(
2301 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2302 offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
2303 offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
2304 +
2305 + if (
2306 + // If this was the root of the reappear.
2307 + wasHidden &&
2308 + !isHidden &&
2309 + !prevOffscreenSubtreeIsHidden &&
2310 + !prevOffscreenSubtreeWasHidden &&
2311 + enableProfilerTimer &&
2312 + enableProfilerCommitHooks &&
2313 + enableComponentPerformanceTrack &&
2314 + (finishedWork.mode & ProfileMode) !== NoMode &&
2315 + componentEffectStartTime >= 0 &&
2316 + componentEffectEndTime >= 0 &&
2317 + componentEffectEndTime - componentEffectStartTime > 0.05
2318 + ) {
2319 + logComponentReappeared(
2320 + finishedWork,
2321 + componentEffectStartTime,
2322 + componentEffectEndTime,
2323 + );
2324 + }
2325 } else {
2326 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2327 }
@@ -2259,6 +2357,22 @@ function commitMutationEffectsOnFiber(
2357 ) {
2358 // Disappear the layout effects of all the children
2359 recursivelyTraverseDisappearLayoutEffects(finishedWork);
2360 +
2361 + if (
2362 + enableProfilerTimer &&
2363 + enableProfilerCommitHooks &&
2364 + enableComponentPerformanceTrack &&
2365 + (finishedWork.mode & ProfileMode) !== NoMode &&
2366 + componentEffectStartTime >= 0 &&
2367 + componentEffectEndTime >= 0 &&
2368 + componentEffectEndTime - componentEffectStartTime > 0.05
2369 + ) {
2370 + logComponentDisappeared(
2371 + finishedWork,
2372 + componentEffectStartTime,
2373 + componentEffectEndTime,
2374 + );
2375 + }
2376 }
2377 }
2378 }
@@ -2371,16 +2485,36 @@ function commitMutationEffectsOnFiber(
2485 enableComponentPerformanceTrack &&
2486 (finishedWork.mode & ProfileMode) !== NoMode &&
2487 componentEffectStartTime >= 0 &&
2374 - componentEffectEndTime >= 0 &&
2375 - componentEffectDuration > 0.05
2488 + componentEffectEndTime >= 0
2489 ) {
2377 - logComponentEffect(
2378 - finishedWork,
2379 - componentEffectStartTime,
2380 - componentEffectEndTime,
2381 - componentEffectDuration,
2382 - componentEffectErrors,
2383 - );
2490 + if (componentEffectDuration > 0.05) {
2491 + logComponentEffect(
2492 + finishedWork,
2493 + componentEffectStartTime,
2494 + componentEffectEndTime,
2495 + componentEffectDuration,
2496 + componentEffectErrors,
2497 + );
2498 + }
2499 + if (
2500 + // Insertion
2501 + finishedWork.alternate === null &&
2502 + finishedWork.return !== null &&
2503 + finishedWork.return.alternate !== null &&
2504 + componentEffectEndTime - componentEffectStartTime > 0.05
2505 + ) {
2506 + const isHydration = isHydratingParent(
2507 + finishedWork.return.alternate,
2508 + finishedWork.return,
2509 + );
2510 + if (!isHydration) {
2511 + logComponentMount(
2512 + finishedWork,
2513 + componentEffectStartTime,
2514 + componentEffectEndTime,
2515 + );
2516 + }
2517 + }
2518 }
2519
2520 popComponentEffectStart(prevEffectStart);
@@ -3608,6 +3742,31 @@ function commitPassiveMountOnFiber(
3742 includeWorkInProgressEffects,
3743 endTime,
3744 );
3745 +
3746 + if (
3747 + enableProfilerTimer &&
3748 + enableProfilerCommitHooks &&
3749 + enableComponentPerformanceTrack &&
3750 + (finishedWork.mode & ProfileMode) !== NoMode &&
3751 + !inHydratedSubtree
3752 + ) {
3753 + // Log the reappear in the render phase.
3754 + const startTime = ((finishedWork.actualStartTime: any): number);
3755 + if (endTime - startTime > 0.05) {
3756 + logComponentReappeared(finishedWork, startTime, endTime);
3757 + }
3758 + if (
3759 + componentEffectStartTime >= 0 &&
3760 + componentEffectEndTime >= 0 &&
3761 + componentEffectEndTime - componentEffectStartTime > 0.05
3762 + ) {
3763 + logComponentReappeared(
3764 + finishedWork,
3765 + componentEffectStartTime,
3766 + componentEffectEndTime,
3767 + );
3768 + }
3769 + }
3770 }
3771 }
3772
@@ -3688,18 +3847,38 @@ function commitPassiveMountOnFiber(
3847 enableProfilerTimer &&
3848 enableProfilerCommitHooks &&
3849 enableComponentPerformanceTrack &&
3691 - (finishedWork.mode & ProfileMode) !== NoMode &&
3692 - componentEffectStartTime >= 0 &&
3693 - componentEffectEndTime >= 0 &&
3694 - componentEffectDuration > 0.05
3850 + (finishedWork.mode & ProfileMode) !== NoMode
3851 ) {
3696 - logComponentEffect(
3697 - finishedWork,
3698 - componentEffectStartTime,
3699 - componentEffectEndTime,
3700 - componentEffectDuration,
3701 - componentEffectErrors,
3702 - );
3852 + const isMount =
3853 + !inHydratedSubtree &&
3854 + finishedWork.alternate === null &&
3855 + finishedWork.return !== null &&
3856 + finishedWork.return.alternate !== null;
3857 + if (isMount) {
3858 + // Log the mount in the render phase.
3859 + const startTime = ((finishedWork.actualStartTime: any): number);
3860 + if (endTime - startTime > 0.05) {
3861 + logComponentMount(finishedWork, startTime, endTime);
3862 + }
3863 + }
3864 + if (componentEffectStartTime >= 0 && componentEffectEndTime >= 0) {
3865 + if (componentEffectDuration > 0.05) {
3866 + logComponentEffect(
3867 + finishedWork,
3868 + componentEffectStartTime,
3869 + componentEffectEndTime,
3870 + componentEffectDuration,
3871 + componentEffectErrors,
3872 + );
3873 + }
3874 + if (isMount && componentEffectEndTime - componentEffectStartTime > 0.05) {
3875 + logComponentMount(
3876 + finishedWork,
3877 + componentEffectStartTime,
3878 + componentEffectEndTime,
3879 + );
3880 + }
3881 + }
3882 }
3883
3884 popComponentEffectStart(prevEffectStart);
@@ -4244,12 +4423,29 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
4423 if (deletions !== null) {
4424 for (let i = 0; i < deletions.length; i++) {
4425 const childToDelete = deletions[i];
4426 + const prevEffectStart = pushComponentEffectStart();
4427 // TODO: Convert this to use recursion
4428 nextEffect = childToDelete;
4429 commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
4430 childToDelete,
4431 parentFiber,
4432 );
4433 + if (
4434 + enableProfilerTimer &&
4435 + enableProfilerCommitHooks &&
4436 + enableComponentPerformanceTrack &&
4437 + (childToDelete.mode & ProfileMode) !== NoMode &&
4438 + componentEffectStartTime >= 0 &&
4439 + componentEffectEndTime >= 0 &&
4440 + componentEffectEndTime - componentEffectStartTime > 0.05
4441 + ) {
4442 + logComponentUnmount(
4443 + childToDelete,
4444 + componentEffectStartTime,
4445 + componentEffectEndTime,
4446 + );
4447 + }
4448 + popComponentEffectStart(prevEffectStart);
4449 }
4450 }
4451 detachAlternateSiblings(parentFiber);
@@ -4328,7 +4524,24 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4524 // effects. Then if the tree reappears before the delay has elapsed, we
4525 // can skip toggling the effects entirely.
4526 instance._visibility &= ~OffscreenPassiveEffectsConnected;
4527 +
4528 recursivelyTraverseDisconnectPassiveEffects(finishedWork);
4529 +
4530 + if (
4531 + enableProfilerTimer &&
4532 + enableProfilerCommitHooks &&
4533 + enableComponentPerformanceTrack &&
4534 + (finishedWork.mode & ProfileMode) !== NoMode &&
4535 + componentEffectStartTime >= 0 &&
4536 + componentEffectEndTime >= 0 &&
4537 + componentEffectEndTime - componentEffectStartTime > 0.05
4538 + ) {
4539 + logComponentDisappeared(
4540 + finishedWork,
4541 + componentEffectStartTime,
4542 + componentEffectEndTime,
4543 + );
4544 + }
4545 } else {
4546 recursivelyTraversePassiveUnmountEffects(finishedWork);
4547 }
@@ -4373,12 +4586,34 @@ function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
4586 if (deletions !== null) {
4587 for (let i = 0; i < deletions.length; i++) {
4588 const childToDelete = deletions[i];
4589 + const prevEffectStart = pushComponentEffectStart();
4590 +
4591 // TODO: Convert this to use recursion
4592 nextEffect = childToDelete;
4593 commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
4594 childToDelete,
4595 parentFiber,
4596 );
4597 +
4598 + if (
4599 + enableProfilerTimer &&
4600 + enableProfilerCommitHooks &&
4601 + enableComponentPerformanceTrack &&
4602 + (childToDelete.mode & ProfileMode) !== NoMode &&
4603 + componentEffectStartTime >= 0 &&
4604 + componentEffectEndTime >= 0 &&
4605 + componentEffectEndTime - componentEffectStartTime > 0.05
4606 + ) {
4607 + // While this is inside the disconnect path. This is a deletion within the
4608 + // disconnected tree. We currently log this for deletions in the mutation
4609 + // phase since it's shared by the disappear path.
4610 + logComponentUnmount(
4611 + childToDelete,
4612 + componentEffectStartTime,
4613 + componentEffectEndTime,
4614 + );
4615 + }
4616 + popComponentEffectStart(prevEffectStart);
4617 }
4618 }
4619 detachAlternateSiblings(parentFiber);
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+46
@@ -123,6 +123,52 @@ export function markAllLanesInOrder() {
123 }
124 }
125
126 +function logComponentTrigger(
127 + fiber: Fiber,
128 + startTime: number,
129 + endTime: number,
130 + trigger: string,
131 +) {
132 + if (supportsUserTiming) {
133 + reusableComponentDevToolDetails.color = 'warning';
134 + reusableComponentOptions.start = startTime;
135 + reusableComponentOptions.end = endTime;
136 + performance.measure(trigger, reusableComponentOptions);
137 + }
138 +}
139 +
140 +export function logComponentMount(
141 + fiber: Fiber,
142 + startTime: number,
143 + endTime: number,
144 +): void {
145 + logComponentTrigger(fiber, startTime, endTime, 'Mount');
146 +}
147 +
148 +export function logComponentUnmount(
149 + fiber: Fiber,
150 + startTime: number,
151 + endTime: number,
152 +): void {
153 + logComponentTrigger(fiber, startTime, endTime, 'Unmount');
154 +}
155 +
156 +export function logComponentReappeared(
157 + fiber: Fiber,
158 + startTime: number,
159 + endTime: number,
160 +): void {
161 + logComponentTrigger(fiber, startTime, endTime, 'Reconnect');
162 +}
163 +
164 +export function logComponentDisappeared(
165 + fiber: Fiber,
166 + startTime: number,
167 + endTime: number,
168 +): void {
169 + logComponentTrigger(fiber, startTime, endTime, 'Disconnect');
170 +}
171 +
172 export function logComponentRender(
173 fiber: Fiber,
174 startTime: number,