Atomics `wait`/`waitAsync` as native `setTimeout`?

There's a long-standing design decision to defer timers and such to hosts. However, I found a need for a sync setTimeout and realized I could (ab)use Atomics.wait for it by simply using a thread-local shared array buffer and just pass a test value that conveniently will never meet the request. This makes me feel that yes, the primitives for native timers already exist in the language.

let timeoutId = 1
let refs = new Map()

globalThis.setTimeout = (fn, ms, ...args) => {
    let id = timeoutId++
    let handle = new Int32Array(new SharedArrayBuffer(4))
    refs.set(id, handle)
    Atomics.waitAsync(handle, 0, 0, ms).then(result => {
        refs.delete(id, handle)
        if (result === "timed-out") fn(...args)
    })
    return id
}

globalThis.clearTimeout = id => {
    let handle = refs.get(id)
    refs.delete(id)
    if (handle !== undefined) Atomics.notify(handle, 0)
}

globalThis.setInterval = (fn, ms, ...args) => {
    let id = timeoutId++
    let handle = new Int32Array(new SharedArrayBuffer(4))
    refs.set(id, handle)

    function loop(result) {
        if (result === "timed-out") {
            Atomics.waitAsync(handle, 0, 0, ms).then(loop)
            fn(...args)
        }
    }

    Atomics.waitAsync(handle, 0, 0, ms).then(loop)
    return id
}

globalThis.clearTimeout = id => {
    let handle = refs.get(id)
    refs.delete(id)
    if (handle !== undefined) Atomics.notify(handle, 0)
}

Whether this will/should lead to a native Promise.delay or Atomics.delay is something I'll decline to comment on, but I felt it was pretty important to make it clear that it's trivial to implement timer abstractions using Atomics.waitAsync.

3 Likes

Wait, has this been considered in the Atomics.waitAsync proposal?
So far certain environments (e.g. "serverless functions", "embedded systems") had the ability to opt-in to synchronous only (or at least one "event loop", emptying the Promise resolution queue once) execution,
preventing halting through setting AgentCanSuspend ( ) to false. As far as I can see, this is not the case anymore, and these environments cannot be spec compliant anymore. IMO similar to AgentCanSuspend there should really be a way to opt-out from asynchronous behavior from the environment.

Async timers that time out invoke the new host hook HostResolveInAgent, but explicitly do not enqueue anything. The clause in HostResolveInAgent that goes "The host may delay resolving promiseCapability in agentSignifier, e.g. for resource management reasons, but the promise must eventually be resolved." only applies to the extent it's observable in ECMAScript code, and so as long as a strong reference to it is retained for the rest of the program's runtime, an implementation can freely just not settle the promise as there's no way the code could detect such a condition.

True, killing the agent somewhen is always an option (and is probably always needed unless we solve the halting problem :)), and such an environment should be able to handle if an unsettled Promise is present after evaluation. It just feels strange having such a possibility in the first place (scheduling a future Promise settlement, knowing that it actually won't be settled), just like an agent that cannot suspend can still "busy wait" for something (which will probably lead to the agent being killed too).

First, my understanding is that waitAsync is still at Stage 3. And while Atomics is normative, its wait and waitAsync rely on SharedArrayBuffer, for which the constructor being added to the global object is currently normative optional. That means from an ECMA262 point of view, even when waitAsync reaches stage 4, the host is not obligated to offer a clock API, as it can simply deny SAB. Furthermore, wait can only be used when the agent can suspend, which is not a guarantee.

As @claudiameadows mentions, the way promise job enqueuing works and the fact waitAsync uses a promise resolved by the host at its convenience means there is no guarantee such a promise would resolve the way the program might have come to expect from setTimeout APIs, aka that the promise job queue would drain before the timeout job is executed, and multiple delegates would object to providing any stronger scheduling guarantees.

1 Like

To be fair, not even HTML provides particularly strong guarantees about its timers. You're still at the mercy of a number of things, and implementors are free to throttle timers as they commonly do with inactive pages.

It might be slightly stronger than Atomics.waitAsync's proposed spec text, but not by all that much if I recall correctly.

HTML does provide multiple priority queues, which other ECMA262 hosts are not obligated to do. I think the biggest assumptions of programs written for node.js or the Web is that when the setTimeout callback executes, the promise queue will have been drained along with all reactions from these promise jobs. There is no such guarantee with waitAsync and I believe there never should be.

That is true, though in practice I'm not sure very many programs even make that assumption.