-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
fix: use state
instead of source
in reactive classes
#16239
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
base: main
Are you sure you want to change the base?
Conversation
|
|
New idea I'll try to implement: store the active reaction in the constructor of the Map/Set and use a |
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.
Great work!
@@ -56,13 +56,17 @@ export class SvelteMap extends Map { | |||
#sources = new Map(); | |||
#version = state(0); | |||
#size = state(0); | |||
/**@type {Reaction | null} */ | |||
#initial_reaction = null; |
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.
Wondering if there are cases when map still lives, while the reaction could have been GCed and this would prevent it?
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.
Yeah I was wondering about that too...technically it shouldn't be the case because either there's no active reaction (it's outside of a derived/effect) or it's inside it but this means that when the derived/effect is no longer used the function should be GCd and everything inside it too...but I need to do a better check
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.
Maybe this can become initial_reaction = WeakSet(active_reaction)
and we can check with this.initial_reaction.has(active_reaction)
?
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.
Just for the sake of history, I've also discovered there is a WeakRef struct that could be alternative here. In case of any future issues might be a possible alternative implementation
Also tried installing this version locally, enabling runes mode forcefully and clicking through the interface. Looks like not hitting |
teardown(() => { | ||
this.#initial_reaction = null; | ||
}); |
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.
presumably if there's a situation that would create a memory leak with this (which I guess means smuggling a reference to the map to something outside the current reaction?), teardown
only mitigates it, since the reaction in question might never be torn down, if it's an unowned derived.
do we therefore need a way to run code as soon as the current reaction has finished updating? #16316
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.
The alternative is to use a weak ref to reference the reaction. Perf impact should be close to zero since it only applies once for new sources
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.
We could also use WeakRef, we were discussing with Simon this morning that this seems the perfect use case for it
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.
I've pushed a commit to use WeakRef
since it's way easier than handling #16316 for this edge case and this is kinda the perfect use case for it. We can always revert if we don't feel like going for it.
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.
Not sure I love the WeakRef solution tbh — it increases overall memory usage (since every map/set now has to have a WeakRef instance) and slows down every #source
call, since WeakRef is known to impact performance
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.
We tested with @dummdidumm today and it was at least 90% of normal access...also as @dummdidumm said this is really only accessed when we create the source for the first time.
If you think merging #16316 it's better we can also do that (or just ignore the edge case which btw should also be present in proxy
)
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.
second that, WeakRef only has theoretical perf implications here. It only applies when you create a source anew, which isn't that often compared to the reads in the vast majority of cases. I suspect that creating teardown
or having an array like in #16316 (I think it's overkill, would just use the teardown
) is the same memory overhead.
We can also determine this "not worth it" and live the potential memory leak. It can only leak if you create the map inside effect context A and then pass it outside to another context, and context A is destroyed but not the context it's passed to. Pretty sure our proxy has the same leak because we never clean up the original context either in case it's gone, and I haven't heard complaints.
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.
I don't think the memory consumption is comparable at all — the array (and the callbacks it contains) are short-lived, while the WeakRefs live as long as their owners do.
But yeah I would just live with the edge case honestly. As with proxies it's only 'leaky' if the map/set is itself leaked, and you really have to work at it.
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.
Would disagree with "keeping" the leak. I'd say that in this specific case it's not intuitive at all for the developer that by doing code like the following they introduce a memory leak:
$effect(() => {
foo.bar = new SvelteMap()
})
As someone who is developing apps with long-lived browser tabs I'd say that leaks are the most difficult things out there to find and fix. Generally in a lot of cases it's clear from the code and knowledge about how GC works to understand that hey, we are keeping a reference. But in this specific case the code looks like it shouldn't leak, so it's what is called "hard to reason about". It is actively messing with my expectations about it.
Yeah, this code might be weird, and I can't come up with a reason to do something like that, but it doesn't mean some developer won't find a need for it.
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.
Consider it to be the same as previous values in teardown. It's not about if it's right or wrong, it's about the expectations and not making it confusing on the DX side of things
Closes #16237
So the problem with using
source
is that:$effect
/$derived
that reads asource
created in itself will also depend on it...since normally state created in a reaction doesn't depend on itself this is counter intuitiveThis is a bit problematic in some cases because we create the sources lazily...in the case of
Map
for example we don't create asource
unless youget
orhas
orset
that first. However this means that if you create a newMap
in a derived ask if ithas
something, then you try to write to it in the same derived the newly createdsource
throws the error.For
set
it was as simple as changing source tostate
...however withget
andhas
it's a bit different: the effect needs to have a dependency on thesource
sostate
is out of question but then you don't want writes to it to throw. For that my solution was to add the source to thereaction_values
after theget
(only if the sources was newly created). For#read_all
it was a bit more complex and i had to create aWeakSet
for the newly created signals (not sure about memory/performance impact of this tho).I also changed the
source
's tostate
's inTweened
andSpring
since that also could've been problematic in cases like thissource
is also used insidecreateSubscriber
and I think it could have a problem there too but i need to check to be sure.It's also used inside
await
,each
and other blocks that "create" a value but i think for that it's probably fine.As I've said in the issue I'm not satisfied with this solution since it feels a bit hack-ish to add the dependency and still allow the user to write to it.
I also need to add a bunch of tests for this but wanted to give the code to @gyzerok
Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.packages/svelte/src
, add a changeset (npx changeset
).Tests and linting
pnpm test
and lint the project withpnpm lint