-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Potential memory leak in store.query() #4041
Comments
@bmac this is the leak I spent a few hours investigating with @trumb1mj. I haven't had time to revisit it but I've had a few thoughts on what's causing the leak. The initial |
I'm not sure this is a leak.Query intentionally creates a new record array each time as the query is executed by the server so we can't re-use single arrays. |
@fivetanley it's not a "leak" in the sense that nothing is unintentionally retained, but it is a leak. The memory growth in this setup is ridiculous, and it's a fairly common use case. |
@runspired in my example there is no query done on the model hook return. @fivetanley even when you use the .clear() method to empty out that array, the heap continues to grow so there is something else going on here. |
@trumb1mj What's the result when you use |
Using destroy seems to slow the memory growth but I'm still seeing about a .5mb per minute. |
Although the result from `store.query` is intended to never be re-used as we delegate to the server for the truth of a query, the user still needs an escape hatch to destroy these AdapterPopulatedRecordArrays created by query in order to prevent excessive memory. Before, calling `destroy` on an AdapterPopulatedRecordArray (the type returned from store.query), the logic in RecordArrayManager#unregisterRecordArray did not account for adapter populated record arrays resulting in a leak. This commit changes the logic to search for the array in the RecordArrayManager's AdapterPopulatedRecordArray collection when unregistering a record array. Fixes emberjs#4041
i have opened #4042 which i think is a cause of the leak |
@trumb1mj if you can verify these changes help that'd be great. I didn't have time to check them against the sample app. |
I grabbed the code from https://github.com/fivetanley/data.git#adapter-populated-record-array-leaks and retested with the .destroy() method. The leak still seems to be occurring. |
I think the leak after my change is Pretender. It never seems to cycle out its This includes all the FakeResponses and their json responses. The stuff in yellow in the Chrome inspector are retained nodes between snapshots. I don't see anything from Ember Data but there is a lot of stuff from Pretender besides the globals it exposes: |
The two heap snapshots in this screenshot indicate that the memory leak should be fixed. Yes one is slightly larger than the other but I came back after a couple minutes of making coffee and this was the result: I plugged pretender to reset There may be a slight leak left but I'm not sure at this point. The liveness of the demo and us not being able to take snapshots with precise cutoff points doesn't really help either. A good update would to be to add a button that calls query instead of the polling so you could click and then take a heap snapshot after each click rather than end up on some moment where the browser chooses to use more memory randomly, and also messes up retainers. |
@fivetanley reopened? Did you find more? |
No, it was accidentally auto-closed when I merged the PR. I'd like to close it if you two don't have anything else to look into. |
I haven't had a moment to test out the fix, but will do so. |
I had some more thoughts on this. Right now the For example,
That way a user would not have to wait for the promise to be returned and do a |
Hey @trumb1mj sorry I didn't have much time to look at this last week. What do you mean by "retains model history"? |
I think part of the problem was not a leak but rather on each |
That make sense. Thanks @trumb1mj. I'll try to fid out why the results of |
Although the result from `store.query` is intended to never be re-used as we delegate to the server for the truth of a query, the user still needs an escape hatch to destroy these AdapterPopulatedRecordArrays created by query in order to prevent excessive memory. Before, calling `destroy` on an AdapterPopulatedRecordArray (the type returned from store.query), the logic in RecordArrayManager#unregisterRecordArray did not account for adapter populated record arrays resulting in a leak. This commit changes the logic to search for the array in the RecordArrayManager's AdapterPopulatedRecordArray collection when unregistering a record array. Fixes #4041 (cherry picked from commit 0eedecb)
Is there any guidance on how best to handle this situation. I have a page that polls using query with dynamic filters & sets the promise to a controller property so the template can show loaders, failures, etc as the promise does its state transitions. What I do now is get the old resolved promise from the query & call destroy on it & then fire off the next one setting it to the same controller property. I'm seeing the same leak mentioned in this thread. Is calling destroy not enough & should we be doing more? Thanks. |
Calling |
bummer. I may need to do the same thing. Thanks man. |
This issue was discussed on the Ember Data call today and it was determined that the fact that Ember Data hold a reference to the query's record array is not something that Ember Data needs to do anymore. It was likely added at a time when Ember Data worked differently and is no longer relevant after some internal refactorings. I think the path forward to closing this issue would be to stop registering the query's |
+1 |
Great, thanks for the follow up on this guys! |
I've updated the demo repo to the latest versions of ember and ember-data and the Here is the repo that demonstrates the leak: https://github.com/trumb1mj/ember-data-growing-recordarrays |
This seems to be working as expected. For live recordArrays such as the ones returned from
I don't believe it is related. Although that may be a leak onto its own? Manual solution would be something like (clearly not pretty): {
filteredList: ember.computed(function() {
let list = this.store.query('foo', { something: 1 });
if (this._lastFilteredList) {
this._lastFilteredList.destroy();
}
this._lastFilteredList = list;
return list;
}),
destroy() {
this._super(...arguments);
this._lastFilteredList && this._lastFilteredList.destroy();
}
} Another approach, is to create a CP or something that did this automatically when recomputed. filteredList: ensureDestroyed(dependentKey, function() {
return this.store.query('foo', { something: 1 });
}); Finally, a future would exist where anything fetched from |
Just spoke with @runspired:
Future work:
|
You can use the repo (linked at the top of this issue) as an example: https://github.com/trumb1mj/ember-data-growing-recordarrays |
@trumb1mj that is a pretty old version of ember-data. I'll upgrade and investigate... |
upgraded your example to the recent stuff: trumb1mj/ember-data-growing-recordarrays#1 I am unable to see the leak in question. It is possible I missed something though: Its worth pointing out, there was work fixing up correctness of RecordArrayManager stuff in October that may have addressed this. If I have missed something, let me know. If i can reproduce locally, i can likely fix quickly. |
After merging your PR I'm still reproducing a small leak like I was seeing before. Are you saying the old leak is no more and the leak on display here is within mirage? |
As far as I can tell there is no obvious leak here any longer, but if someone can demonstrate one I'll gladly debug it. I've opened an issue to improve documentation related to this here. We should also explore, more ergonomic ways of auto-deleting things. |
@fpauser is still seeing it in his app on the same version of ember-data. This should not be closed yet. |
@stefanpenner how do you recommend testing this without mirage? |
I'll add destroy to our code and let you know whether the leak goes away. Good way to show it in our code is to repeatedly |
We all agree that having to call destroy is just a hack for an underlying problem, right? Why in the world would ED keep those record arrays around after the record gets removed? |
@trumb1mj after which record gets removed? I'm seeing it by getting a hasMany on a model. It appears that you get a new array proxy each time you do that, with the same inner array |
Sorry, I misspoke. I think the recommendation is to call |
here's what we're doing with your advice, @stefanpenner - but still leaking observers.
This code is called by
I'm console logging result.length in ember.debug.js |
In your application adapter, implement the
We will need a reproduction, if provided I will absolutely debug and address (i actually enjoy debugging memory leaks...)
I do not believe ember-concurrency does anything to auto-delete record arrays for you. So the above hypothesis stands.
I don't believe so. RecordArrays (as currently designed) are live and can mutate in reaction to the store changing (in the query case, when records are deleted/unloaded). To accomplish this,book keeping from store to recordArray is required, which necessarily causes the store to retain the recordArrays. To release a RecordArray, manual intervention is required. There are some conflicting constraints at play, it is most likely not possible for a live RecordArray, and a automatically garbage collected RecordArray to be the same RecordArray. It would be possible to introduce new type of RecordArray which is "frozen" which would automatically garbage collect. Unfortunately, a side-affect of that would be if the "frozen" record array survives longer then a record it contains, the record it contains would be retained... If the language had WeakCollection / WeakRef, more automagical things could happen... I am sympathetic to the problem described here, I suspect we need to do a better job of documenting and providing good memory management strategies for our ember-data users. Something I hope to have time to help with over the next little while... |
Yes the RecordArray is what should be destroyed.
Ya, unfortunately that is a breaking change. So for now, it will need to be opt-out, which today means manually invoking
I suspect the "polling" use-case itself could become more first class, one could imagine several approaches:
|
@BryanCrotaz if you can provide a reproduction of the leaking observers, I will gladly debug. |
@stefanpenner Happy to send you a reproducible case but it's using private repos so can I send you a zip with node_modules populated? |
@stefanpenner and any hints and tips about how to debug memory leaks in JS much appreciated. I can use the chrome profiler to show there's a leak but no idea how to turn that into a line of code :) |
I typically debug memory leaks like I play sudoku... More seriously, my general path is using all the tools available to me (mem profiler, debugger, pen & paper etc) but most importantly building a mental model of the ownership and object lifecycle graph, and using the above tools to validate/inform that mental model. I realize this sounds cryptic, I hope to someday find time to write this down (or even better find someones existing resource to share)... |
@BryanCrotaz sure |
@stefanpenner so does this also affect query results in a route's model hook, or does ember somehow destroy the RecordArray when it tears down the view? |
Ember does not do automatic cleanups here. Although this is something we will be exploring (as a new feature). |
@stefanpenner 🙇 thanks for all the thorough explanations, I really appreciate how Ember's core team specifically is patient and responsive on a lot of this stuff. Hopefully I'm not about to test said patience 😬 Fully acknowledging that I have a limited grasp of the Ember Data internals, and that I as a developer represent a very tiny slice of the concerns and use cases of Ember Data, and that my awareness of the full scope of said concerns and use cases is very limited, I think this design is really problematic and would imagine that to be the case for many developers. It seems that you are aware of this and have some potential solutions in mind and obviously these things take time, but I still want to throw my 🎩 in the ring on this discussion. From a naive developer perspective I would fully expect a RecordArray to be garbage collected when I am no longer using it (when I leave a route, when my poller function has finished executing, etc). I would also not really want to concern myself with the difference between the different types of RecordArrays, nor would I ever have known that I was expected to manually destroy these RecordArrays (as up until now it was not in the documentation or docs backlog). I don't think these are unreasonable assumptions, and my guess is that they would be pretty common among developers. I know that's where we are now, and that time and effort will be required to make any of the changes you described above, but I personally would still consider this a leak and a fairly serious usability issue even if it's partly by design. If I'm understanding all of this correctly, it means that any time the average dev is using store.query() in pretty much any normal way the store is retaining memory without their knowledge/consent. That, to me, seems like it qualifies as a memory leak. So I guess what I"m saying is that I humbly request that this be treated with a reasonably high level of priority. This was originally opened over a year ago and it seems like we're just now digging into some of the core problems. Also, is this limited to just results from store.query or do the other store methods also retain RecordArrays (findAll, peekAll, etc) which must be manually destroyed? |
@dknutsen ya, I get the pain here. Unfortunately the system is working as designed (the design is just unfortunate), and there exists no solution that both maintains compatibility with I believe the only reasonable path forward, is to:
note: I am hoping to find some cycles over the next little while to improve the current state of things here, as I (and many others on ember/data core) share your feelings – this is simply not reasonable
|
@stefanpenner understood, thanks a lot for the thorough explanation and the info about the other methods too. |
When calling store.query(), the heap grows. I've setup a sample project that demonstrates the issue here: https://github.com/trumb1mj/ember-data-growing-recordarrays
The text was updated successfully, but these errors were encountered: