@samitouri / QOS-React-1 / commits / 4549be0f84

[Fiber] Optimize enableProfilerCommitHooks by Collecting Elapsed Effect Duration in Module Scope (#30981)

Stacked on #30979. The problem with the previous approach is that it recursively walked the tree up to propagate the resulting time from recording a layout effect. Instead, we keep a running count of the effect duration on the module scope. Then we reset it when entering a nested Profiler and then we add its elapsed count when we exit the Profiler. This also fixes a bug where we weren't previously including unmount times for some detached trees since they couldn't bubble up to find the profiler.

Sebastian Markbåge committed Sep 17, 2024 at 15:12 UTC 4549be0f846e7df5a4eaabf06369d93bd120271e
7 files changed +272 -375
packages/react-reconciler/src/ReactFiberBeginWork.js
+4 -4
@@ -1033,8 +1033,8 @@ function updateProfiler(
1033 // Reset effect durations for the next eventual effect phase.
1034 // These are reset during render to allow the DevTools commit hook a chance to read them,
1035 const stateNode = workInProgress.stateNode;
1036 - stateNode.effectDuration = 0;
1037 - stateNode.passiveEffectDuration = 0;
1036 + stateNode.effectDuration = -0;
1037 + stateNode.passiveEffectDuration = -0;
1038 }
1039 }
1040 const nextProps = workInProgress.pendingProps;
@@ -3711,8 +3711,8 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3711 // Reset effect durations for the next eventual effect phase.
3712 // These are reset during render to allow the DevTools commit hook a chance to read them,
3713 const stateNode = workInProgress.stateNode;
3714 - stateNode.effectDuration = 0;
3715 - stateNode.passiveEffectDuration = 0;
3714 + stateNode.effectDuration = -0;
3715 + stateNode.passiveEffectDuration = -0;
3716 }
3717 }
3718 break;
packages/react-reconciler/src/ReactFiberCommitEffects.js
+48 -23
@@ -31,10 +31,8 @@ import {NoFlags} from './ReactFiberFlags';
31 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
32 import {resolveClassComponentProps} from './ReactFiberClassComponent';
33 import {
34 - recordLayoutEffectDuration,
35 - startLayoutEffectTimer,
36 - recordPassiveEffectDuration,
37 - startPassiveEffectTimer,
34 + recordEffectDuration,
35 + startEffectTimer,
36 isCurrentUpdateNested,
37 } from './ReactProfilerTimer';
38 import {NoMode, ProfileMode} from './ReactTypeOfMode';
@@ -91,14 +89,41 @@ export function commitHookLayoutEffects(
89 // e.g. a destroy function in one component should never override a ref set
90 // by a create function in another component during the same commit.
91 if (shouldProfile(finishedWork)) {
94 - startLayoutEffectTimer();
92 + startEffectTimer();
93 commitHookEffectListMount(hookFlags, finishedWork);
96 - recordLayoutEffectDuration(finishedWork);
94 + recordEffectDuration(finishedWork);
95 } else {
96 commitHookEffectListMount(hookFlags, finishedWork);
97 }
98 }
99
100 +export function commitHookLayoutUnmountEffects(
101 + finishedWork: Fiber,
102 + nearestMountedAncestor: null | Fiber,
103 + hookFlags: HookFlags,
104 +) {
105 + // Layout effects are destroyed during the mutation phase so that all
106 + // destroy functions for all fibers are called before any create functions.
107 + // This prevents sibling component effects from interfering with each other,
108 + // e.g. a destroy function in one component should never override a ref set
109 + // by a create function in another component during the same commit.
110 + if (shouldProfile(finishedWork)) {
111 + startEffectTimer();
112 + commitHookEffectListUnmount(
113 + hookFlags,
114 + finishedWork,
115 + nearestMountedAncestor,
116 + );
117 + recordEffectDuration(finishedWork);
118 + } else {
119 + commitHookEffectListUnmount(
120 + hookFlags,
121 + finishedWork,
122 + nearestMountedAncestor,
123 + );
124 + }
125 +}
126 +
127 export function commitHookEffectListMount(
128 flags: HookFlags,
129 finishedWork: Fiber,
@@ -265,9 +290,9 @@ export function commitHookPassiveMountEffects(
290 hookFlags: HookFlags,
291 ) {
292 if (shouldProfile(finishedWork)) {
268 - startPassiveEffectTimer();
293 + startEffectTimer();
294 commitHookEffectListMount(hookFlags, finishedWork);
270 - recordPassiveEffectDuration(finishedWork);
295 + recordEffectDuration(finishedWork);
296 } else {
297 commitHookEffectListMount(hookFlags, finishedWork);
298 }
@@ -279,13 +304,13 @@ export function commitHookPassiveUnmountEffects(
304 hookFlags: HookFlags,
305 ) {
306 if (shouldProfile(finishedWork)) {
282 - startPassiveEffectTimer();
307 + startEffectTimer();
308 commitHookEffectListUnmount(
309 hookFlags,
310 finishedWork,
311 nearestMountedAncestor,
312 );
288 - recordPassiveEffectDuration(finishedWork);
313 + recordEffectDuration(finishedWork);
314 } else {
315 commitHookEffectListUnmount(
316 hookFlags,
@@ -333,7 +358,7 @@ export function commitClassLayoutLifecycles(
358 }
359 }
360 if (shouldProfile(finishedWork)) {
336 - startLayoutEffectTimer();
361 + startEffectTimer();
362 if (__DEV__) {
363 runWithFiberInDEV(
364 finishedWork,
@@ -348,7 +373,7 @@ export function commitClassLayoutLifecycles(
373 captureCommitPhaseError(finishedWork, finishedWork.return, error);
374 }
375 }
351 - recordLayoutEffectDuration(finishedWork);
376 + recordEffectDuration(finishedWork);
377 } else {
378 if (__DEV__) {
379 runWithFiberInDEV(
@@ -404,7 +429,7 @@ export function commitClassLayoutLifecycles(
429 }
430 }
431 if (shouldProfile(finishedWork)) {
407 - startLayoutEffectTimer();
432 + startEffectTimer();
433 if (__DEV__) {
434 runWithFiberInDEV(
435 finishedWork,
@@ -426,7 +451,7 @@ export function commitClassLayoutLifecycles(
451 captureCommitPhaseError(finishedWork, finishedWork.return, error);
452 }
453 }
429 - recordLayoutEffectDuration(finishedWork);
454 + recordEffectDuration(finishedWork);
455 } else {
456 if (__DEV__) {
457 runWithFiberInDEV(
@@ -679,7 +704,7 @@ export function safelyCallComponentWillUnmount(
704 );
705 instance.state = current.memoizedState;
706 if (shouldProfile(current)) {
682 - startLayoutEffectTimer();
707 + startEffectTimer();
708 if (__DEV__) {
709 runWithFiberInDEV(
710 current,
@@ -695,7 +720,7 @@ export function safelyCallComponentWillUnmount(
720 captureCommitPhaseError(current, nearestMountedAncestor, error);
721 }
722 }
698 - recordLayoutEffectDuration(current);
723 + recordEffectDuration(current);
724 } else {
725 if (__DEV__) {
726 runWithFiberInDEV(
@@ -736,10 +761,10 @@ function commitAttachRef(finishedWork: Fiber) {
761 if (typeof ref === 'function') {
762 if (shouldProfile(finishedWork)) {
763 try {
739 - startLayoutEffectTimer();
764 + startEffectTimer();
765 finishedWork.refCleanup = ref(instanceToUse);
766 } finally {
742 - recordLayoutEffectDuration(finishedWork);
767 + recordEffectDuration(finishedWork);
768 }
769 } else {
770 finishedWork.refCleanup = ref(instanceToUse);
@@ -793,14 +818,14 @@ export function safelyDetachRef(
818 try {
819 if (shouldProfile(current)) {
820 try {
796 - startLayoutEffectTimer();
821 + startEffectTimer();
822 if (__DEV__) {
823 runWithFiberInDEV(current, refCleanup);
824 } else {
825 refCleanup();
826 }
827 } finally {
803 - recordLayoutEffectDuration(current);
828 + recordEffectDuration(current);
829 }
830 } else {
831 if (__DEV__) {
@@ -823,14 +848,14 @@ export function safelyDetachRef(
848 try {
849 if (shouldProfile(current)) {
850 try {
826 - startLayoutEffectTimer();
851 + startEffectTimer();
852 if (__DEV__) {
853 (runWithFiberInDEV(current, ref, null): void);
854 } else {
855 ref(null);
856 }
857 } finally {
833 - recordLayoutEffectDuration(current);
858 + recordEffectDuration(current);
859 }
860 } else {
861 if (__DEV__) {
@@ -849,7 +874,7 @@ export function safelyDetachRef(
874 }
875 }
876
852 -export function safelyCallDestroy(
877 +function safelyCallDestroy(
878 current: Fiber,
879 nearestMountedAncestor: Fiber | null,
880 destroy: () => void,
packages/react-reconciler/src/ReactFiberCommitWork.js
+164 -264
@@ -44,7 +44,6 @@ import {
44 enablePersistedModeClonedFlag,
45 enableProfilerTimer,
46 enableProfilerCommitHooks,
47 - enableSchedulingProfiler,
47 enableSuspenseCallback,
48 enableScopeAPI,
49 enableUpdaterTracking,
@@ -101,9 +100,10 @@ import {
100 } from './ReactFiberFlags';
101 import {
102 getCommitTime,
104 - recordLayoutEffectDuration,
105 - startLayoutEffectTimer,
103 getCompleteTime,
104 + pushNestedEffectDurations,
105 + popNestedEffectDurations,
106 + bubbleNestedEffectDurations,
107 } from './ReactProfilerTimer';
108 import {logComponentRender} from './ReactFiberPerformanceTrack';
109 import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
@@ -145,22 +145,15 @@ import {
145 addMarkerProgressCallbackToPendingTransition,
146 addMarkerIncompleteCallbackToPendingTransition,
147 addMarkerCompleteCallbackToPendingTransition,
148 - setIsRunningInsertionEffect,
148 } from './ReactFiberWorkLoop';
149 import {
151 - NoFlags as NoHookEffect,
150 HasEffect as HookHasEffect,
151 Layout as HookLayout,
152 Insertion as HookInsertion,
153 Passive as HookPassive,
154 } from './ReactHookEffectTags';
155 import {doesFiberContain} from './ReactFiberTreeReflection';
158 -import {
159 - isDevToolsPresent,
160 - markComponentLayoutEffectUnmountStarted,
161 - markComponentLayoutEffectUnmountStopped,
162 - onCommitUnmount,
163 -} from './ReactFiberDevToolsHook';
156 +import {isDevToolsPresent, onCommitUnmount} from './ReactFiberDevToolsHook';
157 import {releaseCache, retainCache} from './ReactFiberCacheComponent';
158 import {clearTransitionsForLanes} from './ReactFiberLane';
159 import {
@@ -176,6 +169,7 @@ import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
169 import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
170 import {
171 commitHookLayoutEffects,
172 + commitHookLayoutUnmountEffects,
173 commitHookEffectListMount,
174 commitHookEffectListUnmount,
175 commitHookPassiveMountEffects,
@@ -188,7 +182,6 @@ import {
182 safelyCallComponentWillUnmount,
183 safelyAttachRef,
184 safelyDetachRef,
191 - safelyCallDestroy,
185 commitProfilerUpdate,
186 commitProfilerPostCommit,
187 commitRootCallbacks,
@@ -226,14 +219,6 @@ let nextEffect: Fiber | null = null;
219 let inProgressLanes: Lanes | null = null;
220 let inProgressRoot: FiberRoot | null = null;
221
229 -function shouldProfile(current: Fiber): boolean {
230 - return (
231 - enableProfilerTimer &&
232 - enableProfilerCommitHooks &&
233 - (current.mode & ProfileMode) !== NoMode
234 - );
235 -}
236 -
222 let focusedInstanceHandle: null | Fiber = null;
223 let shouldFireAfterActiveInstanceBlur: boolean = false;
224
@@ -434,6 +419,7 @@ function commitLayoutEffectOnFiber(
419 break;
420 }
421 case HostRoot: {
422 + const prevEffectDuration = pushNestedEffectDurations();
423 recursivelyTraverseLayoutEffects(
424 finishedRoot,
425 finishedWork,
@@ -442,6 +428,10 @@ function commitLayoutEffectOnFiber(
428 if (flags & Callback) {
429 commitRootCallbacks(finishedWork);
430 }
431 + if (enableProfilerTimer && enableProfilerCommitHooks) {
432 + finishedRoot.effectDuration +=
433 + popNestedEffectDurations(prevEffectDuration);
434 + }
435 break;
436 }
437 case HostHoistable: {
@@ -481,39 +471,38 @@ function commitLayoutEffectOnFiber(
471 break;
472 }
473 case Profiler: {
484 - recursivelyTraverseLayoutEffects(
485 - finishedRoot,
486 - finishedWork,
487 - committedLanes,
488 - );
474 // TODO: Should this fire inside an offscreen tree? Or should it wait to
475 // fire when the tree becomes visible again.
476 if (flags & Update) {
492 - const {effectDuration} = finishedWork.stateNode;
477 + const prevEffectDuration = pushNestedEffectDurations();
478 +
479 + recursivelyTraverseLayoutEffects(
480 + finishedRoot,
481 + finishedWork,
482 + committedLanes,
483 + );
484 +
485 + const profilerInstance = finishedWork.stateNode;
486 +
487 + if (enableProfilerTimer && enableProfilerCommitHooks) {
488 + // Propagate layout effect durations to the next nearest Profiler ancestor.
489 + // Do not reset these values until the next render so DevTools has a chance to read them first.
490 + profilerInstance.effectDuration +=
491 + bubbleNestedEffectDurations(prevEffectDuration);
492 + }
493
494 commitProfilerUpdate(
495 finishedWork,
496 current,
497 getCommitTime(),
498 - effectDuration,
498 + profilerInstance.effectDuration,
499 + );
500 + } else {
501 + recursivelyTraverseLayoutEffects(
502 + finishedRoot,
503 + finishedWork,
504 + committedLanes,
505 );
500 -
501 - // Propagate layout effect durations to the next nearest Profiler ancestor.
502 - // Do not reset these values until the next render so DevTools has a chance to read them first.
503 - let parentFiber = finishedWork.return;
504 - outer: while (parentFiber !== null) {
505 - switch (parentFiber.tag) {
506 - case HostRoot:
507 - const root = parentFiber.stateNode;
508 - root.effectDuration += effectDuration;
509 - break outer;
510 - case Profiler:
511 - const parentStateNode = parentFiber.stateNode;
512 - parentStateNode.effectDuration += effectDuration;
513 - break outer;
514 - }
515 - parentFiber = parentFiber.return;
516 - }
506 }
507 break;
508 }
@@ -1262,132 +1251,24 @@ function commitDeletionEffectsOnFiber(
1251 case ForwardRef:
1252 case MemoComponent:
1253 case SimpleMemoComponent: {
1265 - if (enableHiddenSubtreeInsertionEffectCleanup) {
1266 - // When deleting a fiber, we may need to destroy insertion or layout effects.
1267 - // Insertion effects are not destroyed on hidden, only when destroyed, so now
1268 - // we need to destroy them. Layout effects are destroyed when hidden, so
1269 - // we only need to destroy them if the tree is visible.
1270 - const updateQueue: FunctionComponentUpdateQueue | null =
1271 - (deletedFiber.updateQueue: any);
1272 - if (updateQueue !== null) {
1273 - const lastEffect = updateQueue.lastEffect;
1274 - if (lastEffect !== null) {
1275 - const firstEffect = lastEffect.next;
1276 -
1277 - let effect = firstEffect;
1278 - do {
1279 - const tag = effect.tag;
1280 - const inst = effect.inst;
1281 - const destroy = inst.destroy;
1282 - if (destroy !== undefined) {
1283 - if ((tag & HookInsertion) !== NoHookEffect) {
1284 - // TODO: add insertion effect marks and profiling.
1285 - if (__DEV__) {
1286 - setIsRunningInsertionEffect(true);
1287 - }
1288 -
1289 - inst.destroy = undefined;
1290 - safelyCallDestroy(
1291 - deletedFiber,
1292 - nearestMountedAncestor,
1293 - destroy,
1294 - );
1295 -
1296 - if (__DEV__) {
1297 - setIsRunningInsertionEffect(false);
1298 - }
1299 - } else if (
1300 - !offscreenSubtreeWasHidden &&
1301 - (tag & HookLayout) !== NoHookEffect
1302 - ) {
1303 - // Offscreen fibers already unmounted their layout effects.
1304 - // We only need to destroy layout effects for visible trees.
1305 - if (enableSchedulingProfiler) {
1306 - markComponentLayoutEffectUnmountStarted(deletedFiber);
1307 - }
1308 -
1309 - if (shouldProfile(deletedFiber)) {
1310 - startLayoutEffectTimer();
1311 - inst.destroy = undefined;
1312 - safelyCallDestroy(
1313 - deletedFiber,
1314 - nearestMountedAncestor,
1315 - destroy,
1316 - );
1317 - recordLayoutEffectDuration(deletedFiber);
1318 - } else {
1319 - inst.destroy = undefined;
1320 - safelyCallDestroy(
1321 - deletedFiber,
1322 - nearestMountedAncestor,
1323 - destroy,
1324 - );
1325 - }
1326 -
1327 - if (enableSchedulingProfiler) {
1328 - markComponentLayoutEffectUnmountStopped();
1329 - }
1330 - }
1331 - }
1332 - effect = effect.next;
1333 - } while (effect !== firstEffect);
1334 - }
1335 - }
1336 - } else if (!offscreenSubtreeWasHidden) {
1337 - const updateQueue: FunctionComponentUpdateQueue | null =
1338 - (deletedFiber.updateQueue: any);
1339 - if (updateQueue !== null) {
1340 - const lastEffect = updateQueue.lastEffect;
1341 - if (lastEffect !== null) {
1342 - const firstEffect = lastEffect.next;
1343 -
1344 - let effect = firstEffect;
1345 - do {
1346 - const tag = effect.tag;
1347 - const inst = effect.inst;
1348 - const destroy = inst.destroy;
1349 - if (destroy !== undefined) {
1350 - if ((tag & HookInsertion) !== NoHookEffect) {
1351 - inst.destroy = undefined;
1352 - safelyCallDestroy(
1353 - deletedFiber,
1354 - nearestMountedAncestor,
1355 - destroy,
1356 - );
1357 - } else if ((tag & HookLayout) !== NoHookEffect) {
1358 - if (enableSchedulingProfiler) {
1359 - markComponentLayoutEffectUnmountStarted(deletedFiber);
1360 - }
1361 -
1362 - if (shouldProfile(deletedFiber)) {
1363 - startLayoutEffectTimer();
1364 - inst.destroy = undefined;
1365 - safelyCallDestroy(
1366 - deletedFiber,
1367 - nearestMountedAncestor,
1368 - destroy,
1369 - );
1370 - recordLayoutEffectDuration(deletedFiber);
1371 - } else {
1372 - inst.destroy = undefined;
1373 - safelyCallDestroy(
1374 - deletedFiber,
1375 - nearestMountedAncestor,
1376 - destroy,
1377 - );
1378 - }
1379 -
1380 - if (enableSchedulingProfiler) {
1381 - markComponentLayoutEffectUnmountStopped();
1382 - }
1383 - }
1384 - }
1385 - effect = effect.next;
1386 - } while (effect !== firstEffect);
1387 - }
1388 - }
1254 + if (
1255 + enableHiddenSubtreeInsertionEffectCleanup ||
1256 + !offscreenSubtreeWasHidden
1257 + ) {
1258 + // TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings.
1259 + commitHookEffectListUnmount(
1260 + HookInsertion,
1261 + deletedFiber,
1262 + nearestMountedAncestor,
1263 + );
1264 + }
1265 + if (!offscreenSubtreeWasHidden) {
1266 + commitHookLayoutUnmountEffects(
1267 + deletedFiber,
1268 + nearestMountedAncestor,
1269 + HookLayout,
1270 + );
1271 }
1390 -
1272 recursivelyTraverseDeletionEffects(
1273 finishedRoot,
1274 nearestMountedAncestor,
@@ -1709,27 +1590,13 @@ function commitMutationEffectsOnFiber(
1590 finishedWork,
1591 finishedWork.return,
1592 );
1593 + // TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings.
1594 commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
1713 - // Layout effects are destroyed during the mutation phase so that all
1714 - // destroy functions for all fibers are called before any create functions.
1715 - // This prevents sibling component effects from interfering with each other,
1716 - // e.g. a destroy function in one component should never override a ref set
1717 - // by a create function in another component during the same commit.
1718 - if (shouldProfile(finishedWork)) {
1719 - startLayoutEffectTimer();
1720 - commitHookEffectListUnmount(
1721 - HookLayout | HookHasEffect,
1722 - finishedWork,
1723 - finishedWork.return,
1724 - );
1725 - recordLayoutEffectDuration(finishedWork);
1726 - } else {
1727 - commitHookEffectListUnmount(
1728 - HookLayout | HookHasEffect,
1729 - finishedWork,
1730 - finishedWork.return,
1731 - );
1732 - }
1595 + commitHookLayoutUnmountEffects(
1596 + finishedWork,
1597 + finishedWork.return,
1598 + HookLayout | HookHasEffect,
1599 + );
1600 }
1601 return;
1602 }
@@ -1917,6 +1784,8 @@ function commitMutationEffectsOnFiber(
1784 return;
1785 }
1786 case HostRoot: {
1787 + const prevEffectDuration = pushNestedEffectDurations();
1788 +
1789 if (supportsResources) {
1790 prepareToCommitHoistables();
1791
@@ -1960,6 +1829,10 @@ function commitMutationEffectsOnFiber(
1829 recursivelyResetForms(finishedWork);
1830 }
1831
1832 + if (enableProfilerTimer && enableProfilerCommitHooks) {
1833 + root.effectDuration += popNestedEffectDurations(prevEffectDuration);
1834 + }
1835 +
1836 return;
1837 }
1838 case HostPortal: {
@@ -1987,6 +1860,21 @@ function commitMutationEffectsOnFiber(
1860 }
1861 return;
1862 }
1863 + case Profiler: {
1864 + const prevEffectDuration = pushNestedEffectDurations();
1865 +
1866 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
1867 + commitReconciliationEffects(finishedWork);
1868 +
1869 + if (enableProfilerTimer && enableProfilerCommitHooks) {
1870 + const profilerInstance = finishedWork.stateNode;
1871 + // Propagate layout effect durations to the next nearest Profiler ancestor.
1872 + // Do not reset these values until the next render so DevTools has a chance to read them first.
1873 + profilerInstance.effectDuration +=
1874 + bubbleNestedEffectDurations(prevEffectDuration);
1875 + }
1876 + return;
1877 + }
1878 case SuspenseComponent: {
1879 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
1880 commitReconciliationEffects(finishedWork);
@@ -2247,25 +2135,11 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2135 case MemoComponent:
2136 case SimpleMemoComponent: {
2137 // TODO (Offscreen) Check: flags & LayoutStatic
2250 - if (shouldProfile(finishedWork)) {
2251 - try {
2252 - startLayoutEffectTimer();
2253 - commitHookEffectListUnmount(
2254 - HookLayout,
2255 - finishedWork,
2256 - finishedWork.return,
2257 - );
2258 - } finally {
2259 - recordLayoutEffectDuration(finishedWork);
2260 - }
2261 - } else {
2262 - commitHookEffectListUnmount(
2263 - HookLayout,
2264 - finishedWork,
2265 - finishedWork.return,
2266 - );
2267 - }
2268 -
2138 + commitHookLayoutUnmountEffects(
2139 + finishedWork,
2140 + finishedWork.return,
2141 + HookLayout,
2142 + );
2143 recursivelyTraverseDisappearLayoutEffects(finishedWork);
2144 break;
2145 }
@@ -2395,38 +2269,37 @@ export function reappearLayoutEffects(
2269 break;
2270 }
2271 case Profiler: {
2398 - recursivelyTraverseReappearLayoutEffects(
2399 - finishedRoot,
2400 - finishedWork,
2401 - includeWorkInProgressEffects,
2402 - );
2272 // TODO: Figure out how Profiler updates should work with Offscreen
2273 if (includeWorkInProgressEffects && flags & Update) {
2405 - const {effectDuration} = finishedWork.stateNode;
2274 + const prevEffectDuration = pushNestedEffectDurations();
2275 +
2276 + recursivelyTraverseReappearLayoutEffects(
2277 + finishedRoot,
2278 + finishedWork,
2279 + includeWorkInProgressEffects,
2280 + );
2281 +
2282 + const profilerInstance = finishedWork.stateNode;
2283 +
2284 + if (enableProfilerTimer && enableProfilerCommitHooks) {
2285 + // Propagate layout effect durations to the next nearest Profiler ancestor.
2286 + // Do not reset these values until the next render so DevTools has a chance to read them first.
2287 + profilerInstance.effectDuration +=
2288 + bubbleNestedEffectDurations(prevEffectDuration);
2289 + }
2290
2291 commitProfilerUpdate(
2292 finishedWork,
2293 current,
2294 getCommitTime(),
2411 - effectDuration,
2295 + profilerInstance.effectDuration,
2296 + );
2297 + } else {
2298 + recursivelyTraverseReappearLayoutEffects(
2299 + finishedRoot,
2300 + finishedWork,
2301 + includeWorkInProgressEffects,
2302 );
2413 -
2414 - // Propagate layout effect durations to the next nearest Profiler ancestor.
2415 - // Do not reset these values until the next render so DevTools has a chance to read them first.
2416 - let parentFiber = finishedWork.return;
2417 - outer: while (parentFiber !== null) {
2418 - switch (parentFiber.tag) {
2419 - case HostRoot:
2420 - const root = parentFiber.stateNode;
2421 - root.effectDuration += effectDuration;
2422 - break outer;
2423 - case Profiler:
2424 - const parentStateNode = parentFiber.stateNode;
2425 - parentStateNode.effectDuration += effectDuration;
2426 - break outer;
2427 - }
2428 - parentFiber = parentFiber.return;
2429 - }
2303 }
2304 break;
2305 }
@@ -2745,6 +2618,7 @@ function commitPassiveMountOnFiber(
2618 break;
2619 }
2620 case HostRoot: {
2621 + const prevEffectDuration = pushNestedEffectDurations();
2622 recursivelyTraversePassiveMountEffects(
2623 finishedRoot,
2624 finishedWork,
@@ -2800,48 +2674,50 @@ function commitPassiveMountOnFiber(
2674 clearTransitionsForLanes(finishedRoot, committedLanes);
2675 }
2676 }
2677 + if (enableProfilerTimer && enableProfilerCommitHooks) {
2678 + finishedRoot.passiveEffectDuration +=
2679 + popNestedEffectDurations(prevEffectDuration);
2680 + }
2681 break;
2682 }
2683 case Profiler: {
2806 - recursivelyTraversePassiveMountEffects(
2807 - finishedRoot,
2808 - finishedWork,
2809 - committedLanes,
2810 - committedTransitions,
2811 - endTime,
2812 - );
2813 -
2684 // Only Profilers with work in their subtree will have a Passive effect scheduled.
2685 if (flags & Passive) {
2816 - if (enableProfilerTimer && enableProfilerCommitHooks) {
2817 - const {passiveEffectDuration} = finishedWork.stateNode;
2686 + const prevEffectDuration = pushNestedEffectDurations();
2687
2819 - commitProfilerPostCommit(
2820 - finishedWork,
2821 - finishedWork.alternate,
2822 - // This value will still reflect the previous commit phase.
2823 - // It does not get reset until the start of the next commit phase.
2824 - getCommitTime(),
2825 - passiveEffectDuration,
2826 - );
2688 + recursivelyTraversePassiveMountEffects(
2689 + finishedRoot,
2690 + finishedWork,
2691 + committedLanes,
2692 + committedTransitions,
2693 + endTime,
2694 + );
2695 +
2696 + const profilerInstance = finishedWork.stateNode;
2697
2698 + if (enableProfilerTimer && enableProfilerCommitHooks) {
2699 // Bubble times to the next nearest ancestor Profiler.
2700 // After we process that Profiler, we'll bubble further up.
2830 - let parentFiber = finishedWork.return;
2831 - outer: while (parentFiber !== null) {
2832 - switch (parentFiber.tag) {
2833 - case HostRoot:
2834 - const root = parentFiber.stateNode;
2835 - root.passiveEffectDuration += passiveEffectDuration;
2836 - break outer;
2837 - case Profiler:
2838 - const parentStateNode = parentFiber.stateNode;
2839 - parentStateNode.passiveEffectDuration += passiveEffectDuration;
2840 - break outer;
2841 - }
2842 - parentFiber = parentFiber.return;
2843 - }
2701 + profilerInstance.passiveEffectDuration +=
2702 + bubbleNestedEffectDurations(prevEffectDuration);
2703 }
2704 +
2705 + commitProfilerPostCommit(
2706 + finishedWork,
2707 + finishedWork.alternate,
2708 + // This value will still reflect the previous commit phase.
2709 + // It does not get reset until the start of the next commit phase.
2710 + getCommitTime(),
2711 + profilerInstance.passiveEffectDuration,
2712 + );
2713 + } else {
2714 + recursivelyTraversePassiveMountEffects(
2715 + finishedRoot,
2716 + finishedWork,
2717 + committedLanes,
2718 + committedTransitions,
2719 + endTime,
2720 + );
2721 }
2722 break;
2723 }
@@ -3427,6 +3303,30 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
3303 }
3304 break;
3305 }
3306 + case HostRoot: {
3307 + const prevEffectDuration = pushNestedEffectDurations();
3308 + recursivelyTraversePassiveUnmountEffects(finishedWork);
3309 + if (enableProfilerTimer && enableProfilerCommitHooks) {
3310 + const finishedRoot: FiberRoot = finishedWork.stateNode;
3311 + finishedRoot.passiveEffectDuration +=
3312 + popNestedEffectDurations(prevEffectDuration);
3313 + }
3314 + break;
3315 + }
3316 + case Profiler: {
3317 + const prevEffectDuration = pushNestedEffectDurations();
3318 +
3319 + recursivelyTraversePassiveUnmountEffects(finishedWork);
3320 +
3321 + if (enableProfilerTimer && enableProfilerCommitHooks) {
3322 + const profilerInstance = finishedWork.stateNode;
3323 + // Propagate layout effect durations to the next nearest Profiler ancestor.
3324 + // Do not reset these values until the next render so DevTools has a chance to read them first.
3325 + profilerInstance.passiveEffectDuration +=
3326 + bubbleNestedEffectDurations(prevEffectDuration);
3327 + }
3328 + break;
3329 + }
3330 case OffscreenComponent: {
3331 const instance: OffscreenInstance = finishedWork.stateNode;
3332 const nextState: OffscreenState | null = finishedWork.memoizedState;
packages/react-reconciler/src/ReactFiberRoot.js
+2 -2
@@ -112,8 +112,8 @@ function FiberRootNode(
112 }
113
114 if (enableProfilerTimer && enableProfilerCommitHooks) {
115 - this.effectDuration = 0;
116 - this.passiveEffectDuration = 0;
115 + this.effectDuration = -0;
116 + this.passiveEffectDuration = -0;
117 }
118
119 if (enableUpdaterTracking) {
packages/react-reconciler/src/ReactInternalTypes.js
+6 -1
@@ -373,6 +373,11 @@ type TransitionTracingOnlyFiberRootProperties = {
373 incompleteTransitions: Map<Transition, TracingMarkerInstance>,
374 };
375
376 +type ProfilerCommitHooksOnlyFiberRootProperties = {
377 + effectDuration: number,
378 + passiveEffectDuration: number,
379 +};
380 +
381 // Exported FiberRoot type includes all properties,
382 // To avoid requiring potentially error-prone :any casts throughout the project.
383 // The types are defined separately within this file to ensure they stay in sync.
@@ -381,7 +386,7 @@ export type FiberRoot = {
386 ...SuspenseCallbackOnlyFiberRootProperties,
387 ...UpdaterTrackingOnlyFiberRootProperties,
388 ...TransitionTracingOnlyFiberRootProperties,
384 - ...
389 + ...ProfilerCommitHooksOnlyFiberRootProperties,
390 };
391
392 type BasicStateAction<S> = (S => S) | S;
packages/react-reconciler/src/ReactProfilerTimer.js
+44 -73
@@ -14,7 +14,6 @@ import {
14 enableProfilerNestedUpdatePhase,
15 enableProfilerTimer,
16 } from 'shared/ReactFeatureFlags';
17 -import {HostRoot, Profiler} from './ReactWorkTags';
17
18 // Intentionally not named imports because Rollup would use dynamic dispatch for
19 // CommonJS interop named imports.
@@ -35,11 +34,38 @@ export type ProfilerTimer = {
34 ...
35 };
36
38 -let completeTime: number = 0;
39 -let commitTime: number = 0;
40 -let layoutEffectStartTime: number = -1;
41 -let profilerStartTime: number = -1;
42 -let passiveEffectStartTime: number = -1;
37 +let completeTime: number = -0;
38 +let commitTime: number = -0;
39 +let profilerStartTime: number = -1.1;
40 +let profilerEffectDuration: number = -0;
41 +
42 +function pushNestedEffectDurations(): number {
43 + if (!enableProfilerTimer || !enableProfilerCommitHooks) {
44 + return 0;
45 + }
46 + const prevEffectDuration = profilerEffectDuration;
47 + profilerEffectDuration = 0; // Reset counter.
48 + return prevEffectDuration;
49 +}
50 +
51 +function popNestedEffectDurations(prevEffectDuration: number): number {
52 + if (!enableProfilerTimer || !enableProfilerCommitHooks) {
53 + return 0;
54 + }
55 + const elapsedTime = profilerEffectDuration;
56 + profilerEffectDuration = prevEffectDuration;
57 + return elapsedTime;
58 +}
59 +
60 +// Like pop but it also adds the current elapsed time to the parent scope.
61 +function bubbleNestedEffectDurations(prevEffectDuration: number): number {
62 + if (!enableProfilerTimer || !enableProfilerCommitHooks) {
63 + return 0;
64 + }
65 + const elapsedTime = profilerEffectDuration;
66 + profilerEffectDuration += prevEffectDuration;
67 + return elapsedTime;
68 +}
69
70 /**
71 * Tracks whether the current update was a nested/cascading update (scheduled from a layout effect).
@@ -153,83 +179,27 @@ function stopProfilerTimerIfRunningAndRecordIncompleteDuration(
179 }
180 }
181
156 -function recordLayoutEffectDuration(fiber: Fiber): void {
157 - if (!enableProfilerTimer || !enableProfilerCommitHooks) {
158 - return;
159 - }
160 -
161 - if (layoutEffectStartTime >= 0) {
162 - const elapsedTime = now() - layoutEffectStartTime;
163 -
164 - layoutEffectStartTime = -1;
165 -
166 - // Store duration on the next nearest Profiler ancestor
167 - // Or the root (for the DevTools Profiler to read)
168 - let parentFiber = fiber.return;
169 - while (parentFiber !== null) {
170 - switch (parentFiber.tag) {
171 - case HostRoot:
172 - const root = parentFiber.stateNode;
173 - root.effectDuration += elapsedTime;
174 - return;
175 - case Profiler:
176 - const parentStateNode = parentFiber.stateNode;
177 - parentStateNode.effectDuration += elapsedTime;
178 - return;
179 - }
180 - parentFiber = parentFiber.return;
181 - }
182 - }
183 -}
184 -
185 -function recordPassiveEffectDuration(fiber: Fiber): void {
182 +function recordEffectDuration(fiber: Fiber): void {
183 if (!enableProfilerTimer || !enableProfilerCommitHooks) {
184 return;
185 }
186
190 - if (passiveEffectStartTime >= 0) {
191 - const elapsedTime = now() - passiveEffectStartTime;
187 + if (profilerStartTime >= 0) {
188 + const elapsedTime = now() - profilerStartTime;
189
193 - passiveEffectStartTime = -1;
190 + profilerStartTime = -1;
191
192 // Store duration on the next nearest Profiler ancestor
193 // Or the root (for the DevTools Profiler to read)
197 - let parentFiber = fiber.return;
198 - while (parentFiber !== null) {
199 - switch (parentFiber.tag) {
200 - case HostRoot:
201 - const root = parentFiber.stateNode;
202 - if (root !== null) {
203 - root.passiveEffectDuration += elapsedTime;
204 - }
205 - return;
206 - case Profiler:
207 - const parentStateNode = parentFiber.stateNode;
208 - if (parentStateNode !== null) {
209 - // Detached fibers have their state node cleared out.
210 - // In this case, the return pointer is also cleared out,
211 - // so we won't be able to report the time spent in this Profiler's subtree.
212 - parentStateNode.passiveEffectDuration += elapsedTime;
213 - }
214 - return;
215 - }
216 - parentFiber = parentFiber.return;
217 - }
218 - }
219 -}
220 -
221 -function startLayoutEffectTimer(): void {
222 - if (!enableProfilerTimer || !enableProfilerCommitHooks) {
223 - return;
194 + profilerEffectDuration += elapsedTime;
195 }
225 - layoutEffectStartTime = now();
196 }
197
228 -function startPassiveEffectTimer(): void {
198 +function startEffectTimer(): void {
199 if (!enableProfilerTimer || !enableProfilerCommitHooks) {
200 return;
201 }
232 - passiveEffectStartTime = now();
202 + profilerStartTime = now();
203 }
204
205 function transferActualDuration(fiber: Fiber): void {
@@ -251,15 +221,16 @@ export {
221 recordCommitTime,
222 isCurrentUpdateNested,
223 markNestedUpdateScheduled,
254 - recordLayoutEffectDuration,
255 - recordPassiveEffectDuration,
224 + recordEffectDuration,
225 resetNestedUpdateFlag,
257 - startLayoutEffectTimer,
258 - startPassiveEffectTimer,
226 + startEffectTimer,
227 startProfilerTimer,
228 stopProfilerTimerIfRunning,
229 stopProfilerTimerIfRunningAndRecordDuration,
230 stopProfilerTimerIfRunningAndRecordIncompleteDuration,
231 syncNestedUpdateFlag,
232 transferActualDuration,
233 + pushNestedEffectDurations,
234 + popNestedEffectDurations,
235 + bubbleNestedEffectDurations,
236 };
packages/react/src/__tests__/ReactProfiler-test.internal.js
+4 -8
@@ -1643,7 +1643,7 @@ describe(`onCommit`, () => {
1643 expect(call).toHaveLength(4);
1644 expect(call[0]).toBe('root-update');
1645 expect(call[1]).toBe('update');
1646 - expect(call[2]).toBe(1100); // durations
1646 + expect(call[2]).toBe(11100); // durations
1647 expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
1648 });
1649
@@ -1952,11 +1952,7 @@ describe(`onPostCommit`, () => {
1952 expect(call).toHaveLength(4);
1953 expect(call[0]).toBe('unmount-test');
1954 expect(call[1]).toBe('update');
1955 - // TODO (bvaughn) The duration reported below should be 10100, but is 0
1956 - // by the time the passive effect is flushed its parent Fiber pointer is gone.
1957 - // If we refactor to preserve the unmounted Fiber tree we could fix this.
1958 - // The current implementation would require too much extra overhead to track this.
1959 - expect(call[2]).toBe(0); // durations
1955 + expect(call[2]).toBe(10100); // durations
1956 expect(call[3]).toBe(12030); // commit start time (before mutations or effects)
1957 });
1958
@@ -2085,7 +2081,7 @@ describe(`onPostCommit`, () => {
2081 expect(call).toHaveLength(4);
2082 expect(call[0]).toBe('root-update');
2083 expect(call[1]).toBe('update');
2088 - expect(call[2]).toBe(1100); // durations
2084 + expect(call[2]).toBe(11100); // durations
2085 expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
2086 });
2087
@@ -2300,7 +2296,7 @@ describe(`onPostCommit`, () => {
2296 expect(call).toHaveLength(4);
2297 expect(call[0]).toBe('root');
2298 expect(call[1]).toBe('update');
2303 - expect(call[2]).toBe(100000000); // durations
2299 + expect(call[2]).toBe(100001000); // durations
2300 // The commit time varies because the above duration time varies
2301 expect(call[3]).toBe(11221221); // commit start time (before mutations or effects)
2302 });