Skip to content
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(reactivity): fix dep memory leak #7827

Closed
wants to merge 1 commit into from

Conversation

skirtles-code
Copy link
Contributor

@skirtles-code skirtles-code commented Mar 5, 2023

Here's a Playground where you can observe the leak:

Playground

Click the button a few times and then take a Heap Snapshot. Filtering on VClass, you should see something like this:

Heap Snapshot

The instance with distance 7 is supposed to be there. That is a current dependency of the computed property. But the other instances, with distance 10, shouldn't be there. They are no longer in use and should have been garbage collected.

The targetMap shown in the Retainers is from effect.ts. That's a WeakMap. The keys in that map are reactive objects, so in my example it would be the thing I've called set. If the set object is GCed then the leak will be cleaned up, but in cases where that object is retained long term, e.g. within a global store, the WeakMap won't save us.

The value inside the WeakMap is a Map. The key for this Map is the property being tracked. The corresponding value is a Set of effects that are tracking that property, referred to as Dep in the types.

When a dependency is removed from an effect, it is removed from that Set of effects. However, if that Set is now empty, it isn't removed. That is a small leak, but arguably not worth the effort to tidy up.

The potential for a much bigger leak occurs when the reactive object is a Set or a Map. In that case, the 'property' being tracked can also be an object. So while the empty Set of dependencies may only be a small leak, the leak of the key can be an arbitrary, and potentially large, object. In my example it is the relatively small VClass object, but in principle that object could be much bigger.

To put it another way:

const targetMap = new WeakMap<any, Map<any, Set<ReactiveEffect>>>()
//                                     ^^^
//                                      |
//                         This 'any' can be an object
//                         that leaks when the
//                         Set<ReactiveEffect> is empty

The leaking objects don't need to have ever been in the reactive Set or Map. It's enough just to call has(), as that establishes a dependency on them, even if they are never in the collection.

Here is the same example but using the code in this PR:

Playground - leak fixed

Comment on lines +3 to +7
export type Dep = Set<ReactiveEffect> &
TrackedMarkers & {
target?: unknown
key?: unknown
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if there was a reason why the properties w and n use single-letter names. I've used target and key here, but they can easily be changed to t and k if required.

Comment on lines +152 to +162
export function removeEffectFromDep(dep: Dep, effect: ReactiveEffect) {
dep.delete(effect)
if (dep.target && dep.size === 0) {
const depsMap = targetMap.get(dep.target)
if (depsMap) {
depsMap.delete(dep.key)
}
dep.target = dep.key = null
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered using a setTimeout() here to defer the purging of the empty dependencies. Removing them immediately like this isn't strictly necessary, and could lead to some extra churn if empty dependencies are removed and then immediately re-added.

However, in the most common cases, the trackOpBit stuff avoids this extra churn, so I decided it wasn't worth the extra complexity.

Comment on lines +355 to +409
triggerEffects(new Set(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
triggerEffects(new Set(effects))
}
}
}

export function triggerEffects(
dep: Dep | ReactiveEffect[],
dep: Set<ReactiveEffect>,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
const effects = [...dep]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes were inspired by #6185.

The createDep(effects) call isn't really creating a Dep, it's just using a Set to remove duplicates. This is the only place that passes an argument to createDep(), and I wanted to change the signature of createDep() to be able to pass the target and key instead. The other changes here follow from that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can keep the signature of triggerEffects, then create the Set inside the triggerEffects if needed.

@yyx990803 yyx990803 added the ❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. label Mar 23, 2023
johnsoncodehk added a commit to johnsoncodehk/core that referenced this pull request Oct 18, 2023
Co-Authored-By: skirtle <65301168+skirtles-code@users.noreply.github.com>

vuejs#7827
johnsoncodehk added a commit to johnsoncodehk/core that referenced this pull request Oct 18, 2023
Co-Authored-By: skirtle <65301168+skirtles-code@users.noreply.github.com>

vuejs#7827
@github-actions
Copy link

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 86 kB (+142 B) 32.7 kB (+59 B) 29.6 kB (+62 B)
vue.global.prod.js 132 kB (+142 B) 49.4 kB (+50 B) 44.3 kB (+39 B)

Usages

Name Size Gzip Brotli
createApp 48 kB (+142 B) 18.9 kB (+63 B) 17.3 kB (+98 B)
createSSRApp 50.8 kB (+142 B) 20 kB (+64 B) 18.3 kB (+56 B)
defineCustomElement 50.4 kB (+142 B) 19.7 kB (+64 B) 18 kB (+48 B)
overall 61.4 kB (+142 B) 23.7 kB (+59 B) 21.6 kB (+36 B)

@skirtles-code
Copy link
Contributor Author

I believe this problem has been fixed by the overhaul of the reactivity system in 3.4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf.
Projects
Status: Rejected
Development

Successfully merging this pull request may close these issues.

3 participants