Atomics.waitUntil

Background

I am using Atomics daily as full time job to enable WASM FFI and whatnot and it's gorgeous ... but ... it requires me a double orchestration on Atomics.wait in order to grab the SAB desired length for a result first, then wait on the new SAB buffer population to retrieve results.

Today I've just thought that if a Worker can be paused, or put in idle yet blocking state for the worker itself, maybe we could have a "soft idle" mechanism that wouldn't block entirely the event loop so that postMessage dance can be allowed while still blocking the rest of the code execution up to that waitUntil call?

Proposal

The waitUntil method would be available and work exactly like wait but its argument (one of its arguments) is a promise that would either resolve or reject at some point in time. If it resolves it returns ok if it rejects it returns aborted (a new entry for the current state of affairs)

All other values and behavior around timeout, delay, or count would be the same and the only special thing here is that listeners, or at least the very next message event received in that worker, would be possible to trigger from the main thread, or the foreign thread that was handling such SAB (which is SharedArrayBuffer in case it wasn't clear).

Thoughts?

What does it solve?

Currently I need to invoke elsewhere a function, serialize its result, notify the length of such serialized result, wait again over a new SAB with enough length to fill all chars from that result, notify it's done, unserialize at the target realm and move on.

Because structuredClone doesn't offer a way to serialize in a buffer friendly way (and deserialize at the target), this is unnecessarily slow due me needing to use a library to serialize recursive + complex data and deserialize it ... it all works already, but it's unnecessarily slower than me asking to invoke something elsewhere, return the result via postMessage and get rid of the middle home-made serializer logic ... it will save ms after all, but when many worker <-> main operations happen, those ms will be seconds or more in the long run.

Previous work

We have event.waitUntil in ServiceWorkers handlers so that naming it similarly was kinda natural to me but I don't have any strong opionion about the naming here, it's the ability to improve the worker <-> main dance that matters.

Related issue on WHATWG:

  • if that gets solved, there's probably nothing to do here
  • if that gets closed, ignored, blocked, I am begging for better solutions around my daily working code, thanks!

never mind, the WHATWG request to make sense of structuredClone features just got closed as it's something people want since 2018 but until now nobody is paying attention at all.

Can TC39 be different on that regard? I don't mind seeing this closed as well, it's just this "users' feedback matters" utopia that I am after in both technical working groups.

I'm not sure I understand. Either you block the execution, or you yield to other execution such as the host dispatching events. The former is achieved by calling Atomics.wait(). The latter can be achieved with an async function awaiting Atomics.waitAsync().

The JavaScript execution model simply doesn't allow the host to start a new execution if there is an existing execution context. That kind of reentrancy is just not advisable.

1 Like

I can't wait async in some case but there is a delay timer in wait that I use for interrupts ... is it impossible to implement a way that through such interrupt I can consume (greedly) pending listeners?

while (wait(view, 0, 0, 100) === 'time-out')
  synchronizePendingListeners();

the only similar concept that comes to mind is the MutationObserver ability to grab records that haven't been passed yet through the callback ... if this would be possible it would allow me to avoid using the SharedArrayBuffer to collect the stringified version of the data, which works but it's kinda ugly.

If that's not possible, we can close this.

That would be a question for the web platform if they want to expose an API to let the program synchronously dispatch or consume some pending I/O. That seems to be going against the event loop model of the web, but maybe it's a problem that contexts agents like web worklets are already facing in some fashion since I believe they don't have a normal event loop? Regardless the JS spec does not specify I/O or how execution is scheduled, these are host responsibilities. The only requirement from the JS spec is that an execution must be started when there are no running contexts (aka the execution context stack is empty)

1 Like

not sure I fully understand but Atomics.wait works only in workers ... are workers considered an empty context/stack? :thinking:

Sorry I used the confusing "context" word to describe a worklet.

The JS spec has no notion of what kind of agent the host may implement. The only thing it knows about is whether an agent may block or not, aka whether Atomics.wait can block. All agents have an execution context stack, and JS defines host hook invariants specifying that an execution job can only start in an agent when there are no running execution context (empty stack). This assumes that the host implements some kind of event loop.

The web has different type of agents, in particular main/rendering "threads", Workers (regular, shared, service) and Worklets. The main thread is an agent that cannot block. Worklets are sort of lightweight agents that do not implement a normal event loop.

I am not the most familiar with worklets but since they don't have a regular event loop model, I was thinking that maybe the host APIs offered in them allow to perform some limited I/O without going through scheduled tasks (the event loop).

At the end of the day, what you seem to be looking for is a way to perform "postMessage I/O" in web workers without using the event loop.

1 Like

that's exactly what I do already ... I use Atomics.wait in two rounds to use the SharedArrayBuffer as the communication channel, avoiding completely the postMessage dance at the Worker level, because while waiting that cannot ever happen ... I think most of my issues would be gone if Web platform would ever consider storing into an ArrayBuffer an intermediate structuredClone that can then be retrieved on the other end but no matter what I ask to the Web platform, the answer is always the same: little interest + no time + better do it yourself.

The two ideas are also not strictly related but anyway, I can live with my current solution, it's just a bummer nobody can use that structuredClone logic outside engines internals for both IndexedDB or postMessage related dance ... but specially because it works with IndexedDB I believe it can be represented as buffer more efficiently than me converting stuff into string, populate Uint16Array views and parse back results ... anyway, this is also off-topic, thanks for the anwers though.

That is not quite the same as I suggested. While it would accomplish the same outcome for you, asking the web platform for an API to structureClone into an ArrayBuffer exposes a lot of currently internal implementation details, namely the exact binary serialization format.

On the other hand. An API like giveMeTheNextMessageIfAny(): MessageEvent would simply let you synchronously consume from the message queue without needing to reveal how that message queue works any more than it is today.

1 Like

with BSON out there since quite a while what is the issue around exposing implementation details, if I might ask? The need for a binary format to serialize even just JSON has been around forever and I'd be even OK if JSON could somehow be stored in there in a binary format and come back as clone elsewhere, it would deprecate BSON and it would help MongoDB and other JSON like storages to satisfy their requirements, thanks to the revive and stringify helpers too ... would that be that hard to have natively?

Also, IIRC NodeJS had an utils.parse and utils.stringify or anything able to represent an intermediate state for storing and reviving JS primitives (and maybe more) ... if there was a use case for that, why that connet be brought to the Web?

The issue with giveMeTheNextMessageIfAny() to me is that I do need to override users' Atomics operations details, specially the delay one, and take over that handling too, while checking on delay 0 (as performance matters too) that nothing has happened to that event loop in the meanwhile ... the more I think about it, the more I am convincing myself having a way for structured clone to be portable across realms (via buffers) would solve everything.

Also, details won't necessarily be clear to anyone because the use case is same browser / engine communicating either with the client engine (still Chromium, let's say) and its created Worker (still running on Chromium) ... if that detail won't survive WebKit or Gecko revival it's irrelevant to me (and my use case, of course), as that's not the scenario I have in mind: I need to drop user-land JS code to pass along data otherwise fine to be passed via postMessage and that's really a bummer for the complicated WASM related things I am working on these days.

let's stop guessing and see what NodeJS can offer already, and that's a v8 method too:
https://nodejs.org/api/v8.html#v8serializevalue

Why can't we have this on Web too?

There's a lot to unpack here.

Because your use case wouldn't need the binary representation of structured clone to be compatible across environments doesn't mean it wouldn't be required. The reality is that once you have bytes, you can take those anywhere or store them for later, so now you've created a format that needs to be fully specified and supported by all these environments that implement that feature.

Node.js is simply exposing an internal v8 implementation detail, which they can afford to do because they have no commitment on cross version compatibility for that module.

Regarding the web and/or JS having support for some binary encoding, that is always a possibility, but there are also a ton of available such encodings, e.g. CBOR, syrup, etc, so which one is the right one to chose? Also I'm not sure it'd be much different conceptually than text encoding / decoding a json representation, which doesn't have all the capabilities that structured cloning has.

1 Like

all of them? We have already Compression and Decompression Streams where the user is in charge of picking deflate over gzip (too bad brotli is not an option) so, if there is previous work around this topic, we can let the user decide which "encoding" is desired as long as all of them are compatible with structuredClone types?

Internally, all browsers already have a preferred (ad-hoc) choice for that, so that the API I can see is something like:

const serializer = new Serializer('CBOR' || 'syrup' || 'default');
// default menas ... whatever this browser/engine can provide itself

const buffer = serializer.serialize(anyStructuredCloneFriendlyData);
const clone = serializer.unserialize(buffer);

It doesn't even have to be synchroonus for Atomics.wait use cases, as it can land async and then be resolved into the SharedArray buffer so anything similar would work to me ... is that really so hard to implement?