Closed Bug 1467846 Opened 7 years ago Closed 3 months ago

Implement the Atomics.waitAsync proposal

Categories

(Core :: JavaScript Engine, enhancement, P3)

57 Branch
enhancement

Tracking

()

RESOLVED FIXED
140 Branch
Tracking Status
firefox140 --- fixed

People

(Reporter: afmenez, Assigned: yulia)

References

(Blocks 8 open bugs, )

Details

(Keywords: dev-doc-complete, parity-chrome, parity-safari, Whiteboard: webcompat:risk-moderate )

User Story

platform-scheduled:2025-06-30

Attachments

(30 files, 22 obsolete files)

48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
The proposal is currently in Stage 2.
We probably should not implement this *yet*. The proposal is on hold until SharedArrayBuffer becomes a thing again (I am the champion).
Priority: -- → P3

The proposal is now on Stage 3.

The first round of tests for Atomics.waitAsync have landed in Test262: https://github.com/tc39/test262/tree/master/test/built-ins/Atomics/waitAsync

Would be cool to see this for Firefox as well. Chrome is shipping waitAsync in the next release (87) and will be enabled by default. For reference https://bugs.chromium.org/p/v8/issues/detail?id=10239.

Any updates on this?

There is still some spec work that we are waiting on, to ensure that this can integrate well with wasm.

(In reply to Yulia Startsev from comment #5)

There is still some spec work that we are waiting on, to ensure that this can integrate well with wasm.

To clarify, wasm needs to do some work to support a compatible feature (and it's very early days for that), but I doubt that that work will have any significant impact on what the JS spec looks like.

Ah, my mistake. Then it might appear sooner rather than later.

Assignee: nobody → ystartsev
Assignee: ystartsev → nobody

:ccullen had some interest in this feature, so I will write a few pointers on getting started. This is a tricky feature, but doable and might be really interesting.

For Inspiration, here is how chrome approached the problem, and this revision gives an overall snapshot. Their design document and design revision is also good reading to see what problems they encountered.

V8 did a nice write up for this: https://v8.dev/features/atomics -- there is a good example there to help us get started. Some historical examples can be found here: https://github.com/tc39/proposal-atomics-wait-async/blob/master/example.html

Host hook: https://github.com/whatwg/html/pull/4613/files -- referenced in the specification here: https://tc39.es/proposal-atomics-wait-async/#sec-hostresolveinagent.

The polyfill for this uses a worker to implement the non-blocking behavior on the main thread: https://github.com/tc39/proposal-atomics-wait-async/blob/master/polyfill.js but I think we want to do a full implementation for this. But it might be a good place to look, just to get an idea of the intended behavior.

You can compare async.wait with doWait to get a grasp of things have changed, in relation to our code, as well as how the challenges that chrome ran into while doing their implementation. Also useful reading is the spec text on shared memory access.

Flags: needinfo?(ccullen)
Assignee: nobody → ccullen
Status: NEW → ASSIGNED
Attachment #9273932 - Attachment description: Bug 1467846 - Part 1: Add option for Atomics.waitAsync. r?iain! → Bug 1467846 - Part 1: Add option for Atomics.waitAsync.
Attachment #9273935 - Attachment description: Bug 1467846 - Part 4: Implement Atomics.waitAsync. r?iain! → WIP: Bug 1467846 - Part 4: Implement Atomics.waitAsync.
Attachment #9273935 - Attachment description: WIP: Bug 1467846 - Part 4: Implement Atomics.waitAsync. → Bug 1467846 - Part 4: Implement Atomics.waitAsync.
Attachment #9273933 - Attachment description: Bug 1467846 - Part 2: Update futex waiter to support asynchronous waits. r?iain! → Bug 1467846 - Part 2: Update futex waiter to support asynchronous waits.
Attachment #9273934 - Attachment description: Bug 1467846 - Part 3: Add error for specifying timeout in Atomics.waitAsync and disallow test262 waitAsync tests in the browser. r?iain! → Bug 1467846 - Part 3: Add error for specifying timeout in Atomics.waitAsync and disallow test262 waitAsync tests in the browser.
Attachment #9273936 - Attachment description: Bug 1467846 - Part 5: Implement notify for async waits. r?iain! → Bug 1467846 - Part 5: Implement notify for async waits.
Attachment #9273937 - Attachment description: Bug 1467846 - Part 6: Add shell debugging functions for shared array buffer atomic operations. r?iain! → Bug 1467846 - Part 6: Add shell debugging functions for shared array buffer atomic operations.
Attachment #9273938 - Attachment description: Bug 1467846 - Part 7: Enable waitAsync Atomics test. r?iain! → Bug 1467846 - Part 7: Enable waitAsync Atomics test.
Attachment #9273932 - Attachment description: Bug 1467846 - Part 1: Add option for Atomics.waitAsync. → WIP: Bug 1467846 - Part 1: Add option for Atomics.waitAsync. r?iain!
Attachment #9273933 - Attachment description: Bug 1467846 - Part 2: Update futex waiter to support asynchronous waits. → WIP: Bug 1467846 - Part 2: Update futex waiter to support asynchronous waits. r?iain!
Attachment #9273934 - Attachment description: Bug 1467846 - Part 3: Add error for specifying timeout in Atomics.waitAsync and disallow test262 waitAsync tests in the browser. → WIP: Bug 1467846 - Part 3: Add error for specifying timeout in Atomics.waitAsync and disallow test262 waitAsync tests in the browser. r?iain!
Attachment #9273935 - Attachment description: Bug 1467846 - Part 4: Implement Atomics.waitAsync. → WIP: Bug 1467846 - Part 4: Implement Atomics.waitAsync. r?iain!
Attachment #9273936 - Attachment description: Bug 1467846 - Part 5: Implement notify for async waits. → WIP: Bug 1467846 - Part 5: Implement notify for async waits. r?iain!
Attachment #9273937 - Attachment description: Bug 1467846 - Part 6: Add shell debugging functions for shared array buffer atomic operations. → WIP: Bug 1467846 - Part 6: Add shell debugging functions for shared array buffer atomic operations. r?iain!
Attachment #9273938 - Attachment description: Bug 1467846 - Part 7: Enable waitAsync Atomics test. → WIP: Bug 1467846 - Part 7: Enable waitAsync Atomics test. r?iain!
Attachment #9273932 - Attachment description: WIP: Bug 1467846 - Part 1: Add option for Atomics.waitAsync. r?iain! → WIP: Bug 1467846 - Part 1: Add option for Atomics.waitAsync.
Attachment #9273933 - Attachment description: WIP: Bug 1467846 - Part 2: Update futex waiter to support asynchronous waits. r?iain! → WIP: Bug 1467846 - Part 2: Update futex waiter to support asynchronous waits.
Attachment #9273934 - Attachment description: WIP: Bug 1467846 - Part 3: Add error for specifying timeout in Atomics.waitAsync and disallow test262 waitAsync tests in the browser. r?iain! → WIP: Bug 1467846 - Part 3: Add error for specifying timeout in Atomics.waitAsync and disallow test262 waitAsync tests in the browser.
Attachment #9273935 - Attachment description: WIP: Bug 1467846 - Part 4: Implement Atomics.waitAsync. r?iain! → WIP: Bug 1467846 - Part 4: Implement Atomics.waitAsync.
Attachment #9273936 - Attachment description: WIP: Bug 1467846 - Part 5: Implement notify for async waits. r?iain! → WIP: Bug 1467846 - Part 5: Implement notify for async waits.
Attachment #9273937 - Attachment description: WIP: Bug 1467846 - Part 6: Add shell debugging functions for shared array buffer atomic operations. r?iain! → WIP: Bug 1467846 - Part 6: Add shell debugging functions for shared array buffer atomic operations.
Attachment #9273938 - Attachment description: WIP: Bug 1467846 - Part 7: Enable waitAsync Atomics test. r?iain! → WIP: Bug 1467846 - Part 7: Enable waitAsync Atomics test.

Redirect a needinfo that is pending on an inactive user to the triage owner.
:sdetar, since the bug has recent activity, could you have a look please?

For more information, please visit auto_nag documentation.

Flags: needinfo?(ccullen) → needinfo?(sdetar)
Assignee: ccullen → iireland
Flags: needinfo?(sdetar)
Severity: normal → S3

The waiter list for Atomics.wait is homogeneous. With the addition of Atomics.waitAsync, the waiter list will contain both sync and async waiters. (Waiters must be notified in the order in which they waited, so it has to be a single list.) While we're refactoring this code, I added a dedicated header node to the waiter list, which lets us remove a node from the list without needing a reference to the SharedArrayRawBuffer.

Depends on D159743

We currently wait until all OffThreadPromiseTasks are resolved before shutting down the runtime. This is not ideal for something like waitAsync: if the notify never comes, then we will never shut down. This patch adds a mechanism for OffThreadPromiseTasks to opt into being cancellable, in which case we will delete them immediately on shutdown (after calling prepareForCancel on them) instead of blocking.

Depends on D159744

Atomics.notify can be called from any context, but the promise created for Atomics.waitAsync must be resolved in the context that created it. OffThreadPromiseTask will dispatch the message to the correct event loop.

When the runtime shuts down -- for example, when a worker is terminated -- we clear out the async waiters associated with that runtime. The interaction between agent termination and waiters is currently somewhat underspecified (see https://github.com/whatwg/html/issues/8276). This approach is consistent with Chrome's existing implementation.

Depends on D159745

This moves some code around and renumbers steps in preparation for the next patch.

Depends on D159746

This implements Atomics.waitAsync, excluding cases with non-zero timeouts. The remainder of this patch stack is dedicated to adding timeout support.

Breaking out the critical section into its own function made locking easier. The AutoEnterUnsafeOOMRegion is not strictly necessary yet, but a subsequent patch will add code to initialize timeouts, and my pitiful human brain was not up to the task of safely cleaning up if we OOMed halfway through initializing.

Depends on D159747

Synchronous timeouts can wake up the sleeping thread, but we need a separate thread to be woken when asynchronous timeouts fire. In the future we may be able to use the DOM TimeoutManager in browser builds, but for now (and for shell builds) I'm adding an internal TimeoutManager, with a single process-wide timeout thread.

Depends on D159748

We want to use a priority queue to keep track of pending timeouts, but the natural way to prioritize them (lowest timestamp first) is not compatible with taking the highest priority item. Instead of messing around with negative TimeDurations, it seems nicest to just update PriorityQueue to be more flexible.

This patch also adds a highest helper to peek at the highest priority item without removing it.

Depends on D159749

When a waiter is notified, or when the corresponding runtime shuts down, we have to be able to cancel a timeout. A linear scan is not the most efficient way to implement this, but it looks like the DOM TimeoutManager has O(n) insertions (it uses a linked list), so this is probably fast enough for now.

If it turns out that O(n) removal is too slow, we can get O(log(n)) removal by keeping a hashtable mapping keys to heap indices, and updating the keys whenever we swap elements. This would require refactoring PriorityQueue to expose swaps, so I'm leaving it for later.

Depends on D159751

assertHoldsCancelLocks is intended to minimize footguns from a tricky potential race condition.

CancelTimeout is useful when you have a timeout task that is racing with some other event. (In the case of Atomics.waitAsync, this is a call to Atomics.notify.) When the event occurs, we want to cancel the timeout. If the timeout fires, we want to disable the event. Because the timeout fires on a separate thread, the run method presumably needs to claim a lock to disable the event. (For waitAsync, this is the FutexAPI lock.)

However, consider what happens in the following series of events:

  1. Thread T calls Atomics.notify and claims the FutexAPI lock.
  2. A timeout expires and the timeout thread is woken up.
  3. The timeout task is removed from the priority queue. Its run method is called.
  4. The run method must claim the FutexAPI lock to clean up the waiter. We block waiting for thread T to release the lock.
  5. On thread T, we call CancelTimeout to cancel the timeout.

The timeout task has already been removed from the priority queue, so we can't cancel it. We need some way to decide whether the notify or the timeout won the race, and coordinate the cleanup.

I think the least error-prone solution is to say that the timeout always loses the race. We set a cancelled flag on the TimeoutTask, and the run method is responsible for checking that flag before doing anything. Awkwardly, that flag must be guarded by the implementation-specific mutex (eg FutexAPI), not the internal timeout manager lock. To make sure implementations of TimeoutTask get this right, assertHoldsCancelLocks is called when cancelling a timeout, and again when checking the flag.

The next patch has an example of what this looks like in practice.

Alternatives I considered and rejected:

  1. We could instead decide that the timeout always wins this race. In this case the return value of CancelTimeout would indicate whether the timeout had already fired, and the caller (eg Atomics.notify) would be responsible for doing the right thing. The current approach seemed like it put less of a burden on users of the API.
  2. It would be nice if we could return opaque cancellation tokens from AddTimeoutToTimeoutManager and pass those back in to CancelTimeout, instead of the TimeoutTask itself. I couldn't find a way to make that work, because we need access to the TimeoutTask to call assertHoldsCancelLocks.

Depends on D159752

Depends on D159753

Right now, we don't support await in worker threads in the shell: we never drain the job queue, so enqueued promise resolutions never fire.

Depends on D159754

The alternative to this patch is having each AsyncWaiter increment the refcount of the underlying SharedArrayRawBuffer, keeping it alive as long as there's an unnotified waiter.

Depends on D159755

Depends on D159756

Hey! Curious, what is the status on this?

The status is that I wrote a stack of patches that pass the test262 tests, but was not 100% confident of correctness, because there's a bunch of tricky multithreading going on. I got distracted with other work and didn't have time to shepherd them through review. We currently aren't prioritizing this very heavily, since AFAIK there's not a lot of real-world demand, but the work is mostly done, so if we had a compelling reason to prioritize this, I don't think it would take too long to get it into shape.

I am the team lead at StackBlitz for WebContainer, a Node.js runtime for the browser that allows you to run Node.js natively in the browser. I think WebContainer makes for a compelling use case especially because this has been shipped in Chrome and Safari. WebContainer is very performance critical and the fact that Firefox doesn't yet implement Atomics.waitAsync means that our kernel has to rely on a polyfill which uses a dedicated worker. The problem is that every worker adds more pressure to the tab and consumes resources, and userland code often times spawns a handful of processes (workers) as well. So anything that is more "native" such as Atomics.waitAsync that would avoid spawning more workers has a positive effect on the resource consumption of WebContainer and would also positively contribute to the performance. A decent chunk of our users use Firefox and it's unfortunate that for Firefox we still need to rely on the polyfill.

CC @bholley

Safari currently doesn't support this feature. The only engine supporting it at the moment is v8. If your web container is working without a polyfill in both Safari and Chrome, perhaps you mean a different feature? Can you give us more details about the difference in how you are running it in Safari and Firefox?

Yea it's not yet in stable for Safari but it's been shipped to TP. I mean we are still working on support for Safari and for the time being we still have to reply on the polyfill because waitAsync hasn't been shipped for stable yet.

Part of the reason for that may be because the waitAsync specification is still stage 3. They do not ship until a feature is in stage 4 for complex features, as they expressed in committee recently.

As you mention in the safari bug, you are able to polyfill in firefox, but were unable to do so in safari -- https://bugs.webkit.org/show_bug.cgi?id=241414 -- in other words the situation was more severe in the case of safari in that your web containers did not work via the polyfill. In the case here, you have a working polyfill, but it is not as performant as it should be. This is of course not ideal.

We consider performance to be an important part of the web. However at the moment we have our focus on performance elsewhere, meaning that our ability to do feature work related to TC39 is impacted for a large chunk of this year. Given that this is a stage 3 proposal, and an adequate though not ideal solution exists, it is unlikely that our current prioritization will change right now. We will discuss your case at our next meeting. As Iain mentioned, the feature is almost finished and once we have time for it again it will likely go quickly. We of course welcome contributions, and if you have some engineers with expertise in this space who have cycles to address Iain's correctness concerns, it may go faster.

From reading the safari bug, you still have blockers on safari for fully implementing your web components work, namely shared array buffer cloning. Is this still true?

Note that the current synchronous implementation of Atomics.wait is causing troubles to DevTools which is unable to execute its JS codebase while the worker is paused by this method. I provided details, with a minimal test reproducing the issue in bug 1821250 comment 16.

If the async implementation prevent these troubles, it would be an argument to move forward with the async flavor.

Otherwise some help would be appreciated to make the DevTools/DOM codebase work with both API flavors.
I'm wondering if the direct usage of pthread_cond_wait isn't bypassing the dom/ codebase, which ends up being paused unexpectedly.

Note that wasm-bindgen, which is the typical/popular way to use Rust in a web page through WASM uses a shim to provide async wait based on sync wait:
https://github.com/rustwasm/wasm-bindgen/blob/9958e2ec44651fd7b02b023439118328181b1733/crates/futures/src/task/worker.js#L4
https://github.com/rustwasm/wasm-bindgen/blob/9958e2ec44651fd7b02b023439118328181b1733/crates/futures/src/task/wait_async_polyfill.rs#L55-L56
This may broaden the issue with DevTools.

Attachment #9299320 - Attachment description: WIP: Bug 1467846: Part 13: Drain job queue in worker thread → WIP: Bug 1467846: Part 13: Support `await` in test262 worker threads

Depends on D159757

Depends on D186818

Hey all. I wanted mention that Atomics.waitAsync is now fully supported in both Chrome and Safari. We are running into issues with WebContainer due to the missing support for a native version of this and the polyfill that uses a dedicated worker doesn't fully work as waitAsync cannot be accurately polyfilled. It also causes an overhead of dedicated workers which is very unfortunate for an environment like WebContainer which is already putting some strain on the process. I think it'd be really nice if Firefox would support this natively as well.

Blocks: 1884148

The waiter list for Atomics.wait is homogeneous. With the addition of Atomics.waitAsync, the waiter list will contain both sync and async waiters. (Waiters must be notified in the order in which they waited, so it has to be a single list.) While we're refactoring this code, I added a dedicated header node to the waiter list, which lets us remove a node from the list without needing a reference to the SharedArrayRawBuffer.

We currently wait until all OffThreadPromiseTasks are resolved before shutting down the runtime. This is not ideal for something like waitAsync: if the notify never comes, then we will never shut down. This patch adds a mechanism for OffThreadPromiseTasks to opt into being cancellable, in which case we will delete them immediately on shutdown (after calling prepareForCancel on them) instead of blocking.

Atomics.notify can be called from any context, but the promise created for Atomics.waitAsync must be resolved in the context that created it. OffThreadPromiseTask will dispatch the message to the correct event loop.

When the runtime shuts down -- for example, when a worker is terminated -- we clear out the async waiters associated with that runtime. The interaction between agent termination and waiters is currently somewhat underspecified (see https://github.com/whatwg/html/issues/8276). This approach is consistent with Chrome's existing implementation.

This moves some code around and renumbers steps in preparation for the next patch.

This implements Atomics.waitAsync, excluding cases with non-zero timeouts. Most of the rest of this patch stack is dedicated to adding timeout support.

Breaking out the critical section into its own function made locking easier.

Atomics.waitAsync creates a promise that can be resolved via Atomics.notify, or after an optional timeout. The timeout is implemented using the HostEnqueueTimeoutJob hook, which queues a global task (not a microtask).

This patch adds a delay parameter to the DispatchToEventLoop callback, and implements it in cases where the infrastructure already exists (the main thread and worklets). The next patch adds an implementation in worker threads. A later patch in the stack adds an implementation for the shell.

I don't really know what I'm doing with the CC macros, but this seemed to work.

We want to use a priority queue to keep track of pending timeouts, but the natural way to prioritize them (lowest timestamp first) is not compatible with taking the highest priority item. Instead of messing around with negative TimeDurations, it seems nicest to just update PriorityQueue to be more flexible.

This patch also adds a highest helper to peek at the highest priority item without removing it.

The task/microtask implementation in the shell is a bit of a mess. In particular, where the browser expects the microtask queue to be drained before running any tasks, the shell (which only supports a very limited set of tasks) drains the task queue before draining the microtask queue. I made a few attempts at fixing this, but ran into problems. For example, the setTimeout mechanism in the test262 Atomics.notify/wait/waitAsync shell code implements timeouts using a chain of promises. If we dispatch delayed tasks to an outer task queue, and wait for the microtask queue to drain before draining the outer task queue, then the delayed task can't fire until after the promise-based setTimeout has expired.

This patch is slightly less principled, but has the benefit of actually working. At some point we should consider fixing the task implementation in the shell.

Right now, we don't support await in worker threads in the shell: we never drain the job queue, so enqueued promise resolutions never fire.

In addition, in test262 tests, we have to make sure we don't terminate the worker before it's done. This can be a problem if it's awaiting something being done in another thread. Conveniently, these tests all call $262.agent.leaving() to signal that there's nothing left to do, so we can block waiting for that.

The alternative to this patch is having each AsyncWaiter increment the refcount of the underlying SharedArrayRawBuffer, keeping it alive as long as there's an unnotified waiter.

This change is necessary to make the test262 Atomics implementation of setTimeout work with timeout tasks. Without this change, setTimeout, which uses a chain of promises, would spin the microtask loop and prevent any delayed-dispatch tasks from resolving.

This may be too detailed, but I wrote it out in an attempt to convince myself that I knew what was going on.

After writing the main patch stack, I noticed step 3 of NotifyWaiter, which specifies that resolving a promise from the same thread that created it should go straight to the microtask queue, instead of enqueuing a task.

Whiteboard: webcompat:risk-moderate

This is a simple test if anyone wants to confirm or understand how this works in other browsers and should work in Firefox.

// create a mutex with a value of 0 at index 0
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);

var b0 = document.createElement('input');
b0.type = 'button';
b0.value = 'unlock mutex';
document.body.appendChild(b0);

b0.addEventListener('pointerdown', function(e) {
        // set mutex value to 1 at index 0
        Atomics.store(int32, 0, 1);
        // notify the mutex (any wait or waitAsync) of index 0
        Atomics.notify(int32, 0);
});

// wait for index 0 to change to 1
let never_name_res_intent = Atomics.waitAsync(int32, 0, 0);

if (never_name_res_intent.async) {

        never_name_res_intent.value.then(function(fulfillment) {

                console.log('mutex fulfillment', fulfillment);
                console.log('mutex int32 changed from 0 to 1 at index 0.');

        });

}
User Story: (updated)

I am adopting this to hopefully get it across the line.

Assignee: iireland → ystartsev
Attachment #9406013 - Attachment description: Bug 1467846: Part 7: Add delay parameter to DispatchToEventLoopCallback r=arai!,#dom-worker-reviewers! → Bug 1467846: Part 7: Introduce DelayedDispatchToEventLoopCallback r=arai!,#dom-worker-reviewers!,padenot!,smaug!
Attachment #9406025 - Attachment is obsolete: true

This clears the relationships between all AsyncFutexWaiter. We discussed this as "orphaning" the
list members and having them point to themselves as lists of 1 element. But on reflection I am not
sure we gain all that much from this approach and we may as well just remove it from the list, as
future removals from a list will just be no-ops. We could also add a "isLinked" bool to avoid
additional work here, if that feels cleaner.

If I missed some subtle reason why we want a list of 1 element here instead, let me know and I can
add that back in.

Depends on D212886

Attachment #9406009 - Attachment description: Bug 1467846: Part 3: Make OffThreadPromiseTasks cancellable r=arai! → Bug 1467846: Part 3: Refactor "cancellable" tasks and allow track undispatched OffThreadPromiseTasks selectively; r=arai!
Attachment #9406015 - Attachment description: Bug 1467846: Part 9: Refactor PriorityQueue r=arai! → Bug 1467846: Part 4: Refactor PriorityQueue r=arai!

This patch is a major refactoring that introduces a concept of ownership of JS::Dispatchables via
UniquePtrs. Embeddings must now use UniquePtrs to reference Dispatchables passed by the engine.

Two static methods are introduced to Dispatchable: Run and ReleaseFailedTask.

Run is used to release the UniquePtr prior to running. We do this because we still rely on the
js_delete(this) call inside of OffThreadPromiseTask::run in cases of web assembly, namely cases when
we never dispatch to the embedding and need to clean up
1.
This will be refactored in a future step covered by Bug# 1959032.

ReleaseFailedTask similarily releases the task. This relies on the task having been registered with
the OffThreadPromiseRuntimeState class. As the only override of run is in OffThreadPromiseTask,
thus far there have been no guarantees of how this is done. This will be addressed in the next
patch.

Depends on D212878

This patch removes the live state all together. The reasoning for this is as follows:

Previously, the live set had the following responsibilities:

  • the set was a set of raw pointers. This functioned as a list of tasks that would potentially be
    dispatched and had been initialized.
  • If we attempted to dispatch, but failed to do so (such as during shutdown), we would increment the
    numFailed_ (previously termed numCanceled_) counter. However with the shift to using a UniquePtr
    in the embedding side, we now transfer ownership of the UniquePtr when a task fails to the dead()
    list. We no longer need the live list to manage this information, instead we can iterate over the
    dead Fifo.
  • In the future, the plan was the use the live list for cancellable. However, this too is better
    represented as an owned list, as the runtime can hold the token for cancellable tasks until it is
    dispatched. This becomes clearer when we implement the WaitNotifyTask, see further in the patch queue.

Now, the live set has been replaced with a numRegistered_ counter. This is the number of tasks
currently registered and not yet completed. If a task fails, or is registered as cancellable, it
will go into one of two collections for tracking owned Dispatchables that potentially need to be
cleaned up on shutdown.

In the case of Cancellables, they are blocked from using the regular DispatchResolveAndDestroy
method. They now have a special method: releaseCancellableAndDispatch, that will remove itself from
the cancellable list, and transfer ownership of the UniquePtr to the embedding. If this fails, it
will follow the same failure route as for all other dispatchables.

In the case of dead Dispatchables: They can not be redispatched, instead they wait until shutdown to
be cleared. They are cleared in a very similar way as the old live list, but with less book-keeping.

We still cannot remove the js_delete pattern, as WASM relies on it.

Depends on D244936

Attachment #9406010 - Attachment description: Bug 1467846: Part 4: Add AsyncFutexWaiter and WaitAsyncNotifyTask r=arai! → Bug 1467846: Part 7: Add AsyncFutexWaiter and WaitAsyncNotifyTask r=arai!
Attachment #9406011 - Attachment description: Bug 1467846: Part 5: Refactor in preparation for waitAsync r=arai! → Bug 1467846: Part 8: Refactor in preparation for waitAsync r=arai!
Attachment #9406012 - Attachment description: Bug 1467846: Part 6: Implement waitAsync r=arai! → Bug 1467846: Part 9: Implement waitAsync r=arai!
Attachment #9406013 - Attachment description: Bug 1467846: Part 7: Introduce DelayedDispatchToEventLoopCallback r=arai!,#dom-worker-reviewers!,padenot!,smaug! → Bug 1467846: Part 10: Introduce DelayedDispatchToEventLoopCallback r=arai!,#dom-worker-reviewers!,padenot!,smaug!
Attachment #9406014 - Attachment description: Bug 1467846: Part 8: Implement delayed dispatch for worker threads. r=#dom-worker-reviewers! → Bug 1467846: Part 11: Implement delayed dispatch for worker threads. r=#dom-worker-reviewers!
Attachment #9406016 - Attachment description: Bug 1467846: Part 10: Implement delayed dispatch in the shell r=arai! → Bug 1467846: Part 12: Implement delayed dispatch in the shell r=arai!
Attachment #9406017 - Attachment description: Bug 1467846: Part 11: Add WaitAsyncTimeoutTask r=arai! → Bug 1467846: Part 13: Add WaitAsyncTimeoutTask r=arai!
Attachment #9406019 - Attachment description: Bug 1467846: Part 12: Support `await` in test262 worker threads r=arai! → Bug 1467846: Part 14: Support `await` in test262 worker threads r=arai!
Attachment #9406020 - Attachment description: Bug 1467846: Part 13: Clean up dead waiters when SharedArrayRawBuffer dies r=arai! → Bug 1467846: Part 15: Clean up dead waiters when SharedArrayRawBuffer dies r=arai!
Attachment #9406021 - Attachment description: Bug 1467846: Part 14: Enable test262 tests r=arai! → Bug 1467846: Part 16: Enable test262 tests r=arai!
Attachment #9406022 - Attachment description: Bug 1467846: Part 15: Fix task draining r=arai! → Bug 1467846: Part 17: Fix task draining r=arai!
Attachment #9406023 - Attachment description: Bug 1467846: Part 16: Appease hazard analysis r=arai! → Bug 1467846: Part 18: Appease hazard analysis r=arai!
Attachment #9406024 - Attachment description: Bug 1467846: Part 17: Add SMDOC r=arai! → Bug 1467846: Part 19: Add SMDOC r=arai!
Attachment #9473058 - Attachment is obsolete: true
Attachment #9473320 - Attachment is obsolete: true
Attachment #9473354 - Attachment is obsolete: true
Attachment #9474731 - Attachment is obsolete: true
Attachment #9471782 - Attachment description: Bug 1467846: Part 18: Don't dispatch task for same-thread notification. r=arai!,rhunt! → Bug 1467846: Part 20: Don't dispatch task for same-thread notification. r=arai!,rhunt!
Severity: S3 → N/A
Pushed by ystartsev@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/b650a663c8bf Part 1: Add pref for Atomics.waitAsync r=arai https://hg.mozilla.org/integration/autoland/rev/33cf31b8a76d Part 2: Refactor FutexWaiter r=arai https://hg.mozilla.org/integration/autoland/rev/ba7fceabe79d Part 3: Refactor "cancellable" tasks and allow track undispatched OffThreadPromiseTasks selectively; r=arai https://hg.mozilla.org/integration/autoland/rev/42672e9179c7 Part 4: Refactor PriorityQueue r=arai https://hg.mozilla.org/integration/autoland/rev/3cd70f69a47b Part 5: Refactor JS::Dispatchable consumers to use a UniquePtr; r=arai,smaug https://hg.mozilla.org/integration/autoland/rev/4d05b5882f15 Part 6: Remove Live list from OffThreadPromiseRuntimeState; r=arai https://hg.mozilla.org/integration/autoland/rev/752f36fbf3bf Part 7: Add AsyncFutexWaiter and WaitAsyncNotifyTask r=arai https://hg.mozilla.org/integration/autoland/rev/a14e8387886b Part 8: Refactor in preparation for waitAsync r=arai https://hg.mozilla.org/integration/autoland/rev/ba97fd6dea02 Part 9: Implement waitAsync r=arai https://hg.mozilla.org/integration/autoland/rev/58143ce1c9e9 Part 10: Introduce DelayedDispatchToEventLoopCallback r=arai,dom-worker-reviewers,smaug https://hg.mozilla.org/integration/autoland/rev/83420669a560 Part 11: Implement delayed dispatch for worker threads. r=dom-worker-reviewers,smaug https://hg.mozilla.org/integration/autoland/rev/7bb5b4643f2e Part 12: Implement delayed dispatch in the shell r=arai https://hg.mozilla.org/integration/autoland/rev/688ce4dafd50 Part 13: Add WaitAsyncTimeoutTask r=arai https://hg.mozilla.org/integration/autoland/rev/f1096734b177 Part 14: Support `await` in test262 worker threads r=arai https://hg.mozilla.org/integration/autoland/rev/eed8118ec3ef Part 15: Clean up dead waiters when SharedArrayRawBuffer dies r=arai https://hg.mozilla.org/integration/autoland/rev/437ffb94e211 Part 16: Enable test262 tests r=arai https://hg.mozilla.org/integration/autoland/rev/89a8439c29a5 Part 17: Fix task draining r=arai https://hg.mozilla.org/integration/autoland/rev/5833a4f27870 Part 18: Appease hazard analysis r=arai https://hg.mozilla.org/integration/autoland/rev/35fa69f66528 Part 19: Add SMDOC r=arai https://hg.mozilla.org/integration/autoland/rev/51eb71b80c66 Part 20: Don't dispatch task for same-thread notification. r=arai,rhunt

Yulia, you're my hero.

Regressions: 1966657

FF140 MDN docs work for this can be tracked in https://github.com/mdn/content/issues/39615.

Just FMI, what's the logic of only checking the memory value when you first call the method and always returning OK if notify() is called? Docs for this having minor changes in https://github.com/mdn/content/pull/39679 but it wasn't clear to me why you ever need to check this value.

Flags: needinfo?(ystartsev)

I'm not sure what you mean by "checking the memory value when you first call the method". Do you mean this line?

"The Atomics.waitAsync() static method verifies that a shared memory location contains a given value, immediately returning with "not-equal" if the memory location does not match the given value, or "timed-out" if the timeout was set to zero."

The "not-equal" value is used for both wait and waitAsync, my understanding is it works the same way as futex does on linux: https://www.man7.org/linux/man-pages/man7/futex.7.html

I think there is a pretty good explanation of how this works in this blog post: https://eli.thegreenplace.net/2018/basics-of-futexes/

Effectively, futexes are a lower level construct, which allow efficient user-space mutex implementation. The way that wait and notify operations play into this is that they allow a user to implement a simple, efficient "test and sleep" loop. In particular, it is designed this way so that there is minimal kernal involvement. The test is done at the beginning, followed by a sleep, after which we can perform the test again (or do other work). If the thread is woken using notify, then we know that the value has changed and should either check again or start doing other work. The semantics of the actual locking mechanism are left up to the user to define. Until that point we can continue to test and sleep (or bail out) until we get what we are looking for. For example, let's say we have two competing threads. One is reading data and the other is writing data. Let's say that reading data actually takes some time. We are waiting for the data we are reading to change. There is a possibility that, between us starting to read the data, and us waiting for the data to change, the data changes. In this case -- not-equal would be returned, as the test part of test-and-sleep failed. basically, "not-equal" is for the case where there was a change written from another thread just before we start to sleep.

What is important is that these steps are all atomic. That means that they cannot be interleaved or interrupted. If atomic.wait(.., .., 0) succeeds, that means that the value at the moment that the wait begins is definitely 0, even if it changes after. Once the thread times out, it means that we can wait again, so long as no interleaved instructions changed the value since we started waiting. There is a possibility of this, especially if a user forgets to use notify after storing a new value (but this would be bad and could lead to a hang). But it might also be possible in the long-read case described above, or if a user space mutex schedules other work on the thread in the interim.

Is that what you were asking? Multi-threaded code is pretty complex

Flags: needinfo?(ystartsev)

@Yulia. Thank you, that is what I meant, and that explanation covers it.

FYI only, TL;DR
The user of this method wants to be woken up when the monitored value is changed (by any other thread).

My assumption was that the notification would be generated by some monitor code in the method, because I was thinking that the purpose was to monitor values. But the actual purpose is to synchronize threads.

So essentially we have a value in shared memory that indicates a sync point. A thread can use awaitSync() to check the value is not the expected sync value and immediately proceed. If it is the expected sync value then the thread waits for the timeout, after which it can poll again, or the promise to resolve with value "ok" which indicates another thread is calling notify() - i.e. it is at the sync point.

The detail of how you'd actually set this up with multiple sync points is beyond me, but I think this broad understanding is probably enough for now.

QA Whiteboard: [qa-triage-done-c141/b140]

My assumption was that the notification would be generated by some monitor code in the method, because I was thinking that the purpose was to monitor values. But the actual purpose is to synchronize threads.

That is right. A common pattern demonstrating this is a reader/writer pattern. You can have any number of readers and any number of writers, but the version I'm most familiar with is single producer multiple consumer.

I've created an example for you of a writer on the main thread, with several workers reading the data. It also demonstrates what the new feature enables users to do, which is to do a promise based wait on the main thread. This is better than the alternatives we had before.

Thank you so much - makes a lot more sense to me now. I've asked @Josh-Cena on MDN if he can integrate this as an even simpler example, but if not, I will try.

Attachment #9299308 - Attachment is obsolete: true
Attachment #9299309 - Attachment is obsolete: true
Attachment #9299310 - Attachment is obsolete: true
Attachment #9299311 - Attachment is obsolete: true
Attachment #9299312 - Attachment is obsolete: true
Attachment #9299313 - Attachment is obsolete: true
Attachment #9299314 - Attachment is obsolete: true
Attachment #9299315 - Attachment is obsolete: true
Attachment #9299316 - Attachment is obsolete: true
Attachment #9299317 - Attachment is obsolete: true
Attachment #9299318 - Attachment is obsolete: true
Attachment #9299319 - Attachment is obsolete: true
Attachment #9299320 - Attachment is obsolete: true
Attachment #9299321 - Attachment is obsolete: true
Attachment #9299322 - Attachment is obsolete: true
Attachment #9350180 - Attachment is obsolete: true
Attachment #9350181 - Attachment is obsolete: true
Blocks: 1507112
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: