Traverse commit phase effects iteratively (#20094)

* Move traversal logic to ReactFiberCommitWork

The current traversal logic is spread between ReactFiberWorkLoop and
ReactFiberCommitWork, and it's a bit awkward, especially when
refactoring. Idk the ideal module structure, so for now I'd rather keep
it all in one file.

* Traverse commit phase effects iteratively

We suspect that using the JS stack to traverse through the tree in the
commit phase is slower than traversing iteratively.

I've kept the recursive implementation behind a flag, both so we have
the option to run an experiment comparing the two, and so we can revert
it easily later if needed.
This commit is contained in:
Andrew Clark 2020-10-27 14:02:19 -05:00 committed by GitHub
parent 06a4615be2
commit 25b18d31c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1244 additions and 612 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,21 +15,18 @@ import type {Interaction} from 'scheduler/src/Tracing';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {StackCursor} from './ReactFiberStack.new';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
import type {Flags} from './ReactFiberFlags';
import {
warnAboutDeprecatedLifecycles,
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableProfilerTimer,
enableProfilerCommitHooks,
enableSchedulerTracing,
warnAboutUnmockedScheduler,
deferRenderPhaseUpdateToNextBatch,
decoupleUpdatePriorityFromScheduler,
enableDebugTracing,
enableSchedulingProfiler,
enableScopeAPI,
skipUnmountedBoundaries,
enableDoubleInvokingEffects,
} from 'shared/ReactFeatureFlags';
@ -83,13 +80,11 @@ import * as Scheduler from 'scheduler';
import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
import {
prepareForCommit,
resetAfterCommit,
scheduleTimeout,
cancelTimeout,
noTimeout,
warnsIfNotActing,
beforeActiveInstanceBlur,
afterActiveInstanceBlur,
clearContainer,
} from './ReactFiberHostConfig';
@ -116,31 +111,19 @@ import {
MemoComponent,
SimpleMemoComponent,
Block,
ScopeComponent,
Profiler,
} from './ReactWorkTags';
import {LegacyRoot} from './ReactRootTags';
import {
NoFlags,
Placement,
Update,
PlacementAndUpdate,
Ref,
ContentReset,
Snapshot,
Passive,
PassiveStatic,
Incomplete,
HostEffectMask,
Hydrating,
HydratingAndUpdate,
Visibility,
BeforeMutationMask,
MutationMask,
LayoutMask,
PassiveMask,
MountPassiveDev,
MountLayoutDev,
} from './ReactFiberFlags';
import {
NoLanePriority,
@ -191,22 +174,12 @@ import {
createClassErrorUpdate,
} from './ReactFiberThrow.new';
import {
commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
commitPlacement,
commitWork,
commitDeletion,
commitPassiveUnmount as commitPassiveUnmountOnFiber,
commitPassiveUnmountInsideDeletedTree as commitPassiveUnmountInsideDeletedTreeOnFiber,
commitPassiveMount as commitPassiveMountOnFiber,
commitDetachRef,
commitAttachRef,
commitResetTextContent,
isSuspenseBoundaryBeingHidden,
invokeLayoutEffectMountInDEV,
invokePassiveEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectUnmountInDEV,
recursivelyCommitLayoutEffects,
commitBeforeMutationEffects,
commitMutationEffects,
commitLayoutEffects,
commitPassiveMountEffects,
commitPassiveUnmountEffects,
commitDoubleInvokeEffectsInDEV,
} from './ReactFiberCommitWork.new';
import {enqueueUpdate} from './ReactUpdateQueue.new';
import {resetContextDependencies} from './ReactFiberNewContext.new';
@ -247,7 +220,6 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
// Used by `act`
import enqueueTask from 'shared/enqueueTask';
import {doesFiberContain} from './ReactFiberTreeReflection';
const ceil = Math.ceil;
@ -327,9 +299,6 @@ let workInProgressRootRenderTargetTime: number = Infinity;
// suspense heuristics and opt out of rendering more content.
const RENDER_TIMEOUT_MS = 500;
// Used to avoid traversing the return path to find the nearest Profiler ancestor during commit.
let nearestProfilerOnStack: Fiber | null = null;
function resetRenderTimer() {
workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
}
@ -374,9 +343,6 @@ let currentEventPendingLanes: Lanes = NoLanes;
// We warn about state updates for unmounted components differently in this case.
let isFlushingPassiveEffects = false;
let focusedInstanceHandle: null | Fiber = null;
let shouldFireAfterActiveInstanceBlur: boolean = false;
export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}
@ -1916,13 +1882,10 @@ function commitRootImpl(root, renderPriorityLevel) {
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
commitBeforeMutationEffects(finishedWork);
// We no longer need to track the active instance fiber
focusedInstanceHandle = null;
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
@ -1957,27 +1920,7 @@ function commitRootImpl(root, renderPriorityLevel) {
markLayoutEffectsStarted(lanes);
}
if (__DEV__) {
setCurrentDebugFiberInDEV(finishedWork);
invokeGuardedCallback(
null,
recursivelyCommitLayoutEffects,
null,
finishedWork,
root,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseErrorOnRoot(finishedWork, finishedWork, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
recursivelyCommitLayoutEffects(finishedWork, root);
} catch (error) {
captureCommitPhaseErrorOnRoot(finishedWork, finishedWork, error);
}
}
commitLayoutEffects(finishedWork, root);
if (__DEV__) {
if (enableDebugTracing) {
@ -2117,238 +2060,6 @@ function commitRootImpl(root, renderPriorityLevel) {
return null;
}
function commitBeforeMutationEffects(firstChild: Fiber) {
let fiber = firstChild;
while (fiber !== null) {
if (fiber.deletions !== null) {
commitBeforeMutationEffectsDeletions(fiber.deletions);
}
if (fiber.child !== null) {
const primarySubtreeFlags = fiber.subtreeFlags & BeforeMutationMask;
if (primarySubtreeFlags !== NoFlags) {
commitBeforeMutationEffects(fiber.child);
}
}
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
invokeGuardedCallback(null, commitBeforeMutationEffectsImpl, null, fiber);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
commitBeforeMutationEffectsImpl(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
fiber = fiber.sibling;
}
}
function commitBeforeMutationEffectsImpl(fiber: Fiber) {
const current = fiber.alternate;
const flags = fiber.flags;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
if (
// TODO: Can optimize this further with separate Hide and Show flags. We
// only care about Hide here.
(flags & Visibility) !== NoFlags &&
fiber.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, fiber) &&
doesFiberContain(fiber, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(fiber);
}
}
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(fiber);
commitBeforeMutationEffectOnFiber(current, fiber);
resetCurrentDebugFiberInDEV();
}
}
function commitBeforeMutationEffectsDeletions(deletions: Array<Fiber>) {
for (let i = 0; i < deletions.length; i++) {
const fiber = deletions[i];
// TODO (effects) It would be nice to avoid calling doesFiberContain()
// Maybe we can repurpose one of the subtreeFlags positions for this instead?
// Use it to store which part of the tree the focused instance is in?
// This assumes we can safely determine that instance during the "render" phase.
if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(fiber);
}
}
}
function commitMutationEffects(
firstChild: Fiber,
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
let fiber = firstChild;
while (fiber !== null) {
const deletions = fiber.deletions;
if (deletions !== null) {
commitMutationEffectsDeletions(
deletions,
fiber,
root,
renderPriorityLevel,
);
}
if (fiber.child !== null) {
const mutationFlags = fiber.subtreeFlags & MutationMask;
if (mutationFlags !== NoFlags) {
commitMutationEffects(fiber.child, root, renderPriorityLevel);
}
}
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
invokeGuardedCallback(
null,
commitMutationEffectsImpl,
null,
fiber,
root,
renderPriorityLevel,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
fiber = fiber.sibling;
}
}
function commitMutationEffectsImpl(
fiber: Fiber,
root: FiberRoot,
renderPriorityLevel,
) {
const flags = fiber.flags;
if (flags & ContentReset) {
commitResetTextContent(fiber);
}
if (flags & Ref) {
const current = fiber.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away from React Flare on www.
if (fiber.tag === ScopeComponent) {
commitAttachRef(fiber);
}
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
const primaryFlags = flags & (Placement | Update | Hydrating);
switch (primaryFlags) {
case Placement: {
commitPlacement(fiber);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
fiber.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(fiber);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
fiber.flags &= ~Placement;
// Update
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
case Hydrating: {
fiber.flags &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
fiber.flags &= ~Hydrating;
// Update
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
case Update: {
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
}
}
function commitMutationEffectsDeletions(
deletions: Array<Fiber>,
nearestMountedAncestor: Fiber,
root: FiberRoot,
renderPriorityLevel,
) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
if (__DEV__) {
invokeGuardedCallback(
null,
commitDeletion,
null,
root,
childToDelete,
nearestMountedAncestor,
renderPriorityLevel,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(childToDelete, nearestMountedAncestor, error);
}
} else {
try {
commitDeletion(
root,
childToDelete,
nearestMountedAncestor,
renderPriorityLevel,
);
} catch (error) {
captureCommitPhaseError(childToDelete, nearestMountedAncestor, error);
}
}
}
}
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
@ -2374,130 +2085,6 @@ export function flushPassiveEffects(): boolean {
return false;
}
function flushPassiveMountEffects(root, firstChild: Fiber): void {
let fiber = firstChild;
while (fiber !== null) {
let prevProfilerOnStack = null;
if (enableProfilerTimer && enableProfilerCommitHooks) {
if (fiber.tag === Profiler) {
prevProfilerOnStack = nearestProfilerOnStack;
nearestProfilerOnStack = fiber;
}
}
const primarySubtreeFlags = fiber.subtreeFlags & PassiveMask;
if (fiber.child !== null && primarySubtreeFlags !== NoFlags) {
flushPassiveMountEffects(root, fiber.child);
}
if ((fiber.flags & Passive) !== NoFlags) {
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
invokeGuardedCallback(
null,
commitPassiveMountOnFiber,
null,
root,
fiber,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
commitPassiveMountOnFiber(root, fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
}
if (enableProfilerTimer && enableProfilerCommitHooks) {
if (fiber.tag === Profiler) {
// Bubble times to the next nearest ancestor Profiler.
// After we process that Profiler, we'll bubble further up.
if (prevProfilerOnStack !== null) {
prevProfilerOnStack.stateNode.passiveEffectDuration +=
fiber.stateNode.passiveEffectDuration;
}
nearestProfilerOnStack = prevProfilerOnStack;
}
}
fiber = fiber.sibling;
}
}
function flushPassiveUnmountEffects(firstChild: Fiber): void {
let fiber = firstChild;
while (fiber !== null) {
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const fiberToDelete = deletions[i];
flushPassiveUnmountEffectsInsideOfDeletedTree(fiberToDelete, fiber);
// Now that passive effects have been processed, it's safe to detach lingering pointers.
detachFiberAfterEffects(fiberToDelete);
}
}
const child = fiber.child;
if (child !== null) {
// If any children have passive effects then traverse the subtree.
// Note that this requires checking subtreeFlags of the current Fiber,
// rather than the subtreeFlags/effectsTag of the first child,
// since that would not cover passive effects in siblings.
const passiveFlags = fiber.subtreeFlags & PassiveMask;
if (passiveFlags !== NoFlags) {
flushPassiveUnmountEffects(child);
}
}
const primaryFlags = fiber.flags & Passive;
if (primaryFlags !== NoFlags) {
setCurrentDebugFiberInDEV(fiber);
commitPassiveUnmountOnFiber(fiber);
resetCurrentDebugFiberInDEV();
}
fiber = fiber.sibling;
}
}
function flushPassiveUnmountEffectsInsideOfDeletedTree(
fiberToDelete: Fiber,
nearestMountedAncestor: Fiber,
): void {
if ((fiberToDelete.subtreeFlags & PassiveStatic) !== NoFlags) {
// If any children have passive effects then traverse the subtree.
// Note that this requires checking subtreeFlags of the current Fiber,
// rather than the subtreeFlags/effectsTag of the first child,
// since that would not cover passive effects in siblings.
let child = fiberToDelete.child;
while (child !== null) {
flushPassiveUnmountEffectsInsideOfDeletedTree(
child,
nearestMountedAncestor,
);
child = child.sibling;
}
}
if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) {
setCurrentDebugFiberInDEV(fiberToDelete);
commitPassiveUnmountInsideDeletedTreeOnFiber(
fiberToDelete,
nearestMountedAncestor,
);
resetCurrentDebugFiberInDEV();
}
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
@ -2537,8 +2124,8 @@ function flushPassiveEffectsImpl() {
// e.g. a destroy function in one component may unintentionally override a ref
// value set by a create function in another component.
// Layout effects have the same constraint.
flushPassiveUnmountEffects(root.current);
flushPassiveMountEffects(root, root.current);
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current);
if (__DEV__) {
if (enableDebugTracing) {
@ -2840,59 +2427,6 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
}
}
function commitDoubleInvokeEffectsInDEV(
fiber: Fiber,
hasPassiveEffects: boolean,
) {
if (__DEV__ && enableDoubleInvokingEffects) {
// Never double-invoke effects for legacy roots.
if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) {
return;
}
setCurrentDebugFiberInDEV(fiber);
invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
if (hasPassiveEffects) {
invokeEffectsInDev(
fiber,
MountPassiveDev,
invokePassiveEffectUnmountInDEV,
);
}
invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
if (hasPassiveEffects) {
invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
}
resetCurrentDebugFiberInDEV();
}
}
function invokeEffectsInDev(
firstChild: Fiber,
fiberFlags: Flags,
invokeEffectFn: (fiber: Fiber) => void,
): void {
if (__DEV__ && enableDoubleInvokingEffects) {
// We don't need to re-check for legacy roots here.
// This function will not be called within legacy roots.
let fiber = firstChild;
while (fiber !== null) {
if (fiber.child !== null) {
const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags;
if (primarySubtreeFlag !== NoFlags) {
invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn);
}
}
if ((fiber.flags & fiberFlags) !== NoFlags) {
invokeEffectFn(fiber);
}
fiber = fiber.sibling;
}
}
}
let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
if (__DEV__) {
@ -3675,21 +3209,3 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
};
}
}
function detachFiberAfterEffects(fiber: Fiber): void {
// Null out fields to improve GC for references that may be lingering (e.g. DevTools).
// Note that we already cleared the return pointer in detachFiberMutation().
fiber.child = null;
fiber.deletions = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;
if (__DEV__) {
fiber._debugOwner = null;
}
}

View File

@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;

View File

@ -53,6 +53,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = true;
export const enableDoubleInvokingEffects = false;
export const enableUseRefAccessWarning = false;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@ -84,6 +84,8 @@ export const enableDiscreteEventFlushingChange = true;
// to the correct value.
export const enableNewReconciler = __VARIANT__;
export const enableRecursiveCommitTraversal = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;