From cbcc3f51413be0b0ec64b7f16fb67e2062c42a53 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 3 Apr 2021 09:18:00 -0400 Subject: [PATCH] Track which fibers scheduled the current render work This change is gated behind a new profiling-mode-only feature flag, "enableUpdaterTracking". --- .../src/ReactFiberCommitWork.new.js | 40 +- .../src/ReactFiberCommitWork.old.js | 40 +- .../src/ReactFiberLane.new.js | 58 +- .../src/ReactFiberLane.old.js | 58 +- .../src/ReactFiberRoot.new.js | 10 + .../src/ReactFiberRoot.old.js | 10 + .../src/ReactFiberThrow.new.js | 17 +- .../src/ReactFiberThrow.old.js | 17 +- .../src/ReactFiberWorkLoop.new.js | 69 +- .../src/ReactFiberWorkLoop.old.js | 69 +- .../src/ReactInternalTypes.js | 8 + .../__tests__/ReactUpdaters-test.internal.js | 603 ++++++++++++++++++ packages/shared/ReactFeatureFlags.js | 3 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 22 files changed, 999 insertions(+), 12 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 48150f021a32b..b026d37db15e0 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -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, @@ -164,6 +166,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; @@ -2096,6 +2102,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); } }); @@ -2126,9 +2146,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) { @@ -2282,8 +2312,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( diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index ca3dbe363f071..3a3962cc7f82d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -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, @@ -164,6 +166,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; @@ -2096,6 +2102,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); } }); @@ -2126,9 +2146,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) { @@ -2282,8 +2312,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( diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 2a9571ece689c..32bb8139c7657 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -35,7 +35,12 @@ export type Lanes = number; export type Lane = number; export type LaneMap = Array; -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. diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 2a9571ece689c..b577a67aa213f 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -35,7 +35,12 @@ export type Lanes = number; export type Lane = number; export type LaneMap = Array; -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. diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 1eb637891ebe6..9201a35980753 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index e44757248b427..f2968b314ecae 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index 2641bdd3b345e..e100561ed86e5 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -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' && diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 294b807ebcb1b..cdb02cb8f1886 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -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' && diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 455067e47e263..6f08c1ddf77fd 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -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, @@ -229,7 +232,10 @@ import { hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; -import {onCommitRoot as onCommitRootDevTools} from './ReactFiberDevToolsHook.new'; +import { + isDevToolsPresent, + onCommitRoot as onCommitRootDevTools, +} from './ReactFiberDevToolsHook.new'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` @@ -467,6 +473,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); @@ -1424,6 +1436,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); } @@ -1499,6 +1527,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); @@ -1850,7 +1894,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(); @@ -1982,6 +2026,12 @@ function commitRootImpl(root, renderPriorityLevel) { onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel); + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + root.memoizedUpdaters.clear(); + } + } + if (__DEV__) { onCommitRootTestSelector(); } @@ -2779,6 +2829,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 ( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 9d056a46402c3..62ed06c228962 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -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, @@ -229,7 +232,10 @@ import { hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; -import {onCommitRoot as onCommitRootDevTools} from './ReactFiberDevToolsHook.old'; +import { + isDevToolsPresent, + onCommitRoot as onCommitRootDevTools, +} from './ReactFiberDevToolsHook.old'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` @@ -467,6 +473,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); @@ -1424,6 +1436,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); } @@ -1499,6 +1527,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); @@ -1850,7 +1894,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(); @@ -1982,6 +2026,12 @@ function commitRootImpl(root, renderPriorityLevel) { onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel); + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + root.memoizedUpdaters.clear(); + } + } + if (__DEV__) { onCommitRootTestSelector(); } @@ -2779,6 +2829,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 ( diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 4ad4cb122b5c9..0c34eeda8ed63 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -249,6 +249,13 @@ type ProfilingOnlyFiberRootProperties = {| pendingInteractionMap: Map>, |}; +// 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, + pendingUpdatersLaneMap: LaneMap>, +|}; + export type SuspenseHydrationCallbacks = { onHydrated?: (suspenseInstance: SuspenseInstance) => void, onDeleted?: (suspenseInstance: SuspenseInstance) => void, @@ -269,6 +276,7 @@ export type FiberRoot = { ...BaseFiberRootProperties, ...ProfilingOnlyFiberRootProperties, ...SuspenseCallbackOnlyFiberRootProperties, + ...UpdaterTrackingOnlyFiberRootProperties, ... }; diff --git a/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js new file mode 100644 index 0000000000000..38bbc8d66b8c8 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js @@ -0,0 +1,603 @@ +/** + * 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(() => {}), + 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 = () => ; + const Child = () => null; + const container = document.createElement('div'); + + await ReactTestUtils.act(async () => { + ReactDOM.render(, container); + }); + expect(allSchedulerTags).toHaveLength(1); + expect(allSchedulerTags[0]).toHaveLength(1); + expect(allSchedulerTags[0]).toContain(HostRoot); + + await ReactTestUtils.act(async () => { + ReactDOM.render(, container); + }); + expect(allSchedulerTags).toHaveLength(2); + expect(allSchedulerTags[1]).toHaveLength(1); + expect(allSchedulerTags[1]).toContain(HostRoot); + }); + + it('should report a function component as the scheduler for a hooks update', async () => { + let scheduleForA = null; + let scheduleForB = null; + + const Parent = () => ( + + + + + ); + const SchedulingComponentA = () => { + const [count, setCount] = React.useState(0); + scheduleForA = () => setCount(prevCount => prevCount + 1); + return ; + }; + const SchedulingComponentB = () => { + const [count, setCount] = React.useState(0); + scheduleForB = () => setCount(prevCount => prevCount + 1); + return ; + }; + const Child = () => null; + + await ReactTestUtils.act(async () => { + ReactDOM.render(, document.createElement('div')); + }); + expect(scheduleForA).not.toBeNull(); + expect(scheduleForB).not.toBeNull(); + expect(allSchedulerTypes).toHaveLength(1); + + await ReactTestUtils.act(async () => { + scheduleForA(); + }); + expect(allSchedulerTypes).toHaveLength(2); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(SchedulingComponentA); + + await ReactTestUtils.act(async () => { + scheduleForB(); + }); + expect(allSchedulerTypes).toHaveLength(3); + expect(allSchedulerTypes[2]).toHaveLength(1); + expect(allSchedulerTypes[2]).toContain(SchedulingComponentB); + }); + + it('should report a class component as the scheduler for a setState update', async () => { + const Parent = () => ; + class SchedulingComponent extends React.Component { + state = {}; + render() { + instance = this; + return ; + } + } + const Child = () => null; + let instance; + await ReactTestUtils.act(async () => { + ReactDOM.render(, document.createElement('div')); + }); + expect(allSchedulerTypes).toHaveLength(1); + + expect(instance).not.toBeNull(); + await ReactTestUtils.act(async () => { + instance.setState({}); + }); + expect(allSchedulerTypes).toHaveLength(2); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(SchedulingComponent); + }); + + // @gate experimental + it('should cover cascading updates', async () => { + let triggerActiveCascade = null; + let triggerPassiveCascade = null; + + const Parent = () => ; + const SchedulingComponent = () => { + const [cascade, setCascade] = React.useState(null); + triggerActiveCascade = () => setCascade('active'); + triggerPassiveCascade = () => setCascade('passive'); + return ; + }; + 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(); + expect(Scheduler).toFlushAndYieldThrough([ + 'CascadingChild 0', + 'onCommitRoot', + ]); + }); + expect(triggerActiveCascade).not.toBeNull(); + expect(triggerPassiveCascade).not.toBeNull(); + expect(allSchedulerTypes).toHaveLength(1); + + await ReactTestUtils.act(async () => { + triggerActiveCascade(); + expect(Scheduler).toFlushAndYieldThrough([ + 'CascadingChild 0', + 'onCommitRoot', + 'CascadingChild 1', + 'onCommitRoot', + ]); + }); + expect(allSchedulerTypes).toHaveLength(3); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(SchedulingComponent); + expect(allSchedulerTypes[2]).toHaveLength(1); + expect(allSchedulerTypes[2]).toContain(CascadingChild); + + await ReactTestUtils.act(async () => { + triggerPassiveCascade(); + expect(Scheduler).toFlushAndYieldThrough([ + 'CascadingChild 1', + 'onCommitRoot', + 'CascadingChild 2', + 'onCommitRoot', + ]); + }); + expect(allSchedulerTypes).toHaveLength(5); + expect(allSchedulerTypes[3]).toHaveLength(1); + expect(allSchedulerTypes[3]).toContain(SchedulingComponent); + expect(allSchedulerTypes[4]).toHaveLength(1); + expect(allSchedulerTypes[4]).toContain(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 = () => ( + }> + + + ); + 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(, document.createElement('div')); + expect(Scheduler).toHaveYielded(['onCommitRoot']); + }); + expect(setShouldSuspend).not.toBeNull(); + expect(allSchedulerTypes).toHaveLength(1); + + await ReactTestUtils.act(async () => { + setShouldSuspend(true); + }); + expect(Scheduler).toHaveYielded(['onCommitRoot']); + expect(allSchedulerTypes).toHaveLength(2); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(Suspender); + + expect(resolver).not.toBeNull(); + await ReactTestUtils.act(() => { + resolver('abc'); + return promise; + }); + expect(Scheduler).toHaveYielded(['onCommitRoot']); + expect(allSchedulerTypes).toHaveLength(3); + expect(allSchedulerTypes[2]).toHaveLength(1); + expect(allSchedulerTypes[2]).toContain(Suspender); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + + done(); + }); + + // @gate experimental + it('traces interaction through hidden subtree', async () => { + const {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 ( + + ); + } + + 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
; + }; + + const App = () => { + Scheduler.unstable_yieldValue('App'); + React.useEffect(() => { + Scheduler.unstable_yieldValue('App:mount'); + }, []); + return ( + + + + ); + }; + + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container); + await ReactTestUtils.act(async () => { + root.render(); + }); + + // 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', + ]); + // Initial render + expect(allSchedulerTypes).toHaveLength(4); + expect(allSchedulerTags[0]).toHaveLength(1); + expect(allSchedulerTags[0]).toContain(HostRoot); + // Offscreen update + expect(allSchedulerTypes[1]).toHaveLength(0); + // Child passive effect + expect(allSchedulerTypes[2]).toHaveLength(1); + expect(allSchedulerTypes[2]).toContain(Child); + // Offscreen update + expect(allSchedulerTypes[3]).toHaveLength(0); + }); + + // @gate experimental + it('should cover error handling', async () => { + let triggerError = null; + + const Parent = () => { + const [shouldError, setShouldError] = React.useState(false); + triggerError = () => setShouldError(true); + return shouldError ? ( + + + + ) : ( + + + + ); + }; + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + 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(); + }); + 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).toHaveLength(2); + expect(allSchedulerTypes[0]).toHaveLength(1); + expect(allSchedulerTypes[0]).toContain(Parent); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(ErrorBoundary); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + }); + + // @gate experimental + it('should distinguish between updaters in the case of interleaved work', async () => { + let triggerLowPriorityUpdate = null; + let triggerSyncPriorityUpdate = null; + + const HighPriorityUpdater = () => { + const [count, setCount] = React.useState(0); + triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1); + Scheduler.unstable_yieldValue(`HighPriorityUpdater ${count}`); + return ; + }; + const LowPriorityUpdater = () => { + const [count, setCount] = React.useState(0); + triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1); + Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`); + return ; + }; + const Yield = ({value}) => { + Scheduler.unstable_yieldValue(`Yield ${value}`); + return null; + }; + + const root = ReactDOM.unstable_createRoot(document.createElement('div')); + ReactTestUtils.act(() => { + root.render( + + + + , + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'HighPriorityUpdater 0', + 'Yield HighPriority 0', + 'LowPriorityUpdater 0', + 'Yield LowPriority 0', + 'onCommitRoot', + ]); + }); + expect(triggerLowPriorityUpdate).not.toBeNull(); + expect(triggerSyncPriorityUpdate).not.toBeNull(); + expect(allSchedulerTypes).toHaveLength(1); + + // Render a partially update, but don't finish. + ReactTestUtils.act(() => { + triggerLowPriorityUpdate(); + expect(Scheduler).toFlushAndYieldThrough(['LowPriorityUpdater 1']); + expect(allSchedulerTypes).toHaveLength(1); + + // Interrupt with higher priority work. + ReactDOM.flushSync(triggerSyncPriorityUpdate); + expect(Scheduler).toHaveYielded([ + 'HighPriorityUpdater 1', + 'Yield HighPriority 1', + 'onCommitRoot', + ]); + expect(allSchedulerTypes).toHaveLength(2); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(HighPriorityUpdater); + + // Finish the initial partial update + triggerLowPriorityUpdate(); + expect(Scheduler).toFlushAndYieldThrough([ + 'LowPriorityUpdater 2', + 'Yield LowPriority 2', + 'onCommitRoot', + ]); + }); + expect(allSchedulerTypes).toHaveLength(3); + expect(allSchedulerTypes[2]).toHaveLength(1); + expect(allSchedulerTypes[2]).toContain(LowPriorityUpdater); + + // Verify no outstanding flushes + Scheduler.unstable_flushAll(); + }); + + // @gate experimental + it('should not lose track of updaters if work yields before finishing', async () => { + const {HostRoot} = require('react-reconciler/src/ReactWorkTags'); + + const Yield = ({renderTime}) => { + Scheduler.unstable_advanceTime(renderTime); + Scheduler.unstable_yieldValue('Yield:' + renderTime); + return null; + }; + + let first; + class FirstComponent extends React.Component { + state = {renderTime: 1}; + render() { + first = this; + Scheduler.unstable_advanceTime(this.state.renderTime); + Scheduler.unstable_yieldValue( + 'FirstComponent:' + this.state.renderTime, + ); + return ; + } + } + let second; + class SecondComponent extends React.Component { + state = {renderTime: 2}; + render() { + second = this; + Scheduler.unstable_advanceTime(this.state.renderTime); + Scheduler.unstable_yieldValue( + 'SecondComponent:' + this.state.renderTime, + ); + return ; + } + } + + Scheduler.unstable_advanceTime(5); // 0 -> 5 + + const root = ReactDOM.unstable_createRoot(document.createElement('div')); + root.render( + + + + , + ); + + // Render everything initially. + expect(Scheduler).toFlushAndYield([ + 'FirstComponent:1', + 'Yield:4', + 'SecondComponent:2', + 'Yield:7', + 'onCommitRoot', + ]); + expect(allSchedulerTags).toHaveLength(1); + expect(allSchedulerTags[0]).toHaveLength(1); + expect(allSchedulerTags[0]).toContain(HostRoot); + + // Render a partial update, but don't finish. + first.setState({renderTime: 10}); + expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']); + expect(allSchedulerTypes).toHaveLength(1); + + // Interrupt with higher priority work. + ReactDOM.flushSync(() => second.setState({renderTime: 30})); + expect(Scheduler).toHaveYielded([ + 'SecondComponent:30', + 'Yield:7', + 'onCommitRoot', + ]); + expect(allSchedulerTypes).toHaveLength(2); + expect(allSchedulerTypes[1]).toHaveLength(1); + expect(allSchedulerTypes[1]).toContain(SecondComponent); + + // Resume the original low priority update. + expect(Scheduler).toFlushAndYield([ + 'FirstComponent:10', + 'Yield:4', + 'onCommitRoot', + ]); + expect(allSchedulerTypes).toHaveLength(3); + expect(allSchedulerTypes[2]).toHaveLength(1); + expect(allSchedulerTypes[2]).toContain(FirstComponent); + }); +}); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 538a64e193dc8..0729d53d7f3d0 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -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__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0b1659d1625b9..826afbc1c9e18 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 80e113c8e5bfd..99fbcd61b5be1 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f5440f7a25f2e..72a390ca9cd7c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 8f31c4e7f84bb..16243e404611d 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 2e04322a32ced..190d006b27fa5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index bb1c40537287b..61a48fbbd5bce 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 7a12f6041d934..71e77cf6971ab 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -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; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 01937c7b125b7..9b9ec4310c9c3 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -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__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 1e8908ea4cde5..b1e2675a7ba73 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -25,6 +25,7 @@ export const { enableLegacyFBSupport, deferRenderPhaseUpdateToNextBatch, enableDebugTracing, + enableUpdaterTracking, skipUnmountedBoundaries, enableStrictEffects, createRootStrictEffectsByDefault,