Skip to content

Commit dc108b0

Browse files
author
Brian Vaughn
authored
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.
1 parent 6ea7491 commit dc108b0

22 files changed

+912
-10
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
enableStrictEffects,
3838
deletedTreeCleanUpLevel,
3939
enableSuspenseLayoutEffectSemantics,
40+
enableUpdaterTracking,
4041
} from 'shared/ReactFeatureFlags';
4142
import {
4243
FunctionComponent,
@@ -89,7 +90,7 @@ import {
8990
resetCurrentFiber as resetCurrentDebugFiberInDEV,
9091
setCurrentFiber as setCurrentDebugFiberInDEV,
9192
} from './ReactCurrentFiber';
92-
93+
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
9394
import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
9495
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
9596
import {
@@ -137,6 +138,7 @@ import {
137138
resolveRetryWakeable,
138139
markCommitTimeOfFallback,
139140
enqueuePendingPassiveProfilerEffect,
141+
restorePendingUpdaters,
140142
} from './ReactFiberWorkLoop.new';
141143
import {
142144
NoFlags as NoHookEffect,
@@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
162164

163165
let nextEffect: Fiber | null = null;
164166

167+
// Used for Profiling builds to track updaters.
168+
let inProgressLanes: Lanes | null = null;
169+
let inProgressRoot: FiberRoot | null = null;
170+
165171
const callComponentWillUnmountWithTimer = function(current, instance) {
166172
instance.props = current.memoizedProps;
167173
instance.state = current.memoizedState;
@@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
20942100
}
20952101
}
20962102
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+
20972117
wakeable.then(retry, retry);
20982118
}
20992119
});
@@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) {
21242144
resetTextContent(current.stateNode);
21252145
}
21262146

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;
21282154
nextEffect = firstChild;
2155+
21292156
commitMutationEffects_begin(root);
2157+
2158+
inProgressLanes = null;
2159+
inProgressRoot = null;
21302160
}
21312161

21322162
function commitMutationEffects_begin(root: FiberRoot) {
@@ -2280,8 +2310,14 @@ export function commitLayoutEffects(
22802310
root: FiberRoot,
22812311
committedLanes: Lanes,
22822312
): void {
2313+
inProgressLanes = committedLanes;
2314+
inProgressRoot = root;
22832315
nextEffect = finishedWork;
2316+
22842317
commitLayoutEffects_begin(finishedWork, root, committedLanes);
2318+
2319+
inProgressLanes = null;
2320+
inProgressRoot = null;
22852321
}
22862322

22872323
function commitLayoutEffects_begin(

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
enableStrictEffects,
3838
deletedTreeCleanUpLevel,
3939
enableSuspenseLayoutEffectSemantics,
40+
enableUpdaterTracking,
4041
} from 'shared/ReactFeatureFlags';
4142
import {
4243
FunctionComponent,
@@ -89,7 +90,7 @@ import {
8990
resetCurrentFiber as resetCurrentDebugFiberInDEV,
9091
setCurrentFiber as setCurrentDebugFiberInDEV,
9192
} from './ReactCurrentFiber';
92-
93+
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
9394
import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
9495
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
9596
import {
@@ -137,6 +138,7 @@ import {
137138
resolveRetryWakeable,
138139
markCommitTimeOfFallback,
139140
enqueuePendingPassiveProfilerEffect,
141+
restorePendingUpdaters,
140142
} from './ReactFiberWorkLoop.old';
141143
import {
142144
NoFlags as NoHookEffect,
@@ -162,6 +164,10 @@ const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
162164

163165
let nextEffect: Fiber | null = null;
164166

167+
// Used for Profiling builds to track updaters.
168+
let inProgressLanes: Lanes | null = null;
169+
let inProgressRoot: FiberRoot | null = null;
170+
165171
const callComponentWillUnmountWithTimer = function(current, instance) {
166172
instance.props = current.memoizedProps;
167173
instance.state = current.memoizedState;
@@ -2094,6 +2100,20 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
20942100
}
20952101
}
20962102
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+
20972117
wakeable.then(retry, retry);
20982118
}
20992119
});
@@ -2124,9 +2144,19 @@ function commitResetTextContent(current: Fiber) {
21242144
resetTextContent(current.stateNode);
21252145
}
21262146

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;
21282154
nextEffect = firstChild;
2155+
21292156
commitMutationEffects_begin(root);
2157+
2158+
inProgressLanes = null;
2159+
inProgressRoot = null;
21302160
}
21312161

21322162
function commitMutationEffects_begin(root: FiberRoot) {
@@ -2280,8 +2310,14 @@ export function commitLayoutEffects(
22802310
root: FiberRoot,
22812311
committedLanes: Lanes,
22822312
): void {
2313+
inProgressLanes = committedLanes;
2314+
inProgressRoot = root;
22832315
nextEffect = finishedWork;
2316+
22842317
commitLayoutEffects_begin(finishedWork, root, committedLanes);
2318+
2319+
inProgressLanes = null;
2320+
inProgressRoot = null;
22852321
}
22862322

22872323
function commitLayoutEffects_begin(

packages/react-reconciler/src/ReactFiberLane.new.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ export type Lanes = number;
3535
export type Lane = number;
3636
export type LaneMap<T> = Array<T>;
3737

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';
3944

4045
// Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler.
4146
// If those values are changed that package should be rebuilt and redeployed.
@@ -742,6 +747,57 @@ export function getBumpedLaneForHydration(
742747
return lane;
743748
}
744749

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+
745801
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
746802

747803
// Count leading zeros. Only used on lanes, so assume input is an integer.

packages/react-reconciler/src/ReactFiberLane.old.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ export type Lanes = number;
3535
export type Lane = number;
3636
export type LaneMap<T> = Array<T>;
3737

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';
3944

4045
// Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler.
4146
// If those values are changed that package should be rebuilt and redeployed.
@@ -742,6 +747,57 @@ export function getBumpedLaneForHydration(
742747
return lane;
743748
}
744749

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+
745801
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
746802

747803
// Count leading zeros. Only used on lanes, so assume input is an integer.

packages/react-reconciler/src/ReactFiberRoot.new.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
NoLane,
1717
NoLanes,
1818
NoTimestamp,
19+
TotalLanes,
1920
createLaneMap,
2021
} from './ReactFiberLane.new';
2122
import {
@@ -24,6 +25,7 @@ import {
2425
enableCache,
2526
enableProfilerCommitHooks,
2627
enableProfilerTimer,
28+
enableUpdaterTracking,
2729
} from 'shared/ReactFeatureFlags';
2830
import {unstable_getThreadID} from 'scheduler/tracing';
2931
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
@@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) {
7779
this.passiveEffectDuration = 0;
7880
}
7981

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+
8090
if (__DEV__) {
8191
switch (tag) {
8292
case ConcurrentRoot:

packages/react-reconciler/src/ReactFiberRoot.old.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
NoLane,
1717
NoLanes,
1818
NoTimestamp,
19+
TotalLanes,
1920
createLaneMap,
2021
} from './ReactFiberLane.old';
2122
import {
@@ -24,6 +25,7 @@ import {
2425
enableCache,
2526
enableProfilerCommitHooks,
2627
enableProfilerTimer,
28+
enableUpdaterTracking,
2729
} from 'shared/ReactFeatureFlags';
2830
import {unstable_getThreadID} from 'scheduler/tracing';
2931
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
@@ -77,6 +79,14 @@ function FiberRootNode(containerInfo, tag, hydrate) {
7779
this.passiveEffectDuration = 0;
7880
}
7981

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+
8090
if (__DEV__) {
8191
switch (tag) {
8292
case ConcurrentRoot:

0 commit comments

Comments
 (0)