18:02
<shu>
rbuckton: working session call happening now if you can make it
19:17
<shu>
Mathieu Hofman: so if we do allow them in weak collections, it's probably not the case that they become eternal the minute they get put in there, but that they become eternal if there is a cycle
19:17
<shu>
which is an implementation problem that i can live with, i guess
19:17
<shu>
the spec can say, they are allowed in weak collections
19:17
<Mathieu Hofman>
correct, engines could always figure out directed graph through internal weakrefs
19:18
<shu>
but until XX years from now when massive rearchitecting has been undertaken, know that in practice cycles will leak
19:18
<shu>
which is still compliant, but unfortunate
19:18
<shu>
there's no good language reason to disallow them
19:18
<Mathieu Hofman>
I just wanted to point out you were opening pandora's box
19:18
<shu>
there is a safety aspect of disallowing shared -> unshared edges, since obviously multiple threads accessing an unshared thing can't work
19:19
<Mathieu Hofman>
I've been trying to keep it closed in a few places where these things came up
19:19
<shu>
this is not a problem for weak collections, since there's a per-thread view of the weak collection
19:19
<shu>
what's the pandora's box? cycles between shared and unshared?
19:19
<Mathieu Hofman>
the requirement for a distributed garbage collection
19:19
<shu>
ah, i see
19:20
<shu>
yes, indeed, it is inherent in a shared memory proposal to open that pandora's box
19:20
<shu>
much of the work internally i've been doing before proposing this in public is to get a roadmap worked out for GC evolution to support a shared memory future
19:20
<shu>
and it sounds like at least V8 and JSC are converging on what to do
19:20
<Mathieu Hofman>
well SharedArrayBuffer avoided that bullet with unstable identities between agents
19:20
<shu>
correct
19:20
<shu>
but it's also a "solved" problem in the literature, at least
19:21
<shu>
there are plenty of GCed languages with shared memory
19:21
<Mathieu Hofman>
it's solved if you have a single GC, nothing is published for cooperative distributed gc
19:21
<shu>
i guess i don't know what you mean by distributed
19:22
<shu>
independently collected heaps that point to each other?
19:22
<Mathieu Hofman>
multiple local GCs coordinating to identify and prune distributed cycles
19:22
<shu>
ah i see
19:22
<shu>
but that implementation is not a requirement
19:23
<shu>
the prevailing wisdom seems to be to do a single marking phase across all threads
19:23
<Mathieu Hofman>
I do have a proposal to solve that, but as you mentioned, the motivation is probably not going to be Web user agents
19:25
<Mathieu Hofman>
I am skeptical however that all engines will be on board with a requirement for a single gc per agent cluster. As you mentioned, afaik it requires a stop the world that is proportional to the amount of threads
19:25
<shu>
only if you want to handle cycles
19:25
<Mathieu Hofman>
of course
19:27
<shu>
even with a naive STW, if you remember all ephemeron edges, can't you still collect the cycles without having to mark all local threads?
19:29
<shu>
remember all ephemeron edges in which a shared struct participates, that is
19:29
<shu>
local GCs defer sweeping local objects in an ephemeron edge with a shared object until the shared GC happens
19:29
<shu>
when the shared GC happens, process shared ephemeron edges
19:30
<Mathieu Hofman>
right, you're basically getting into collaborative gc territory ;) Being able to trace exits to other gc from a local ephemeron is basically the crux of my idea
19:31
<shu>
ah okay, cool
19:31
<shu>
i'm just thinking out loud that there might be a "good enough" implementation strategy that we can implement in the meantime
19:31
<Mathieu Hofman>
as a 3rd phase style
19:31
<shu>
without a huge performance cliff
19:31
<shu>
until we get a shared heap architecture
19:32
<Mathieu Hofman>
And introducing a reification of that mechanism is what my proposal hoped to accomplish. I just haven't had time to work on it in the past couple years
19:32
<shu>
so i'm cautiously optimistically now changing my position to: we'll allow these in WeakCollections, and we can probably implement it, but it'll likely be not as fast as you'd like and you'll incur some GC pause penalty if you heavily use shared structs in WeakMaps
19:32
<shu>
but the GC pause penalty won't be catastrophic
19:33
<shu>
(i'm cautiously optimistic because the ephemeron collection is already a separate phase and already complicated, so why not tack on more? :P)
19:33
<Mathieu Hofman>
yeah the drawback is that it might take a much longer time to figure out distributed cycles, but the pause can be made so that it's not worse than a regular local gc
19:34
<shu>
right, it'll definitely take longer for shared structs to get collected from weak collections
19:34
<shu>
because shared structs will be collected at a lower frequency, period, until rearchitected to a single heap
19:34
<shu>
but this matches well with my intuition that JS is still staunchly "single-threaded first", and we're carving out multithreading here as explicit opt-ins with its own caveats
19:35
<shu>
i want it to be possible, but i recognize that to have it be good from a PL perspective is not realistically attainable IMO
19:35
<shu>
the use case pressure is just too great to not solve it, however
19:37
<shu>
gotta run, but fascinating chat
19:42
<Mathieu Hofman>
btw, here is a thought. The identity of the shared struct is not directly observable by programs between agents, the only thing that is observable is that when sending a shared struct back and forth between agents, you get the same local identity. The same object wrappers could be an implementation optimization. Once you've done that, you could design it so that a shared struct have their methods declared in a module block, which is automatically loaded once per realm where the shared struct is used. Those methods from the module instance define the "prototype" object of the shared struct in that realm. Now the optimization is that in practice you don't have a different wrapper object per realm, and you have a dynamic prototype look up that takes into consideration the calling realm.
19:44
<Mathieu Hofman>
Btw, since JS doesn't specify the mechanism how these wrappers are shared between agents, the only thing ecam262 needs to say nothing, aka these objects don't need any more mechanisms than e.g. SharedArrayBuffer
19:46
<Mathieu Hofman>
The modulo here is legacy realms (as always). ShadowRealms can have the same dynamic dispatch mechanism since object graphs are not entangled
20:19
<shu>
it is a nonstarter to implement it with wrappers
20:19
<shu>
i'm not sure what that spec fiction buys us
20:20
<shu>
the automatic loading thing worries me -- i'd rather there be no magic with module blocks, but that they be a pure workaround without extra mechanism
20:21
<Mathieu Hofman>
sure pure functions would be great. You might want to chat with the Moddable folks about their idea on that
20:21
<shu>
i didn't mean "pure" in that sense
20:21
<shu>
i meant pure as in module blocks do not have extra mechanism to interact with shared structs
20:23
<shu>
i suppose i object more to a dynamic per-realm prototype lookup than the automatic loading
20:23
<Mathieu Hofman>
So the loading could be part of the out of scope mechanism that introduces the shared struct to the realm. And the spec fiction allows you to pretend there is no realm sensitive resolution of the prototype object
20:24
<shu>
ah i see what you're getting at for introducing the definition
20:25
<shu>
how does that idea work if i send a struct instance to a realm that didn't load the module block that defines it?
20:26
<shu>
(implementation wise i'm not sure how the dynamic prototype lookup can be efficient implemented either)
20:26
<Mathieu Hofman>
To be honest that would be in scope of the channel that does the sharing, which would be host defined. I assume it would grab the module associated to the struct, and send it along, and load the module when receiving it.
20:28
<Mathieu Hofman>
With some logic to avoid trying to reload modules that were already loaded.
20:28
<Mathieu Hofman>
Don't we effectively have dynamic prototype lookup in primitives today? I assume implementation optimize that away?
20:29
<shu>
well, they get boxed
20:29
<shu>
i don't think it's some magic
20:29
<shu>
i don't want extra allocations here
20:30
<Mathieu Hofman>
right but you dynamically figure out the box to use depending on the realm
20:33
<Mathieu Hofman>
Anyway, I believe you, I am clueless when it comes to implementation. I was hoping this could be way to make it more ergonomic without introducing weird spec
20:44
<shu>
yeah, per-realm (thread?) prototypes is something dan ehrenberg has brought up as well before
20:44
<shu>
but i think if we're holding out hope for eventually actually sharing prototypes
20:45
<shu>
we can't really do that then, right?
20:48
<Mathieu Hofman>
I just don't see how actually sharing prototypes would work, it'd require you to have a shared version of the intrinsics, which would have to be deeply frozen at least, and would still create identity discontinuity issues that plague legacy realms today. Unless they're specced like the callable boundary that only primitives and other shared structs can be exchanged through them, but that seems like a worse restriction.
20:49
<shu>
what intrinsics?
20:50
<shu>
i'm not saying all prototypes become shareable. shared struct prototypes are new things we design
20:50
<shu>
i agree having shared versions of intrinsics is not tractable
20:51
<shu>
the prototypes would at least be sealed like shared structs themselves
20:51
<shu>
i don't think deeply frozen is necessary
20:51
<Mathieu Hofman>
function prototype is one, but then if you return an object from your shared method, what prototype does it have ?
20:51
<shu>
you cannot return plain objects from shared functions
20:52
<Mathieu Hofman>
ok so you do have a restriction at the boundary to only accept or return primitives or other shared structs
20:52
<shu>
yeah, i was imagining extending the restriction we have today to the shared functions
20:52
<shu>
i don't see how it works otherwise
20:52
<Mathieu Hofman>
then yes it is like the ShadowRealm callable boundary
20:52
<shu>
i suppose? there's no wrapping
20:52
<shu>
it's a selective boundary, yes
20:52
<Mathieu Hofman>
well if you have instances of methods per realm, they're free to do whatever they want
20:53
<shu>
right, and that's a different model, where the functions are not actually shared
20:53
<shu>
just duplicated
20:53
<Mathieu Hofman>
and return objects of their instantiated realm
20:53
<shu>
and maybe that's fine, but i'm not sure we have agreement on that
20:53
<Mathieu Hofman>
I'm just not aware of the design goals or motivation
20:54
<shu>
well, this part isn't in scope of the mvp structs :)
20:54
<shu>
which is probably why it's so nebulous
20:55
<shu>
personally i don't think i'm really against the duplicate model. functions are much fewer than instances, so duplicating them doesn't concern me as much for performance
20:55
<Mathieu Hofman>
so basically we either have per realm instantiated methods that are free to do whatever they want, or shared functions that internally would be able to do whatever they want, and be defined in a sandbox frozen shared realm, but couldn't return any regular objects through their interface
20:56
<shu>
no, there're more options
20:56
<shu>
we could have shared functions that always have a very threadbare [[Realm]] that don't have anything in it, and must take everything as arguments
20:57
<shu>
e.g. shared function({Math, Atomics } = globalArg, arg2, arg3) {...}
20:57
<shu>
that's probably a nonstarter because syntax to create builtins would stop working
20:58
<Mathieu Hofman>
ok but it'd have to have all the undeniable intrinsics?
20:58
<shu>
not sure i follow
20:58
<Mathieu Hofman>
and by that I mean anything that can be accessed by syntax, eg. function, object, string prototypes etc
20:59
<shu>
ah right. don't know! maybe the syntax just stops working, so probably nonstarter
20:59
<shu>
one reason i don't find the "caller Realm" semantics i proposed so bad is i don't see how we get around it for allocation
21:00
<shu>
if a function is truly shared, and we agree that it's too unergonomic to disallow non-shared object allocation, e.g. {} or [], where does that allocation happen?
21:00
<shu>
i don't see a choice other than for to allocate it in the caller's thread-local Realm
21:01
<Mathieu Hofman>
well I suppose you could have some shared memory that is used when in scope of a shared function, and has it's own object graph and gc ?
21:01
<shu>
also open question. it's easier to start with saying shared functions cannot be closures
21:02
<shu>
but yes, one possible extension is to make them able to close over shared things
21:02
<shu>
but that is a huge can of worms to open in terms of design, and i don't think we should
21:02
<shu>
duplicates may very well be good enough
21:03
<Mathieu Hofman>
right so anything allocated during the shared function execution can be collected at exit (outside of shared structs that made it through the boundary)
21:04
<shu>
yeah, with a non-duplicate model you can be eager about collecting per-activation allocations if you want to but probably don't need to be
21:04
<Mathieu Hofman>
you'd probably need new syntax to define those shared functions in a way that doesn't close over anything and can't keep state.
21:05
<shu>
yeah, i think there are just way too many hurdles and unknowns and impedance mismatches to make realistic progress on actually shared functions right now
21:05
<Mathieu Hofman>
sounds like pure functions to me, no side effects outside of the arguments and return value
21:05
<shu>
depends on what you consider a side effect
21:05
<shu>
allocations must be an allowable side effect, as are causing side effects caused by passed in objects
21:06
<shu>
i think this convo has convinced me that we really should start with a duplicate model for now
21:06
<Mathieu Hofman>
the only things that are impacted are either passed in or returned
21:06
<shu>
well, allocation must be an allowed side effect
21:06
<shu>
you can't meaningfully limit that to "either passed in or returned"
21:07
<Mathieu Hofman>
but the only observable impact of that is the return value or a mutation to the argument. Those make up the whole state that is allowed to be mutated
21:08
<Mathieu Hofman>
I know that moddable has done a lot of work on tracing the purity of functions (they have a model where multiple agents can share the same realm). I honestly don't fully understand all of it. You might be interested in syncing up with them.
21:09
<shu>
zooming out, so i'm thinking the plan is:
1) in the MVP, lean on module blocks to get the guarantee that you are actually loading the exact same code across all workers
2) observe how far this gets us and the DX pain points with early adopter partners (there are interested parties in Google, Microsoft, and Adobe so far)
3a) if duplicates gets us pretty far with transparent code sharing optimizations in the engines, just DX is lacking, we explore avenues like dynamic proto lookup you suggested
3b) if duplicates doesn't get us far enough, think harder about actually sharing functions
21:10
<shu>
dunno how matrix messed up that list
21:10
<shu>
wtf it's changing 1) -> 1.
21:11
<shu>
i know a little of what moddable does
21:11
<shu>
it's cool but not applicable outside of a specialize implementation like that has been my feeling
21:12
<Mathieu Hofman>
Yeah mark is very interested in the purity predicate, but I have my personal doubts that it would ever work for other implementations
21:13
<Mathieu Hofman>
would be neat to come up with something that is, probably through explicit syntax
21:15
<shu>
that sounds super difficult
21:19
<shu>
rbuckton: asumu: see the "i'm thinking the plan is" above for code sharing. thoughts?
21:19
<asumu>
(1) sounds reasonable to me for JS. I do wonder how it will work out with Wasm GC once it gets sharing, as module blocks don't apply for it. Maybe wasm exported functions will be easier/possible to share, and those can be called from other threads? (Wasm exported functions rely on the wasm module instance being shared, but don't rely on the realm I think)
21:20
<shu>
let me unpack into two questions: 1) can we call shared wasmgc exported functions without problem and 2) where do we put them
21:21
<shu>
  1. seems like "yes" to me -- wasm is in charge of making sure there's a single copy if it wants the actual code to be shared. the wasm part is threadsafe by definition, so a duplicate model means we need to create per-thread wrappers. which isn't great but probably isn't too bad either
21:21
<shu>
lol getting real tired of matrix changing what i type
21:23
<shu>
  1. i have no idea about. i imagine e.g. Java compiling down to wasmgc aren't putting code pointers into each instance's field. how do you reflect a particular compiled object model's vtable into JS in a portable way anyway?
21:35
<asumu>
Yeah I imagine it will not make sense to be able to reflect the vtables into JS, given diverse surface languages doing very different things (with interface dispatch, etc). I think there could be some way for wasm producers to be able to specify methods just for the JS API and have it associated with a prototype, but I guess this won't work for this first iteration with module blocks.
21:35
<shu>
agreed, yeah
21:35
<asumu>
In any case, as long as wasm functions are shareable I guess it will be sufficient to express JS interactions even if it's not very convenient at first.
21:35
<shu>
but it doesn't need to work with module blocks, which is just a means to an end to guarantee that workers load the same copy of the code
21:36
<shu>
the wasm/JS API can be the one to provide the guarantee that the same code is being reflected in different realms
21:36
<asumu>
Right, that makes sense.