From 540cd65252ced9f970fb97d5f5b7f029bd7cac83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 3 Apr 2025 23:33:29 -0400 Subject: [PATCH] Log Mount/Unmount/Reconnect/Disconnect in the Component Track (#32816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Screenshot 2025-04-03 at 10 41 02 PM 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. Screenshot 2025-04-03 at 11 05 57 PM For showing / hiding `` the terminology "Reconnect" and "Disconnect" is used instead. --- .../src/ReactFiberCommitWork.js | 293 ++++++++++++++++-- .../src/ReactFiberPerformanceTrack.js | 46 +++ 2 files changed, 310 insertions(+), 29 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 8cb2084ef1..3d6d05dbec 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -132,6 +132,10 @@ import { logComponentRender, logComponentErrored, logComponentEffect, + logComponentMount, + logComponentUnmount, + logComponentReappeared, + logComponentDisappeared, } from './ReactFiberPerformanceTrack'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue'; @@ -287,6 +291,25 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false; let viewTransitionContextChanged: boolean = false; let rootViewTransitionAffected: boolean = false; +function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean { + if (finishedWork.tag === SuspenseComponent) { + const prevState: SuspenseState | null = current.memoizedState; + const nextState: SuspenseState | null = finishedWork.memoizedState; + return ( + prevState !== null && + prevState.dehydrated !== null && + (nextState === null || nextState.dehydrated === null) + ); + } else if (finishedWork.tag === HostRoot) { + return ( + (current.memoizedState: RootState).isDehydrated && + (finishedWork.flags & ForceClientRender) === NoFlags + ); + } else { + return false; + } +} + export function commitBeforeMutationEffects( root: FiberRoot, firstChild: Fiber, @@ -735,6 +758,21 @@ function commitLayoutEffectOnFiber( finishedWork, includeWorkInProgressEffects, ); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentReappeared( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } } else { recursivelyTraverseLayoutEffects( finishedRoot, @@ -796,16 +834,36 @@ function commitLayoutEffectOnFiber( enableComponentPerformanceTrack && (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && - componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + componentEffectEndTime >= 0 ) { - logComponentEffect( - finishedWork, - componentEffectStartTime, - componentEffectEndTime, - componentEffectDuration, - componentEffectErrors, - ); + if (componentEffectDuration > 0.05) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + componentEffectDuration, + componentEffectErrors, + ); + } + if ( + // Insertion + finishedWork.alternate === null && + finishedWork.return !== null && + finishedWork.return.alternate !== null && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + const isHydration = isHydratingParent( + finishedWork.return.alternate, + finishedWork.return, + ); + if (!isHydration) { + logComponentMount( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + } } popComponentEffectStart(prevEffectStart); @@ -1220,6 +1278,8 @@ function commitDeletionEffects( returnFiber: Fiber, deletedFiber: Fiber, ) { + const prevEffectStart = pushComponentEffectStart(); + if (supportsMutation) { // We only have the top Fiber that was deleted but we need to recurse down its // children to find all the terminal nodes. @@ -1282,6 +1342,23 @@ function commitDeletionEffects( commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (deletedFiber.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentUnmount( + deletedFiber, + componentEffectStartTime, + componentEffectEndTime, + ); + } + popComponentEffectStart(prevEffectStart); + detachFiberMutation(deletedFiber); } @@ -2224,6 +2301,27 @@ function commitMutationEffectsOnFiber( recursivelyTraverseMutationEffects(root, finishedWork, lanes); offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden; + + if ( + // If this was the root of the reappear. + wasHidden && + !isHidden && + !prevOffscreenSubtreeIsHidden && + !prevOffscreenSubtreeWasHidden && + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentReappeared( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } } else { recursivelyTraverseMutationEffects(root, finishedWork, lanes); } @@ -2259,6 +2357,22 @@ function commitMutationEffectsOnFiber( ) { // Disappear the layout effects of all the children recursivelyTraverseDisappearLayoutEffects(finishedWork); + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentDisappeared( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } } } } @@ -2371,16 +2485,36 @@ function commitMutationEffectsOnFiber( enableComponentPerformanceTrack && (finishedWork.mode & ProfileMode) !== NoMode && componentEffectStartTime >= 0 && - componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + componentEffectEndTime >= 0 ) { - logComponentEffect( - finishedWork, - componentEffectStartTime, - componentEffectEndTime, - componentEffectDuration, - componentEffectErrors, - ); + if (componentEffectDuration > 0.05) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + componentEffectDuration, + componentEffectErrors, + ); + } + if ( + // Insertion + finishedWork.alternate === null && + finishedWork.return !== null && + finishedWork.return.alternate !== null && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + const isHydration = isHydratingParent( + finishedWork.return.alternate, + finishedWork.return, + ); + if (!isHydration) { + logComponentMount( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + } } popComponentEffectStart(prevEffectStart); @@ -3608,6 +3742,31 @@ function commitPassiveMountOnFiber( includeWorkInProgressEffects, endTime, ); + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + !inHydratedSubtree + ) { + // Log the reappear in the render phase. + const startTime = ((finishedWork.actualStartTime: any): number); + if (endTime - startTime > 0.05) { + logComponentReappeared(finishedWork, startTime, endTime); + } + if ( + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentReappeared( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + } } } @@ -3688,18 +3847,38 @@ function commitPassiveMountOnFiber( enableProfilerTimer && enableProfilerCommitHooks && enableComponentPerformanceTrack && - (finishedWork.mode & ProfileMode) !== NoMode && - componentEffectStartTime >= 0 && - componentEffectEndTime >= 0 && - componentEffectDuration > 0.05 + (finishedWork.mode & ProfileMode) !== NoMode ) { - logComponentEffect( - finishedWork, - componentEffectStartTime, - componentEffectEndTime, - componentEffectDuration, - componentEffectErrors, - ); + const isMount = + !inHydratedSubtree && + finishedWork.alternate === null && + finishedWork.return !== null && + finishedWork.return.alternate !== null; + if (isMount) { + // Log the mount in the render phase. + const startTime = ((finishedWork.actualStartTime: any): number); + if (endTime - startTime > 0.05) { + logComponentMount(finishedWork, startTime, endTime); + } + } + if (componentEffectStartTime >= 0 && componentEffectEndTime >= 0) { + if (componentEffectDuration > 0.05) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + componentEffectDuration, + componentEffectErrors, + ); + } + if (isMount && componentEffectEndTime - componentEffectStartTime > 0.05) { + logComponentMount( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + } } popComponentEffectStart(prevEffectStart); @@ -4244,12 +4423,29 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void { if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; + const prevEffectStart = pushComponentEffectStart(); // TODO: Convert this to use recursion nextEffect = childToDelete; commitPassiveUnmountEffectsInsideOfDeletedTree_begin( childToDelete, parentFiber, ); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (childToDelete.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentUnmount( + childToDelete, + componentEffectStartTime, + componentEffectEndTime, + ); + } + popComponentEffectStart(prevEffectStart); } } detachAlternateSiblings(parentFiber); @@ -4328,7 +4524,24 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { // effects. Then if the tree reappears before the delay has elapsed, we // can skip toggling the effects entirely. instance._visibility &= ~OffscreenPassiveEffectsConnected; + recursivelyTraverseDisconnectPassiveEffects(finishedWork); + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + logComponentDisappeared( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } } else { recursivelyTraversePassiveUnmountEffects(finishedWork); } @@ -4373,12 +4586,34 @@ function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void { if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; + const prevEffectStart = pushComponentEffectStart(); + // TODO: Convert this to use recursion nextEffect = childToDelete; commitPassiveUnmountEffectsInsideOfDeletedTree_begin( childToDelete, parentFiber, ); + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (childToDelete.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 && + componentEffectEndTime - componentEffectStartTime > 0.05 + ) { + // While this is inside the disconnect path. This is a deletion within the + // disconnected tree. We currently log this for deletions in the mutation + // phase since it's shared by the disappear path. + logComponentUnmount( + childToDelete, + componentEffectStartTime, + componentEffectEndTime, + ); + } + popComponentEffectStart(prevEffectStart); } } detachAlternateSiblings(parentFiber); diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 3a36572e8a..9a59997521 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -123,6 +123,52 @@ export function markAllLanesInOrder() { } } +function logComponentTrigger( + fiber: Fiber, + startTime: number, + endTime: number, + trigger: string, +) { + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'warning'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure(trigger, reusableComponentOptions); + } +} + +export function logComponentMount( + fiber: Fiber, + startTime: number, + endTime: number, +): void { + logComponentTrigger(fiber, startTime, endTime, 'Mount'); +} + +export function logComponentUnmount( + fiber: Fiber, + startTime: number, + endTime: number, +): void { + logComponentTrigger(fiber, startTime, endTime, 'Unmount'); +} + +export function logComponentReappeared( + fiber: Fiber, + startTime: number, + endTime: number, +): void { + logComponentTrigger(fiber, startTime, endTime, 'Reconnect'); +} + +export function logComponentDisappeared( + fiber: Fiber, + startTime: number, + endTime: number, +): void { + logComponentTrigger(fiber, startTime, endTime, 'Disconnect'); +} + export function logComponentRender( fiber: Fiber, startTime: number,