Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clear memoizedState on unmount of fiber to avoid memory leak #14218

Merged
merged 3 commits into from
Nov 19, 2018

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Nov 13, 2018

This is an alternative approach to #14153. This PR aims at clearing the memory leak when the associated fibers containing the references to the closures (stored in memoizedState) still exist after being unmounted. The fix is to simply null out the memoizedState field which seems to clear up the retained functions.

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

How about we clear these other fields too?

  • memoizedProps
  • updateQueue
  • firstEffect, lastEffect, nextEffect

And maybe type and elementType? Those don't seem as important though.

@acdlite
Copy link
Collaborator

acdlite commented Nov 13, 2018

Oh and sibling

@sophiebits
Copy link
Collaborator

I still don't understand why this is necessary. Isn't the expectation that all the pointers to the unmounted Fiber are removed? In which case you don't need to null out all its fields.

@acdlite
Copy link
Collaborator

acdlite commented Nov 13, 2018

@sophiebits The parent alternate could still have a reference via its child pointer

@sophiebits
Copy link
Collaborator

Can't we null out .return.alternate.child? If I understand our pooling correctly, we would never end up reusing the parent alternate fiber without re-cloning anyway.

@acdlite
Copy link
Collaborator

acdlite commented Nov 13, 2018

The code comment explains that we don't detach the alternate pointer because we're not sure if it's current but that should be fixable. Excepting resuming.

@acdlite
Copy link
Collaborator

acdlite commented Nov 13, 2018

That's a riskier change, though. Return pointers are confusing. Could be worth it.

@sophiebits
Copy link
Collaborator

I'm all about risky.

@sophiebits
Copy link
Collaborator

sophiebits commented Nov 13, 2018

I guess it's not safe. With this diff

diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index bed780c2b..2fdc77ee0 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -256,6 +256,7 @@ function ChildReconciler(shouldTrackSideEffects) {
     }
     childToDelete.nextEffect = null;
     childToDelete.effectTag = Deletion;
+    childToDelete.return = returnFiber;
   }
 
   function deleteRemainingChildren(
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 55a39ee64..eee0659dc 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -726,6 +726,10 @@ function detachFiber(current: Fiber) {
   // get GC:ed but we don't know which for sure which parent is the current
   // one so we'll settle for GC:ing the subtree of this child. This child
   // itself will be GC:ed when the parent updates the next time.
+  const returnAlternate = current.return.alternate;
+  if (returnAlternate !== null) {
+    returnAlternate.child = null;
+  }
   current.return = null;
   current.child = null;
   if (current.alternate) {
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index fbd3b5b65..4da60e633 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -725,6 +725,7 @@ function completeWork(
             currentFallbackChild.nextEffect = null;
           }
           currentFallbackChild.effectTag = Deletion;
+          currentFallbackChild.return = current.child;
         }
       }
 

all but one of our tests pass. But I realized that some of our traversals – like in findDOMNode – can run in between render and commit on the alternate tree and will mutate the .return pointers. So we can't rely on this to do the right thing.

If we could store the return pointer for a deletion somewhere else where it wouldn't be clobbered then I guess it would work. (Or we could do a variant of findCurrentFiberUsingSlowPath where we iterate the children of .return and .return.alternate, but it's still easy to get n^2 doing that.)

@sizebot
Copy link

sizebot commented Nov 16, 2018

ReactDOM: size: 🔺+0.2%, gzip: 🔺+0.1%

Details of bundled changes.

Comparing: d204747...5d65390

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 716.11 KB 716.49 KB 165.75 KB 165.84 KB UMD_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.1% 97.66 KB 97.85 KB 31.8 KB 31.82 KB UMD_PROD
react-dom.profiling.min.js +0.2% 0.0% 100.56 KB 100.75 KB 32.5 KB 32.51 KB UMD_PROFILING
react-dom.development.js +0.1% +0.1% 711.42 KB 711.8 KB 164.38 KB 164.46 KB NODE_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.1% 97.65 KB 97.84 KB 31.31 KB 31.35 KB NODE_PROD
react-dom.profiling.min.js +0.2% +0.1% 100.65 KB 100.84 KB 31.93 KB 31.96 KB NODE_PROFILING
ReactDOM-dev.js +0.1% 0.0% 732.57 KB 732.95 KB 165.51 KB 165.59 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 🔺+0.1% 309.63 KB 310.04 KB 57.06 KB 57.1 KB FB_WWW_PROD
ReactDOM-profiling.js +0.1% +0.1% 316.53 KB 316.91 KB 58.46 KB 58.51 KB FB_WWW_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% +0.1% 496.63 KB 496.99 KB 109.61 KB 109.69 KB UMD_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.2% 89.83 KB 90.01 KB 27.61 KB 27.65 KB UMD_PROD
react-art.development.js +0.1% +0.1% 428.41 KB 428.79 KB 92.56 KB 92.66 KB NODE_DEV
react-art.production.min.js 🔺+0.3% 🔺+0.2% 54.81 KB 55 KB 16.9 KB 16.94 KB NODE_PROD
ReactART-dev.js +0.1% +0.1% 435.36 KB 435.74 KB 91.45 KB 91.54 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.2% 🔺+0.1% 183.41 KB 183.82 KB 31.35 KB 31.39 KB FB_WWW_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.1% +0.1% 560.55 KB 560.93 KB 122.18 KB 122.25 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.2% 🔺+0.1% 238.84 KB 239.25 KB 42.04 KB 42.08 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.2% +0.1% 244.81 KB 245.18 KB 43.39 KB 43.44 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js +0.1% +0.1% 560.25 KB 560.63 KB 122.08 KB 122.16 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+0.2% 🔺+0.1% 224.28 KB 224.69 KB 38.96 KB 39.01 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.2% +0.1% 230 KB 230.38 KB 40.31 KB 40.36 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.1% +0.1% 550.87 KB 551.25 KB 119.69 KB 119.77 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.2% 🔺+0.1% 218.27 KB 218.66 KB 37.63 KB 37.66 KB RN_FB_PROD
ReactFabric-profiling.js +0.2% +0.1% 223.05 KB 223.4 KB 39.04 KB 39.08 KB RN_FB_PROFILING
ReactFabric-dev.js +0.1% +0.1% 550.9 KB 551.28 KB 119.71 KB 119.79 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.2% 🔺+0.1% 218.31 KB 218.69 KB 37.64 KB 37.68 KB RN_OSS_PROD
ReactFabric-profiling.js +0.2% +0.1% 223.09 KB 223.44 KB 39.05 KB 39.1 KB RN_OSS_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.1% +0.1% 441.07 KB 441.45 KB 95.26 KB 95.35 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.3% 🔺+0.2% 56.03 KB 56.22 KB 17.19 KB 17.22 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 436.29 KB 436.67 KB 94.11 KB 94.2 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.3% 🔺+0.1% 55.7 KB 55.89 KB 17.05 KB 17.07 KB NODE_PROD
ReactTestRenderer-dev.js +0.1% +0.1% 443.42 KB 443.8 KB 93.29 KB 93.37 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.1% 426.25 KB 426.63 KB 91.07 KB 91.17 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.3% 🔺+0.3% 55.94 KB 56.13 KB 16.77 KB 16.82 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% +0.1% 424.7 KB 425.08 KB 90.44 KB 90.53 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.3% 🔺+0.3% 55.95 KB 56.14 KB 16.78 KB 16.83 KB NODE_PROD

Generated by 🚫 dangerJS

@trueadm
Copy link
Contributor Author

trueadm commented Nov 16, 2018

I added a bunch of fields as suggested by @acdlite, minus nextEffect as that causes many tests to fail.

@trueadm trueadm merged commit 9b2fb24 into facebook:master Nov 19, 2018
@@ -726,11 +726,20 @@ function detachFiber(current: Fiber) {
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
// We do not null out the 'nextEffect' field as it causes tests to fail.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a better understanding of this? "Causes tests to fail" doesn't say anything about the intent and whether those tests should fail or if there's some other mistake.

This feels dangerous to me if we don't understand it. How do you know clearing firstEffect is safe if nextEffect is unsafe? Maybe we just accidentally hit some cases in our tests but not others.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I'd prefer that we don't use vague wording like "causes tests to fail" in the comments. Either we know why it fails and explain the conceptual reason. Or we don't know, and should understand before merging.

Copy link
Contributor Author

@trueadm trueadm Nov 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaearon I was more asking @acdlite for some guidance on this. I also reverted the PR, as I also agree that this needs more exploration.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 19, 2018

Follow up PR created #14276.

jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
…k#14218)

* Clear properties on unmount of fiber to ensure objects are not retained
jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
n8schloss pushed a commit to n8schloss/react that referenced this pull request Jan 31, 2019
…k#14218)

* Clear properties on unmount of fiber to ensure objects are not retained
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants