Skip to content

Commit 393c452

Browse files
author
Brian Vaughn
authoredNov 10, 2020
Add "nested-update" phase to Profiler API (#20163)
Background: State updates that are scheduled in a layout effect (useLayoutEffect or componentDidMount / componentDidUpdate) get processed synchronously by React before it yields to the browser to paint. This is done so that components can adjust their layout (e.g. position and size a tooltip) without any visible shifting being seen by users. This type of update is often called a "nested update" or a "cascading update". Because they delay paint, nested updates are considered expensive and should be avoided when possible. For example, effects that do not impact layout (e.g. adding event handlers, logging impressions) can be safely deferred to the passive effect phase by using useEffect instead. This PR updates the Profiler API to explicitly flag nested updates so they can be monitored for and avoided when possible. Implementation: I considered a few approaches for this. Add a new callback (e.g. onNestedUpdateScheduled) to the Profiler that gets called when a nested updates gets scheduled. Add an additional boolean parameter to the end of existing callbacks (e.g. wasNestedUpdate). Update the phase param to add an additional variant: "mount", "update", or "nested-update" (new). I think the third option makes for the best API so that's what I've implemented in this PR. Because the Profiler API is stable, this change will need to remain behind a feature flag until v18. I've turned the feature flag on for Facebook builds though after confirming that Web Speed does not currently make use of the phase parameter. Quirks: One quirk about the implementation I've chosen is that errors thrown during the layout phase are also reported as nested updates. I believe this is appropriate since these errors get processed synchronously and block paint. Errors thrown during render or from within passive effects are not affected by this change.
1 parent 93c3dc5 commit 393c452

16 files changed

+173
-30
lines changed
 

‎packages/react-reconciler/src/ReactFiberCommitWork.new.js

+22-11
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
enableSchedulerTracing,
3232
enableProfilerTimer,
3333
enableProfilerCommitHooks,
34+
enableProfilerNestedUpdatePhase,
3435
enableSuspenseServerRenderer,
3536
enableFundamentalAPI,
3637
enableSuspenseCallback,
@@ -94,6 +95,7 @@ import {
9495
import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
9596
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
9697
import {
98+
isCurrentUpdateNested,
9799
getCommitTime,
98100
recordLayoutEffectDuration,
99101
startLayoutEffectTimer,
@@ -357,22 +359,24 @@ function commitProfilerPassiveEffect(
357359
// It does not get reset until the start of the next commit phase.
358360
const commitTime = getCommitTime();
359361

362+
let phase = finishedWork.alternate === null ? 'mount' : 'update';
363+
if (enableProfilerNestedUpdatePhase) {
364+
if (isCurrentUpdateNested()) {
365+
phase = 'nested-update';
366+
}
367+
}
368+
360369
if (typeof onPostCommit === 'function') {
361370
if (enableSchedulerTracing) {
362371
onPostCommit(
363372
id,
364-
finishedWork.alternate === null ? 'mount' : 'update',
373+
phase,
365374
passiveEffectDuration,
366375
commitTime,
367376
finishedRoot.memoizedInteractions,
368377
);
369378
} else {
370-
onPostCommit(
371-
id,
372-
finishedWork.alternate === null ? 'mount' : 'update',
373-
passiveEffectDuration,
374-
commitTime,
375-
);
379+
onPostCommit(id, phase, passiveEffectDuration, commitTime);
376380
}
377381
}
378382
break;
@@ -1333,11 +1337,18 @@ function commitLayoutEffectsForProfiler(
13331337
const OnRenderFlag = Update;
13341338
const OnCommitFlag = Callback;
13351339

1340+
let phase = current === null ? 'mount' : 'update';
1341+
if (enableProfilerNestedUpdatePhase) {
1342+
if (isCurrentUpdateNested()) {
1343+
phase = 'nested-update';
1344+
}
1345+
}
1346+
13361347
if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') {
13371348
if (enableSchedulerTracing) {
13381349
onRender(
13391350
finishedWork.memoizedProps.id,
1340-
current === null ? 'mount' : 'update',
1351+
phase,
13411352
finishedWork.actualDuration,
13421353
finishedWork.treeBaseDuration,
13431354
finishedWork.actualStartTime,
@@ -1347,7 +1358,7 @@ function commitLayoutEffectsForProfiler(
13471358
} else {
13481359
onRender(
13491360
finishedWork.memoizedProps.id,
1350-
current === null ? 'mount' : 'update',
1361+
phase,
13511362
finishedWork.actualDuration,
13521363
finishedWork.treeBaseDuration,
13531364
finishedWork.actualStartTime,
@@ -1364,15 +1375,15 @@ function commitLayoutEffectsForProfiler(
13641375
if (enableSchedulerTracing) {
13651376
onCommit(
13661377
finishedWork.memoizedProps.id,
1367-
current === null ? 'mount' : 'update',
1378+
phase,
13681379
effectDuration,
13691380
commitTime,
13701381
finishedRoot.memoizedInteractions,
13711382
);
13721383
} else {
13731384
onCommit(
13741385
finishedWork.memoizedProps.id,
1375-
current === null ? 'mount' : 'update',
1386+
phase,
13761387
effectDuration,
13771388
commitTime,
13781389
);

‎packages/react-reconciler/src/ReactFiberCommitWork.old.js

+22-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
enableSchedulerTracing,
3131
enableProfilerTimer,
3232
enableProfilerCommitHooks,
33+
enableProfilerNestedUpdatePhase,
3334
enableSuspenseServerRenderer,
3435
enableFundamentalAPI,
3536
enableSuspenseCallback,
@@ -73,6 +74,7 @@ import invariant from 'shared/invariant';
7374
import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
7475
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
7576
import {
77+
isCurrentUpdateNested,
7678
getCommitTime,
7779
recordLayoutEffectDuration,
7880
startLayoutEffectTimer,
@@ -434,22 +436,24 @@ export function commitPassiveEffectDurations(
434436
// It does not get reset until the start of the next commit phase.
435437
const commitTime = getCommitTime();
436438

439+
let phase = finishedWork.alternate === null ? 'mount' : 'update';
440+
if (enableProfilerNestedUpdatePhase) {
441+
if (isCurrentUpdateNested()) {
442+
phase = 'nested-update';
443+
}
444+
}
445+
437446
if (typeof onPostCommit === 'function') {
438447
if (enableSchedulerTracing) {
439448
onPostCommit(
440449
id,
441-
finishedWork.alternate === null ? 'mount' : 'update',
450+
phase,
442451
passiveEffectDuration,
443452
commitTime,
444453
finishedRoot.memoizedInteractions,
445454
);
446455
} else {
447-
onPostCommit(
448-
id,
449-
finishedWork.alternate === null ? 'mount' : 'update',
450-
passiveEffectDuration,
451-
commitTime,
452-
);
456+
onPostCommit(id, phase, passiveEffectDuration, commitTime);
453457
}
454458
}
455459

@@ -706,11 +710,18 @@ function commitLifeCycles(
706710

707711
const commitTime = getCommitTime();
708712

713+
let phase = current === null ? 'mount' : 'update';
714+
if (enableProfilerNestedUpdatePhase) {
715+
if (isCurrentUpdateNested()) {
716+
phase = 'nested-update';
717+
}
718+
}
719+
709720
if (typeof onRender === 'function') {
710721
if (enableSchedulerTracing) {
711722
onRender(
712723
finishedWork.memoizedProps.id,
713-
current === null ? 'mount' : 'update',
724+
phase,
714725
finishedWork.actualDuration,
715726
finishedWork.treeBaseDuration,
716727
finishedWork.actualStartTime,
@@ -720,7 +731,7 @@ function commitLifeCycles(
720731
} else {
721732
onRender(
722733
finishedWork.memoizedProps.id,
723-
current === null ? 'mount' : 'update',
734+
phase,
724735
finishedWork.actualDuration,
725736
finishedWork.treeBaseDuration,
726737
finishedWork.actualStartTime,
@@ -734,15 +745,15 @@ function commitLifeCycles(
734745
if (enableSchedulerTracing) {
735746
onCommit(
736747
finishedWork.memoizedProps.id,
737-
current === null ? 'mount' : 'update',
748+
phase,
738749
effectDuration,
739750
commitTime,
740751
finishedRoot.memoizedInteractions,
741752
);
742753
} else {
743754
onCommit(
744755
finishedWork.memoizedProps.id,
745-
current === null ? 'mount' : 'update',
756+
phase,
746757
effectDuration,
747758
commitTime,
748759
);

‎packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
enableSuspenseServerRenderer,
2222
replayFailedUnitOfWorkWithInvokeGuardedCallback,
2323
enableProfilerTimer,
24+
enableProfilerNestedUpdatePhase,
2425
enableSchedulerTracing,
2526
warnAboutUnmockedScheduler,
2627
deferRenderPhaseUpdateToNextBatch,
@@ -194,9 +195,11 @@ import {
194195
} from './ReactFiberStack.new';
195196

196197
import {
198+
markNestedUpdateScheduled,
197199
recordCommitTime,
198200
startProfilerTimer,
199201
stopProfilerTimerIfRunningAndRecordDelta,
202+
syncNestedUpdateFlag,
200203
} from './ReactProfilerTimer.new';
201204

202205
// DEV stuff
@@ -938,6 +941,10 @@ function markRootSuspended(root, suspendedLanes) {
938941
// This is the entry point for synchronous tasks that don't go
939942
// through Scheduler
940943
function performSyncWorkOnRoot(root) {
944+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
945+
syncNestedUpdateFlag();
946+
}
947+
941948
invariant(
942949
(executionContext & (RenderContext | CommitContext)) === NoContext,
943950
'Should not already be working.',
@@ -1995,6 +2002,10 @@ function commitRootImpl(root, renderPriorityLevel) {
19952002
}
19962003

19972004
if (remainingLanes === SyncLane) {
2005+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
2006+
markNestedUpdateScheduled();
2007+
}
2008+
19982009
// Count the number of times the root synchronously re-renders without
19992010
// finishing. If there are too many, it indicates an infinite update loop.
20002011
if (root === rootWithNestedUpdates) {

‎packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
replayFailedUnitOfWorkWithInvokeGuardedCallback,
2323
enableProfilerTimer,
2424
enableProfilerCommitHooks,
25+
enableProfilerNestedUpdatePhase,
2526
enableSchedulerTracing,
2627
warnAboutUnmockedScheduler,
2728
deferRenderPhaseUpdateToNextBatch,
@@ -207,11 +208,13 @@ import {
207208
} from './ReactFiberStack.old';
208209

209210
import {
211+
markNestedUpdateScheduled,
210212
recordCommitTime,
211213
recordPassiveEffectDuration,
212214
startPassiveEffectTimer,
213215
startProfilerTimer,
214216
stopProfilerTimerIfRunningAndRecordDelta,
217+
syncNestedUpdateFlag,
215218
} from './ReactProfilerTimer.old';
216219

217220
// DEV stuff
@@ -962,6 +965,10 @@ function markRootSuspended(root, suspendedLanes) {
962965
// This is the entry point for synchronous tasks that don't go
963966
// through Scheduler
964967
function performSyncWorkOnRoot(root) {
968+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
969+
syncNestedUpdateFlag();
970+
}
971+
965972
invariant(
966973
(executionContext & (RenderContext | CommitContext)) === NoContext,
967974
'Should not already be working.',
@@ -2189,6 +2196,10 @@ function commitRootImpl(root, renderPriorityLevel) {
21892196
}
21902197

21912198
if (remainingLanes === SyncLane) {
2199+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
2200+
markNestedUpdateScheduled();
2201+
}
2202+
21922203
// Count the number of times the root synchronously re-renders without
21932204
// finishing. If there are too many, it indicates an infinite update loop.
21942205
if (root === rootWithNestedUpdates) {

‎packages/react-reconciler/src/ReactProfilerTimer.new.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import type {Fiber} from './ReactInternalTypes';
1111

1212
import {
13-
enableProfilerTimer,
1413
enableProfilerCommitHooks,
14+
enableProfilerNestedUpdatePhase,
15+
enableProfilerTimer,
1516
} from 'shared/ReactFeatureFlags';
1617
import {Profiler} from './ReactWorkTags';
1718

@@ -23,10 +24,13 @@ const {unstable_now: now} = Scheduler;
2324

2425
export type ProfilerTimer = {
2526
getCommitTime(): number,
27+
isCurrentUpdateNested(): boolean,
28+
markNestedUpdateScheduled(): void,
2629
recordCommitTime(): void,
2730
startProfilerTimer(fiber: Fiber): void,
2831
stopProfilerTimerIfRunning(fiber: Fiber): void,
2932
stopProfilerTimerIfRunningAndRecordDelta(fiber: Fiber): void,
33+
syncNestedUpdateFlag(): void,
3034
...
3135
};
3236

@@ -35,6 +39,42 @@ let layoutEffectStartTime: number = -1;
3539
let profilerStartTime: number = -1;
3640
let passiveEffectStartTime: number = -1;
3741

42+
/**
43+
* Tracks whether the current update was a nested/cascading update (scheduled from a layout effect).
44+
*
45+
* The overall sequence is:
46+
* 1. render
47+
* 2. commit (and call `onRender`, `onCommit`)
48+
* 3. check for nested updates
49+
* 4. flush passive effects (and call `onPostCommit`)
50+
*
51+
* Nested updates are identified in step 3 above,
52+
* but step 4 still applies to the work that was just committed.
53+
* We use two flags to track nested updates then:
54+
* one tracks whether the upcoming update is a nested update,
55+
* and the other tracks whether the current update was a nested update.
56+
* The first value gets synced to the second at the start of the render phase.
57+
*/
58+
let currentUpdateIsNested: boolean = false;
59+
let nestedUpdateScheduled: boolean = false;
60+
61+
function isCurrentUpdateNested(): boolean {
62+
return currentUpdateIsNested;
63+
}
64+
65+
function markNestedUpdateScheduled(): void {
66+
if (enableProfilerNestedUpdatePhase) {
67+
nestedUpdateScheduled = true;
68+
}
69+
}
70+
71+
function syncNestedUpdateFlag(): void {
72+
if (enableProfilerNestedUpdatePhase) {
73+
currentUpdateIsNested = nestedUpdateScheduled;
74+
nestedUpdateScheduled = false;
75+
}
76+
}
77+
3878
function getCommitTime(): number {
3979
return commitTime;
4080
}
@@ -161,6 +201,8 @@ function transferActualDuration(fiber: Fiber): void {
161201

162202
export {
163203
getCommitTime,
204+
isCurrentUpdateNested,
205+
markNestedUpdateScheduled,
164206
recordCommitTime,
165207
recordLayoutEffectDuration,
166208
recordPassiveEffectDuration,
@@ -169,5 +211,6 @@ export {
169211
startProfilerTimer,
170212
stopProfilerTimerIfRunning,
171213
stopProfilerTimerIfRunningAndRecordDelta,
214+
syncNestedUpdateFlag,
172215
transferActualDuration,
173216
};

0 commit comments

Comments
 (0)