Track which fibers scheduled the current render work (#15658)

Tracked Fibers are called "updaters" and are exposed to DevTools via a 'memoizedUpdaters' property on the ReactFiberRoot. The implementation of this feature follows a vaguely similar approach as interaction tracing, but does not require reference counting since there is no subscriptions API.

This change is in support of a new DevTools Profiler feature that shows which Fiber(s) scheduled the selected commit in the Profiler.

All changes have been gated behind a new feature flag, 'enableUpdaterTracking', which is enabled for Profiling builds by default. We also only track updaters when DevTools has been detected, to avoid doing unnecessary work.
This commit is contained in:
Brian Vaughn
2021-04-09 10:34:33 -04:00
committed by GitHub
parent 6ea749170b
commit dc108b0f55
22 changed files with 912 additions and 10 deletions
@@ -37,6 +37,7 @@ import {
enableStrictEffects,
deletedTreeCleanUpLevel,
enableSuspenseLayoutEffectSemantics,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -89,7 +90,7 @@ import {
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
} from './ReactCurrentFiber';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
import {
@@ -137,6 +138,7 @@ import {
resolveRetryWakeable,
markCommitTimeOfFallback,
enqueuePendingPassiveProfilerEffect,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.new';
import {
NoFlags as NoHookEffect,
@@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
let nextEffect: Fiber | null = null;
// Used for Profiling builds to track updaters.
let inProgressLanes: Lanes | null = null;
let inProgressRoot: FiberRoot | null = null;
const callComponentWillUnmountWithTimer = function(current, instance) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
@@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
}
}
retryCache.add(wakeable);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
}
}
wakeable.then(retry, retry);
}
});
@@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) {
resetTextContent(current.stateNode);
}
export function commitMutationEffects(root: FiberRoot, firstChild: Fiber) {
export function commitMutationEffects(
root: FiberRoot,
firstChild: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = firstChild;
commitMutationEffects_begin(root);
inProgressLanes = null;
inProgressRoot = null;
}
function commitMutationEffects_begin(root: FiberRoot) {
@@ -2280,8 +2310,14 @@ export function commitLayoutEffects(
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
function commitLayoutEffects_begin(
@@ -37,6 +37,7 @@ import {
enableStrictEffects,
deletedTreeCleanUpLevel,
enableSuspenseLayoutEffectSemantics,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -89,7 +90,7 @@ import {
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
} from './ReactCurrentFiber';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
import {
@@ -137,6 +138,7 @@ import {
resolveRetryWakeable,
markCommitTimeOfFallback,
enqueuePendingPassiveProfilerEffect,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.old';
import {
NoFlags as NoHookEffect,
@@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
let nextEffect: Fiber | null = null;
// Used for Profiling builds to track updaters.
let inProgressLanes: Lanes | null = null;
let inProgressRoot: FiberRoot | null = null;
const callComponentWillUnmountWithTimer = function(current, instance) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
@@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
}
}
retryCache.add(wakeable);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
}
}
wakeable.then(retry, retry);
}
});
@@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) {
resetTextContent(current.stateNode);
}
export function commitMutationEffects(root: FiberRoot, firstChild: Fiber) {
export function commitMutationEffects(
root: FiberRoot,
firstChild: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = firstChild;
commitMutationEffects_begin(root);
inProgressLanes = null;
inProgressRoot = null;
}
function commitMutationEffects_begin(root: FiberRoot) {
@@ -2280,8 +2310,14 @@ export function commitLayoutEffects(
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
function commitLayoutEffects_begin(
@@ -35,7 +35,12 @@ export type Lanes = number;
export type Lane = number;
export type LaneMap<T> = Array<T>;
import {enableCache, enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
import {
enableCache,
enableSchedulingProfiler,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
// Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler.
// If those values are changed that package should be rebuilt and redeployed.
@@ -742,6 +747,57 @@ export function getBumpedLaneForHydration(
return lane;
}
export function addFiberToLanesMap(
root: FiberRoot,
fiber: Fiber,
lanes: Lanes | Lane,
) {
if (!enableUpdaterTracking) {
return;
}
if (!isDevToolsPresent) {
return;
}
const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
while (lanes > 0) {
const index = laneToIndex(lanes);
const lane = 1 << index;
const updaters = pendingUpdatersLaneMap[index];
updaters.add(fiber);
lanes &= ~lane;
}
}
export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
if (!enableUpdaterTracking) {
return;
}
if (!isDevToolsPresent) {
return;
}
const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
const memoizedUpdaters = root.memoizedUpdaters;
while (lanes > 0) {
const index = laneToIndex(lanes);
const lane = 1 << index;
const updaters = pendingUpdatersLaneMap[index];
if (updaters.size > 0) {
updaters.forEach(fiber => {
const alternate = fiber.alternate;
if (alternate === null || !memoizedUpdaters.has(alternate)) {
memoizedUpdaters.add(fiber);
}
});
updaters.clear();
}
lanes &= ~lane;
}
}
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Count leading zeros. Only used on lanes, so assume input is an integer.
@@ -35,7 +35,12 @@ export type Lanes = number;
export type Lane = number;
export type LaneMap<T> = Array<T>;
import {enableCache, enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
import {
enableCache,
enableSchedulingProfiler,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
// Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler.
// If those values are changed that package should be rebuilt and redeployed.
@@ -742,6 +747,57 @@ export function getBumpedLaneForHydration(
return lane;
}
export function addFiberToLanesMap(
root: FiberRoot,
fiber: Fiber,
lanes: Lanes | Lane,
) {
if (!enableUpdaterTracking) {
return;
}
if (!isDevToolsPresent) {
return;
}
const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
while (lanes > 0) {
const index = laneToIndex(lanes);
const lane = 1 << index;
const updaters = pendingUpdatersLaneMap[index];
updaters.add(fiber);
lanes &= ~lane;
}
}
export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
if (!enableUpdaterTracking) {
return;
}
if (!isDevToolsPresent) {
return;
}
const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
const memoizedUpdaters = root.memoizedUpdaters;
while (lanes > 0) {
const index = laneToIndex(lanes);
const lane = 1 << index;
const updaters = pendingUpdatersLaneMap[index];
if (updaters.size > 0) {
updaters.forEach(fiber => {
const alternate = fiber.alternate;
if (alternate === null || !memoizedUpdaters.has(alternate)) {
memoizedUpdaters.add(fiber);
}
});
updaters.clear();
}
lanes &= ~lane;
}
}
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Count leading zeros. Only used on lanes, so assume input is an integer.
@@ -16,6 +16,7 @@ import {
NoLane,
NoLanes,
NoTimestamp,
TotalLanes,
createLaneMap,
} from './ReactFiberLane.new';
import {
@@ -24,6 +25,7 @@ import {
enableCache,
enableProfilerCommitHooks,
enableProfilerTimer,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
@@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.passiveEffectDuration = 0;
}
if (enableUpdaterTracking) {
this.memoizedUpdaters = new Set();
const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
for (let i = 0; i < TotalLanes; i++) {
pendingUpdatersLaneMap.push(new Set());
}
}
if (__DEV__) {
switch (tag) {
case ConcurrentRoot:
@@ -16,6 +16,7 @@ import {
NoLane,
NoLanes,
NoTimestamp,
TotalLanes,
createLaneMap,
} from './ReactFiberLane.old';
import {
@@ -24,6 +25,7 @@ import {
enableCache,
enableProfilerCommitHooks,
enableProfilerTimer,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
@@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.passiveEffectDuration = 0;
}
if (enableUpdaterTracking) {
this.memoizedUpdaters = new Set();
const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
for (let i = 0; i < TotalLanes; i++) {
pendingUpdatersLaneMap.push(new Set());
}
}
if (__DEV__) {
switch (tag) {
case ConcurrentRoot:
@@ -39,6 +39,7 @@ import {
enableDebugTracing,
enableSchedulingProfiler,
enableLazyContextPropagation,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {createCapturedValue} from './ReactCapturedValue';
import {
@@ -60,12 +61,13 @@ import {
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
pingSuspendedRoot,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.new';
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
import {logCapturedError} from './ReactFiberErrorLogger';
import {logComponentSuspended} from './DebugTracing';
import {markComponentSuspended} from './SchedulingProfiler';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
import {
SyncLane,
NoTimestamp,
@@ -177,6 +179,12 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, lanes);
}
}
wakeable.then(ping, ping);
}
}
@@ -191,6 +199,13 @@ function throwException(
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, rootRenderLanes);
}
}
if (
value !== null &&
typeof value === 'object' &&
@@ -39,6 +39,7 @@ import {
enableDebugTracing,
enableSchedulingProfiler,
enableLazyContextPropagation,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import {createCapturedValue} from './ReactCapturedValue';
import {
@@ -60,12 +61,13 @@ import {
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
pingSuspendedRoot,
restorePendingUpdaters,
} from './ReactFiberWorkLoop.old';
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
import {logCapturedError} from './ReactFiberErrorLogger';
import {logComponentSuspended} from './DebugTracing';
import {markComponentSuspended} from './SchedulingProfiler';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
import {
SyncLane,
NoTimestamp,
@@ -177,6 +179,12 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, lanes);
}
}
wakeable.then(ping, ping);
}
}
@@ -191,6 +199,13 @@ function throwException(
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updaters
restorePendingUpdaters(root, rootRenderLanes);
}
}
if (
value !== null &&
typeof value === 'object' &&
@@ -32,6 +32,7 @@ import {
disableSchedulerTimeoutInWorkLoop,
enableStrictEffects,
skipUnmountedBoundaries,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
@@ -159,6 +160,8 @@ import {
markRootFinished,
areLanesExpired,
getHighestPriorityLane,
addFiberToLanesMap,
movePendingFibersToMemoized,
} from './ReactFiberLane.new';
import {
DiscreteEventPriority,
@@ -232,6 +235,7 @@ import {
import {
onCommitRoot as onCommitRootDevTools,
onPostCommitRoot as onPostCommitRootDevTools,
isDevToolsPresent,
} from './ReactFiberDevToolsHook.new';
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
@@ -470,6 +474,12 @@ export function scheduleUpdateOnFiber(
return null;
}
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}
// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);
@@ -1427,6 +1437,22 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
@@ -1502,6 +1528,22 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
resetRenderTimer();
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
@@ -1853,7 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) {
}
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork);
commitMutationEffects(root, finishedWork, lanes);
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
@@ -1985,6 +2027,12 @@ function commitRootImpl(root, renderPriorityLevel) {
onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
root.memoizedUpdaters.clear();
}
}
if (__DEV__) {
onCommitRootTestSelector();
}
@@ -2788,6 +2836,21 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
// a 'shared' variable that changes when act() opens/closes in tests.
export const IsThisRendererActing = {current: (false: boolean)};
export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
memoizedUpdaters.forEach(schedulingFiber => {
addFiberToLanesMap(root, schedulingFiber, lanes);
});
// This function intentionally does not clear memoized updaters.
// Those may still be relevant to the current commit
// and a future one (e.g. Suspense).
}
}
}
export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
if (__DEV__) {
if (
@@ -32,6 +32,7 @@ import {
disableSchedulerTimeoutInWorkLoop,
enableStrictEffects,
skipUnmountedBoundaries,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
@@ -159,6 +160,8 @@ import {
markRootFinished,
areLanesExpired,
getHighestPriorityLane,
addFiberToLanesMap,
movePendingFibersToMemoized,
} from './ReactFiberLane.old';
import {
DiscreteEventPriority,
@@ -232,6 +235,7 @@ import {
import {
onCommitRoot as onCommitRootDevTools,
onPostCommitRoot as onPostCommitRootDevTools,
isDevToolsPresent,
} from './ReactFiberDevToolsHook.old';
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
@@ -470,6 +474,12 @@ export function scheduleUpdateOnFiber(
return null;
}
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}
// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);
@@ -1427,6 +1437,22 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
@@ -1502,6 +1528,22 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
resetRenderTimer();
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
@@ -1853,7 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) {
}
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork);
commitMutationEffects(root, finishedWork, lanes);
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
@@ -1985,6 +2027,12 @@ function commitRootImpl(root, renderPriorityLevel) {
onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
root.memoizedUpdaters.clear();
}
}
if (__DEV__) {
onCommitRootTestSelector();
}
@@ -2788,6 +2836,21 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
// a 'shared' variable that changes when act() opens/closes in tests.
export const IsThisRendererActing = {current: (false: boolean)};
export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
memoizedUpdaters.forEach(schedulingFiber => {
addFiberToLanesMap(root, schedulingFiber, lanes);
});
// This function intentionally does not clear memoized updaters.
// Those may still be relevant to the current commit
// and a future one (e.g. Suspense).
}
}
}
export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
if (__DEV__) {
if (
+8
View File
@@ -249,6 +249,13 @@ type ProfilingOnlyFiberRootProperties = {|
pendingInteractionMap: Map<Lane | Lanes, Set<Interaction>>,
|};
// The following attributes are only used by DevTools and are only present in DEV builds.
// They enable DevTools Profiler UI to show which Fiber(s) scheduled a given commit.
type UpdaterTrackingOnlyFiberRootProperties = {|
memoizedUpdaters: Set<Fiber>,
pendingUpdatersLaneMap: LaneMap<Set<Fiber>>,
|};
export type SuspenseHydrationCallbacks = {
onHydrated?: (suspenseInstance: SuspenseInstance) => void,
onDeleted?: (suspenseInstance: SuspenseInstance) => void,
@@ -269,6 +276,7 @@ export type FiberRoot = {
...BaseFiberRootProperties,
...ProfilingOnlyFiberRootProperties,
...SuspenseCallbackOnlyFiberRootProperties,
...UpdaterTrackingOnlyFiberRootProperties,
...
};
@@ -0,0 +1,521 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
let React;
let ReactFeatureFlags;
let ReactDOM;
let ReactTestUtils;
let Scheduler;
let mockDevToolsHook;
let allSchedulerTags;
let allSchedulerTypes;
let onCommitRootShouldYield;
describe('updaters', () => {
beforeEach(() => {
jest.resetModules();
allSchedulerTags = [];
allSchedulerTypes = [];
onCommitRootShouldYield = true;
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableUpdaterTracking = true;
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
mockDevToolsHook = {
injectInternals: jest.fn(() => {}),
isDevToolsPresent: true,
onCommitRoot: jest.fn(fiberRoot => {
if (onCommitRootShouldYield) {
Scheduler.unstable_yieldValue('onCommitRoot');
}
const schedulerTags = [];
const schedulerTypes = [];
fiberRoot.memoizedUpdaters.forEach(fiber => {
schedulerTags.push(fiber.tag);
schedulerTypes.push(fiber.elementType);
});
allSchedulerTags.push(schedulerTags);
allSchedulerTypes.push(schedulerTypes);
}),
onCommitUnmount: jest.fn(() => {}),
onPostCommitRoot: jest.fn(() => {}),
onScheduleRoot: jest.fn(() => {}),
};
jest.mock(
'react-reconciler/src/ReactFiberDevToolsHook.old',
() => mockDevToolsHook,
);
jest.mock(
'react-reconciler/src/ReactFiberDevToolsHook.new',
() => mockDevToolsHook,
);
React = require('react');
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
});
it('should report the (host) root as the scheduler for root-level render', async () => {
const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
const Parent = () => <Child />;
const Child = () => null;
const container = document.createElement('div');
await ReactTestUtils.act(async () => {
ReactDOM.render(<Parent />, container);
});
expect(allSchedulerTags).toEqual([[HostRoot]]);
await ReactTestUtils.act(async () => {
ReactDOM.render(<Parent />, container);
});
expect(allSchedulerTags).toEqual([[HostRoot], [HostRoot]]);
});
it('should report a function component as the scheduler for a hooks update', async () => {
let scheduleForA = null;
let scheduleForB = null;
const Parent = () => (
<React.Fragment>
<SchedulingComponentA />
<SchedulingComponentB />
</React.Fragment>
);
const SchedulingComponentA = () => {
const [count, setCount] = React.useState(0);
scheduleForA = () => setCount(prevCount => prevCount + 1);
return <Child count={count} />;
};
const SchedulingComponentB = () => {
const [count, setCount] = React.useState(0);
scheduleForB = () => setCount(prevCount => prevCount + 1);
return <Child count={count} />;
};
const Child = () => null;
await ReactTestUtils.act(async () => {
ReactDOM.render(<Parent />, document.createElement('div'));
});
expect(scheduleForA).not.toBeNull();
expect(scheduleForB).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);
await ReactTestUtils.act(async () => {
scheduleForA();
});
expect(allSchedulerTypes).toEqual([[null], [SchedulingComponentA]]);
await ReactTestUtils.act(async () => {
scheduleForB();
});
expect(allSchedulerTypes).toEqual([
[null],
[SchedulingComponentA],
[SchedulingComponentB],
]);
});
it('should report a class component as the scheduler for a setState update', async () => {
const Parent = () => <SchedulingComponent />;
class SchedulingComponent extends React.Component {
state = {};
render() {
instance = this;
return <Child />;
}
}
const Child = () => null;
let instance;
await ReactTestUtils.act(async () => {
ReactDOM.render(<Parent />, document.createElement('div'));
});
expect(allSchedulerTypes).toEqual([[null]]);
expect(instance).not.toBeNull();
await ReactTestUtils.act(async () => {
instance.setState({});
});
expect(allSchedulerTypes).toEqual([[null], [SchedulingComponent]]);
});
// @gate experimental
it('should cover cascading updates', async () => {
let triggerActiveCascade = null;
let triggerPassiveCascade = null;
const Parent = () => <SchedulingComponent />;
const SchedulingComponent = () => {
const [cascade, setCascade] = React.useState(null);
triggerActiveCascade = () => setCascade('active');
triggerPassiveCascade = () => setCascade('passive');
return <CascadingChild cascade={cascade} />;
};
const CascadingChild = ({cascade}) => {
const [count, setCount] = React.useState(0);
Scheduler.unstable_yieldValue(`CascadingChild ${count}`);
React.useLayoutEffect(() => {
if (cascade === 'active') {
setCount(prevCount => prevCount + 1);
}
return () => {};
}, [cascade]);
React.useEffect(() => {
if (cascade === 'passive') {
setCount(prevCount => prevCount + 1);
}
return () => {};
}, [cascade]);
return count;
};
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
await ReactTestUtils.act(async () => {
root.render(<Parent />);
expect(Scheduler).toFlushAndYieldThrough([
'CascadingChild 0',
'onCommitRoot',
]);
});
expect(triggerActiveCascade).not.toBeNull();
expect(triggerPassiveCascade).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);
await ReactTestUtils.act(async () => {
triggerActiveCascade();
expect(Scheduler).toFlushAndYieldThrough([
'CascadingChild 0',
'onCommitRoot',
'CascadingChild 1',
'onCommitRoot',
]);
});
expect(allSchedulerTypes).toEqual([
[null],
[SchedulingComponent],
[CascadingChild],
]);
await ReactTestUtils.act(async () => {
triggerPassiveCascade();
expect(Scheduler).toFlushAndYieldThrough([
'CascadingChild 1',
'onCommitRoot',
'CascadingChild 2',
'onCommitRoot',
]);
});
expect(allSchedulerTypes).toEqual([
[null],
[SchedulingComponent],
[CascadingChild],
[SchedulingComponent],
[CascadingChild],
]);
// Verify no outstanding flushes
Scheduler.unstable_flushAll();
});
it('should cover suspense pings', async done => {
let data = null;
let resolver = null;
let promise = null;
const fakeCacheRead = () => {
if (data === null) {
promise = new Promise(resolve => {
resolver = resolvedData => {
data = resolvedData;
resolve(resolvedData);
};
});
throw promise;
} else {
return data;
}
};
const Parent = () => (
<React.Suspense fallback={<Fallback />}>
<Suspender />
</React.Suspense>
);
const Fallback = () => null;
let setShouldSuspend = null;
const Suspender = ({suspend}) => {
const tuple = React.useState(false);
setShouldSuspend = tuple[1];
if (tuple[0] === true) {
return fakeCacheRead();
} else {
return null;
}
};
await ReactTestUtils.act(async () => {
ReactDOM.render(<Parent />, document.createElement('div'));
expect(Scheduler).toHaveYielded(['onCommitRoot']);
});
expect(setShouldSuspend).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);
await ReactTestUtils.act(async () => {
setShouldSuspend(true);
});
expect(Scheduler).toHaveYielded(['onCommitRoot']);
expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
expect(resolver).not.toBeNull();
await ReactTestUtils.act(() => {
resolver('abc');
return promise;
});
expect(Scheduler).toHaveYielded(['onCommitRoot']);
expect(allSchedulerTypes).toEqual([[null], [Suspender], [Suspender]]);
// Verify no outstanding flushes
Scheduler.unstable_flushAll();
done();
});
// @gate experimental
it('traces interaction through hidden subtree', async () => {
const {
FunctionComponent,
HostRoot,
} = require('react-reconciler/src/ReactWorkTags');
// Note: This is based on a similar component we use in www. We can delete once
// the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}
const Child = () => {
const [didMount, setDidMount] = React.useState(false);
Scheduler.unstable_yieldValue('Child');
React.useEffect(() => {
if (didMount) {
Scheduler.unstable_yieldValue('Child:update');
} else {
Scheduler.unstable_yieldValue('Child:mount');
setDidMount(true);
}
}, [didMount]);
return <div />;
};
const App = () => {
Scheduler.unstable_yieldValue('App');
React.useEffect(() => {
Scheduler.unstable_yieldValue('App:mount');
}, []);
return (
<LegacyHiddenDiv mode="hidden">
<Child />
</LegacyHiddenDiv>
);
};
const container = document.createElement('div');
const root = ReactDOM.createRoot(container);
await ReactTestUtils.act(async () => {
root.render(<App />);
});
// TODO: There are 4 commits here instead of 3
// because this update was scheduled at idle priority,
// and idle updates are slightly higher priority than offscreen work.
// So it takes two render passes to finish it.
// The onCommit hook is called even after the no-op bailout update.
expect(Scheduler).toHaveYielded([
'App',
'onCommitRoot',
'App:mount',
'Child',
'onCommitRoot',
'Child:mount',
'onCommitRoot',
'Child',
'onCommitRoot',
'Child:update',
]);
expect(allSchedulerTypes).toEqual([
// Initial render
[null],
// Offscreen update
[],
// Child passive effect
[Child],
// Offscreen update
[],
]);
expect(allSchedulerTags).toEqual([[HostRoot], [], [FunctionComponent], []]);
});
// @gate experimental
it('should cover error handling', async () => {
let triggerError = null;
const Parent = () => {
const [shouldError, setShouldError] = React.useState(false);
triggerError = () => setShouldError(true);
return shouldError ? (
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
) : (
<ErrorBoundary>
<Yield value="initial" />
</ErrorBoundary>
);
};
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <Yield value="error" />;
}
return this.props.children;
}
}
const Yield = ({value}) => {
Scheduler.unstable_yieldValue(value);
return null;
};
const BrokenRender = () => {
throw new Error('Hello');
};
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
await ReactTestUtils.act(async () => {
root.render(<Parent shouldError={false} />);
});
expect(Scheduler).toHaveYielded(['initial', 'onCommitRoot']);
expect(triggerError).not.toBeNull();
allSchedulerTypes.splice(0);
onCommitRootShouldYield = true;
await ReactTestUtils.act(async () => {
triggerError();
});
expect(Scheduler).toHaveYielded(['onCommitRoot', 'error', 'onCommitRoot']);
expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]);
// Verify no outstanding flushes
Scheduler.unstable_flushAll();
});
// @gate experimental
it('should distinguish between updaters in the case of interleaved work', async () => {
const {
FunctionComponent,
HostRoot,
} = require('react-reconciler/src/ReactWorkTags');
let triggerLowPriorityUpdate = null;
let triggerSyncPriorityUpdate = null;
const SyncPriorityUpdater = () => {
const [count, setCount] = React.useState(0);
triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1);
Scheduler.unstable_yieldValue(`SyncPriorityUpdater ${count}`);
return <Yield value={`HighPriority ${count}`} />;
};
const LowPriorityUpdater = () => {
const [count, setCount] = React.useState(0);
triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1);
Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`);
return <Yield value={`LowPriority ${count}`} />;
};
const Yield = ({value}) => {
Scheduler.unstable_yieldValue(`Yield ${value}`);
return null;
};
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
root.render(
<React.Fragment>
<SyncPriorityUpdater />
<LowPriorityUpdater />
</React.Fragment>,
);
// Render everything initially.
expect(Scheduler).toFlushAndYield([
'SyncPriorityUpdater 0',
'Yield HighPriority 0',
'LowPriorityUpdater 0',
'Yield LowPriority 0',
'onCommitRoot',
]);
expect(triggerLowPriorityUpdate).not.toBeNull();
expect(triggerSyncPriorityUpdate).not.toBeNull();
expect(allSchedulerTags).toEqual([[HostRoot]]);
// Render a partial update, but don't finish.
ReactTestUtils.act(() => {
triggerLowPriorityUpdate();
expect(Scheduler).toFlushAndYieldThrough(['LowPriorityUpdater 1']);
expect(allSchedulerTags).toEqual([[HostRoot]]);
// Interrupt with higher priority work.
ReactDOM.flushSync(triggerSyncPriorityUpdate);
expect(Scheduler).toHaveYielded([
'SyncPriorityUpdater 1',
'Yield HighPriority 1',
'onCommitRoot',
]);
expect(allSchedulerTypes).toEqual([[null], [SyncPriorityUpdater]]);
// Finish the initial partial update
triggerLowPriorityUpdate();
expect(Scheduler).toFlushAndYield([
'LowPriorityUpdater 2',
'Yield LowPriority 2',
'onCommitRoot',
]);
});
expect(allSchedulerTags).toEqual([
[HostRoot],
[FunctionComponent],
[FunctionComponent],
]);
expect(allSchedulerTypes).toEqual([
[null],
[SyncPriorityUpdater],
[LowPriorityUpdater],
]);
// Verify no outstanding flushes
Scheduler.unstable_flushAll();
});
});
+3
View File
@@ -54,6 +54,9 @@ export const enableProfilerNestedUpdateScheduledHook = false;
// Trace which interactions trigger each commit.
export const enableSchedulerTracing = __PROFILE__;
// Track which Fiber(s) schedule render work.
export const enableUpdaterTracking = __PROFILE__;
// SSR experiments
export const enableSuspenseServerRenderer = __EXPERIMENTAL__;
export const enableSelectiveHydration = __EXPERIMENTAL__;
@@ -18,6 +18,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = __PROFILE__;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = __PROFILE__;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = __PROFILE__;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = __PROFILE__;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = __PROFILE__;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = __PROFILE__;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = false;
export const enableSelectiveHydration = false;
export const enableLazyElements = false;
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
export const enableProfilerNestedUpdatePhase = false;
export const enableProfilerNestedUpdateScheduledHook = false;
export const enableSchedulerTracing = false;
export const enableUpdaterTracking = false;
export const enableSuspenseServerRenderer = true;
export const enableSelectiveHydration = true;
export const enableLazyElements = false;
@@ -25,6 +25,7 @@ export const enableSuspenseLayoutEffectSemantics = __VARIANT__;
//
// NOTE: This feature will only work in DEV mode; all callsights are wrapped with __DEV__.
export const enableDebugTracing = __EXPERIMENTAL__;
export const enableUpdaterTracking = false;
export const enableSchedulingProfiler = __VARIANT__;
@@ -44,6 +44,8 @@ export const enableProfilerCommitHooks = __PROFILE__;
export const enableProfilerNestedUpdatePhase = __PROFILE__;
export const enableProfilerNestedUpdateScheduledHook =
__PROFILE__ && dynamicFeatureFlags.enableProfilerNestedUpdateScheduledHook;
export const enableUpdaterTracking =
__PROFILE__ && dynamicFeatureFlags.enableUpdaterTracking;
// Logs additional User Timing API marks for use with an experimental profiling tool.
export const enableSchedulingProfiler =