-
Notifications
You must be signed in to change notification settings - Fork 212
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
Virtual objects should use WeakRefs to enable === #1968
Comments
That's a great (and nicely simplifying) feature to provide. And I'm completely fine with relying upon a My biggest concern is to make sure this doesn't expose non-determinism to vats (which sounds unlikely). And to understand how it interacts with the GC issues that #2724 raises. I don't understand the latter at all yet, but I know that one of our likely tools is to forbid userspace access to a raw |
The trick that |
@dtribble asked me how quickly we could implement this, so we could move Zoe APIs to the better model now, instead of causing churn later. I think we can do it before the #2724 infrastructure, however we need to be careful about non-determinism, and that may require giving up some performance. This issue is that we must do a The lifecycle of a Representative is: creation, use, drop, collection. We need a Representative each time an inbound message references the virtual object, and each time userspace reads from a virtual collection and the hitherto-offline data becomes online. In our current implementation, we make a new Representative each time. In the proposed change, we use a mapping from vref to WeakRef to re-use an existing Representative, so that if userspace still holds on to one, they'll always get that same JS In this proposed world, the creation of new Representatives for a single vref may become nondeterministic. We must ensure that this does not result in a nondeterministic sequence of
because the following might happen within a single crank:
If the "lot more work" triggers engine-level GC, it might collect the Representative, and then the second reference will need to create a new one. But if it doesn't, the Representative will still be available (not reachable from userspace code, only reachable through our WeakRef), and we won't make a new one. If these two cases cause different sequences of syscalls, we've exposed nondeterminism. We've decided that we're willing to rely upon engines doing a complete GC (with prompting) at the end of a crank. But the property being exercised here: not doing GC in the middle of a crank, is beyond that, and I don't think we're comfortable relying upon it. So we need a solution that keeps the syscalls deterministic. Userspace will (by definition) provide a deterministic sequence of interactions with our virtual objects. That will consist of:
By virtue of the first three, the transition back and forth between REACHABLE and UNREACHABLE (using #1872 terminology) is a deterministic function of userspace activity, however liveslots cannot distinguish the two. The transition between UNREACHABLE and COLLECTED can be sensed by liveslots (by querying a WeakRef), but only by polling. The transition between COLLECTED and FINALIZED is sensed by a FinalizationRegistry callback being invoked (so "interrupt driven" instead of "polling"). So whatever our virtual object manager does, it must not reveal (through syscalls) the difference between UNREACHABLE/COLLECTED/FINALIZED. It is allowed to reveal the difference between REACHABLE and UNREACHABLE, however that isn't helpful because it can't even sense that difference. SolutionsI see two approaches that might work. Both give up some performance, specifically they give up some of the benefits of caching. vatstoreRead on every property accessWe could create Representatives non-deterministically, as long as the resulting syscalls are still deterministic. Userspace will read properties deterministically, so this solution is to maintain a one-to-one relationship between userspace doing a property read and liveslots doing a One implementation would make the getters that live on the Representative's for(const prop of props) {
state.defineProperty(prop, {
get: () => unserialize(syscall.vatstoreGet(vref))[prop],
set: (value) => syscall.vatstoreSet(vref, serialize({
prop: value, ...(unserialize(syscall.vatstoreGet(vref))) })),
});
} We can't cache the data, because the presence of the cache is nondeterministic. We can't even cache the In this approach, we can create a Representative at any time: it does not need to be deterministic. But we must be careful to not perform any syscalls when the Representative is created. We cannot use The performance consequences are:
Pin the Representative until the end of the crankWhen we get more of the GC work done, liveslots will be aware of end-of-crank: it will regain control after userspace has lost agency, so it can provoke engine-level GC and give all finalizers a chance to run. We could have liveslots maintain a strong reference to every Representative from the moment of instantiation (start of crank as the delivery is unserialized, or retrieval from a virtual store, both of which are deterministic), until this end-of-crank release phase (which is also deterministic). We'd just have a At end of crank, we empty this We could defer doing secondary-store writes until this end-of-crank phase, if we wanted, which would amortize multiple writes during a single crank. The performance consequences are:
First stepsI'm inclined to go with the "immediate read/write" approach for now, because I haven't finished the "liveslots gets control at end-of-crank" GC work necessary for the second solution. I know it would be disappointing to give up the cache, but I think the design will be nicely simple. I do need to apply a tiny bit of the GC work, just to make sure WeakRef is available to liveslots in all worker types. If this isn't already the case, it should only be a few lines of code. We must also think about how to get rid of the Map entries. I think it is safe to use a FinalizationRegistry, with a callback that checks After the GC improvements, we'll have the option of using the second approach. I don't have a clear prediction as to which performance characteristics will be better. My hunch is that we're more likely to touch a lot of virtual objects in a single crank (iterating through tables) than we are to do a lot of reads/writes to any single object, so my hunch is that we should stick with the "do all the reads" approach, and not use the "pin the representatives until end-of-crank" approach. The "do all the reads" approach is also way simpler. |
I like the "pin representatives until end of crank" approach much more. The "read every time" approach essentially sacrifices 90% of the benefit of having representatives in the first place; if we were going to pay the cost of a read every time, I'd just redesign the virtual object API entirely (making it much less "virtual"). Also, the pin approach seems like less work to implement. |
I thought you'd say that :). I kinda think the primary benefit of virtual objects is the ability to move them out of RAM, which happens somewhat more in the first approach (GC could run in the middle of a crank). I consider consolidation of reads/writes to be a secondary benefit, but of course it depends upon how userspace code actually uses it. I currently like the bare simplicity of the first approach, I think we'd delete at least half of |
I'm caught up. It sounds like we're agreed that
Is this right? |
Just noting that JS standard weakrefs already do something like pinning for the rest of the turn. But since this is per turn rather than per crank, it probably doesn't help. |
yeah, that sounds about right I'm not convinced that pinning is what we want long-term. It's a tradeoff that optimizes for different things. The pinning approach is bad for RAM usage when we use a large number of virtual objects within a single crank: memory usage will grow to O(num_used) by the time the crank finishes (and then drop back down to O(1)). But the DB usage is constant whether you make a lot of reads/writes or just one. The no-cache approach is bad for DB calls when we make a lot of reads/writes. But it's great for RAM usage in all cases, because it never keeps anything in RAM: O(0). It would be lovely if we could split the difference with an LRU cache, but that won't get us determinism. |
Oh, and I should point out that any of these approaches will still expose non-determinism to user code until we fix #2724, because it might use a Representative as the key of a WeakMap, and then drop all strong references to the Representative. The map entry will disappear, and someone sending it back to them (e.g. looking up a Purse balance later) will get nothing. I think we can implement this in stages, since current non-adversarial code is restricting itself to putting Representatives in our special
|
@FUDCo and I talked this through, and we think we can retain the LRU cache, as long as we maintain the property that the creation of a Representative does not cause any reads (in fact no syscalls of any kind). Userspace will read/write properties of a Representative's So the approach will be:
To reiterate:
In terms of scheduling, we think:
We'll be ok with landing the persistent-Representative changes before replacing WeakMap, if it comes out that way, but we'll need to remember that it exposes nondeterminism to adversarial code. But we really want to get quickly to the point where WeakMap can be used with both Presences and Representatives, because that's the most natural way for developers to program, and we don't want to have to teach developers one thing and then replace it with something better a month later. Best to make it better first, and then only teach them a single thing. |
As I was refamiliarizing myself with the virtual object code in preparation for making the changes to ensure that no vatstore access happens until deterministic vat code tries to access the state on purpose, it struck me that the use of weakRefs to hold onto the representatives will allow us to eliminate one layer of indirection in the underlying implementation and simplify the virtual object machinery as a result. Currently there are three layers: the representatives, the "inner self", and the state. The state is a separate layer because it can come and go from memory, and so it must be a separate thing so that that thing can sometimes not be there. The inner self is a separate thing because we need to have a single common entity that all the representatives can point to in order to share the state, since there can be multiple representatives. However, the weakRef-based implementation means that when the virtual object is actively being referenced in memory, there will always be exactly one representative instead of possibly many, and thus that representative and the inner self it points to will always be in one-to-one correspondence. The distinction between the representative and the inner self can go away. All the code that currently works with the inner self can instead use the representative directly, and a layer of mechanism can be removed. |
This will be used by the virtual object manager when creating a Representative, so that both inbound and outbound tables are prepared for it. refs #1968
This will be used by the virtual object manager when creating a Representative, so that both inbound and outbound tables are prepared for it. refs #1968
In the discussion of #1960 (comment) @erights points out that using
WeakRef
s to associate virtual objects with their data would allow us to eliminate the virtual object representative vs. inner self distinction, letting us have a single, unique representative for each active virtual object. This would enable vat code to compare virtual objects for equivalence using object identity (===
) comparison instead of forcing them to use a special comparator such assameKey
. This would (1) simplify the application developer's model of the world to be more consistent with their expectations based on regular JavaScript experience and (2) make for fewer complicated weird rules that we have to explain and justify. The tradeoff is that this will require an engine that supportsWeakRef
, forcing us to dictate Node 14 as the minimum supported Node version.The text was updated successfully, but these errors were encountered: