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 =