@samitouri / QOS-React / commits / dc108b0f55

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.

Brian Vaughn committed Apr 9, 2021 at 10:34 UTC dc108b0f55387c811899aeb92e583294b0707b1c
22 files changed +912 -10
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+38 -2
@@ -37,6 +37,7 @@ import {
37 enableStrictEffects,
38 deletedTreeCleanUpLevel,
39 enableSuspenseLayoutEffectSemantics,
40 + enableUpdaterTracking,
41 } from 'shared/ReactFeatureFlags';
42 import {
43 FunctionComponent,
@@ -89,7 +90,7 @@ import {
90 resetCurrentFiber as resetCurrentDebugFiberInDEV,
91 setCurrentFiber as setCurrentDebugFiberInDEV,
92 } from './ReactCurrentFiber';
92 -
93 +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
94 import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
95 import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
96 import {
@@ -137,6 +138,7 @@ import {
138 resolveRetryWakeable,
139 markCommitTimeOfFallback,
140 enqueuePendingPassiveProfilerEffect,
141 + restorePendingUpdaters,
142 } from './ReactFiberWorkLoop.new';
143 import {
144 NoFlags as NoHookEffect,
@@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
164
165 let nextEffect: Fiber | null = null;
166
167 +// Used for Profiling builds to track updaters.
168 +let inProgressLanes: Lanes | null = null;
169 +let inProgressRoot: FiberRoot | null = null;
170 +
171 const callComponentWillUnmountWithTimer = function(current, instance) {
172 instance.props = current.memoizedProps;
173 instance.state = current.memoizedState;
@@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
2100 }
2101 }
2102 retryCache.add(wakeable);
2103 +
2104 + if (enableUpdaterTracking) {
2105 + if (isDevToolsPresent) {
2106 + if (inProgressLanes !== null && inProgressRoot !== null) {
2107 + // If we have pending work still, associate the original updaters with it.
2108 + restorePendingUpdaters(inProgressRoot, inProgressLanes);
2109 + } else {
2110 + throw Error(
2111 + 'Expected finished root and lanes to be set. This is a bug in React.',
2112 + );
2113 + }
2114 + }
2115 + }
2116 +
2117 wakeable.then(retry, retry);
2118 }
2119 });
@@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) {
2144 resetTextContent(current.stateNode);
2145 }
2146
2127 -export function commitMutationEffects(root: FiberRoot, firstChild: Fiber) {
2147 +export function commitMutationEffects(
2148 + root: FiberRoot,
2149 + firstChild: Fiber,
2150 + committedLanes: Lanes,
2151 +) {
2152 + inProgressLanes = committedLanes;
2153 + inProgressRoot = root;
2154 nextEffect = firstChild;
2155 +
2156 commitMutationEffects_begin(root);
2157 +
2158 + inProgressLanes = null;
2159 + inProgressRoot = null;
2160 }
2161
2162 function commitMutationEffects_begin(root: FiberRoot) {
@@ -2280,8 +2310,14 @@ export function commitLayoutEffects(
2310 root: FiberRoot,
2311 committedLanes: Lanes,
2312 ): void {
2313 + inProgressLanes = committedLanes;
2314 + inProgressRoot = root;
2315 nextEffect = finishedWork;
2316 +
2317 commitLayoutEffects_begin(finishedWork, root, committedLanes);
2318 +
2319 + inProgressLanes = null;
2320 + inProgressRoot = null;
2321 }
2322
2323 function commitLayoutEffects_begin(
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+38 -2
@@ -37,6 +37,7 @@ import {
37 enableStrictEffects,
38 deletedTreeCleanUpLevel,
39 enableSuspenseLayoutEffectSemantics,
40 + enableUpdaterTracking,
41 } from 'shared/ReactFeatureFlags';
42 import {
43 FunctionComponent,
@@ -89,7 +90,7 @@ import {
90 resetCurrentFiber as resetCurrentDebugFiberInDEV,
91 setCurrentFiber as setCurrentDebugFiberInDEV,
92 } from './ReactCurrentFiber';
92 -
93 +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
94 import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
95 import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
96 import {
@@ -137,6 +138,7 @@ import {
138 resolveRetryWakeable,
139 markCommitTimeOfFallback,
140 enqueuePendingPassiveProfilerEffect,
141 + restorePendingUpdaters,
142 } from './ReactFiberWorkLoop.old';
143 import {
144 NoFlags as NoHookEffect,
@@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
164
165 let nextEffect: Fiber | null = null;
166
167 +// Used for Profiling builds to track updaters.
168 +let inProgressLanes: Lanes | null = null;
169 +let inProgressRoot: FiberRoot | null = null;
170 +
171 const callComponentWillUnmountWithTimer = function(current, instance) {
172 instance.props = current.memoizedProps;
173 instance.state = current.memoizedState;
@@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
2100 }
2101 }
2102 retryCache.add(wakeable);
2103 +
2104 + if (enableUpdaterTracking) {
2105 + if (isDevToolsPresent) {
2106 + if (inProgressLanes !== null && inProgressRoot !== null) {
2107 + // If we have pending work still, associate the original updaters with it.
2108 + restorePendingUpdaters(inProgressRoot, inProgressLanes);
2109 + } else {
2110 + throw Error(
2111 + 'Expected finished root and lanes to be set. This is a bug in React.',
2112 + );
2113 + }
2114 + }
2115 + }
2116 +
2117 wakeable.then(retry, retry);
2118 }
2119 });
@@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) {
2144 resetTextContent(current.stateNode);
2145 }
2146
2127 -export function commitMutationEffects(root: FiberRoot, firstChild: Fiber) {
2147 +export function commitMutationEffects(
2148 + root: FiberRoot,
2149 + firstChild: Fiber,
2150 + committedLanes: Lanes,
2151 +) {
2152 + inProgressLanes = committedLanes;
2153 + inProgressRoot = root;
2154 nextEffect = firstChild;
2155 +
2156 commitMutationEffects_begin(root);
2157 +
2158 + inProgressLanes = null;
2159 + inProgressRoot = null;
2160 }
2161
2162 function commitMutationEffects_begin(root: FiberRoot) {
@@ -2280,8 +2310,14 @@ export function commitLayoutEffects(
2310 root: FiberRoot,
2311 committedLanes: Lanes,
2312 ): void {
2313 + inProgressLanes = committedLanes;
2314 + inProgressRoot = root;
2315 nextEffect = finishedWork;
2316 +
2317 commitLayoutEffects_begin(finishedWork, root, committedLanes);
2318 +
2319 + inProgressLanes = null;
2320 + inProgressRoot = null;
2321 }
2322
2323 function commitLayoutEffects_begin(
packages/react-reconciler/src/ReactFiberLane.new.js
+57 -1
@@ -35,7 +35,12 @@ export type Lanes = number;
35 export type Lane = number;
36 export type LaneMap<T> = Array<T>;
37
38 -import {enableCache, enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
38 +import {
39 + enableCache,
40 + enableSchedulingProfiler,
41 + enableUpdaterTracking,
42 +} from 'shared/ReactFeatureFlags';
43 +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
44
45 // Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler.
46 // If those values are changed that package should be rebuilt and redeployed.
@@ -742,6 +747,57 @@ export function getBumpedLaneForHydration(
747 return lane;
748 }
749
750 +export function addFiberToLanesMap(
751 + root: FiberRoot,
752 + fiber: Fiber,
753 + lanes: Lanes | Lane,
754 +) {
755 + if (!enableUpdaterTracking) {
756 + return;
757 + }
758 + if (!isDevToolsPresent) {
759 + return;
760 + }
761 + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
762 + while (lanes > 0) {
763 + const index = laneToIndex(lanes);
764 + const lane = 1 << index;
765 +
766 + const updaters = pendingUpdatersLaneMap[index];
767 + updaters.add(fiber);
768 +
769 + lanes &= ~lane;
770 + }
771 +}
772 +
773 +export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
774 + if (!enableUpdaterTracking) {
775 + return;
776 + }
777 + if (!isDevToolsPresent) {
778 + return;
779 + }
780 + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
781 + const memoizedUpdaters = root.memoizedUpdaters;
782 + while (lanes > 0) {
783 + const index = laneToIndex(lanes);
784 + const lane = 1 << index;
785 +
786 + const updaters = pendingUpdatersLaneMap[index];
787 + if (updaters.size > 0) {
788 + updaters.forEach(fiber => {
789 + const alternate = fiber.alternate;
790 + if (alternate === null || !memoizedUpdaters.has(alternate)) {
791 + memoizedUpdaters.add(fiber);
792 + }
793 + });
794 + updaters.clear();
795 + }
796 +
797 + lanes &= ~lane;
798 + }
799 +}
800 +
801 const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
802
803 // Count leading zeros. Only used on lanes, so assume input is an integer.
packages/react-reconciler/src/ReactFiberLane.old.js
+57 -1
@@ -35,7 +35,12 @@ export type Lanes = number;
35 export type Lane = number;
36 export type LaneMap<T> = Array<T>;
37
38 -import {enableCache, enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
38 +import {
39 + enableCache,
40 + enableSchedulingProfiler,
41 + enableUpdaterTracking,
42 +} from 'shared/ReactFeatureFlags';
43 +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
44
45 // Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler.
46 // If those values are changed that package should be rebuilt and redeployed.
@@ -742,6 +747,57 @@ export function getBumpedLaneForHydration(
747 return lane;
748 }
749
750 +export function addFiberToLanesMap(
751 + root: FiberRoot,
752 + fiber: Fiber,
753 + lanes: Lanes | Lane,
754 +) {
755 + if (!enableUpdaterTracking) {
756 + return;
757 + }
758 + if (!isDevToolsPresent) {
759 + return;
760 + }
761 + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
762 + while (lanes > 0) {
763 + const index = laneToIndex(lanes);
764 + const lane = 1 << index;
765 +
766 + const updaters = pendingUpdatersLaneMap[index];
767 + updaters.add(fiber);
768 +
769 + lanes &= ~lane;
770 + }
771 +}
772 +
773 +export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
774 + if (!enableUpdaterTracking) {
775 + return;
776 + }
777 + if (!isDevToolsPresent) {
778 + return;
779 + }
780 + const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
781 + const memoizedUpdaters = root.memoizedUpdaters;
782 + while (lanes > 0) {
783 + const index = laneToIndex(lanes);
784 + const lane = 1 << index;
785 +
786 + const updaters = pendingUpdatersLaneMap[index];
787 + if (updaters.size > 0) {
788 + updaters.forEach(fiber => {
789 + const alternate = fiber.alternate;
790 + if (alternate === null || !memoizedUpdaters.has(alternate)) {
791 + memoizedUpdaters.add(fiber);
792 + }
793 + });
794 + updaters.clear();
795 + }
796 +
797 + lanes &= ~lane;
798 + }
799 +}
800 +
801 const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
802
803 // Count leading zeros. Only used on lanes, so assume input is an integer.
packages/react-reconciler/src/ReactFiberRoot.new.js
+10
@@ -16,6 +16,7 @@ import {
16 NoLane,
17 NoLanes,
18 NoTimestamp,
19 + TotalLanes,
20 createLaneMap,
21 } from './ReactFiberLane.new';
22 import {
@@ -24,6 +25,7 @@ import {
25 enableCache,
26 enableProfilerCommitHooks,
27 enableProfilerTimer,
28 + enableUpdaterTracking,
29 } from 'shared/ReactFeatureFlags';
30 import {unstable_getThreadID} from 'scheduler/tracing';
31 import {initializeUpdateQueue} from './ReactUpdateQueue.new';
@@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) {
79 this.passiveEffectDuration = 0;
80 }
81
82 + if (enableUpdaterTracking) {
83 + this.memoizedUpdaters = new Set();
84 + const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
85 + for (let i = 0; i < TotalLanes; i++) {
86 + pendingUpdatersLaneMap.push(new Set());
87 + }
88 + }
89 +
90 if (__DEV__) {
91 switch (tag) {
92 case ConcurrentRoot:
packages/react-reconciler/src/ReactFiberRoot.old.js
+10
@@ -16,6 +16,7 @@ import {
16 NoLane,
17 NoLanes,
18 NoTimestamp,
19 + TotalLanes,
20 createLaneMap,
21 } from './ReactFiberLane.old';
22 import {
@@ -24,6 +25,7 @@ import {
25 enableCache,
26 enableProfilerCommitHooks,
27 enableProfilerTimer,
28 + enableUpdaterTracking,
29 } from 'shared/ReactFeatureFlags';
30 import {unstable_getThreadID} from 'scheduler/tracing';
31 import {initializeUpdateQueue} from './ReactUpdateQueue.old';
@@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) {
79 this.passiveEffectDuration = 0;
80 }
81
82 + if (enableUpdaterTracking) {
83 + this.memoizedUpdaters = new Set();
84 + const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
85 + for (let i = 0; i < TotalLanes; i++) {
86 + pendingUpdatersLaneMap.push(new Set());
87 + }
88 + }
89 +
90 if (__DEV__) {
91 switch (tag) {
92 case ConcurrentRoot:
packages/react-reconciler/src/ReactFiberThrow.new.js
+16 -1
@@ -39,6 +39,7 @@ import {
39 enableDebugTracing,
40 enableSchedulingProfiler,
41 enableLazyContextPropagation,
42 + enableUpdaterTracking,
43 } from 'shared/ReactFeatureFlags';
44 import {createCapturedValue} from './ReactCapturedValue';
45 import {
@@ -60,12 +61,13 @@ import {
61 markLegacyErrorBoundaryAsFailed,
62 isAlreadyFailedLegacyErrorBoundary,
63 pingSuspendedRoot,
64 + restorePendingUpdaters,
65 } from './ReactFiberWorkLoop.new';
66 import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
67 import {logCapturedError} from './ReactFiberErrorLogger';
68 import {logComponentSuspended} from './DebugTracing';
69 import {markComponentSuspended} from './SchedulingProfiler';
68 -
70 +import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
71 import {
72 SyncLane,
73 NoTimestamp,
@@ -177,6 +179,12 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
179 // Memoize using the thread ID to prevent redundant listeners.
180 threadIDs.add(lanes);
181 const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
182 + if (enableUpdaterTracking) {
183 + if (isDevToolsPresent) {
184 + // If we have pending work still, restore the original updaters
185 + restorePendingUpdaters(root, lanes);
186 + }
187 + }
188 wakeable.then(ping, ping);
189 }
190 }
@@ -191,6 +199,13 @@ function throwException(
199 // The source fiber did not complete.
200 sourceFiber.flags |= Incomplete;
201
202 + if (enableUpdaterTracking) {
203 + if (isDevToolsPresent) {
204 + // If we have pending work still, restore the original updaters
205 + restorePendingUpdaters(root, rootRenderLanes);
206 + }
207 + }
208 +
209 if (
210 value !== null &&
211 typeof value === 'object' &&
packages/react-reconciler/src/ReactFiberThrow.old.js
+16 -1
@@ -39,6 +39,7 @@ import {
39 enableDebugTracing,
40 enableSchedulingProfiler,
41 enableLazyContextPropagation,
42 + enableUpdaterTracking,
43 } from 'shared/ReactFeatureFlags';
44 import {createCapturedValue} from './ReactCapturedValue';
45 import {
@@ -60,12 +61,13 @@ import {
61 markLegacyErrorBoundaryAsFailed,
62 isAlreadyFailedLegacyErrorBoundary,
63 pingSuspendedRoot,
64 + restorePendingUpdaters,
65 } from './ReactFiberWorkLoop.old';
66 import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
67 import {logCapturedError} from './ReactFiberErrorLogger';
68 import {logComponentSuspended} from './DebugTracing';
69 import {markComponentSuspended} from './SchedulingProfiler';
68 -
70 +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
71 import {
72 SyncLane,
73 NoTimestamp,
@@ -177,6 +179,12 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
179 // Memoize using the thread ID to prevent redundant listeners.
180 threadIDs.add(lanes);
181 const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
182 + if (enableUpdaterTracking) {
183 + if (isDevToolsPresent) {
184 + // If we have pending work still, restore the original updaters
185 + restorePendingUpdaters(root, lanes);
186 + }
187 + }
188 wakeable.then(ping, ping);
189 }
190 }
@@ -191,6 +199,13 @@ function throwException(
199 // The source fiber did not complete.
200 sourceFiber.flags |= Incomplete;
201
202 + if (enableUpdaterTracking) {
203 + if (isDevToolsPresent) {
204 + // If we have pending work still, restore the original updaters
205 + restorePendingUpdaters(root, rootRenderLanes);
206 + }
207 + }
208 +
209 if (
210 value !== null &&
211 typeof value === 'object' &&
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+64 -1
@@ -32,6 +32,7 @@ import {
32 disableSchedulerTimeoutInWorkLoop,
33 enableStrictEffects,
34 skipUnmountedBoundaries,
35 + enableUpdaterTracking,
36 } from 'shared/ReactFeatureFlags';
37 import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import invariant from 'shared/invariant';
@@ -159,6 +160,8 @@ import {
160 markRootFinished,
161 areLanesExpired,
162 getHighestPriorityLane,
163 + addFiberToLanesMap,
164 + movePendingFibersToMemoized,
165 } from './ReactFiberLane.new';
166 import {
167 DiscreteEventPriority,
@@ -232,6 +235,7 @@ import {
235 import {
236 onCommitRoot as onCommitRootDevTools,
237 onPostCommitRoot as onPostCommitRootDevTools,
238 + isDevToolsPresent,
239 } from './ReactFiberDevToolsHook.new';
240 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
241
@@ -470,6 +474,12 @@ export function scheduleUpdateOnFiber(
474 return null;
475 }
476
477 + if (enableUpdaterTracking) {
478 + if (isDevToolsPresent) {
479 + addFiberToLanesMap(root, fiber, lane);
480 + }
481 + }
482 +
483 // Mark that the root has a pending update.
484 markRootUpdated(root, lane, eventTime);
485
@@ -1427,6 +1437,22 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1437 // If the root or lanes have changed, throw out the existing stack
1438 // and prepare a fresh one. Otherwise we'll continue where we left off.
1439 if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1440 + if (enableUpdaterTracking) {
1441 + if (isDevToolsPresent) {
1442 + const memoizedUpdaters = root.memoizedUpdaters;
1443 + if (memoizedUpdaters.size > 0) {
1444 + restorePendingUpdaters(root, workInProgressRootRenderLanes);
1445 + memoizedUpdaters.clear();
1446 + }
1447 +
1448 + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
1449 + // If we bailout on this work, we'll move them back (like above).
1450 + // It's important to move them now in case the work spawns more work at the same priority with different updaters.
1451 + // That way we can keep the current update and future updates separate.
1452 + movePendingFibersToMemoized(root, lanes);
1453 + }
1454 + }
1455 +
1456 prepareFreshStack(root, lanes);
1457 startWorkOnPendingInteractions(root, lanes);
1458 }
@@ -1502,6 +1528,22 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1528 // If the root or lanes have changed, throw out the existing stack
1529 // and prepare a fresh one. Otherwise we'll continue where we left off.
1530 if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1531 + if (enableUpdaterTracking) {
1532 + if (isDevToolsPresent) {
1533 + const memoizedUpdaters = root.memoizedUpdaters;
1534 + if (memoizedUpdaters.size > 0) {
1535 + restorePendingUpdaters(root, workInProgressRootRenderLanes);
1536 + memoizedUpdaters.clear();
1537 + }
1538 +
1539 + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
1540 + // If we bailout on this work, we'll move them back (like above).
1541 + // It's important to move them now in case the work spawns more work at the same priority with different updaters.
1542 + // That way we can keep the current update and future updates separate.
1543 + movePendingFibersToMemoized(root, lanes);
1544 + }
1545 + }
1546 +
1547 resetRenderTimer();
1548 prepareFreshStack(root, lanes);
1549 startWorkOnPendingInteractions(root, lanes);
@@ -1853,7 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1895 }
1896
1897 // The next phase is the mutation phase, where we mutate the host tree.
1856 - commitMutationEffects(root, finishedWork);
1898 + commitMutationEffects(root, finishedWork, lanes);
1899
1900 if (shouldFireAfterActiveInstanceBlur) {
1901 afterActiveInstanceBlur();
@@ -1985,6 +2027,12 @@ function commitRootImpl(root, renderPriorityLevel) {
2027
2028 onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);
2029
2030 + if (enableUpdaterTracking) {
2031 + if (isDevToolsPresent) {
2032 + root.memoizedUpdaters.clear();
2033 + }
2034 + }
2035 +
2036 if (__DEV__) {
2037 onCommitRootTestSelector();
2038 }
@@ -2788,6 +2836,21 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
2836 // a 'shared' variable that changes when act() opens/closes in tests.
2837 export const IsThisRendererActing = {current: (false: boolean)};
2838
2839 +export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void {
2840 + if (enableUpdaterTracking) {
2841 + if (isDevToolsPresent) {
2842 + const memoizedUpdaters = root.memoizedUpdaters;
2843 + memoizedUpdaters.forEach(schedulingFiber => {
2844 + addFiberToLanesMap(root, schedulingFiber, lanes);
2845 + });
2846 +
2847 + // This function intentionally does not clear memoized updaters.
2848 + // Those may still be relevant to the current commit
2849 + // and a future one (e.g. Suspense).
2850 + }
2851 + }
2852 +}
2853 +
2854 export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2855 if (__DEV__) {
2856 if (
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+64 -1
@@ -32,6 +32,7 @@ import {
32 disableSchedulerTimeoutInWorkLoop,
33 enableStrictEffects,
34 skipUnmountedBoundaries,
35 + enableUpdaterTracking,
36 } from 'shared/ReactFeatureFlags';
37 import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import invariant from 'shared/invariant';
@@ -159,6 +160,8 @@ import {
160 markRootFinished,
161 areLanesExpired,
162 getHighestPriorityLane,
163 + addFiberToLanesMap,
164 + movePendingFibersToMemoized,
165 } from './ReactFiberLane.old';
166 import {
167 DiscreteEventPriority,
@@ -232,6 +235,7 @@ import {
235 import {
236 onCommitRoot as onCommitRootDevTools,
237 onPostCommitRoot as onPostCommitRootDevTools,
238 + isDevToolsPresent,
239 } from './ReactFiberDevToolsHook.old';
240 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
241
@@ -470,6 +474,12 @@ export function scheduleUpdateOnFiber(
474 return null;
475 }
476
477 + if (enableUpdaterTracking) {
478 + if (isDevToolsPresent) {
479 + addFiberToLanesMap(root, fiber, lane);
480 + }
481 + }
482 +
483 // Mark that the root has a pending update.
484 markRootUpdated(root, lane, eventTime);
485
@@ -1427,6 +1437,22 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1437 // If the root or lanes have changed, throw out the existing stack
1438 // and prepare a fresh one. Otherwise we'll continue where we left off.
1439 if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1440 + if (enableUpdaterTracking) {
1441 + if (isDevToolsPresent) {
1442 + const memoizedUpdaters = root.memoizedUpdaters;
1443 + if (memoizedUpdaters.size > 0) {
1444 + restorePendingUpdaters(root, workInProgressRootRenderLanes);
1445 + memoizedUpdaters.clear();
1446 + }
1447 +
1448 + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
1449 + // If we bailout on this work, we'll move them back (like above).
1450 + // It's important to move them now in case the work spawns more work at the same priority with different updaters.
1451 + // That way we can keep the current update and future updates separate.
1452 + movePendingFibersToMemoized(root, lanes);
1453 + }
1454 + }
1455 +
1456 prepareFreshStack(root, lanes);
1457 startWorkOnPendingInteractions(root, lanes);
1458 }
@@ -1502,6 +1528,22 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1528 // If the root or lanes have changed, throw out the existing stack
1529 // and prepare a fresh one. Otherwise we'll continue where we left off.
1530 if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1531 + if (enableUpdaterTracking) {
1532 + if (isDevToolsPresent) {
1533 + const memoizedUpdaters = root.memoizedUpdaters;
1534 + if (memoizedUpdaters.size > 0) {
1535 + restorePendingUpdaters(root, workInProgressRootRenderLanes);
1536 + memoizedUpdaters.clear();
1537 + }
1538 +
1539 + // At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
1540 + // If we bailout on this work, we'll move them back (like above).
1541 + // It's important to move them now in case the work spawns more work at the same priority with different updaters.
1542 + // That way we can keep the current update and future updates separate.
1543 + movePendingFibersToMemoized(root, lanes);
1544 + }
1545 + }
1546 +
1547 resetRenderTimer();
1548 prepareFreshStack(root, lanes);
1549 startWorkOnPendingInteractions(root, lanes);
@@ -1853,7 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1895 }
1896
1897 // The next phase is the mutation phase, where we mutate the host tree.
1856 - commitMutationEffects(root, finishedWork);
1898 + commitMutationEffects(root, finishedWork, lanes);
1899
1900 if (shouldFireAfterActiveInstanceBlur) {
1901 afterActiveInstanceBlur();
@@ -1985,6 +2027,12 @@ function commitRootImpl(root, renderPriorityLevel) {
2027
2028 onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);
2029
2030 + if (enableUpdaterTracking) {
2031 + if (isDevToolsPresent) {
2032 + root.memoizedUpdaters.clear();
2033 + }
2034 + }
2035 +
2036 if (__DEV__) {
2037 onCommitRootTestSelector();
2038 }
@@ -2788,6 +2836,21 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
2836 // a 'shared' variable that changes when act() opens/closes in tests.
2837 export const IsThisRendererActing = {current: (false: boolean)};
2838
2839 +export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void {
2840 + if (enableUpdaterTracking) {
2841 + if (isDevToolsPresent) {
2842 + const memoizedUpdaters = root.memoizedUpdaters;
2843 + memoizedUpdaters.forEach(schedulingFiber => {
2844 + addFiberToLanesMap(root, schedulingFiber, lanes);
2845 + });
2846 +
2847 + // This function intentionally does not clear memoized updaters.
2848 + // Those may still be relevant to the current commit
2849 + // and a future one (e.g. Suspense).
2850 + }
2851 + }
2852 +}
2853 +
2854 export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2855 if (__DEV__) {
2856 if (
packages/react-reconciler/src/ReactInternalTypes.js
+8
@@ -249,6 +249,13 @@ type ProfilingOnlyFiberRootProperties = {|
249 pendingInteractionMap: Map<Lane | Lanes, Set<Interaction>>,
250 |};
251
252 +// The following attributes are only used by DevTools and are only present in DEV builds.
253 +// They enable DevTools Profiler UI to show which Fiber(s) scheduled a given commit.
254 +type UpdaterTrackingOnlyFiberRootProperties = {|
255 + memoizedUpdaters: Set<Fiber>,
256 + pendingUpdatersLaneMap: LaneMap<Set<Fiber>>,
257 +|};
258 +
259 export type SuspenseHydrationCallbacks = {
260 onHydrated?: (suspenseInstance: SuspenseInstance) => void,
261 onDeleted?: (suspenseInstance: SuspenseInstance) => void,
@@ -269,6 +276,7 @@ export type FiberRoot = {
276 ...BaseFiberRootProperties,
277 ...ProfilingOnlyFiberRootProperties,
278 ...SuspenseCallbackOnlyFiberRootProperties,
279 + ...UpdaterTrackingOnlyFiberRootProperties,
280 ...
281 };
282
packages/react-reconciler/src/__tests__/ReactUpdaters-test.internal.js new
+521
@@ -0,0 +1,521 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactDOM;
15 +let ReactTestUtils;
16 +let Scheduler;
17 +let mockDevToolsHook;
18 +let allSchedulerTags;
19 +let allSchedulerTypes;
20 +let onCommitRootShouldYield;
21 +
22 +describe('updaters', () => {
23 + beforeEach(() => {
24 + jest.resetModules();
25 +
26 + allSchedulerTags = [];
27 + allSchedulerTypes = [];
28 +
29 + onCommitRootShouldYield = true;
30 +
31 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
32 + ReactFeatureFlags.enableUpdaterTracking = true;
33 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
34 +
35 + mockDevToolsHook = {
36 + injectInternals: jest.fn(() => {}),
37 + isDevToolsPresent: true,
38 + onCommitRoot: jest.fn(fiberRoot => {
39 + if (onCommitRootShouldYield) {
40 + Scheduler.unstable_yieldValue('onCommitRoot');
41 + }
42 + const schedulerTags = [];
43 + const schedulerTypes = [];
44 + fiberRoot.memoizedUpdaters.forEach(fiber => {
45 + schedulerTags.push(fiber.tag);
46 + schedulerTypes.push(fiber.elementType);
47 + });
48 + allSchedulerTags.push(schedulerTags);
49 + allSchedulerTypes.push(schedulerTypes);
50 + }),
51 + onCommitUnmount: jest.fn(() => {}),
52 + onPostCommitRoot: jest.fn(() => {}),
53 + onScheduleRoot: jest.fn(() => {}),
54 + };
55 +
56 + jest.mock(
57 + 'react-reconciler/src/ReactFiberDevToolsHook.old',
58 + () => mockDevToolsHook,
59 + );
60 + jest.mock(
61 + 'react-reconciler/src/ReactFiberDevToolsHook.new',
62 + () => mockDevToolsHook,
63 + );
64 +
65 + React = require('react');
66 + ReactDOM = require('react-dom');
67 + ReactTestUtils = require('react-dom/test-utils');
68 + Scheduler = require('scheduler');
69 + });
70 +
71 + it('should report the (host) root as the scheduler for root-level render', async () => {
72 + const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
73 +
74 + const Parent = () => <Child />;
75 + const Child = () => null;
76 + const container = document.createElement('div');
77 +
78 + await ReactTestUtils.act(async () => {
79 + ReactDOM.render(<Parent />, container);
80 + });
81 + expect(allSchedulerTags).toEqual([[HostRoot]]);
82 +
83 + await ReactTestUtils.act(async () => {
84 + ReactDOM.render(<Parent />, container);
85 + });
86 + expect(allSchedulerTags).toEqual([[HostRoot], [HostRoot]]);
87 + });
88 +
89 + it('should report a function component as the scheduler for a hooks update', async () => {
90 + let scheduleForA = null;
91 + let scheduleForB = null;
92 +
93 + const Parent = () => (
94 + <React.Fragment>
95 + <SchedulingComponentA />
96 + <SchedulingComponentB />
97 + </React.Fragment>
98 + );
99 + const SchedulingComponentA = () => {
100 + const [count, setCount] = React.useState(0);
101 + scheduleForA = () => setCount(prevCount => prevCount + 1);
102 + return <Child count={count} />;
103 + };
104 + const SchedulingComponentB = () => {
105 + const [count, setCount] = React.useState(0);
106 + scheduleForB = () => setCount(prevCount => prevCount + 1);
107 + return <Child count={count} />;
108 + };
109 + const Child = () => null;
110 +
111 + await ReactTestUtils.act(async () => {
112 + ReactDOM.render(<Parent />, document.createElement('div'));
113 + });
114 + expect(scheduleForA).not.toBeNull();
115 + expect(scheduleForB).not.toBeNull();
116 + expect(allSchedulerTypes).toEqual([[null]]);
117 +
118 + await ReactTestUtils.act(async () => {
119 + scheduleForA();
120 + });
121 + expect(allSchedulerTypes).toEqual([[null], [SchedulingComponentA]]);
122 +
123 + await ReactTestUtils.act(async () => {
124 + scheduleForB();
125 + });
126 + expect(allSchedulerTypes).toEqual([
127 + [null],
128 + [SchedulingComponentA],
129 + [SchedulingComponentB],
130 + ]);
131 + });
132 +
133 + it('should report a class component as the scheduler for a setState update', async () => {
134 + const Parent = () => <SchedulingComponent />;
135 + class SchedulingComponent extends React.Component {
136 + state = {};
137 + render() {
138 + instance = this;
139 + return <Child />;
140 + }
141 + }
142 + const Child = () => null;
143 + let instance;
144 + await ReactTestUtils.act(async () => {
145 + ReactDOM.render(<Parent />, document.createElement('div'));
146 + });
147 + expect(allSchedulerTypes).toEqual([[null]]);
148 +
149 + expect(instance).not.toBeNull();
150 + await ReactTestUtils.act(async () => {
151 + instance.setState({});
152 + });
153 + expect(allSchedulerTypes).toEqual([[null], [SchedulingComponent]]);
154 + });
155 +
156 + // @gate experimental
157 + it('should cover cascading updates', async () => {
158 + let triggerActiveCascade = null;
159 + let triggerPassiveCascade = null;
160 +
161 + const Parent = () => <SchedulingComponent />;
162 + const SchedulingComponent = () => {
163 + const [cascade, setCascade] = React.useState(null);
164 + triggerActiveCascade = () => setCascade('active');
165 + triggerPassiveCascade = () => setCascade('passive');
166 + return <CascadingChild cascade={cascade} />;
167 + };
168 + const CascadingChild = ({cascade}) => {
169 + const [count, setCount] = React.useState(0);
170 + Scheduler.unstable_yieldValue(`CascadingChild ${count}`);
171 + React.useLayoutEffect(() => {
172 + if (cascade === 'active') {
173 + setCount(prevCount => prevCount + 1);
174 + }
175 + return () => {};
176 + }, [cascade]);
177 + React.useEffect(() => {
178 + if (cascade === 'passive') {
179 + setCount(prevCount => prevCount + 1);
180 + }
181 + return () => {};
182 + }, [cascade]);
183 + return count;
184 + };
185 +
186 + const root = ReactDOM.unstable_createRoot(document.createElement('div'));
187 + await ReactTestUtils.act(async () => {
188 + root.render(<Parent />);
189 + expect(Scheduler).toFlushAndYieldThrough([
190 + 'CascadingChild 0',
191 + 'onCommitRoot',
192 + ]);
193 + });
194 + expect(triggerActiveCascade).not.toBeNull();
195 + expect(triggerPassiveCascade).not.toBeNull();
196 + expect(allSchedulerTypes).toEqual([[null]]);
197 +
198 + await ReactTestUtils.act(async () => {
199 + triggerActiveCascade();
200 + expect(Scheduler).toFlushAndYieldThrough([
201 + 'CascadingChild 0',
202 + 'onCommitRoot',
203 + 'CascadingChild 1',
204 + 'onCommitRoot',
205 + ]);
206 + });
207 + expect(allSchedulerTypes).toEqual([
208 + [null],
209 + [SchedulingComponent],
210 + [CascadingChild],
211 + ]);
212 +
213 + await ReactTestUtils.act(async () => {
214 + triggerPassiveCascade();
215 + expect(Scheduler).toFlushAndYieldThrough([
216 + 'CascadingChild 1',
217 + 'onCommitRoot',
218 + 'CascadingChild 2',
219 + 'onCommitRoot',
220 + ]);
221 + });
222 + expect(allSchedulerTypes).toEqual([
223 + [null],
224 + [SchedulingComponent],
225 + [CascadingChild],
226 + [SchedulingComponent],
227 + [CascadingChild],
228 + ]);
229 +
230 + // Verify no outstanding flushes
231 + Scheduler.unstable_flushAll();
232 + });
233 +
234 + it('should cover suspense pings', async done => {
235 + let data = null;
236 + let resolver = null;
237 + let promise = null;
238 + const fakeCacheRead = () => {
239 + if (data === null) {
240 + promise = new Promise(resolve => {
241 + resolver = resolvedData => {
242 + data = resolvedData;
243 + resolve(resolvedData);
244 + };
245 + });
246 + throw promise;
247 + } else {
248 + return data;
249 + }
250 + };
251 + const Parent = () => (
252 + <React.Suspense fallback={<Fallback />}>
253 + <Suspender />
254 + </React.Suspense>
255 + );
256 + const Fallback = () => null;
257 + let setShouldSuspend = null;
258 + const Suspender = ({suspend}) => {
259 + const tuple = React.useState(false);
260 + setShouldSuspend = tuple[1];
261 + if (tuple[0] === true) {
262 + return fakeCacheRead();
263 + } else {
264 + return null;
265 + }
266 + };
267 +
268 + await ReactTestUtils.act(async () => {
269 + ReactDOM.render(<Parent />, document.createElement('div'));
270 + expect(Scheduler).toHaveYielded(['onCommitRoot']);
271 + });
272 + expect(setShouldSuspend).not.toBeNull();
273 + expect(allSchedulerTypes).toEqual([[null]]);
274 +
275 + await ReactTestUtils.act(async () => {
276 + setShouldSuspend(true);
277 + });
278 + expect(Scheduler).toHaveYielded(['onCommitRoot']);
279 + expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
280 +
281 + expect(resolver).not.toBeNull();
282 + await ReactTestUtils.act(() => {
283 + resolver('abc');
284 + return promise;
285 + });
286 + expect(Scheduler).toHaveYielded(['onCommitRoot']);
287 + expect(allSchedulerTypes).toEqual([[null], [Suspender], [Suspender]]);
288 +
289 + // Verify no outstanding flushes
290 + Scheduler.unstable_flushAll();
291 +
292 + done();
293 + });
294 +
295 + // @gate experimental
296 + it('traces interaction through hidden subtree', async () => {
297 + const {
298 + FunctionComponent,
299 + HostRoot,
300 + } = require('react-reconciler/src/ReactWorkTags');
301 +
302 + // Note: This is based on a similar component we use in www. We can delete once
303 + // the extra div wrapper is no longer necessary.
304 + function LegacyHiddenDiv({children, mode}) {
305 + return (
306 + <div hidden={mode === 'hidden'}>
307 + <React.unstable_LegacyHidden
308 + mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
309 + {children}
310 + </React.unstable_LegacyHidden>
311 + </div>
312 + );
313 + }
314 +
315 + const Child = () => {
316 + const [didMount, setDidMount] = React.useState(false);
317 + Scheduler.unstable_yieldValue('Child');
318 + React.useEffect(() => {
319 + if (didMount) {
320 + Scheduler.unstable_yieldValue('Child:update');
321 + } else {
322 + Scheduler.unstable_yieldValue('Child:mount');
323 + setDidMount(true);
324 + }
325 + }, [didMount]);
326 + return <div />;
327 + };
328 +
329 + const App = () => {
330 + Scheduler.unstable_yieldValue('App');
331 + React.useEffect(() => {
332 + Scheduler.unstable_yieldValue('App:mount');
333 + }, []);
334 + return (
335 + <LegacyHiddenDiv mode="hidden">
336 + <Child />
337 + </LegacyHiddenDiv>
338 + );
339 + };
340 +
341 + const container = document.createElement('div');
342 + const root = ReactDOM.createRoot(container);
343 + await ReactTestUtils.act(async () => {
344 + root.render(<App />);
345 + });
346 +
347 + // TODO: There are 4 commits here instead of 3
348 + // because this update was scheduled at idle priority,
349 + // and idle updates are slightly higher priority than offscreen work.
350 + // So it takes two render passes to finish it.
351 + // The onCommit hook is called even after the no-op bailout update.
352 + expect(Scheduler).toHaveYielded([
353 + 'App',
354 + 'onCommitRoot',
355 + 'App:mount',
356 +
357 + 'Child',
358 + 'onCommitRoot',
359 + 'Child:mount',
360 +
361 + 'onCommitRoot',
362 +
363 + 'Child',
364 + 'onCommitRoot',
365 + 'Child:update',
366 + ]);
367 + expect(allSchedulerTypes).toEqual([
368 + // Initial render
369 + [null],
370 + // Offscreen update
371 + [],
372 + // Child passive effect
373 + [Child],
374 + // Offscreen update
375 + [],
376 + ]);
377 + expect(allSchedulerTags).toEqual([[HostRoot], [], [FunctionComponent], []]);
378 + });
379 +
380 + // @gate experimental
381 + it('should cover error handling', async () => {
382 + let triggerError = null;
383 +
384 + const Parent = () => {
385 + const [shouldError, setShouldError] = React.useState(false);
386 + triggerError = () => setShouldError(true);
387 + return shouldError ? (
388 + <ErrorBoundary>
389 + <BrokenRender />
390 + </ErrorBoundary>
391 + ) : (
392 + <ErrorBoundary>
393 + <Yield value="initial" />
394 + </ErrorBoundary>
395 + );
396 + };
397 + class ErrorBoundary extends React.Component {
398 + state = {error: null};
399 + componentDidCatch(error) {
400 + this.setState({error});
401 + }
402 + render() {
403 + if (this.state.error) {
404 + return <Yield value="error" />;
405 + }
406 + return this.props.children;
407 + }
408 + }
409 + const Yield = ({value}) => {
410 + Scheduler.unstable_yieldValue(value);
411 + return null;
412 + };
413 + const BrokenRender = () => {
414 + throw new Error('Hello');
415 + };
416 +
417 + const root = ReactDOM.unstable_createRoot(document.createElement('div'));
418 + await ReactTestUtils.act(async () => {
419 + root.render(<Parent shouldError={false} />);
420 + });
421 + expect(Scheduler).toHaveYielded(['initial', 'onCommitRoot']);
422 + expect(triggerError).not.toBeNull();
423 +
424 + allSchedulerTypes.splice(0);
425 + onCommitRootShouldYield = true;
426 +
427 + await ReactTestUtils.act(async () => {
428 + triggerError();
429 + });
430 + expect(Scheduler).toHaveYielded(['onCommitRoot', 'error', 'onCommitRoot']);
431 + expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]);
432 +
433 + // Verify no outstanding flushes
434 + Scheduler.unstable_flushAll();
435 + });
436 +
437 + // @gate experimental
438 + it('should distinguish between updaters in the case of interleaved work', async () => {
439 + const {
440 + FunctionComponent,
441 + HostRoot,
442 + } = require('react-reconciler/src/ReactWorkTags');
443 +
444 + let triggerLowPriorityUpdate = null;
445 + let triggerSyncPriorityUpdate = null;
446 +
447 + const SyncPriorityUpdater = () => {
448 + const [count, setCount] = React.useState(0);
449 + triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1);
450 + Scheduler.unstable_yieldValue(`SyncPriorityUpdater ${count}`);
451 + return <Yield value={`HighPriority ${count}`} />;
452 + };
453 + const LowPriorityUpdater = () => {
454 + const [count, setCount] = React.useState(0);
455 + triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1);
456 + Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`);
457 + return <Yield value={`LowPriority ${count}`} />;
458 + };
459 + const Yield = ({value}) => {
460 + Scheduler.unstable_yieldValue(`Yield ${value}`);
461 + return null;
462 + };
463 +
464 + const root = ReactDOM.unstable_createRoot(document.createElement('div'));
465 + root.render(
466 + <React.Fragment>
467 + <SyncPriorityUpdater />
468 + <LowPriorityUpdater />
469 + </React.Fragment>,
470 + );
471 +
472 + // Render everything initially.
473 + expect(Scheduler).toFlushAndYield([
474 + 'SyncPriorityUpdater 0',
475 + 'Yield HighPriority 0',
476 + 'LowPriorityUpdater 0',
477 + 'Yield LowPriority 0',
478 + 'onCommitRoot',
479 + ]);
480 + expect(triggerLowPriorityUpdate).not.toBeNull();
481 + expect(triggerSyncPriorityUpdate).not.toBeNull();
482 + expect(allSchedulerTags).toEqual([[HostRoot]]);
483 +
484 + // Render a partial update, but don't finish.
485 + ReactTestUtils.act(() => {
486 + triggerLowPriorityUpdate();
487 + expect(Scheduler).toFlushAndYieldThrough(['LowPriorityUpdater 1']);
488 + expect(allSchedulerTags).toEqual([[HostRoot]]);
489 +
490 + // Interrupt with higher priority work.
491 + ReactDOM.flushSync(triggerSyncPriorityUpdate);
492 + expect(Scheduler).toHaveYielded([
493 + 'SyncPriorityUpdater 1',
494 + 'Yield HighPriority 1',
495 + 'onCommitRoot',
496 + ]);
497 + expect(allSchedulerTypes).toEqual([[null], [SyncPriorityUpdater]]);
498 +
499 + // Finish the initial partial update
500 + triggerLowPriorityUpdate();
501 + expect(Scheduler).toFlushAndYield([
502 + 'LowPriorityUpdater 2',
503 + 'Yield LowPriority 2',
504 + 'onCommitRoot',
505 + ]);
506 + });
507 + expect(allSchedulerTags).toEqual([
508 + [HostRoot],
509 + [FunctionComponent],
510 + [FunctionComponent],
511 + ]);
512 + expect(allSchedulerTypes).toEqual([
513 + [null],
514 + [SyncPriorityUpdater],
515 + [LowPriorityUpdater],
516 + ]);
517 +
518 + // Verify no outstanding flushes
519 + Scheduler.unstable_flushAll();
520 + });
521 +});
packages/shared/ReactFeatureFlags.js
+3
@@ -54,6 +54,9 @@ export const enableProfilerNestedUpdateScheduledHook = false;
54 // Trace which interactions trigger each commit.
55 export const enableSchedulerTracing = __PROFILE__;
56
57 +// Track which Fiber(s) schedule render work.
58 +export const enableUpdaterTracking = __PROFILE__;
59 +
60 // SSR experiments
61 export const enableSuspenseServerRenderer = __EXPERIMENTAL__;
62 export const enableSelectiveHydration = __EXPERIMENTAL__;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -18,6 +18,7 @@ export const enableProfilerCommitHooks = false;
18 export const enableProfilerNestedUpdatePhase = false;
19 export const enableProfilerNestedUpdateScheduledHook = false;
20 export const enableSchedulerTracing = __PROFILE__;
21 +export const enableUpdaterTracking = false;
22 export const enableSuspenseServerRenderer = false;
23 export const enableSelectiveHydration = false;
24 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 +export const enableUpdaterTracking = false;
24 export const enableSuspenseServerRenderer = false;
25 export const enableSelectiveHydration = false;
26 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 +export const enableUpdaterTracking = false;
24 export const enableSuspenseServerRenderer = false;
25 export const enableSelectiveHydration = false;
26 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 +export const enableUpdaterTracking = false;
24 export const enableSuspenseServerRenderer = false;
25 export const enableSelectiveHydration = false;
26 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 +export const enableUpdaterTracking = false;
24 export const enableSuspenseServerRenderer = false;
25 export const enableSelectiveHydration = false;
26 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 +export const enableUpdaterTracking = false;
24 export const enableSuspenseServerRenderer = false;
25 export const enableSelectiveHydration = false;
26 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = false;
23 +export const enableUpdaterTracking = false;
24 export const enableSuspenseServerRenderer = true;
25 export const enableSelectiveHydration = true;
26 export const enableLazyElements = false;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -25,6 +25,7 @@ export const enableSuspenseLayoutEffectSemantics = __VARIANT__;
25 //
26 // NOTE: This feature will only work in DEV mode; all callsights are wrapped with __DEV__.
27 export const enableDebugTracing = __EXPERIMENTAL__;
28 +export const enableUpdaterTracking = false;
29
30 export const enableSchedulingProfiler = __VARIANT__;
31
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -44,6 +44,8 @@ export const enableProfilerCommitHooks = __PROFILE__;
44 export const enableProfilerNestedUpdatePhase = __PROFILE__;
45 export const enableProfilerNestedUpdateScheduledHook =
46 __PROFILE__ && dynamicFeatureFlags.enableProfilerNestedUpdateScheduledHook;
47 +export const enableUpdaterTracking =
48 + __PROFILE__ && dynamicFeatureFlags.enableUpdaterTracking;
49
50 // Logs additional User Timing API marks for use with an experimental profiling tool.
51 export const enableSchedulingProfiler =