-
Notifications
You must be signed in to change notification settings - Fork 50
Memory leaks due to heavy memoization #17
Comments
Just found out about this project and curious about this. Memory issues were my immediate concern when thinking about how this works and noticed this issue. Would there be a way to solve this using something like immutable.js? |
The datastructures are not the thing that is leaking, here. The leak is due to the memoizing cache policy. |
It could be great to use ES6 WeakMap for cache to avoid memory leak. There are some polyfill libraries, for example this one: https://github.com/Benvie/WeakMap |
Sorry that I misunderstand the WeakMap ES6 feature (I suppose it is WeakReference feature.) But, I find that WeakMap feature can be used to resolve your memory leak issue too. 😄 About the cache keyFirst of all, the What is a cursorThen, rethink what is a cursor - it is an extension attaching on every sub-object of the global single state. The attaching word means, if the state object is going to be GC, then such extension is useless anymore and should be GC too. Such attaching relationship exactly meets the actual use of WeakMap! Classes relationshipFrom the above definition, the cursor cannot hold a reference to any state object. But instead, the state object holds a reference to the cursor. Concretely, the relationship between the three classes are:
But, of course, the cursor can calculate the state object according to its holding component.state and path. Again, no direct reference. About the
|
Lifecycle issuesWhen implementing the idea, I find a comment:
This comment is checked in one year again (61aace7). In my idea, the cursor does not hold the value directly - it calculates from component when needed. So, in lifecycle mehod like Pandora is back. 😞 Another idea - Cursor wraps state objectOK, it is OK. My previous idea resolve memory issue but get another issue back. So, I proposal another way to resolve these. I draw a relationship for the current implementation:
As you see, the state object and the cursor object hold cycle reference which cause memory leak. In the previous idea, I break the The second attempt, holds
Then, in the Cursor's OptimizationsWe cannot use cache for Fortunately, React immutability helper will try to re-use the original object if it is not changed. So, we can provide Re-visit the life cycle methodsBecause the cursor instance holds the state object reference. In the life cycle method, e.g., No WeakMap anymoreWeakMap is not needed in such pattern. But, of course, it could be cool to store private class variables and other features. Compare to Immutable.js +
|
I have implement the second idea in these check-ins. Those are API breaking changes. I might think that you could not like to merge. But if yes, I could prepare a PR. The mutations example has updated to meet the new APIs. Actually, there are just three lines modifications. I have use the Chrome heap snapshot to validate that, memory issue is resolved in the mutations example. |
rootCmp must be hashed for: the case where an app has multiple state roots that are totally independent - can’t share cursor references path must be hashed for: Suppose a table is sorted, the reconciliation algorithm may choose to not reorder the react components but only fixup their innerHtml |
Sorry, it is a wrong example. There are {cursor.refine('list').value.length} items in the list. Add one
For the path issue, I still don't get the reason. Revisit it later. |
We need more time to think about these ideas. |
After thinking, I agree with you that for the current implementation, cmp and path are necessary. While, my second proposal does not need memorize any more. The memorize code is removed in the check-ins. So, you could take a look. |
@lijunle, I am finally ready to tackle this issue. Many simplifications have landed in master over the last month, that may help with this issue. Here are the two main leaks: first leak, second. refToHash can be trivially implemented with WeakMap. This will make the memory leak vastly smaller as we will be retaining one int per reference seen, not the entire react component. Is there more we can do? The new code has opened new possibilities, I think. |
Essentially, WeakMap gives us Ref => Cursor, what we need is (Ref, Ref) => Cursor |
I think we could even do dumb things like a least-recently-used cache in the memoizers with some parameter over how big to make the cache. Tune the parameter based on how big the app is. |
I noticed that there are huge changes these days. I am reviewing and understanding the current implementation for caching issue. Please let you know if I find something. |
I am considering also adding a special Cursor type for server side rendering, which does not allow cursor updates (which are typically only wired up to user interaction callbacks). This special case only needs to memoize on value (not path) and is thus compatible with WeakMap. However this solution is not fully general as it puts burden on the application code to not use cursor updates in server rendering. |
From StackOverflow:
|
Sorry, @dustingetz I reviewed your changes, it is hard for me to understand the code and predict the behavior. Maybe after you freeze the 2.0 code, I learn the logic and evaluate the usability. Please open/close this issue on your decision. |
I think we can unit test this with |
Great information! Although that API is Chrome only, it could automate the detection and make it accountable! |
Requires thought to fix.
Specifically, every cursor instance ever created for each (react component, path, value) is forever retained so if we see the same (react component, path, value) we can return the same reference.
The text was updated successfully, but these errors were encountered: