Skip to content

Commit

Permalink
Combine into single enum flag
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Mar 23, 2021
1 parent a16ac10 commit 100239e
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 103 deletions.
127 changes: 86 additions & 41 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ import {
enableSuspenseCallback,
enableScopeAPI,
enableStrictEffects,
enableStrongMemoryCleanup,
enableDetachOldChildList,
deletedTreeCleanUpLevel,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -1212,40 +1211,93 @@ function detachFiberMutation(fiber: Fiber) {
// Don't reset the alternate yet, either. We need that so we can detach the
// alternate's fields in the passive phase. Clearing the return pointer is
// sufficient for findDOMNode semantics.
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.return = null;
}
fiber.return = null;
}

export function detachFiberAfterEffects(
fiber: Fiber,
recurseIntoSibbling: ?boolean,
): void {
// Null out fields to improve GC for references that may be lingering (e.g. DevTools).
// Note that we already cleared the return pointer in detachFiberMutation().
if (enableStrongMemoryCleanup) {
if (fiber.child) {
detachFiberAfterEffects(fiber.child, true);
}
if (fiber.sibling && recurseIntoSibbling === true) {
detachFiberAfterEffects(fiber.sibling, true);
}
if (fiber.stateNode) {
unmountNode(fiber.stateNode);
}
fiber.return = null;
}
fiber.alternate = null;
fiber.child = null;
fiber.deletions = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;
function detachFiberAfterEffects(fiber: Fiber) {
const alternate = fiber.alternate;
if (alternate !== null) {
fiber.alternate = null;
detachFiberAfterEffects(alternate);
}
// Note: Defensively using negation instead of < in case
// `deletedTreeCleanUpLevel` is undefined.
if (!(deletedTreeCleanUpLevel >= 2)) {
// This is the default branch (level 0). We do not recursively clear all the
// fiber fields. Only the root of the deleted subtree.
fiber.child = null;
fiber.deletions = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;

if (__DEV__) {
fiber._debugOwner = null;
if (__DEV__) {
fiber._debugOwner = null;
}
} else {
// Recursively traverse the entire deleted tree and clean up fiber fields.
// This is more aggressive than ideal, and the long term goal is to only
// have to detach the deleted tree at the root.
let child = fiber.child;
while (child !== null) {
detachFiberAfterEffects(child);
child = child.sibling;
}
// Clear cyclical Fiber fields. This level alone is designed to roughly
// approximate the planned Fiber refactor. In that world, `setState` will be
// bound to a special "instance" object instead of a Fiber. The Instance
// object will not have any of these fields. It will only be connected to
// the fiber tree via a single link at the root. So if this level alone is
// sufficient to fix memory issues, that bodes well for our plans.
fiber.child = null;
fiber.deletions = null;
fiber.sibling = null;

// I'm intentionally not clearing the `return` field in this level. We
// already disconnect the `return` pointer at the root of the deleted
// subtree (in `detachFiberMutation`). Besides, `return` by itself is not
// cyclical — it's only cyclical when combined with `child`, `sibling`, and
// `alternate`. But we'll clear it in the next level anyway, just in case.

if (__DEV__) {
fiber._debugOwner = null;
}

if (deletedTreeCleanUpLevel >= 3) {
// Theoretically, nothing in here should be necessary, because we already
// disconnected the fiber from the tree. So even if something leaks this
// particular fiber, it won't leak anything else
//
// The purpose of this branch is to be super aggressive so we can measure
// if there's any difference in memory impact. If there is, that could
// indicate a React leak we don't know about.

// For host components, disconnect host instance -> fiber pointer.
if (fiber.tag === HostComponent) {
const hostInstance: Instance = fiber.stateNode;
if (hostInstance !== null) {
unmountNode(hostInstance);
}
}

fiber.return = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.stateNode = null;
// TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead.
fiber.updateQueue = null;
}
}
}

Expand Down Expand Up @@ -1637,11 +1689,8 @@ function commitDeletion(
renderPriorityLevel,
);
}
const alternate = current.alternate;

detachFiberMutation(current);
if (alternate !== null) {
detachFiberMutation(alternate);
}
}

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
Expand Down Expand Up @@ -2318,14 +2367,10 @@ function commitPassiveUnmountEffects_begin() {
);

// Now that passive effects have been processed, it's safe to detach lingering pointers.
const alternate = fiberToDelete.alternate;
detachFiberAfterEffects(fiberToDelete);
if (alternate !== null) {
detachFiberAfterEffects(alternate);
}
}

if (enableDetachOldChildList) {
if (deletedTreeCleanUpLevel >= 1) {
// A fiber was deleted from this parent fiber, but 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
Expand Down
127 changes: 86 additions & 41 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ import {
enableSuspenseCallback,
enableScopeAPI,
enableStrictEffects,
enableStrongMemoryCleanup,
enableDetachOldChildList,
deletedTreeCleanUpLevel,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -1212,40 +1211,93 @@ function detachFiberMutation(fiber: Fiber) {
// Don't reset the alternate yet, either. We need that so we can detach the
// alternate's fields in the passive phase. Clearing the return pointer is
// sufficient for findDOMNode semantics.
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.return = null;
}
fiber.return = null;
}

export function detachFiberAfterEffects(
fiber: Fiber,
recurseIntoSibbling: ?boolean,
): void {
// Null out fields to improve GC for references that may be lingering (e.g. DevTools).
// Note that we already cleared the return pointer in detachFiberMutation().
if (enableStrongMemoryCleanup) {
if (fiber.child) {
detachFiberAfterEffects(fiber.child, true);
}
if (fiber.sibling && recurseIntoSibbling === true) {
detachFiberAfterEffects(fiber.sibling, true);
}
if (fiber.stateNode) {
unmountNode(fiber.stateNode);
}
fiber.return = null;
}
fiber.alternate = null;
fiber.child = null;
fiber.deletions = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;
function detachFiberAfterEffects(fiber: Fiber) {
const alternate = fiber.alternate;
if (alternate !== null) {
fiber.alternate = null;
detachFiberAfterEffects(alternate);
}
// Note: Defensively using negation instead of < in case
// `deletedTreeCleanUpLevel` is undefined.
if (!(deletedTreeCleanUpLevel >= 2)) {
// This is the default branch (level 0). We do not recursively clear all the
// fiber fields. Only the root of the deleted subtree.
fiber.child = null;
fiber.deletions = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.sibling = null;
fiber.stateNode = null;
fiber.updateQueue = null;

if (__DEV__) {
fiber._debugOwner = null;
if (__DEV__) {
fiber._debugOwner = null;
}
} else {
// Recursively traverse the entire deleted tree and clean up fiber fields.
// This is more aggressive than ideal, and the long term goal is to only
// have to detach the deleted tree at the root.
let child = fiber.child;
while (child !== null) {
detachFiberAfterEffects(child);
child = child.sibling;
}
// Clear cyclical Fiber fields. This level alone is designed to roughly
// approximate the planned Fiber refactor. In that world, `setState` will be
// bound to a special "instance" object instead of a Fiber. The Instance
// object will not have any of these fields. It will only be connected to
// the fiber tree via a single link at the root. So if this level alone is
// sufficient to fix memory issues, that bodes well for our plans.
fiber.child = null;
fiber.deletions = null;
fiber.sibling = null;

// I'm intentionally not clearing the `return` field in this level. We
// already disconnect the `return` pointer at the root of the deleted
// subtree (in `detachFiberMutation`). Besides, `return` by itself is not
// cyclical — it's only cyclical when combined with `child`, `sibling`, and
// `alternate`. But we'll clear it in the next level anyway, just in case.

if (__DEV__) {
fiber._debugOwner = null;
}

if (deletedTreeCleanUpLevel >= 3) {
// Theoretically, nothing in here should be necessary, because we already
// disconnected the fiber from the tree. So even if something leaks this
// particular fiber, it won't leak anything else
//
// The purpose of this branch is to be super aggressive so we can measure
// if there's any difference in memory impact. If there is, that could
// indicate a React leak we don't know about.

// For host components, disconnect host instance -> fiber pointer.
if (fiber.tag === HostComponent) {
const hostInstance: Instance = fiber.stateNode;
if (hostInstance !== null) {
unmountNode(hostInstance);
}
}

fiber.return = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.stateNode = null;
// TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead.
fiber.updateQueue = null;
}
}
}

Expand Down Expand Up @@ -1637,11 +1689,8 @@ function commitDeletion(
renderPriorityLevel,
);
}
const alternate = current.alternate;

detachFiberMutation(current);
if (alternate !== null) {
detachFiberMutation(alternate);
}
}

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
Expand Down Expand Up @@ -2318,14 +2367,10 @@ function commitPassiveUnmountEffects_begin() {
);

// Now that passive effects have been processed, it's safe to detach lingering pointers.
const alternate = fiberToDelete.alternate;
detachFiberAfterEffects(fiberToDelete);
if (alternate !== null) {
detachFiberAfterEffects(alternate);
}
}

if (enableDetachOldChildList) {
if (deletedTreeCleanUpLevel >= 1) {
// A fiber was deleted from this parent fiber, but 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
Expand Down
15 changes: 12 additions & 3 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,18 @@ export const disableNativeComponentFrames = false;
// If there are no still-mounted boundaries, the errors should be rethrown.
export const skipUnmountedBoundaries = false;

// When a node is unmounted, recurse into the Fiber subtree and clean out references.
export const enableStrongMemoryCleanup = false;
export const enableDetachOldChildList = false;
// When a node is unmounted, recurse into the Fiber subtree and clean out
// references. Each level cleans up more fiber fields than the previous level.
// As far as we know, React itself doesn't leak, but because the Fiber contains
// cycles, even a single leak in product code can cause us to retain large
// amounts of memory.
//
// The long term plan is to remove the cycles, but in the meantime, we clear
// additional fields to mitigate.
//
// It's an enum so that we can experiment with different levels of
// aggressiveness.
export const deletedTreeCleanUpLevel = 1;

// --------------------------
// Future APIs to be deprecated
Expand Down
3 changes: 1 addition & 2 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
export const disableNativeComponentFrames = false;
export const skipUnmountedBoundaries = false;
export const enableStrongMemoryCleanup = false;
export const enableDetachOldChildList = false;
export const deletedTreeCleanUpLevel = 1;

export const enableNewReconciler = false;
export const deferRenderPhaseUpdateToNextBatch = true;
Expand Down
3 changes: 1 addition & 2 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
export const disableNativeComponentFrames = false;
export const skipUnmountedBoundaries = false;
export const enableStrongMemoryCleanup = false;
export const enableDetachOldChildList = false;
export const deletedTreeCleanUpLevel = 1;

export const enableNewReconciler = false;
export const deferRenderPhaseUpdateToNextBatch = true;
Expand Down
3 changes: 1 addition & 2 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
export const disableNativeComponentFrames = false;
export const skipUnmountedBoundaries = false;
export const enableStrongMemoryCleanup = false;
export const enableDetachOldChildList = false;
export const deletedTreeCleanUpLevel = 1;

export const enableNewReconciler = false;
export const deferRenderPhaseUpdateToNextBatch = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
export const disableNativeComponentFrames = false;
export const skipUnmountedBoundaries = false;
export const enableStrongMemoryCleanup = false;
export const enableDetachOldChildList = false;
export const deletedTreeCleanUpLevel = 1;

export const enableNewReconciler = false;
export const deferRenderPhaseUpdateToNextBatch = true;
Expand Down
3 changes: 1 addition & 2 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
export const disableNativeComponentFrames = false;
export const skipUnmountedBoundaries = false;
export const enableStrongMemoryCleanup = false;
export const enableDetachOldChildList = false;
export const deletedTreeCleanUpLevel = 1;

export const enableNewReconciler = false;
export const deferRenderPhaseUpdateToNextBatch = true;
Expand Down

0 comments on commit 100239e

Please sign in to comment.