Skip to content

Commit 25bfa28

Browse files
bgirardacdlite
andauthored
[Experiment] Add feature flag for more aggressive memory clean-up of deleted fiber trees (#21039)
* Add feature flag: enableStrongMemoryCleanup Add a feature flag that will test doing a recursive clean of an unmount node. This will disconnect the fiber graph making leaks less severe. * Detach sibling pointers in old child list When a fiber is deleted, it's still part of the previous (alternate) parent fiber's list of children. Because children are a linked list, an earlier sibling that's still alive will be connected to the deleted fiber via its alternate: live fiber --alternate--> previous live fiber --sibling--> deleted fiber We can't disconnect `alternate` on nodes that haven't been deleted yet, but we can disconnect the `sibling` and `child` pointers. Will use this feature flag to test the memory impact. * Combine into single enum flag I combined `enableStrongMemoryCleanup` and `enableDetachOldChildList` into a single enum flag. The flag has three possible values. Each level is a superset of the previous one and performs more aggressive clean up. We will use this to compare the memory impact of each level. * Add Flow type to new host config method * Re-use existing recursive clean up path We already have a recursive loop that visits every deleted fiber. We can re-use that one for clean up instead of adding another one. Co-authored-by: Andrew Clark <[email protected]>
1 parent 7c4e6aa commit 25bfa28

20 files changed

+304
-62
lines changed

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,7 @@ export function afterActiveInstanceBlur() {
471471
export function preparePortalMount(portalInstance: any): void {
472472
// noop
473473
}
474+
475+
export function detachDeletedInstance(node: Instance): void {
476+
// noop
477+
}

packages/react-dom/src/client/ReactDOMComponentTree.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey;
4343
const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
4444
const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
4545

46+
export function detachDeletedInstance(node: Instance): void {
47+
// TODO: This function is only called on host components. I don't think all of
48+
// these fields are relevant.
49+
delete (node: any)[internalInstanceKey];
50+
delete (node: any)[internalPropsKey];
51+
delete (node: any)[internalEventHandlersKey];
52+
delete (node: any)[internalEventHandlerListenersKey];
53+
delete (node: any)[internalEventHandlesSetKey];
54+
}
55+
4656
export function precacheFiberNode(
4757
hostInst: Fiber,
4858
node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
getInstanceFromNode as getInstanceFromNodeDOMTree,
2626
isContainerMarkedAsRoot,
2727
} from './ReactDOMComponentTree';
28+
export {detachDeletedInstance} from './ReactDOMComponentTree';
2829
import {hasRole} from './DOMAccessibilityRoles';
2930
import {
3031
createElement,

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,3 +481,7 @@ export function afterActiveInstanceBlur() {
481481
export function preparePortalMount(portalInstance: Instance): void {
482482
// noop
483483
}
484+
485+
export function detachDeletedInstance(node: Instance): void {
486+
// noop
487+
}

packages/react-native-renderer/src/ReactNativeHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,7 @@ export function afterActiveInstanceBlur() {
538538
export function preparePortalMount(portalInstance: Instance): void {
539539
// noop
540540
}
541+
542+
export function detachDeletedInstance(node: Instance): void {
543+
// noop
544+
}

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
420420
getInstanceFromScope() {
421421
throw new Error('Not yet implemented.');
422422
},
423+
424+
detachDeletedInstance() {},
423425
};
424426

425427
const hostConfig = useMutation

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

Lines changed: 126 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
enableSuspenseCallback,
3636
enableScopeAPI,
3737
enableStrictEffects,
38+
deletedTreeCleanUpLevel,
3839
} from 'shared/ReactFeatureFlags';
3940
import {
4041
FunctionComponent,
@@ -60,6 +61,7 @@ import {
6061
hasCaughtError,
6162
clearCaughtError,
6263
} from 'shared/ReactErrorUtils';
64+
import {detachDeletedInstance} from './ReactFiberHostConfig';
6365
import {
6466
NoFlags,
6567
ContentReset,
@@ -1219,25 +1221,84 @@ function detachFiberMutation(fiber: Fiber) {
12191221
// Don't reset the alternate yet, either. We need that so we can detach the
12201222
// alternate's fields in the passive phase. Clearing the return pointer is
12211223
// sufficient for findDOMNode semantics.
1224+
const alternate = fiber.alternate;
1225+
if (alternate !== null) {
1226+
alternate.return = null;
1227+
}
12221228
fiber.return = null;
12231229
}
12241230

1225-
export function detachFiberAfterEffects(fiber: Fiber): void {
1226-
// Null out fields to improve GC for references that may be lingering (e.g. DevTools).
1227-
// Note that we already cleared the return pointer in detachFiberMutation().
1228-
fiber.alternate = null;
1229-
fiber.child = null;
1230-
fiber.deletions = null;
1231-
fiber.dependencies = null;
1232-
fiber.memoizedProps = null;
1233-
fiber.memoizedState = null;
1234-
fiber.pendingProps = null;
1235-
fiber.sibling = null;
1236-
fiber.stateNode = null;
1237-
fiber.updateQueue = null;
1231+
function detachFiberAfterEffects(fiber: Fiber) {
1232+
const alternate = fiber.alternate;
1233+
if (alternate !== null) {
1234+
fiber.alternate = null;
1235+
detachFiberAfterEffects(alternate);
1236+
}
1237+
1238+
// Note: Defensively using negation instead of < in case
1239+
// `deletedTreeCleanUpLevel` is undefined.
1240+
if (!(deletedTreeCleanUpLevel >= 2)) {
1241+
// This is the default branch (level 0).
1242+
fiber.child = null;
1243+
fiber.deletions = null;
1244+
fiber.dependencies = null;
1245+
fiber.memoizedProps = null;
1246+
fiber.memoizedState = null;
1247+
fiber.pendingProps = null;
1248+
fiber.sibling = null;
1249+
fiber.stateNode = null;
1250+
fiber.updateQueue = null;
12381251

1239-
if (__DEV__) {
1240-
fiber._debugOwner = null;
1252+
if (__DEV__) {
1253+
fiber._debugOwner = null;
1254+
}
1255+
} else {
1256+
// Clear cyclical Fiber fields. This level alone is designed to roughly
1257+
// approximate the planned Fiber refactor. In that world, `setState` will be
1258+
// bound to a special "instance" object instead of a Fiber. The Instance
1259+
// object will not have any of these fields. It will only be connected to
1260+
// the fiber tree via a single link at the root. So if this level alone is
1261+
// sufficient to fix memory issues, that bodes well for our plans.
1262+
fiber.child = null;
1263+
fiber.deletions = null;
1264+
fiber.sibling = null;
1265+
1266+
// I'm intentionally not clearing the `return` field in this level. We
1267+
// already disconnect the `return` pointer at the root of the deleted
1268+
// subtree (in `detachFiberMutation`). Besides, `return` by itself is not
1269+
// cyclical — it's only cyclical when combined with `child`, `sibling`, and
1270+
// `alternate`. But we'll clear it in the next level anyway, just in case.
1271+
1272+
if (__DEV__) {
1273+
fiber._debugOwner = null;
1274+
}
1275+
1276+
if (deletedTreeCleanUpLevel >= 3) {
1277+
// Theoretically, nothing in here should be necessary, because we already
1278+
// disconnected the fiber from the tree. So even if something leaks this
1279+
// particular fiber, it won't leak anything else
1280+
//
1281+
// The purpose of this branch is to be super aggressive so we can measure
1282+
// if there's any difference in memory impact. If there is, that could
1283+
// indicate a React leak we don't know about.
1284+
1285+
// For host components, disconnect host instance -> fiber pointer.
1286+
if (fiber.tag === HostComponent) {
1287+
const hostInstance: Instance = fiber.stateNode;
1288+
if (hostInstance !== null) {
1289+
detachDeletedInstance(hostInstance);
1290+
}
1291+
}
1292+
1293+
fiber.return = null;
1294+
fiber.dependencies = null;
1295+
fiber.memoizedProps = null;
1296+
fiber.memoizedState = null;
1297+
fiber.pendingProps = null;
1298+
fiber.stateNode = null;
1299+
// TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead.
1300+
fiber.updateQueue = null;
1301+
}
12411302
}
12421303
}
12431304

@@ -1629,11 +1690,8 @@ function commitDeletion(
16291690
renderPriorityLevel,
16301691
);
16311692
}
1632-
const alternate = current.alternate;
1693+
16331694
detachFiberMutation(current);
1634-
if (alternate !== null) {
1635-
detachFiberMutation(alternate);
1636-
}
16371695
}
16381696

16391697
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
@@ -2308,14 +2366,34 @@ function commitPassiveUnmountEffects_begin() {
23082366
fiberToDelete,
23092367
fiber,
23102368
);
2369+
}
23112370

2312-
// Now that passive effects have been processed, it's safe to detach lingering pointers.
2313-
const alternate = fiberToDelete.alternate;
2314-
detachFiberAfterEffects(fiberToDelete);
2315-
if (alternate !== null) {
2316-
detachFiberAfterEffects(alternate);
2371+
if (deletedTreeCleanUpLevel >= 1) {
2372+
// A fiber was deleted from this parent fiber, but it's still part of
2373+
// the previous (alternate) parent fiber's list of children. Because
2374+
// children are a linked list, an earlier sibling that's still alive
2375+
// will be connected to the deleted fiber via its `alternate`:
2376+
//
2377+
// live fiber
2378+
// --alternate--> previous live fiber
2379+
// --sibling--> deleted fiber
2380+
//
2381+
// We can't disconnect `alternate` on nodes that haven't been deleted
2382+
// yet, but we can disconnect the `sibling` and `child` pointers.
2383+
const previousFiber = fiber.alternate;
2384+
if (previousFiber !== null) {
2385+
let detachedChild = previousFiber.child;
2386+
if (detachedChild !== null) {
2387+
previousFiber.child = null;
2388+
do {
2389+
const detachedSibling = detachedChild.sibling;
2390+
detachedChild.sibling = null;
2391+
detachedChild = detachedSibling;
2392+
} while (detachedChild !== null);
2393+
}
23172394
}
23182395
}
2396+
23192397
nextEffect = fiber;
23202398
}
23212399
}
@@ -2392,7 +2470,8 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
23922470
resetCurrentDebugFiberInDEV();
23932471

23942472
const child = fiber.child;
2395-
// TODO: Only traverse subtree if it has a PassiveStatic flag
2473+
// TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we
2474+
// do this, still need to handle `deletedTreeCleanUpLevel` correctly.)
23962475
if (child !== null) {
23972476
ensureCorrectReturnPointer(child, fiber);
23982477
nextEffect = child;
@@ -2409,19 +2488,35 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
24092488
) {
24102489
while (nextEffect !== null) {
24112490
const fiber = nextEffect;
2412-
if (fiber === deletedSubtreeRoot) {
2413-
nextEffect = null;
2414-
return;
2491+
const sibling = fiber.sibling;
2492+
const returnFiber = fiber.return;
2493+
2494+
if (deletedTreeCleanUpLevel >= 2) {
2495+
// Recursively traverse the entire deleted tree and clean up fiber fields.
2496+
// This is more aggressive than ideal, and the long term goal is to only
2497+
// have to detach the deleted tree at the root.
2498+
detachFiberAfterEffects(fiber);
2499+
if (fiber === deletedSubtreeRoot) {
2500+
nextEffect = null;
2501+
return;
2502+
}
2503+
} else {
2504+
// This is the default branch (level 0). We do not recursively clear all
2505+
// the fiber fields. Only the root of the deleted subtree.
2506+
if (fiber === deletedSubtreeRoot) {
2507+
detachFiberAfterEffects(fiber);
2508+
nextEffect = null;
2509+
return;
2510+
}
24152511
}
24162512

2417-
const sibling = fiber.sibling;
24182513
if (sibling !== null) {
2419-
ensureCorrectReturnPointer(sibling, fiber.return);
2514+
ensureCorrectReturnPointer(sibling, returnFiber);
24202515
nextEffect = sibling;
24212516
return;
24222517
}
24232518

2424-
nextEffect = fiber.return;
2519+
nextEffect = returnFiber;
24252520
}
24262521
}
24272522

0 commit comments

Comments
 (0)