[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
});