-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat: more efficient ownership widening #11136
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
Conversation
This reverts commit 8578857.
|
I think one of the concerns is maintaining the parent relationship. If you have an object with a property and assign that property to another object – so now that property exists on two objects, then thinks might get leaky and also incorrect. |
I addressed that above — a proxy can have multiple parents: let stuff = $state({ a: { b: { c: 42 } } });
let things = $state({});
things.foo = stuff.a; // `stuff.a` now has two parents — `stuff` and `things` Are there cases where that would break down? |
@Rich-Harris it gets tricky though, right? Unless we use WeakRefs: let stuff = $state({ a: { b: { c: 42 } } });
{
let things = $state({});
things.foo = stuff.a; // `stuff.a` now has two parents — `stuff` and `things`
}
// now `things` can't get GC'd as stuff.a has a strong relationship to it |
Ah, interesting. Hadn't considered that. |
@Rich-Harris Technically, there's no guarantee they will work either – they're best avoided unless we want to add a lot of code around their management using FinalizationRegistry. They have performance costs both ways unfortunately. |
When you say 'there's no guarantee they will work', I assume you mean one of two things:
Of those, the second would be a huge problem, but the first seems like it would be fine? The perf costs can only really be assessed relative to the status quo — it seems worth trying, no? |
We should give it a go then! |
This doesn't solve the problem of us accidentally causing side effects by invoking getters and/or needing to traverse too much eagerly. If I have Furthermore, the current ownership widening logic still has false positives as soon as you use methods to retrieve state: warning when using And why is Bottom line, the whole thing has too many holes to be a good guidance on what should or shouldn't work. I vote to remove it. |
I don't consider that a false positive — I think it should cause a warning.
For the same reason that this doesn't warn — you're triggering an accessor rather than mutating a bag of state: let field = $state('foo');
let no_warning = {
get field() {
return field;
},
set field(v) {
field = v;
}
}; Are they equivalent? You can make a case either way, but in practice if you're passing around a class instance with reactive properties it's likely that you mean for people to interact with it. The intent, as expressed through the code, is different. Even if that feels like a gap, it's okay — as long as we avoid false positives, we're doing the right thing by steering people away from writing spaghetti code, even if we don't catch every case. Perfect is the enemy of the good here.
Yeah, I don't think we should be invoking getters during As a first order of business I think we should revert #11094 since it's causing perf regressions and bugs. |
Agreed. Let’s first revert that PR and tackle it differently. |
This is more of a placeholder than a PR — just wanted to jot my thoughts down before spending any real time on it.
#11094 caused a significant performance regression, as noted in #11117, because we do a deep read of context whenever
getContext
is called (so that the caller shares ownership of the context object).I see four options:
getContext
, and insist that people call methods on context rather than mutating it directly (personally I think this is a reasonable idea, but I was previously outvoted)I like option 4. The question is whether it can be done, and I think it can. Currently, we traverse the entire object in order to add an owner to every state proxy, but I'm not sure this is necessary. What if we only traversed to the uppermost state proxy and set the owner there, and determined ownership lazily on mutation? In other words, given this...
...we initially set the owner of
stuff
to beParent.svelte
, but don't set an owner onstuff.a
orstuff.a.b
. Instead, we addstuff
as a parent ofstuff.a
when we read that value, andstuff.a
as a parent ofstuff.a.b
when we read that. (If we later dostuff.a.c = {...}
, we setstuff.a
as the parent ofstuff.a.c
, and if we dothings.foo = stuff.a
thenthings
becomes an another parent ofstuff.a
, and so on.)Inside
getContext
, we traverse the{ stuff }
object until we reach thestuff
state proxy, and set the new owner there. (If we encountered an exotic object, i.e. a customclass
instance, we would need to use the existingdeep_read
logic, but again only until encountering state.)So far, the creation cost is equivalent, in that we're adding something to a set (a parent instead of an owner) every time getting a property of a state proxy results in a new state proxy (or causes a new mini-deep-read if it's an exotic object, though maybe we only need to go one level deep?). But when we widen ownership, the cost is vastly reduced, because we don't need to traverse a potentially huge object.
The cost is paid later, upon mutation, since we need to walk up the parent tree to determine ownership. But writes are much less common than reads, and they happen one at a time, so in practice the cost is greatly reduced.
Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.Tests and linting
pnpm test
and lint the project withpnpm lint