Skip to content

Commit f2df569

Browse files
authored
[Fiber] Log Component Renders to Custom Performance Track (#30967)
Stacked on #30960 and #30966. Behind the enableComponentPerformanceTrack flag. This is the first step of performance logging. This logs the start and end time of a component render in the passive effect phase. We use the data we're already tracking on components when the Profiler component or DevTools is active in the Profiling or Dev builds. By backdating this after committing we avoid adding more overhead in the hot path. By only logging things that actually committed, we avoid the costly unwinding of an interrupted render which was hard to maintain in earlier versions. We already have the start time but we don't have the end time. That's because `actualStartTime + actualDuration` isn't enough since `actualDuration` counts the actual CPU time excluding yields and suspending in the render. Instead, we infer the end time to be the start time of the next sibling or the complete time of the whole root if there are no more siblings. We need to pass this down the passive effect tree. This will mean that any overhead and yields are attributed to this component's span. In a follow up, we'll need to start logging these yields to make it clear that this is not part of the component's self-time. In follow ups, I'll do the same for commit phases. We'll also need to log more information about the phases in the top track. We'll also need to filter out more components from the trees that we don't need to highlight like the internal Offscreen components. It also needs polish on colors etc. Currently, I place the components into separate tracks depending on which lane currently committed. That way you can see what was blocking Transitions or Suspense etc. One problem that I've hit with the new performance.measure extensions is that these tracks show up in the order they're used which is not the order of priority that we use. Even when you add fake markers they have to actually be within the performance run since otherwise the calls are noops so it's not enough to do that once. However, I think this visualization is actually not good because these trees end up so large that you can't see any other lanes once you expand one. Therefore, I think in a follow up I'll actually instead switch to a model where Components is a single track regardless of lane since we don't currently have overlap anyway. Then the description about what is actually rendering can be separate lanes. <img width="1512" alt="Screenshot 2024-09-15 at 10 55 55 PM" src="https://github.com/user-attachments/assets/5ca3fa74-97ce-40c7-97f7-80c1dd7d6470"> <img width="1512" alt="Screenshot 2024-09-15 at 10 56 27 PM" src="https://github.com/user-attachments/assets/557ad65b-4190-465f-843c-0bc6cbb9326d">
1 parent ee1a403 commit f2df569

File tree

7 files changed

+285
-49
lines changed

7 files changed

+285
-49
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
enableUseEffectEventHook,
5454
enableLegacyHidden,
5555
disableLegacyMode,
56+
enableComponentPerformanceTrack,
5657
} from 'shared/ReactFeatureFlags';
5758
import {
5859
FunctionComponent,
@@ -102,7 +103,9 @@ import {
102103
getCommitTime,
103104
recordLayoutEffectDuration,
104105
startLayoutEffectTimer,
106+
getCompleteTime,
105107
} from './ReactProfilerTimer';
108+
import {logComponentRender} from './ReactFiberPerformanceTrack';
106109
import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
107110
import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue';
108111
import {
@@ -2648,6 +2651,9 @@ export function commitPassiveMountEffects(
26482651
finishedWork,
26492652
committedLanes,
26502653
committedTransitions,
2654+
enableProfilerTimer && enableComponentPerformanceTrack
2655+
? getCompleteTime()
2656+
: 0,
26512657
);
26522658
}
26532659

@@ -2656,17 +2662,41 @@ function recursivelyTraversePassiveMountEffects(
26562662
parentFiber: Fiber,
26572663
committedLanes: Lanes,
26582664
committedTransitions: Array<Transition> | null,
2665+
endTime: number, // Profiling-only. The start time of the next Fiber or root completion.
26592666
) {
2660-
if (parentFiber.subtreeFlags & PassiveMask) {
2667+
if (
2668+
parentFiber.subtreeFlags & PassiveMask ||
2669+
// If this subtree rendered with profiling this commit, we need to visit it to log it.
2670+
(enableProfilerTimer &&
2671+
enableComponentPerformanceTrack &&
2672+
parentFiber.actualDuration !== 0 &&
2673+
(parentFiber.alternate === null ||
2674+
parentFiber.alternate.child !== parentFiber.child))
2675+
) {
26612676
let child = parentFiber.child;
26622677
while (child !== null) {
2663-
commitPassiveMountOnFiber(
2664-
root,
2665-
child,
2666-
committedLanes,
2667-
committedTransitions,
2668-
);
2669-
child = child.sibling;
2678+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
2679+
const nextSibling = child.sibling;
2680+
commitPassiveMountOnFiber(
2681+
root,
2682+
child,
2683+
committedLanes,
2684+
committedTransitions,
2685+
nextSibling !== null
2686+
? ((nextSibling.actualStartTime: any): number)
2687+
: endTime,
2688+
);
2689+
child = nextSibling;
2690+
} else {
2691+
commitPassiveMountOnFiber(
2692+
root,
2693+
child,
2694+
committedLanes,
2695+
committedTransitions,
2696+
0,
2697+
);
2698+
child = child.sibling;
2699+
}
26702700
}
26712701
}
26722702
}
@@ -2676,7 +2706,25 @@ function commitPassiveMountOnFiber(
26762706
finishedWork: Fiber,
26772707
committedLanes: Lanes,
26782708
committedTransitions: Array<Transition> | null,
2709+
endTime: number, // Profiling-only. The start time of the next Fiber or root completion.
26792710
): void {
2711+
// If this component rendered in Profiling mode (DEV or in Profiler component) then log its
2712+
// render time. We do this after the fact in the passive effect to avoid the overhead of this
2713+
// getting in the way of the render characteristics and avoid the overhead of unwinding
2714+
// uncommitted renders.
2715+
if (
2716+
enableProfilerTimer &&
2717+
enableComponentPerformanceTrack &&
2718+
(finishedWork.mode & ProfileMode) !== NoMode &&
2719+
((finishedWork.actualStartTime: any): number) > 0
2720+
) {
2721+
logComponentRender(
2722+
finishedWork,
2723+
((finishedWork.actualStartTime: any): number),
2724+
endTime,
2725+
);
2726+
}
2727+
26802728
// When updating this function, also update reconnectPassiveEffects, which does
26812729
// most of the same things when an offscreen tree goes from hidden -> visible,
26822730
// or when toggling effects inside a hidden tree.
@@ -2690,6 +2738,7 @@ function commitPassiveMountOnFiber(
26902738
finishedWork,
26912739
committedLanes,
26922740
committedTransitions,
2741+
endTime,
26932742
);
26942743
if (flags & Passive) {
26952744
commitHookPassiveMountEffects(
@@ -2705,6 +2754,7 @@ function commitPassiveMountOnFiber(
27052754
finishedWork,
27062755
committedLanes,
27072756
committedTransitions,
2757+
endTime,
27082758
);
27092759
if (flags & Passive) {
27102760
if (enableCache) {
@@ -2762,6 +2812,7 @@ function commitPassiveMountOnFiber(
27622812
finishedWork,
27632813
committedLanes,
27642814
committedTransitions,
2815+
endTime,
27652816
);
27662817

27672818
// Only Profilers with work in their subtree will have a Passive effect scheduled.
@@ -2809,6 +2860,7 @@ function commitPassiveMountOnFiber(
28092860
finishedWork,
28102861
committedLanes,
28112862
committedTransitions,
2863+
endTime,
28122864
);
28132865

28142866
if (flags & Passive) {
@@ -2834,6 +2886,7 @@ function commitPassiveMountOnFiber(
28342886
finishedWork,
28352887
committedLanes,
28362888
committedTransitions,
2889+
endTime,
28372890
);
28382891
} else {
28392892
if (disableLegacyMode || finishedWork.mode & ConcurrentMode) {
@@ -2858,6 +2911,7 @@ function commitPassiveMountOnFiber(
28582911
finishedWork,
28592912
committedLanes,
28602913
committedTransitions,
2914+
endTime,
28612915
);
28622916
}
28632917
}
@@ -2870,6 +2924,7 @@ function commitPassiveMountOnFiber(
28702924
finishedWork,
28712925
committedLanes,
28722926
committedTransitions,
2927+
endTime,
28732928
);
28742929
} else {
28752930
// The effects are currently disconnected. Reconnect them, while also
@@ -2901,6 +2956,7 @@ function commitPassiveMountOnFiber(
29012956
finishedWork,
29022957
committedLanes,
29032958
committedTransitions,
2959+
endTime,
29042960
);
29052961
if (flags & Passive) {
29062962
// TODO: Pass `current` as argument to this function
@@ -2916,6 +2972,7 @@ function commitPassiveMountOnFiber(
29162972
finishedWork,
29172973
committedLanes,
29182974
committedTransitions,
2975+
endTime,
29192976
);
29202977
if (flags & Passive) {
29212978
commitTracingMarkerPassiveMountEffect(finishedWork);
@@ -2930,6 +2987,7 @@ function commitPassiveMountOnFiber(
29302987
finishedWork,
29312988
committedLanes,
29322989
committedTransitions,
2990+
endTime,
29332991
);
29342992
break;
29352993
}

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,3 +1143,35 @@ export function clearTransitionsForLanes(root: FiberRoot, lanes: Lane | Lanes) {
11431143
lanes &= ~lane;
11441144
}
11451145
}
1146+
1147+
// Used to name the Performance Track
1148+
export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string {
1149+
if (
1150+
lanes &
1151+
(SyncHydrationLane |
1152+
SyncLane |
1153+
InputContinuousHydrationLane |
1154+
InputContinuousLane |
1155+
DefaultHydrationLane |
1156+
DefaultLane)
1157+
) {
1158+
return 'Blocking';
1159+
}
1160+
if (lanes & (TransitionHydrationLane | TransitionLanes)) {
1161+
return 'Transition';
1162+
}
1163+
if (lanes & RetryLanes) {
1164+
return 'Suspense';
1165+
}
1166+
if (
1167+
lanes &
1168+
(SelectiveHydrationLane |
1169+
IdleHydrationLane |
1170+
IdleLane |
1171+
OffscreenLane |
1172+
DeferredLane)
1173+
) {
1174+
return 'Idle';
1175+
}
1176+
return 'Other';
1177+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Fiber} from './ReactInternalTypes';
11+
12+
import getComponentNameFromFiber from './getComponentNameFromFiber';
13+
14+
import {getGroupNameOfHighestPriorityLane} from './ReactFiberLane';
15+
16+
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
17+
18+
const supportsUserTiming =
19+
enableProfilerTimer &&
20+
typeof performance !== 'undefined' &&
21+
// $FlowFixMe[method-unbinding]
22+
typeof performance.measure === 'function';
23+
24+
const TRACK_GROUP = 'Components ⚛';
25+
26+
// Reused to avoid thrashing the GC.
27+
const reusableComponentDevToolDetails = {
28+
dataType: 'track-entry',
29+
color: 'primary',
30+
track: 'Blocking', // Lane
31+
trackGroup: TRACK_GROUP,
32+
};
33+
const reusableComponentOptions = {
34+
start: -0,
35+
end: -0,
36+
detail: {
37+
devtools: reusableComponentDevToolDetails,
38+
},
39+
};
40+
41+
export function setCurrentTrackFromLanes(lanes: number): void {
42+
reusableComponentDevToolDetails.track =
43+
getGroupNameOfHighestPriorityLane(lanes);
44+
}
45+
46+
export function logComponentRender(
47+
fiber: Fiber,
48+
startTime: number,
49+
endTime: number,
50+
): void {
51+
const name = getComponentNameFromFiber(fiber);
52+
if (name === null) {
53+
// Skip
54+
return;
55+
}
56+
if (supportsUserTiming) {
57+
reusableComponentOptions.start = startTime;
58+
reusableComponentOptions.end = endTime;
59+
performance.measure(name, reusableComponentOptions);
60+
}
61+
}

0 commit comments

Comments
 (0)