@samitouri / QOS-React / commits / 1a8f92a869

[DevTools] Track Tree Base Duration of Virtual Instances (#30817)

These don't have their own time since they don't take up any time to render but they show up in the tree for context. However they never render themselves. Their base tree time is the base time of their children. This way they take up the same space as their combined children in the Profiler tree. (Instead of leaving a blank line which they did before this PR.) The frontend doesn't track the difference between a virtual instance and a Fiber that didn't render this update. This might be a bit confusing as to why it didn't render. I add the word "client" to make it a bit clearer and works for both. We should probably have different verbiage here based on it is a Server Component or something else. <img width="1103" alt="Screenshot 2024-08-26 at 5 00 47 PM" src="https://github.com/user-attachments/assets/87b811d4-7024-466a-845d-542493ed3ca2"> I also took the opportunity to remove idToTreeBaseDurationMap and idToRootMap maps. Cloning the Map isn't really all that super fast anyway and it means we have to maintain the map continuously as we render. Instead, we can track it on the instances and then walk the instances to create a snapshot when starting to profile. This isn't as fast but really fast too and requires less bookkeeping while rendering instead which is more sensitive than that one snapshot in the beginning.

Sebastian Markbåge committed Aug 27, 2024 at 12:05 UTC 1a8f92a8699e79966e65841fcb9110bba4c3df7f
3 files changed +77 -58
packages/react-devtools-shared/src/backend/fiber/renderer.js
+75 -56
@@ -164,6 +164,7 @@ type FiberInstance = {
164 source: null | string | Error | Source, // source location of this component function, or owned child stack
165 errors: null | Map<string, number>, // error messages and count
166 warnings: null | Map<string, number>, // warning messages and count
167 + treeBaseDuration: number, // the profiled time of the last render of this subtree
168 data: Fiber, // one of a Fiber pair
169 };
170
@@ -179,6 +180,7 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
180 source: null,
181 errors: null,
182 warnings: null,
183 + treeBaseDuration: 0,
184 data: fiber,
185 };
186 }
@@ -202,6 +204,7 @@ type VirtualInstance = {
204 // that old errors/warnings don't disappear when the instance is refreshed.
205 errors: null | Map<string, number>, // error messages and count
206 warnings: null | Map<string, number>, // warning messages and count
207 + treeBaseDuration: number, // the profiled time of the last render of this subtree
208 // The latest info for this instance. This can be updated over time and the
209 // same info can appear in more than once ServerComponentInstance.
210 data: ReactComponentInfo,
@@ -221,6 +224,7 @@ function createVirtualInstance(
224 source: null,
225 errors: null,
226 warnings: null,
227 + treeBaseDuration: 0,
228 data: debugEntry,
229 };
230 }
@@ -1355,16 +1359,6 @@ export function attach(
1359 }
1360 }
1361
1358 - // When profiling is supported, we store the latest tree base durations for each Fiber.
1359 - // This is so that we can quickly capture a snapshot of those values if profiling starts.
1360 - // If we didn't store these values, we'd have to crawl the tree when profiling started,
1361 - // and use a slow path to find each of the current Fibers.
1362 - const idToTreeBaseDurationMap: Map<number, number> = new Map();
1363 -
1364 - // When profiling is supported, we store the latest tree base durations for each Fiber.
1365 - // This map enables us to filter these times by root when sending them to the frontend.
1366 - const idToRootMap: Map<number, number> = new Map();
1367 -
1362 // When a mount or update is in progress, this value tracks the root that is being operated on.
1363 let currentRootID: number = -1;
1364
@@ -2211,9 +2205,7 @@ export function attach(
2205 }
2206
2207 if (isProfilingSupported) {
2214 - idToRootMap.set(id, currentRootID);
2215 -
2216 - recordProfilingDurations(fiber);
2208 + recordProfilingDurations(fiberInstance);
2209 }
2210 return fiberInstance;
2211 }
@@ -2226,8 +2218,6 @@ export function attach(
2218
2219 idToDevToolsInstanceMap.set(id, instance);
2220
2229 - const isProfilingSupported = false; // TODO: Support Tree Base Duration Based on Children.
2230 -
2221 const componentInfo = instance.data;
2222
2223 const key =
@@ -2274,12 +2264,6 @@ export function attach(
2264 pushOperation(ownerID);
2265 pushOperation(displayNameStringID);
2266 pushOperation(keyStringID);
2277 -
2278 - if (isProfilingSupported) {
2279 - idToRootMap.set(id, currentRootID);
2280 - // TODO: Include tree base duration of children somehow.
2281 - // recordProfilingDurations(...);
2282 - }
2267 }
2268
2269 function recordUnmount(fiberInstance: FiberInstance): void {
@@ -2314,12 +2298,6 @@ export function attach(
2298 }
2299
2300 untrackFiber(fiberInstance);
2317 -
2318 - const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
2319 - if (isProfilingSupported) {
2320 - idToRootMap.delete(id);
2321 - idToTreeBaseDurationMap.delete(id);
2322 - }
2301 }
2302
2303 // Running state of the remaining children from the previous version of this parent that
@@ -2430,6 +2408,8 @@ export function attach(
2408 traceNearestHostComponentUpdate,
2409 virtualLevel + 1,
2410 );
2411 + // Must be called after all children have been appended.
2412 + recordVirtualProfilingDurations(virtualInstance);
2413 } finally {
2414 reconcilingParent = stashedParent;
2415 previouslyReconciledSibling = stashedPrevious;
@@ -2445,12 +2425,6 @@ export function attach(
2425
2426 const id = instance.id;
2427 pendingRealUnmountedIDs.push(id);
2448 -
2449 - const isProfilingSupported = false; // TODO: Profiling support.
2450 - if (isProfilingSupported) {
2451 - idToRootMap.delete(id);
2452 - idToTreeBaseDurationMap.delete(id);
2453 - }
2428 }
2429
2430 function mountVirtualChildrenRecursively(
@@ -2684,11 +2658,12 @@ export function attach(
2658 removeChild(instance);
2659 }
2660
2687 - function recordProfilingDurations(fiber: Fiber) {
2688 - const id = getFiberIDThrows(fiber);
2661 + function recordProfilingDurations(fiberInstance: FiberInstance) {
2662 + const id = fiberInstance.id;
2663 + const fiber = fiberInstance.data;
2664 const {actualDuration, treeBaseDuration} = fiber;
2665
2691 - idToTreeBaseDurationMap.set(id, treeBaseDuration || 0);
2666 + fiberInstance.treeBaseDuration = treeBaseDuration || 0;
2667
2668 if (isProfiling) {
2669 const {alternate} = fiber;
@@ -2751,6 +2726,38 @@ export function attach(
2726 }
2727 }
2728
2729 + function recordVirtualProfilingDurations(virtualInstance: VirtualInstance) {
2730 + const id = virtualInstance.id;
2731 +
2732 + let treeBaseDuration = 0;
2733 + // Add up the base duration of the child instances. The virtual base duration
2734 + // will be the same as children's duration since we don't take up any render
2735 + // time in the virtual instance.
2736 + for (
2737 + let child = virtualInstance.firstChild;
2738 + child !== null;
2739 + child = child.nextSibling
2740 + ) {
2741 + treeBaseDuration += child.treeBaseDuration;
2742 + }
2743 +
2744 + if (isProfiling) {
2745 + const previousTreeBaseDuration = virtualInstance.treeBaseDuration;
2746 + if (treeBaseDuration !== previousTreeBaseDuration) {
2747 + // Tree base duration updates are included in the operations typed array.
2748 + // So we have to convert them from milliseconds to microseconds so we can send them as ints.
2749 + const convertedTreeBaseDuration = Math.floor(
2750 + (treeBaseDuration || 0) * 1000,
2751 + );
2752 + pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION);
2753 + pushOperation(id);
2754 + pushOperation(convertedTreeBaseDuration);
2755 + }
2756 + }
2757 +
2758 + virtualInstance.treeBaseDuration = treeBaseDuration;
2759 + }
2760 +
2761 function recordResetChildren(parentInstance: DevToolsInstance) {
2762 if (__DEBUG__) {
2763 if (
@@ -2818,6 +2825,8 @@ export function attach(
2825 ) {
2826 recordResetChildren(virtualInstance);
2827 }
2828 + // Must be called after all children have been appended.
2829 + recordVirtualProfilingDurations(virtualInstance);
2830 } finally {
2831 unmountRemainingChildren();
2832 reconcilingParent = stashedParent;
@@ -3265,11 +3274,11 @@ export function attach(
3274 }
3275 }
3276
3268 - if (shouldIncludeInTree) {
3277 + if (fiberInstance !== null) {
3278 const isProfilingSupported =
3279 nextFiber.hasOwnProperty('treeBaseDuration');
3280 if (isProfilingSupported) {
3272 - recordProfilingDurations(nextFiber);
3281 + recordProfilingDurations(fiberInstance);
3282 }
3283 }
3284 if (shouldResetChildren) {
@@ -5121,8 +5130,8 @@ export function attach(
5130 let currentCommitProfilingMetadata: CommitProfilingData | null = null;
5131 let displayNamesByRootID: DisplayNamesByRootID | null = null;
5132 let idToContextsMap: Map<number, any> | null = null;
5124 - let initialTreeBaseDurationsMap: Map<number, number> | null = null;
5125 - let initialIDToRootMap: Map<number, number> | null = null;
5133 + let initialTreeBaseDurationsMap: Map<number, Array<[number, number]>> | null =
5134 + null;
5135 let isProfiling: boolean = false;
5136 let profilingStartTime: number = 0;
5137 let recordChangeDescriptions: boolean = false;
@@ -5141,24 +5150,15 @@ export function attach(
5150 rootToCommitProfilingMetadataMap.forEach(
5151 (commitProfilingMetadata, rootID) => {
5152 const commitData: Array<CommitDataBackend> = [];
5144 - const initialTreeBaseDurations: Array<[number, number]> = [];
5153
5154 const displayName =
5155 (displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) ||
5156 'Unknown';
5157
5150 - if (initialTreeBaseDurationsMap != null) {
5151 - initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => {
5152 - if (
5153 - initialIDToRootMap != null &&
5154 - initialIDToRootMap.get(id) === rootID
5155 - ) {
5156 - // We don't need to convert milliseconds to microseconds in this case,
5157 - // because the profiling summary is JSON serialized.
5158 - initialTreeBaseDurations.push([id, treeBaseDuration]);
5159 - }
5160 - });
5161 - }
5158 + const initialTreeBaseDurations: Array<[number, number]> =
5159 + (initialTreeBaseDurationsMap !== null &&
5160 + initialTreeBaseDurationsMap.get(rootID)) ||
5161 + [];
5162
5163 commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => {
5164 const {
@@ -5245,6 +5245,22 @@ export function attach(
5245 };
5246 }
5247
5248 + function snapshotTreeBaseDurations(
5249 + instance: DevToolsInstance,
5250 + target: Array<[number, number]>,
5251 + ) {
5252 + // We don't need to convert milliseconds to microseconds in this case,
5253 + // because the profiling summary is JSON serialized.
5254 + target.push([instance.id, instance.treeBaseDuration]);
5255 + for (
5256 + let child = instance.firstChild;
5257 + child !== null;
5258 + child = child.nextSibling
5259 + ) {
5260 + snapshotTreeBaseDurations(child, target);
5261 + }
5262 + }
5263 +
5264 function startProfiling(shouldRecordChangeDescriptions: boolean) {
5265 if (isProfiling) {
5266 return;
@@ -5257,16 +5273,19 @@ export function attach(
5273 // since either of these may change during the profiling session
5274 // (e.g. when a fiber is re-rendered or when a fiber gets removed).
5275 displayNamesByRootID = new Map();
5260 - initialTreeBaseDurationsMap = new Map(idToTreeBaseDurationMap);
5261 - initialIDToRootMap = new Map(idToRootMap);
5276 + initialTreeBaseDurationsMap = new Map();
5277 idToContextsMap = new Map();
5278
5279 hook.getFiberRoots(rendererID).forEach(root => {
5265 - const rootID = getFiberIDThrows(root.current);
5280 + const rootInstance = getFiberInstanceThrows(root.current);
5281 + const rootID = rootInstance.id;
5282 ((displayNamesByRootID: any): DisplayNamesByRootID).set(
5283 rootID,
5284 getDisplayNameForRoot(root.current),
5285 );
5286 + const initialTreeBaseDurations: Array<[number, number]> = [];
5287 + snapshotTreeBaseDurations(rootInstance, initialTreeBaseDurations);
5288 + (initialTreeBaseDurationsMap: any).set(rootID, initialTreeBaseDurations);
5289
5290 if (shouldRecordChangeDescriptions) {
5291 // Record all contexts at the time profiling is started.
packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js
+1 -1
@@ -93,7 +93,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
93 )}
94
95 <div className={styles.Content}>
96 - {renderDurationInfo || <div>Did not render.</div>}
96 + {renderDurationInfo || <div>Did not client render.</div>}
97
98 <WhatChanged fiberID={id} />
99 </div>
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js
+1 -1
@@ -142,7 +142,7 @@ export default function SidebarSelectedFiberInfo(): React.Node {
142 </div>
143 )}
144 {listItems.length === 0 && (
145 - <div>Did not render during this profiling session.</div>
145 + <div>Did not render on the client during this profiling session.</div>
146 )}
147 </div>
148 </Fragment>