10:49
<Ashley Claymore>

I'm still tinkering with my parallel parse prototype, and I'm planning to try it on a few large scale projects. I'm not currently seeing the perf-gains I would hope, but its too early to say if its an issue with the shared structs functionality, the size of the projects I've been using for testing, or something about how I've had to hack around parts of the compiler to get something functional.
I wrote a rudimentary work-stealing thread pooling mechanism, but I'm finding that adding more threads slows down parse rather than speeding it up for the monorepo I've been using as a test case. CPU profiling shows a lot of the threads aren't processing work efficiently, and are either spinning around trying to steal work or are waiting to be notified of work. Spinning isn't very efficient because there's no spin-wait mechanism nor the ability to write an efficient one (I can sort-of approximate one using Condition.wait with a short timeout to emulate sleep, but I can't efficiently yield). I also can't write efficient lock-free algorithms with shared structs alone, since I can't do CAS, so the fastest "lock-free"-ish updates I can perform are inside of a Mutex.tryLock unless I want to fall back to also sending a SharedArrayBuffer to the worker just so I can use Atomics.compareExchange.

Here's a rough approximation of the thread pool I'm using right now, if anyone has suggestions or feedback: https://gist.github.com/rbuckton/3648f878595ed4e2ff3d52a15baaf6b9

Looks good to me. Have you experimented with batch sizes? Each task being N files, rather than 1:1 task file ratio?
10:50
<Ashley Claymore>
Also wondering how much the tasks are known up front (one main glob) vs discovered as imports are found. I.e how well the queue can stay pumped?
10:56
<rbuckton>
Tasks are 1:1 per file. With work stealing, batching would be less efficient since you could have threads sitting idle.
10:58
<rbuckton>
How much is known upfront depends on the tsconfig files, include, and exclude options, though I'm using a striping approach to try to collect all imports/references for each pass around the file list.
10:59
<rbuckton>
I need to experiment with a few more projects of different sizes though, it's still fairly early yet.
11:02
<rbuckton>
The current approach is still very waterfall like in the main thread. I would need to do a lot more work to have the child threads scan for imports/references so they don't have to constantly wait for the main thread to hand out more work.
11:04
<rbuckton>
Unfortunately, program.ts is very callback heavy and dependent on caches that would also need to be shared.
11:05
<rbuckton>
There's a lot of idle time waiting for main right now
11:08
<rbuckton>
I currently have a synchronized, shareable Map-like data structure I can use for that, but I may want to see if I can build a lock-free, concurrent Map first so there's less blocking involved
12:26
<Ashley Claymore>
Tasks are 1:1 per file. With work stealing, batching would be less efficient since you could have threads sitting idle.
true tho that assumes the queuing system is zero-cost (no padding around tasks). So might work out that some batching, while theoretically less efficient at packing, leads to better results.
Just an idea :)
12:28
<Ashley Claymore>
In an ideal world parsing the largest files first would also be ideal for work stealing, though finding the largest files may be more costly than that saves too
13:09
<Jack Works>
is there slides of update?
13:09
<Jack Works>
I'm excited about the progress you've made and want to know more details! I can't wait!
14:43
<shu>
Jack Works: there are in fact no slides yet :(
14:43
<shu>
got so much to do this week
14:44
<shu>
rbuckton: i wonder if also web workers sucking somehow is getting in the way of your performance? this is node though so who knows, might be unrelated to web workers even if its worker implementation were less than ideal
16:03
<rbuckton>
true tho that assumes the queuing system is zero-cost (no padding around tasks). So might work out that some batching, while theoretically less efficient at packing, leads to better results.
Just an idea :)
You are possibly correct, though that is a level of fine tuning I'm not anywhere near investigating yet.
16:04
<rbuckton>
rbuckton: i wonder if also web workers sucking somehow is getting in the way of your performance? this is node though so who knows, might be unrelated to web workers even if its worker implementation were less than ideal
Are you imagining there is overhead to reading/writing from shared structs or using mutex/condition caused by the worker? Or are you talking about overhead due as a result of setup, postMessage, etc.?
17:08
<rbuckton>
I've updated the thread pool example to use a lock free Chase-Lev deque, though it still uses a Mutex/Condition to put the thread to sleep when there's no work to do.
17:26
<rbuckton>
It's still somewhat inefficient if a thread ends up sleeping and a task is added to a queue for a different thread that is still active.
19:16
<Mathieu Hofman>
Reading all this, I am still curious to understand how Shared Struct help compared to a synchronization mechanism (to implement a thread pool) coupled with an efficient message passing. How much actual shared mutable state is necessary?
19:51
<rbuckton>
What would you consider to be "efficient message passing"?
19:53
<rbuckton>
The lion's share of what TypeScript would send back and forth for parallel parse is essentially immutable, but a lot of the smaller data structures I need just to do coordination require shared mutable state.
19:55
<rbuckton>
If I wanted to write my own malloc/free over a growable SharedArrayBuffer as a heap, I could mostly do the same things as what we can do with Shared Structs, albeit far slower due to the need for wrappers and indirection, plus I would have to handle string encoding/decoding on my own and could never shrink the size heap. Shared structs are far more efficient in this regard.
19:58
<rbuckton>
And when I say "could mostly do the same things", I mean "have done something very similar" with https://esfx.js.org/esfx/api/struct-type.html, with the downside that it requires fixed sized types for fields and everything is laid out flat within a SharedArrayBuffer.
19:59
<rbuckton>
(and it doesn't support arbitrary string values)
20:29
<shu>
Are you imagining there is overhead to reading/writing from shared structs or using mutex/condition caused by the worker? Or are you talking about overhead due as a result of setup, postMessage, etc.?
i was thinking the latter, and scheduling
20:30
<shu>
Reading all this, I am still curious to understand how Shared Struct help compared to a synchronization mechanism (to implement a thread pool) coupled with an efficient message passing. How much actual shared mutable state is necessary?
my thinking has always been single-writer XOR multiple-reader kind of data sharing will get you pretty far
20:30
<Mathieu Hofman>
I guess I'm wondering how these small data structures for synchronization are used, how much they need to do, and if there's any way to abstract them into higher level concepts. The immutable data could be passed as messages, and does not need to be based on shared struct from what I gather. I am basically still worried we're designing a blunt tool that will be abused when alternatives would be more aligned with the JS ecosystem.
20:31
<shu>
but if your application wants mutable shared state there is no alternative
20:32
<shu>
i continue to strongly disagree with this handwringing about abuse
20:43
<shu>
but i think we remain agreed that shared mutable state is a bad thing to entice people into reaching for from the get go
21:00
<rbuckton>
i was thinking the latter, and scheduling
For TypeScript, I'm not using postMessage at all except for the built-in one NodeJS does to pass the initial value of workerData, so that wouldn't be the cause.
21:04
<rbuckton>
I guess I'm wondering how these small data structures for synchronization are used, how much they need to do, and if there's any way to abstract them into higher level concepts. The immutable data could be passed as messages, and does not need to be based on shared struct from what I gather. I am basically still worried we're designing a blunt tool that will be abused when alternatives would be more aligned with the JS ecosystem.
The problem is that concurrency and coordination often requires far more complex coordination primitives than we are likely to ship in the standard library. With the implementation in the origin trial, I can easily build these more complex coordination capabilities out of the primitives we have through the use of mutable shared state. If we are limited to only a few built-in mutable and shareable data structures and everything else is immutable, then it is possible this proposal won't meet the needs of the applications that need this capability the most.
21:05
<rbuckton>
That's not saying we shouldn't also have immutable data structures, or at least the ability to freeze all or part of a shared struct, as I'd like those too.
21:06
<shu>
rbuckton: yeah that all tracks exactly with my intuition
21:06
<rbuckton>
Even though I would consider most of the TypeScript AST to be immutable, that's not exactly true. It's immutable to our consumers, but we need to be able to attach additional shared data ourselves.
21:08
<rbuckton>
for example, I may build a SourceFile and its AST in parallel parse, but this file hasn't been bound and had its symbols and exports recorded yet. Once parse is complete, we hand the entire program off to the binder which could also do its work in parallel.
21:08
<shu>
in the back of my mind i'm still thinking about the viability of dynamic "ownership" tracking, for lack of a better word. by "ownership" i mean single writer XOR multiple readers
21:09
<rbuckton>
And while our emitter uses tree transformations that produce a new AST for changed subtrees, we still reuse unchanged subtrees as much as possible, and need to attach additional information about how those original nodes should be handled during emit as well.
21:10
<rbuckton>
Weak Maps and thread-local state don't help there as I may want to parallelize emit and transformation for subtrees as well, which means handing parts of the tree off to other threads.
21:12
<rbuckton>
On a per-instance level, or something less fine grained? In my TypeScript experiment I wrote a SharedMutex that supports single writer (exclusive) locks and multiple reader (shared) locks on top of the ones you provide on Atomics.
21:14
<shu>
rbuckton: on a per-instance level
21:14
<rbuckton>
That sounds potentially expensive?
21:14
<shu>
not to provide ordering, or blocking until reading is available, but to e.g. throw, or provide query APIs for whether it's currently safe to read
21:15
<shu>
indeed, that's why i've punted on it
21:15
<shu>
there is a 2-bit lock-free scheme, but that still means an additional load and branch on every access, and then an additional CAS on state changes
21:16
<shu>
2 bits are needed to transition the state between "unused", "being read", and "being written to"
21:16
<rbuckton>
My intuition is that if you're writing JS code that really needs multiple threads of execution, then you want things to be as lean as possible with explicit opt-ins to anything slower or more complex.
21:16
<shu>
that is my intuition as well for shared structs
21:17
<shu>
to be clear i'm thinking of these in the context of additions after the building blocks are there, to encourage a happy path that is a little less performant but a little more safe
21:17
<shu>
but this is probably still too fine-grained to make the safety tradeoff worth it
21:18
<rbuckton>
Was this related to the idea of snapshotting an object for mutation, and then applying the update atomically?
21:19
<rbuckton>
the RCU approach?
21:20
<shu>
yep, in that vicinity for sure
21:22
<rbuckton>

A few years ago there was discussion about the "monocle-mustache" operator, and I wondered if it could be used for this, i.e.:

let copy = obj.{ x, y };
copy.x++;
copy.y--;
obj.{ x, y } = copy;
21:23
<shu>
oh interesting
21:23
<shu>
and you're thinking of things between the { } as comprising a transaction?
21:24
<rbuckton>
i.e., normal JS objects could use it as a pick-operator for read, and like Object.assign for write, but shared structs could return a mutable snapshot that provides an atomic read of the requested values, and could perform an atomic write at the bottom.
21:25
<shu>
cool idea though a little magical feeling
21:25
<shu>
rbuckton: oh btw i wanted to poll your opinion before i made slides for the next meeting...
21:26
<rbuckton>
.{ isn't new to most of the committee though, it's been discussed on and off for almost 9 years now, iirc.
21:26
<rbuckton>
just never formally presented.
21:26
<shu>
since the current prototyping effort is to do agent-local/realm-local (i'd like to discuss the granularity during the meeting) fields, how do you think that should look in syntax?
21:26
<shu>
we have precedent in auto accessors as having modifiers to fields
21:26
<shu>
i was thinking like agentlocal fieldName; or something
21:28
<rbuckton>
https://github.com/rtm/js-pick-notation for the pick notation, and I think there was some discussion in https://github.com/rbuckton/proposal-shorthand-improvements as well
21:30
<rbuckton>
i was thinking like agentlocal fieldName; or something
It's not terrible, I suppose? In other contexts/languages I might call it threadlocal, but another option might be nonshared? Especially if the struct syntax is something like struct Foo {} and shared struct Bar {}, declaring something as nonshared seems semantically consistent without needing to bring in terms like "agent"
21:31
<shu>
yes, i don't love the name agent
21:31
<shu>
i kinda like nonshared, though i wonder if it glosses over the per-thread/per-realm view aspect of the semantics
21:32
<shu>
actually, the bigger possibility for confusion is that the modifier applies values, not the field itself
21:32
<shu>
kind of like the const confusion
21:32
<rbuckton>
We don't say "agent" in any of the Atomics APIs, despite those APIs having to do with memory ordering to support atomic writes across agents, so I don't think its that bad to avoid the terminology.
21:32
<shu>
OTOH we already have that confusion, and the use of nonshared is consistent with how const modifies the binding
21:33
<shu>
or maybe just local
21:33
<shu>
though that's pretty vague
21:33
<rbuckton>
shared struct Data {
  x;
  y;
  nonshared foo;

  // would methods need this keyword too, or automatically be considered nonshared?
  method() { }

  nonshared method2() {}
}
21:34
<rbuckton>
local feels vague and has a different context in some other languages
21:34
<rbuckton>
i.e., in some languages, local refers to how you access shadowed variable bindings
21:35
<shu>
method declarations are currently just disallowed
21:35
<shu>
i don't know what it means to have that in a shared struct, without bringing in ideas we've talked about in the past like packaging it up as a module block that gets re-evaluated
21:36
<rbuckton>
method declarations are currently just disallowed
Yes, but I'm imagining syntax based on what I hope we can get in the end, including an easy Developer experience for the prototype handshake for attaching behavior, as in the Gist I shared several weeks ago.
21:37
<rbuckton>
I'm referring to this: https://gist.github.com/rbuckton/08d020fc80da308ad3a1991384d4ff62
21:37
<shu>
Yes, but I'm imagining syntax based on what I hope we can get in the end, including an easy Developer experience for the prototype handshake for attaching behavior, as in the Gist I shared several weeks ago.
then in that future i favor requiring nonshared method() {} and making method() {} a parse error, to make the semantics explicit
21:38
<shu>
also, just in case by divine inspiration we manage to actually share functions in the future, somehow
21:39
<rbuckton>

essentially, the syntax covers multiple things:

  • Declaring the fields that are shared (with a convenient place to hang type annotations off of)
  • Declaring the fields that are not shared (specific to the current thread/agent/whatnot)
  • Declaring the construction logic that is not shared (specific to the current thread/etc.)
  • Declaring the instance methods that are not shared (specific to the current thread/etc.)
  • Declaring the static methods on the non-shared constructor.
21:40
<shu>
i plan to reference that doc in the update slides
21:41
<rbuckton>

so, would you be suggesting it be this:

shared struct Foo {
  x;
  y;
  
  nonshared constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  nonshared toString() {
    return `${this.x},${this.y}`;
  }
}
21:41
<shu>
yes
21:41
<rbuckton>
It seems somewhat redundant, IMO, unless you expect we would ever have the concept of a "shared constructor" or a "shared method"
21:41
<shu>
(but to be clear i plan to leave out any mention of inline method declarations at all)
21:42
<shu>
in this update stage
21:43
<rbuckton>

btw, in the origin trial this has been pretty convenient in both JS and TS:

// js
class Foo extends SharedStructType(["x", "y"]) {
  constructor(x, y) {
    super();
    this.x = x;
    this.y = y;
  }
}

// ts
class Foo extends SharedStructType(["x", "y"]) {
  declare x: number;
  declare y: number;
  constructor(x: number, y: number) {
    super();
    this.x = x;
    this.y = y;
  }
}
21:44
<shu>
It seems somewhat redundant, IMO, unless you expect we would ever have the concept of a "shared constructor" or a "shared method"
i don't at this time, but things could change? but that's not the main reason for my preference. the main reason is i want the syntax to be explicitly reflect the semantics
21:44
<shu>
my personal design sense is i hate implicit stuff
21:46
<rbuckton>
As someone who has had chronic wrist pain due to a pretty severe break around 20 years ago, my opinion is the less redundancy and repetition when typing, the better.
21:46
<rbuckton>
though I agree with explicitness when necessary.
21:48
<shu>
As someone who has had chronic wrist pain due to a pretty severe break around 20 years ago, my opinion is the less redundancy and repetition when typing, the better.
that's good feedback
21:48
<rbuckton>
If you think we will ever come to a place were we can actually share code across threads or allow threads to coexeist with main thread application memory like they do in many other languages, then I would agree that we need the keyword to avoid painting ourselves into a corner.
21:50
<rbuckton>
I always advocate for "less ceremony is better" when it comes to syntax, though not so much that I agree with using keywords like pub, fn, def.
21:51
<rbuckton>
const-aside
21:58
<Mathieu Hofman>
One question with the syntax as proposed above is how do you attach nonshared properties / methods to a struct definition you received from another thread
22:09
<rbuckton>
That's explained in the gist I linked above.
22:13
<rbuckton>

The gist proposes a simple handshaking mechanism through the use of a string-keyed map. At the most fundamental level, you declare "this name is associated with this exemplar" on one thread, and "this name is associated with this prototype" on the other thread.

Since you want to be able to produce new struct instances on both sides, you could declare these things bidirectionally, i.e. "this name is associated with this exemplar and prototype" on one thread, and "this name is associated with this exemplar and prototype" on another thread:

// main.js
const worker = new Worker(file, {
  preload: "preload.js",
  structs: {
    Foo: { exemplar: FOO_EXEMPLAR, prototype: FOO_PROTOTYPE }
  }
});

// preload.js
prepareWorker({
  structs: {
    Foo: { exemplar: FOO_EXEMPLAR, prototype: FOO_PROTOTYPE }
  }
});
22:15
<rbuckton>
The preload script could run at the startup of the worker thread. It would establish the relationship on the worker's side, but wouldn't be allowed to send or receive messages on the worker. That would allow you to establish the relationship all at once and avoids a mutable registry.
22:16
<rbuckton>
This can then be expanded to introduce something like a built-in symbol-named method that the handshaking process could look at first, before looking for { exemplar, prototype }, and a shared struct declaration would implement that as a static method, returning a suitable exemplar and prototype for the handshake without needing to run the constructor
22:17
<Mathieu Hofman>
only allowing init time registration somewhat concerns me, and I have to think more about this per connection registry.
22:18
<rbuckton>

Thus the handshake can be simplified with shared struct declarations like this:

// foo.js
export shared struct Foo { 
  ...
}

// main.js
import { Foo } from "foo.js";
const worker = new Worker("worker.js", {
  preload: "preload.js",
  structs: { Foo }
});

// preload.js
import { Foo } from "foo.js";
prepareWorker({ structs: { Foo } });

22:18
<rbuckton>
The reason I proposed init-time registration was due to concerns you raised about data exfiltration with a mutable registry.
22:19
<Mathieu Hofman>
also this mechanism means there is technically 2 different point definitions, but since they share a prototype the type discontinuity is not observable?
22:20
<rbuckton>
This approach also avoids giving shared structs an identity based on path, and instead is a user-defined identity declared when the Worker is created. Its no different then just passing an array of workers without the need to ensure you properly marry up element order on both sides, and Foo is easier to remember and debug than an integer value.
22:21
<Mathieu Hofman>
yes non-init time registration does raise the problem of "land-rush", ability to extract information through the registry
22:21
<rbuckton>
also this mechanism means there is technically 2 different point definitions, but since they share a prototype the type discontinuity is not observable?
I thought that was the rationale we were moving towards anyways? To attach behavior to a shared struct in two threads, you must have two different definitions of the behavior, one in each thread.
22:21
<Mathieu Hofman>
I just wish we didn't have to make the trade-off somehow
22:22
<Mathieu Hofman>
I thought that was the rationale we were moving towards anyways? To attach behavior to a shared struct in two threads, you must have two different definitions of the behavior, one in each thread.
yes just wanted to make sure that's actually what's happening, and that it should be fine
22:22
<rbuckton>
I think the preload mechanism is at the very least a palatable way to address it, and its the same approach used by runtimes like electron to provide privileged access when creating sandboxed environments
22:23
<Mathieu Hofman>
a possible 1-to-many relationship from behavior to type definition
22:23
<rbuckton>
This approach presupposes that you know ahead of time all of the possible types you wish to flow through all threads that can talk to each other in your application.
22:25
<rbuckton>

And by "know ahead of time", you can still support types added by libraries if they export a registry of their types in the form of a regular JS object, i.e.:

import { structs as fooStructs } from "foo-package";

new Worker("worker.js", { ..., structs: { ...fooStructs, Bar, Baz } });
22:30
<rbuckton>

The main issue I see with this approach is when you have 3+ threads, where two or more child threads need to communicate without having established a handshake between themselves:

  1. main thread M has struct Foo with type identity 0
  2. child thread A has a struct Foo with type identity 1
  3. child thread B has a struct Foo with type identity 2
  4. M performs handshake with A establishing that Foo-0 on A uses A's Foo prototype, and Foo-1 on M uses M's Foo prototype.
  5. M performs handshake with B establishing that Foo-0 on B uses B's Foo prototype, and Foo-2 on M uses M's Foo prototype.
  6. M creates a MessagePort and hands port1 to thread A, and port2 to thread B
  7. A creates a Foo-1 and sends it to B over the message port.
  8. What does a Foo-1 look like in B?
22:31
<Mathieu Hofman>

right, just wondering if we could still end up with something like

import { Point } from "./point.js";
import { attachBehavior, parentPort } from "worker_threads";
parentPort.on("message", data => {
    if (data.type === 'registerPoint') {
        attachBehavior(data.examplar, Point.prototype);
    }
});

22:31
<rbuckton>
the goal with the API design in the doc is to abstract away as much of that scaffolding as possible.
22:33
<rbuckton>
i.e., assume that the presence of a structs: {} property in the worker constructor will transmit a 'registerPoint' message for you, and that a prepareWorker({ structs: {} }) will automatically handle the on("message") event for you.
22:33
<rbuckton>
There's no reason to have users write all of that out themselves.
22:34
<rbuckton>
I also don't always want to have to depend on postMessage when I intend for most of the processing in the child thread to happen synchronously through the use of Mutex and other synchronization primitives.
22:35
<rbuckton>
One possibility is that an Agent keeps track of the type identity mappings for all of the types on all of the workers, and shares those identities with other agents.
22:37
<rbuckton>
So if A sends a Foo-1 to B, B's Agent can first check if it has an explicit mapping of Foo-1 to something else, then walk back to the Agent that spawned the thread for such a mapping, and so on.
22:37
<rbuckton>
Thus B's Agent would walk back to M to see that a Foo-1 is associated with a Foo-0, and thus we can associate it with a Foo-2 in B.
22:38
<rbuckton>
Whatever we would do would need to work without postMessage after the initial setup, because I can run into the same scenario when just sharing a shared struct between two worker threads
22:39
<rbuckton>
in which case I can't wait for an asynchronous postMessage to establish the relationship for me.
22:39
<Mathieu Hofman>
I agree we can provide sugar like you propose, but I believe having an explicit attachBehavior or similar allows to solve the late registration case without fully opening the can of worms of a mutable string keyed registry
22:41
<rbuckton>

i.e.,:

  1. M hands a shared struct with { mutex, condition, value } to both A and B.
  2. B locks mutex and waits on condition (unlocking the mutex)
  3. A locks mutex, writes a Foo-1 to value, and and wakes B via condition
  4. B reads value and gets a Foo-1
22:41
<rbuckton>
attachBehavior is pretty much what prepareWorker does, though prepareWorker doesn't have to do things one at a time.
22:42
<rbuckton>
And attachBehavior doesn't solve the late registration case I just posted.
22:43
<rbuckton>
Unless you are suggesting that on("message") gets called synchronously the moment B reads from value
22:44
<rbuckton>
To support the synchronous case I proposed above, you really have to establish the relationships before any work is done.
22:45
<rbuckton>
Maybe that means registration isn't just struct: { ... }. Maybe that means you have to create an instance of a StructRegistry object you pass to each worker you create, so that you explicitly establish the relationship between all of the workers.
22:46
<Mathieu Hofman>
I'm saying that in your example, between step 6 and 7, A could send a message to B with an examplar, and B could send a message to A with its examplar, and both could attach their behavior
22:46
<rbuckton>

Something like:

const structs = new StructsRegistry({ Foo, Bar });
const worker1 = new Worker("worker.js", { structs });
const worker2 = new Worker("worker.js", { structs });
22:47
<rbuckton>
That's the asynchronous case using MessagePort. I'm saying that doesn't work with the synchronous case using mutex/condition
22:47
<rbuckton>

i.e.,:

  1. M hands a shared struct with { mutex, condition, value } to both A and B.
  2. B locks mutex and waits on condition (unlocking the mutex)
  3. A locks mutex, writes a Foo-1 to value, and and wakes B via condition
  4. B reads value and gets a Foo-1
this is the synchronous case
22:47
<Mathieu Hofman>
but yes it would be nice to abstract that away to avoid this manual protocol
22:48
<Mathieu Hofman>
oh I see, yeah I don't know how you solve that one
22:49
<rbuckton>
I currently see two mechanisms: either the agents communicate with each other to find a suitable mapping just using the provided structs: {} maps, or you explicitly hand a registry off to each worker that essentially records the per-agent mappings for each struct type in the registry.
22:49
<Mathieu Hofman>
you'd have to pass the behavior definition along, possibly as a module instance that can be synchronously evaluated when creating the realm, not just the local constructor / prototype
22:49
<rbuckton>
you'd have to pass the behavior definition along, possibly as a module instance that can be synchronously evaluated when creating the realm, not just the local constructor / prototype
Why? The point of this is that you don't pass the behavior definition along.
22:50
<rbuckton>
Each thread maintains its own copy of the behavior
22:50
<rbuckton>
This is desirable since you can have a build tool perform static analysis and tree shaking to reduce overall code size that you have to load into a thread.
22:51
<rbuckton>
I'm not opposed to sharing behavior, but that does mean a lot of additional complexity with respect to module resolution, and makes things harder when it comes to checking reference identities.
22:52
<rbuckton>
Plus I may have per-thread setup I perform in the constructor of a shared struct that is side-effecting that can't be reached via a shared definition.
22:53
<Mathieu Hofman>
ok so the struct registry would itself be a shared thing. each string keyed entry would basically have a list of examplars, and the local prototype behavior to use
22:53
<rbuckton>
IMO, passing along shared behavior in a module record is a completely different direction than passing exemplars to attach behavior. They have different issues and solve the problem in different ways.
22:54
<Mathieu Hofman>
during prepare you basically add your examplar to the list, and other threads somehow lookup the examplar / type to find the right behavior to use
22:55
<rbuckton>
ok so the struct registry would itself be a shared thing. each string keyed entry would basically have a list of examplars, and the local prototype behavior to use
Maybe somewhat? StructRegistry is more like a built-in. It says "here's what M things a Foo is". In A, I use prepareWorker to say "Here's what A thinks a Foo is", and the same in B. Both B and A's agents will have access to the registry provided by M, and thus when B and A communicate, they can refer to the same registry.
22:56
<rbuckton>
The registry isn't "mutable" per-se as each Agent only cares about what was provided as a key in that agent, but the registry itself knows what each key maps to in each Agent.
22:56
<Mathieu Hofman>
yeah I'm still wondering if it can be explained in terms of attachBehavior
22:57
<Mathieu Hofman>
I think the registry is mutable in the sense that each thread needs to register its type definition to an existing entry
22:57
<rbuckton>
I could possibly model this in terms of attachBehavior and abstract it away, assuming some other information is available. I can't emulate the thread-localness I'm describing in quite the same way, but could emulate it with a lock-free data structure
22:58
<rbuckton>
Yes, but each thread can't change the entries of other threads.
22:58
<rbuckton>
They can only line up with same-named keys.
22:58
<Mathieu Hofman>
right
22:58
<rbuckton>
And we could throw runtime errors if your exemplars don't have a matching field layout.
22:59
<rbuckton>
And you can't arbitrarily add new keys to a registry in a given thread, only during initial setup.
22:59
<rbuckton>
I need to break for dinner.
23:41
<shu>
oops i had meetings and now there's a lot of backlog