3.0.0
https://pastebin.pl/view/raw/ac6ade7c
MyClass instances are not in the snapshot.
MyClass instances are in the snapshot.
The instances are retained by the 'computedField' computeds, which are retained in someRefThatExistsLongerThanA's depMaps:
https://github.com/vuejs/vue-next/blob/d52d139b854238708195f5d332b7d0ec3a4883a5/packages/reactivity/src/effect.ts#L143
This is because dependencies from someRefThatExistsLongerThanA to computedFields are never cleared, except immediately before re-evaluating them. Computed does not have an exported way to 'cleanup' them, like watch and watchEffect have (WatchStopHandle).
We ran into this when we found a memory leak in one of our applications.
Solutions:
There is another positive side effect, in that dependees that were marked dirty have been removed already, and do not have to be traversed upon every update of the dependency. In our own use case this is greatly beneficial, as we're having up to 20'000 rows that have a computed that depends on one global computed, while only a fraction of rows is actually invoked.
see https://github.com/vuejs/rfcs/pull/212#issuecomment-695842658
Thank you for the boiled down and detailed report. This is a part of the problem related to the fact that reactive properties are only automatically cleaned and must be attached to a component instance to be cleared (called inside of setup). As pointed out, https://github.com/vuejs/rfcs/pull/212 is meant to tackle the larger problem.
As a workaround, you can create something like this https://github.com/antfu/vueuse/blob/039aea9809c45f74a86cdef9248d821a1d24f8f0/packages/core/createGlobalState/index.ts#L3-L16 to be able to unmount the app manually and remove all dependencies.
Thanks @posva
The workaround is not really useful for our situation, in which we have a class-based model using reactivity. We want parts to be cleaned up, not the full app.
The propasal seems good for more advanced use cases, but it seems a bit overcomplicated for simple use cases. I don't want to have to create and cleanup a scope everywhere we're using a computed. The management of creating and cleaning them up is cumbersome, hacky and prevents syntax like:
class MyClass {
public myComputed = computed(() => ...)
}
I can't wrap that computed in a scope at that place. I'd have to move their definitions to the class constructor and then create a manual destructor method which should be called from somewhere else. The management of calling it is an unnecessary complexity. Feels like boiler plate code as well.
All I'm suggesting: when dependees are triggered, just remove them from the depsMap. This is a tiny change, and will automatically cleanup stale computeds from the deps instead of holding on to them forever. Finally allowing the GC to clean them up. And it will save some performance as well when there is a large quantitiy of dependees that are not really used, reducing the deps set. This just works, scope or no scope.
Yeah, The second proposal about clearing the Set might still be worth looking into, it seems to me.
Not necessarily as a bugfix for this rather than an optimization.
This is a tiny change, and will automatically cleanup stale computeds from the deps instead of holding on to them forever.
Tiny change but needs intense review to not miss other edge cases.
Added a PR. Tests are all passing as expected.
I verified with the original example that, after clicking the 'updateRef' button, the computeds and instances are indeed dereferenced.
You are right, it's worth cleaning up the held reference if possible. Thanks for the PR!
Hmm, technically this isn't a leak, because a computed property creates an effect which needs to be stopped to release itself from all depMaps.
When you create a computed inside a component's setup(), its effect is automatically stopped when the component unmounts so the user don't need to think about it.
In your case, you need to make sure your class instances, when "cleared", properly stop all the effects it created (including raw effects, watchers, and the computed properties).
I would say what's proposed in https://github.com/vuejs/rfcs/pull/212 should be the right solution here. In terms of ergonomics, I imagine something like this would help:
function autostop(factory) {
let instance
const scope = effectScope(() => {
instance = factory()
})
instance.teardown = () => stop(scope)
}
// when creating class instances, wrap inside autostop
let aInstances = Array.from({ length: 10}, (x, i) => autostop(() => new MyClass(i)));
// when clearing instances, call stop()
aInstances.forEach(i => i.teardown())
aInstances = [];
Either way, I think we need to provide a way to explicitly stop computed property effects.
Hi Evan, thanks for your response. I can see the advantage of using scopes, and I like that PR. However, it would make sense to be able to stop the computeds individually, as we can do with watchers.