@samitouri / QOS-React-2 / commits / 25bfa287f6

[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 <git@andrewclark.io>

Benoit Girard committed Mar 22, 2021 at 21:54 UTC 25bfa287f6c49950d8ea16f542716835683442d6
20 files changed +304 -62
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -471,3 +471,7 @@ export function afterActiveInstanceBlur() {
471 export function preparePortalMount(portalInstance: any): void {
472 // noop
473 }
474 +
475 +export function detachDeletedInstance(node: Instance): void {
476 + // noop
477 +}
packages/react-dom/src/client/ReactDOMComponentTree.js
+10
@@ -43,6 +43,16 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey;
43 const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
44 const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
45
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 +
56 export function precacheFiberNode(
57 hostInst: Fiber,
58 node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
packages/react-dom/src/client/ReactDOMHostConfig.js
+1
@@ -25,6 +25,7 @@ import {
25 getInstanceFromNode as getInstanceFromNodeDOMTree,
26 isContainerMarkedAsRoot,
27 } from './ReactDOMComponentTree';
28 +export {detachDeletedInstance} from './ReactDOMComponentTree';
29 import {hasRole} from './DOMAccessibilityRoles';
30 import {
31 createElement,
packages/react-native-renderer/src/ReactFabricHostConfig.js
+4
@@ -481,3 +481,7 @@ export function afterActiveInstanceBlur() {
481 export function preparePortalMount(portalInstance: Instance): void {
482 // noop
483 }
484 +
485 +export function detachDeletedInstance(node: Instance): void {
486 + // noop
487 +}
packages/react-native-renderer/src/ReactNativeHostConfig.js
+4
@@ -538,3 +538,7 @@ export function afterActiveInstanceBlur() {
538 export function preparePortalMount(portalInstance: Instance): void {
539 // noop
540 }
541 +
542 +export function detachDeletedInstance(node: Instance): void {
543 + // noop
544 +}
packages/react-noop-renderer/src/createReactNoop.js
+2
@@ -420,6 +420,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
420 getInstanceFromScope() {
421 throw new Error('Not yet implemented.');
422 },
423 +
424 + detachDeletedInstance() {},
425 };
426
427 const hostConfig = useMutation
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+126 -31
@@ -35,6 +35,7 @@ import {
35 enableSuspenseCallback,
36 enableScopeAPI,
37 enableStrictEffects,
38 + deletedTreeCleanUpLevel,
39 } from 'shared/ReactFeatureFlags';
40 import {
41 FunctionComponent,
@@ -60,6 +61,7 @@ import {
61 hasCaughtError,
62 clearCaughtError,
63 } from 'shared/ReactErrorUtils';
64 +import {detachDeletedInstance} from './ReactFiberHostConfig';
65 import {
66 NoFlags,
67 ContentReset,
@@ -1219,25 +1221,84 @@ function detachFiberMutation(fiber: Fiber) {
1221 // Don't reset the alternate yet, either. We need that so we can detach the
1222 // alternate's fields in the passive phase. Clearing the return pointer is
1223 // sufficient for findDOMNode semantics.
1224 + const alternate = fiber.alternate;
1225 + if (alternate !== null) {
1226 + alternate.return = null;
1227 + }
1228 fiber.return = null;
1229 }
1230
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;
1251
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 + }
1302 }
1303 }
1304
@@ -1629,11 +1690,8 @@ function commitDeletion(
1690 renderPriorityLevel,
1691 );
1692 }
1632 - const alternate = current.alternate;
1693 +
1694 detachFiberMutation(current);
1634 - if (alternate !== null) {
1635 - detachFiberMutation(alternate);
1636 - }
1695 }
1696
1697 function commitWork(current: Fiber | null, finishedWork: Fiber): void {
@@ -2308,14 +2366,34 @@ function commitPassiveUnmountEffects_begin() {
2366 fiberToDelete,
2367 fiber,
2368 );
2369 + }
2370
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 + }
2394 }
2395 }
2396 +
2397 nextEffect = fiber;
2398 }
2399 }
@@ -2392,7 +2470,8 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2470 resetCurrentDebugFiberInDEV();
2471
2472 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.)
2475 if (child !== null) {
2476 ensureCorrectReturnPointer(child, fiber);
2477 nextEffect = child;
@@ -2409,19 +2488,35 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
2488 ) {
2489 while (nextEffect !== null) {
2490 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 + }
2511 }
2512
2417 - const sibling = fiber.sibling;
2513 if (sibling !== null) {
2419 - ensureCorrectReturnPointer(sibling, fiber.return);
2514 + ensureCorrectReturnPointer(sibling, returnFiber);
2515 nextEffect = sibling;
2516 return;
2517 }
2518
2424 - nextEffect = fiber.return;
2519 + nextEffect = returnFiber;
2520 }
2521 }
2522
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+126 -31
@@ -35,6 +35,7 @@ import {
35 enableSuspenseCallback,
36 enableScopeAPI,
37 enableStrictEffects,
38 + deletedTreeCleanUpLevel,
39 } from 'shared/ReactFeatureFlags';
40 import {
41 FunctionComponent,
@@ -60,6 +61,7 @@ import {
61 hasCaughtError,
62 clearCaughtError,
63 } from 'shared/ReactErrorUtils';
64 +import {detachDeletedInstance} from './ReactFiberHostConfig';
65 import {
66 NoFlags,
67 ContentReset,
@@ -1219,25 +1221,84 @@ function detachFiberMutation(fiber: Fiber) {
1221 // Don't reset the alternate yet, either. We need that so we can detach the
1222 // alternate's fields in the passive phase. Clearing the return pointer is
1223 // sufficient for findDOMNode semantics.
1224 + const alternate = fiber.alternate;
1225 + if (alternate !== null) {
1226 + alternate.return = null;
1227 + }
1228 fiber.return = null;
1229 }
1230
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;
1251
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 + }
1302 }
1303 }
1304
@@ -1629,11 +1690,8 @@ function commitDeletion(
1690 renderPriorityLevel,
1691 );
1692 }
1632 - const alternate = current.alternate;
1693 +
1694 detachFiberMutation(current);
1634 - if (alternate !== null) {
1635 - detachFiberMutation(alternate);
1636 - }
1695 }
1696
1697 function commitWork(current: Fiber | null, finishedWork: Fiber): void {
@@ -2308,14 +2366,34 @@ function commitPassiveUnmountEffects_begin() {
2366 fiberToDelete,
2367 fiber,
2368 );
2369 + }
2370
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 + }
2394 }
2395 }
2396 +
2397 nextEffect = fiber;
2398 }
2399 }
@@ -2392,7 +2470,8 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2470 resetCurrentDebugFiberInDEV();
2471
2472 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.)
2475 if (child !== null) {
2476 ensureCorrectReturnPointer(child, fiber);
2477 nextEffect = child;
@@ -2409,19 +2488,35 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
2488 ) {
2489 while (nextEffect !== null) {
2490 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 + }
2511 }
2512
2417 - const sibling = fiber.sibling;
2513 if (sibling !== null) {
2419 - ensureCorrectReturnPointer(sibling, fiber.return);
2514 + ensureCorrectReturnPointer(sibling, returnFiber);
2515 nextEffect = sibling;
2516 return;
2517 }
2518
2424 - nextEffect = fiber.return;
2519 + nextEffect = returnFiber;
2520 }
2521 }
2522
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -73,6 +73,7 @@ export const preparePortalMount = $$$hostConfig.preparePortalMount;
73 export const prepareScopeUpdate = $$$hostConfig.preparePortalMount;
74 export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope;
75 export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority;
76 +export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance;
77
78 // -------------------
79 // Microtasks
packages/react-test-renderer/src/ReactTestHostConfig.js
+4
@@ -360,3 +360,7 @@ export function prepareScopeUpdate(scopeInstance: Object, inst: Object): void {
360 export function getInstanceFromScope(scopeInstance: Object): null | Object {
361 return nodeToInstanceMap.get(scopeInstance) || null;
362 }
363 +
364 +export function detachDeletedInstance(node: Instance): void {
365 + // noop
366 +}
packages/shared/ReactFeatureFlags.js
+13
@@ -112,6 +112,19 @@ export const disableNativeComponentFrames = false;
112 // If there are no still-mounted boundaries, the errors should be rethrown.
113 export const skipUnmountedBoundaries = false;
114
115 +// When a node is unmounted, recurse into the Fiber subtree and clean out
116 +// references. Each level cleans up more fiber fields than the previous level.
117 +// As far as we know, React itself doesn't leak, but because the Fiber contains
118 +// cycles, even a single leak in product code can cause us to retain large
119 +// amounts of memory.
120 +//
121 +// The long term plan is to remove the cycles, but in the meantime, we clear
122 +// additional fields to mitigate.
123 +//
124 +// It's an enum so that we can experiment with different levels of
125 +// aggressiveness.
126 +export const deletedTreeCleanUpLevel = 1;
127 +
128 // --------------------------
129 // Future APIs to be deprecated
130 // --------------------------
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -46,6 +46,7 @@ export const enableLegacyFBSupport = false;
46 export const enableFilterEmptyStringAttributesDOM = false;
47 export const disableNativeComponentFrames = false;
48 export const skipUnmountedBoundaries = false;
49 +export const deletedTreeCleanUpLevel = 1;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
45 export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 +export const deletedTreeCleanUpLevel = 1;
49
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
45 export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 +export const deletedTreeCleanUpLevel = 1;
49
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
45 export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 +export const deletedTreeCleanUpLevel = 1;
49
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
45 export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 +export const deletedTreeCleanUpLevel = 1;
49
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
45 export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 +export const deletedTreeCleanUpLevel = 1;
49
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = !__EXPERIMENTAL__;
45 export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = true;
48 +export const deletedTreeCleanUpLevel = 1;
49
50 export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -52,6 +52,7 @@ export const disableNativeComponentFrames = false;
52 export const createRootStrictEffectsByDefault = false;
53 export const enableStrictEffects = false;
54 export const enableUseRefAccessWarning = __VARIANT__;
55 +export const deletedTreeCleanUpLevel = __VARIANT__ ? 3 : 1;
56
57 export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
58 export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -32,6 +32,7 @@ export const {
32 disableNativeComponentFrames,
33 disableSchedulerTimeoutInWorkLoop,
34 enableLazyContextPropagation,
35 + deletedTreeCleanUpLevel,
36 } = dynamicFeatureFlags;
37
38 // On WWW, __EXPERIMENTAL__ is used for a new modern build.