Skip to content

Commit

Permalink
Add "nested-update" phase to Profiler API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Brian Vaughn committed Nov 5, 2020
1 parent 6b28eb6 commit 01c6228
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 30 deletions.
33 changes: 22 additions & 11 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
enableSchedulerTracing,
enableProfilerTimer,
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableSuspenseServerRenderer,
enableFundamentalAPI,
enableSuspenseCallback,
Expand Down Expand Up @@ -94,6 +95,7 @@ import {
import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
import {
isCurrentUpdateNested,
getCommitTime,
recordLayoutEffectDuration,
startLayoutEffectTimer,
Expand Down Expand Up @@ -369,22 +371,24 @@ function commitProfilerPassiveEffect(
// It does not get reset until the start of the next commit phase.
const commitTime = getCommitTime();

let phase = finishedWork.alternate === null ? 'mount' : 'update';
if (enableProfilerNestedUpdatePhase) {
if (isCurrentUpdateNested()) {
phase = 'nested-update';
}
}

if (typeof onPostCommit === 'function') {
if (enableSchedulerTracing) {
onPostCommit(
id,
finishedWork.alternate === null ? 'mount' : 'update',
phase,
passiveEffectDuration,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onPostCommit(
id,
finishedWork.alternate === null ? 'mount' : 'update',
passiveEffectDuration,
commitTime,
);
onPostCommit(id, phase, passiveEffectDuration, commitTime);
}
}
break;
Expand Down Expand Up @@ -1362,11 +1366,18 @@ function commitLayoutEffectsForProfiler(
const OnRenderFlag = Update;
const OnCommitFlag = Callback;

let phase = current === null ? 'mount' : 'update';
if (enableProfilerNestedUpdatePhase) {
if (isCurrentUpdateNested()) {
phase = 'nested-update';
}
}

if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') {
if (enableSchedulerTracing) {
onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
Expand All @@ -1376,7 +1387,7 @@ function commitLayoutEffectsForProfiler(
} else {
onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
Expand All @@ -1393,15 +1404,15 @@ function commitLayoutEffectsForProfiler(
if (enableSchedulerTracing) {
onCommit(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
effectDuration,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onCommit(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
effectDuration,
commitTime,
);
Expand Down
33 changes: 22 additions & 11 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
enableSchedulerTracing,
enableProfilerTimer,
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableSuspenseServerRenderer,
enableFundamentalAPI,
enableSuspenseCallback,
Expand Down Expand Up @@ -73,6 +74,7 @@ import invariant from 'shared/invariant';
import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
import {
isCurrentUpdateNested,
getCommitTime,
recordLayoutEffectDuration,
startLayoutEffectTimer,
Expand Down Expand Up @@ -434,22 +436,24 @@ export function commitPassiveEffectDurations(
// It does not get reset until the start of the next commit phase.
const commitTime = getCommitTime();

let phase = finishedWork.alternate === null ? 'mount' : 'update';
if (enableProfilerNestedUpdatePhase) {
if (isCurrentUpdateNested()) {
phase = 'nested-update';
}
}

if (typeof onPostCommit === 'function') {
if (enableSchedulerTracing) {
onPostCommit(
id,
finishedWork.alternate === null ? 'mount' : 'update',
phase,
passiveEffectDuration,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onPostCommit(
id,
finishedWork.alternate === null ? 'mount' : 'update',
passiveEffectDuration,
commitTime,
);
onPostCommit(id, phase, passiveEffectDuration, commitTime);
}
}

Expand Down Expand Up @@ -706,11 +710,18 @@ function commitLifeCycles(

const commitTime = getCommitTime();

let phase = current === null ? 'mount' : 'update';
if (enableProfilerNestedUpdatePhase) {
if (isCurrentUpdateNested()) {
phase = 'nested-update';
}
}

if (typeof onRender === 'function') {
if (enableSchedulerTracing) {
onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
Expand All @@ -720,7 +731,7 @@ function commitLifeCycles(
} else {
onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
Expand All @@ -734,15 +745,15 @@ function commitLifeCycles(
if (enableSchedulerTracing) {
onCommit(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
effectDuration,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onCommit(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
phase,
effectDuration,
commitTime,
);
Expand Down
11 changes: 11 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableProfilerTimer,
enableProfilerNestedUpdatePhase,
enableSchedulerTracing,
warnAboutUnmockedScheduler,
deferRenderPhaseUpdateToNextBatch,
Expand Down Expand Up @@ -195,9 +196,11 @@ import {
} from './ReactFiberStack.new';

import {
markNestedUpdateScheduled,
recordCommitTime,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
syncNestedUpdateFlag,
} from './ReactProfilerTimer.new';

// DEV stuff
Expand Down Expand Up @@ -939,6 +942,10 @@ function markRootSuspended(root, suspendedLanes) {
// This is the entry point for synchronous tasks that don't go
// through Scheduler
function performSyncWorkOnRoot(root) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
syncNestedUpdateFlag();
}

invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
Expand Down Expand Up @@ -1996,6 +2003,10 @@ function commitRootImpl(root, renderPriorityLevel) {
}

if (remainingLanes === SyncLane) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
markNestedUpdateScheduled();
}

// Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
if (root === rootWithNestedUpdates) {
Expand Down
11 changes: 11 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableProfilerTimer,
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableSchedulerTracing,
warnAboutUnmockedScheduler,
deferRenderPhaseUpdateToNextBatch,
Expand Down Expand Up @@ -207,11 +208,13 @@ import {
} from './ReactFiberStack.old';

import {
markNestedUpdateScheduled,
recordCommitTime,
recordPassiveEffectDuration,
startPassiveEffectTimer,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
syncNestedUpdateFlag,
} from './ReactProfilerTimer.old';

// DEV stuff
Expand Down Expand Up @@ -962,6 +965,10 @@ function markRootSuspended(root, suspendedLanes) {
// This is the entry point for synchronous tasks that don't go
// through Scheduler
function performSyncWorkOnRoot(root) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
syncNestedUpdateFlag();
}

invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
Expand Down Expand Up @@ -2189,6 +2196,10 @@ function commitRootImpl(root, renderPriorityLevel) {
}

if (remainingLanes === SyncLane) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
markNestedUpdateScheduled();
}

// Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
if (root === rootWithNestedUpdates) {
Expand Down
45 changes: 44 additions & 1 deletion packages/react-reconciler/src/ReactProfilerTimer.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
import type {Fiber} from './ReactInternalTypes';

import {
enableProfilerTimer,
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableProfilerTimer,
} from 'shared/ReactFeatureFlags';
import {Profiler} from './ReactWorkTags';

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

export type ProfilerTimer = {
getCommitTime(): number,
isCurrentUpdateNested(): boolean,
markNestedUpdateScheduled(): void,
recordCommitTime(): void,
startProfilerTimer(fiber: Fiber): void,
stopProfilerTimerIfRunning(fiber: Fiber): void,
stopProfilerTimerIfRunningAndRecordDelta(fiber: Fiber): void,
syncNestedUpdateFlag(): void,
...
};

Expand All @@ -35,6 +39,42 @@ let layoutEffectStartTime: number = -1;
let profilerStartTime: number = -1;
let passiveEffectStartTime: number = -1;

/**
* Tracks whether the current update was a nested/cascading update (scheduled from a layout effect).
*
* The overall sequence is:
* 1. render
* 2. commit (and call `onRender`, `onCommit`)
* 3. check for nested updates
* 4. flush passive effects (and call `onPostCommit`)
*
* Nested updates are identified in step 3 above,
* but step 4 still applies to the work that was just committed.
* We use two flags to track nested updates then:
* one tracks whether the upcoming update is a nested update,
* and the other tracks whether the current update was a nested update.
* The first value gets synced to the second at the start of the render phase.
*/
let currentUpdateIsNested: boolean = false;
let nestedUpdateScheduled: boolean = false;

function isCurrentUpdateNested(): boolean {
return currentUpdateIsNested;
}

function markNestedUpdateScheduled(): void {
if (enableProfilerNestedUpdatePhase) {
nestedUpdateScheduled = true;
}
}

function syncNestedUpdateFlag(): void {
if (enableProfilerNestedUpdatePhase) {
currentUpdateIsNested = nestedUpdateScheduled;
nestedUpdateScheduled = false;
}
}

function getCommitTime(): number {
return commitTime;
}
Expand Down Expand Up @@ -161,6 +201,8 @@ function transferActualDuration(fiber: Fiber): void {

export {
getCommitTime,
isCurrentUpdateNested,
markNestedUpdateScheduled,
recordCommitTime,
recordLayoutEffectDuration,
recordPassiveEffectDuration,
Expand All @@ -169,5 +211,6 @@ export {
startProfilerTimer,
stopProfilerTimerIfRunning,
stopProfilerTimerIfRunningAndRecordDelta,
syncNestedUpdateFlag,
transferActualDuration,
};
Loading

0 comments on commit 01c6228

Please sign in to comment.