-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
Clean up unmounted fiber references for GC #15157
Changes from all commits
c6947e4
7aa4b6c
50de1fa
c2bbc8d
2100482
c1f16f3
80b908c
41a983b
4cae222
137d9b0
de27cd4
2edaa1f
ea2c6d7
568ef3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -730,11 +730,11 @@ function commitUnmount(current: Fiber): void { | |
if (typeof instance.componentWillUnmount === 'function') { | ||
safelyCallComponentWillUnmount(current, instance); | ||
} | ||
return; | ||
break; | ||
} | ||
case HostComponent: { | ||
safelyDetachRef(current); | ||
return; | ||
break; | ||
} | ||
case HostPortal: { | ||
// TODO: this is recursive. | ||
|
@@ -745,7 +745,7 @@ function commitUnmount(current: Fiber): void { | |
} else if (supportsPersistence) { | ||
emptyPortalContainer(current); | ||
} | ||
return; | ||
break; | ||
} | ||
case EventComponent: { | ||
if (enableEventAPI) { | ||
|
@@ -756,6 +756,12 @@ function commitUnmount(current: Fiber): void { | |
} | ||
} | ||
} | ||
|
||
// Remove reference for GC | ||
current.stateNode = null; | ||
if (current.alternate != null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably because of this:
It's a nice way to cover |
||
current.alternate.stateNode = null; | ||
} | ||
} | ||
|
||
function commitNestedUnmounts(root: Fiber): void { | ||
|
@@ -1050,19 +1056,15 @@ function unmountHostComponents(current): void { | |
} | ||
|
||
if (node.tag === HostComponent || node.tag === HostText) { | ||
// Save stateNode reference so commitUnmount can clear it. | ||
const stateNode: Instance | TextInstance = node.stateNode; | ||
commitNestedUnmounts(node); | ||
// After all the children have unmounted, it is now safe to remove the | ||
// node from the tree. | ||
if (currentParentIsContainer) { | ||
removeChildFromContainer( | ||
((currentParent: any): Container), | ||
(node.stateNode: Instance | TextInstance), | ||
); | ||
removeChildFromContainer(((currentParent: any): Container), stateNode); | ||
} else { | ||
removeChild( | ||
((currentParent: any): Instance), | ||
(node.stateNode: Instance | TextInstance), | ||
); | ||
removeChild(((currentParent: any): Instance), stateNode); | ||
} | ||
// Don't visit children because we already visited them. | ||
} else if ( | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -1404,6 +1404,15 @@ function commitRootImpl(root, expirationTime) { | |||
rootWithPendingPassiveEffects = root; | ||||
pendingPassiveEffectsExpirationTime = expirationTime; | ||||
} else { | ||||
// We are done with the effect chain at this point so let's clear the | ||||
// nextEffect pointers to assist with GC. If we have passive effects, we'll | ||||
// clear this in flushPassiveEffects. | ||||
nextEffect = firstEffect; | ||||
while (nextEffect !== null) { | ||||
const nextNextEffect = nextEffect.nextEffect; | ||||
nextEffect.nextEffect = null; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this part of the fix makes sense because otherwise if this node (nextEffect) never gets any updates ever again, it'll now have a reference to a node that might later get deleted. Should be pretty easy to come up with a reduced test case for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was able to come up with an isolated repro. https://github.com/paulshen/react-next-effect-leak/blob/master/src/App.js Live I tried to create this repro in the past but was bamboozled by
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just add a comment next to it with a link to your repro. We don't need to add any infra just for this. We can figure that out later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sebmarkbage in case it was missed, I opened #16115 |
||||
nextEffect = nextNextEffect; | ||||
} | ||||
if (enableSchedulerTracing) { | ||||
// If there are no passive effects, then we can complete the pending | ||||
// interactions. Otherwise, we'll wait until after the passive effects | ||||
|
@@ -1620,7 +1629,10 @@ export function flushPassiveEffects() { | |||
captureCommitPhaseError(effect, error); | ||||
} | ||||
} | ||||
effect = effect.nextEffect; | ||||
const nextNextEffect = effect.nextEffect; | ||||
// Remove nextEffect pointer to assist GC | ||||
effect.nextEffect = null; | ||||
effect = nextNextEffect; | ||||
} | ||||
|
||||
if (enableSchedulerTracing) { | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We won't want to do this indiscriminately for all types of Fibers. They might not all have this field. We're looking to use Flow disjoint unions to enforce that they're consistently used for each type. They don't all use it in the same way where this makes sense.
We should be more specific and only do this for the type where it's needed.
However, which type is this supposed to clean up? Is it just DOM nodes? Is it class instance references? What about the equivalent pattern for Hooks? It's hard to tell without knowing more about the particular pattern this fix is solving for.
I'm guessing that this is somehow meant to release DOM nodes that might be part of larger subtrees and therefore might have back pointers into Fibers. However, where is the root that hold onto the first Fiber that might point back into those anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example where mounted DOM/component link back to detached DOM elements. The root cause in this example is probably the effect pointers from mounted fibers to unmounted ones.
I'm not familiar enough with the internals to know where it doesn't make sense. I believe it though 😄 This was from a naive reading that
FiberNode
s have this field. Mainly, I was thinking aboutstateNode
pointers forHostText
/HostComponent
(DOM) andClassComponent
but it didn't seem costly to just reset for all. Should I limit toHostText
andHostComponent
?I know this is the crux of this whole thing but the chains were long, sometimes ending in V8 internals. I very much believe it could be a user-space issue but it was not obvious (to me) and these changes seemed cheap/reasonable enough to prevent cascading effects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This particular example is rooted in a
nextEffect
. With your other fix, that would be reset to null. So would that be sufficient to fix this particular leak?Maybe we can split out this part to a separate PR and just land the nextEffect part like you suggested? That way we can see if that is sufficient or not.