-
Notifications
You must be signed in to change notification settings - Fork 4.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
Fix memory leaks in <Iframe>
#53406
Fix memory leaks in <Iframe>
#53406
Changes from all commits
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 |
---|---|---|
|
@@ -3,7 +3,6 @@ | |
*/ | ||
import { CacheProvider } from '@emotion/react'; | ||
import createCache from '@emotion/cache'; | ||
import memoize from 'memize'; | ||
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. Memoize should never be used for anything except if when defining a max size imo. Always creates memory problems. Maybe we should add a rule to at least always define the maxSize option. 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.
as @kevin940726 noted, this still leaves ample room for memory bloat, but I think it's better than uncapped memoization. maybe we can look into places where we use it and analyze each situation to figure out a reasonable cap. |
||
import * as uuid from 'uuid'; | ||
|
||
/** | ||
|
@@ -12,19 +11,27 @@ import * as uuid from 'uuid'; | |
import type { StyleProviderProps } from './types'; | ||
|
||
const uuidCache = new Set(); | ||
// Use a weak map so that when the container is detached it's automatically | ||
// dereferenced to avoid memory leak. | ||
const containerCacheMap = new WeakMap(); | ||
|
||
const memoizedCreateCacheWithContainer = memoize( | ||
( container: HTMLElement ) => { | ||
// Emotion only accepts alphabetical and hyphenated keys so we just | ||
// strip the numbers from the UUID. It _should_ be fine. | ||
let key = uuid.v4().replace( /[0-9]/g, '' ); | ||
while ( uuidCache.has( key ) ) { | ||
key = uuid.v4().replace( /[0-9]/g, '' ); | ||
} | ||
uuidCache.add( key ); | ||
return createCache( { container, key } ); | ||
const memoizedCreateCacheWithContainer = ( container: HTMLElement ) => { | ||
if ( containerCacheMap.has( container ) ) { | ||
return containerCacheMap.get( container ); | ||
} | ||
); | ||
|
||
// Emotion only accepts alphabetical and hyphenated keys so we just | ||
// strip the numbers from the UUID. It _should_ be fine. | ||
let key = uuid.v4().replace( /[0-9]/g, '' ); | ||
while ( uuidCache.has( key ) ) { | ||
key = uuid.v4().replace( /[0-9]/g, '' ); | ||
} | ||
uuidCache.add( key ); | ||
|
||
const cache = createCache( { container, key } ); | ||
containerCacheMap.set( container, cache ); | ||
return cache; | ||
}; | ||
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. @dmsnell Weren't you talking about emotion going crazy? Does this fix it? 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. @kevin940726 Is there any way we could tests this to prevent regressions? 🤔 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. yeah I suppose this could be a source of the leak. I never thought about in other words, maybe it would be worth attempting this change first as this const memoizedCreatedCacheWithContainer = memoize(
( container: HTMLElement ) => { … },
+ { maxSize: 1000 }
} seems like both approaches might resolve the memory leak, though this one is a smaller change. using a nevermind what I said. you found gold @kevin940726 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. Well, WeakMap is a superior solution if it can be used. But yes, maybe we should always require a maxSize option for memoize #53406 (comment) 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. are we defeating the purpose of the
does emotion allow 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 believe we can test this some way or another in the performance test suite (c.c. @WunderBart). Though I would rather keep it in another PR since it's not trivial to do 😆. 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. Excellent catch 👍 +100% to adding a rule to require max size on memoisation functions and/or to strongly suggest a fall back to a WeakMap. It is really a much better, harder to screw-up option for simple, single-argument cases like 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.
thanks for double-checking @kevin940726 - it was late when I was reading through the docs and I wasn't sure that holding the key as part of a value would remain weak. so a fair amount of time has passed between when I wrote the previous sentence and now. I've done some testing in Safari and Chrome. testing was hard because at least in Chrome, the JavaScript engine seems to overlook small we have marginal insight here because the browser consoles show the contents of the
after a while of testing I found that things freed up more reliably when I tried. I'm guessing this could be related to the engine warming up. in the process I found so take this as validation that we should be good here, but we also in general probably want to make sure we don't store the same object in two 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. @dmsnell Thanks for the validation! If you're using chrome devtools, clicking on the bin icon shown in my video will force garbage collection, which is consistent and instant in my testing. Another thing is that some browsers (chromium-based for instance) only collect garbage every 20 seconds even when you use some advanced memory API. So that might be the reason why you're getting the results late.
Yep! This is working as expected because the value is still reachable by the other key. 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.
right, but I saw plenty of cases where garbage collection didn't free the items in the maybe this appears in some of our measurements as retaining values too long, but those values aren't strong references so they could be purged at any time. the measurements might at times be misleading. |
||
|
||
export function StyleProvider( props: StyleProviderProps ) { | ||
const { children, document } = props; | ||
|
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.
Why does not doing this prevent the node from being removed from memory? When React removes the iframe, this callback is removed as well?
This reminds me of #27544 (comment)
That said, it doesn't hurt of course, I'd just like to understand why.
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 is a tricky one that's several levels deep. I'm not entirely sure why this fix helps either! I'll try to post more details after I have a clearer vision.
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.
Okay, I still don't fully understand what happened. It seems to require a very specific setting to make it reproducible. For instance, there has to be a
.focus()
call on a<input>
somewhere, the iframe has to be rendered under a condition and a wrapper, etc. This might also only be reproducible in a chromium-based browser.However, we can still learn from this mistake and prevent it from happening again by following the general best practice: don't cross-reference between parent and child frames. In particular, the
window.frameElement
call is the code smell we want to avoid. Instead, usepostMessage
to communicate to the parent frame. In this case though, deletingnode._load
has the same effect, and it's easier than refactoring topostMessage
.