Skip to content

[Proxying] Send messages via in-memory mailbox queues - #18852

Merged
tlively merged 4 commits into
mainfrom
proxying-mailbox
Mar 1, 2023
Merged

[Proxying] Send messages via in-memory mailbox queues#18852
tlively merged 4 commits into
mainfrom
proxying-mailbox

Conversation

@tlively

@tlively tlively commented Feb 25, 2023

Copy link
Copy Markdown
Member

Threads were previously notified of new work via postMessage messages that
carried pointers to the task queues to execute. There was no way to synchronously
pump or inspect these pending messages however, and there is no central registry
of all task queues for a thread, so this mechanism afforded no way to discover
or cancel pending work when a thread dies.

In preparation for implementing work cancellation, move the pending messages
into userspace by giving each thread a "mailbox", which is an em_task_queue in
the pthread struct. Instead of using postMessage, proxying queues now use the
thread mailbox API to notify threads of new work.

Internally, thread mailboxes still use postMessage to schedule work to be
executed when a thread returns to its event loop. Since the only task queues
involved in postMessages are now at known locations relative to the pthread
struct, there is no longer any need to store pointers to them in the postMessage
messages themselves. Removing these pointers works around tricky notification
and lifetime management edge cases that would have caused problems such as
dropped work or use-after-free bugs in future PRs.

When a thread dies because it exits or is canceled, it "closes" its mailbox by
decrementing a refcount and waiting to observe a refcount of 0. At this point,
the thread mailbox API ensures that no new messages will be enqueued on the
mailbox. Because the postMessage messages no longer contain task queue pointers,
it is safe to destroy the mailbox immediately after it is closed.

A user-visible behavior change this introduces is that proxied work is more
frequently completed before a thread's main function begins running, since it
no longer gets ordered behind the run message in the JS postMessage queue. A
few tests are updated accordingly.

@tlively

tlively commented Feb 28, 2023

Copy link
Copy Markdown
Member Author

The asan failures (but not other flakes) on test_pthread_dlopen_many seem to be resolved by #18776, so I don't think they're worth investigating here.

Comment thread system/lib/pthread/thread_mailbox.c Outdated
// Wait if possible and otherwise spin.
if (_emscripten_thread_supports_atomics_wait() &&
__builtin_wasm_memory_atomic_wait32(
(int*)&thread->mailbox_refcount, count, -1) == 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we no use something slightly higher level here such as emscripten_futex_wait?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I suppose so. And there's not a great reason not to do so, even though I liked programming so close to the (virtual) metal.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to use the emscripten_futex abstraction if we can.

If nothing else I think its nice to limit the places we use __builtin_wasm_memory_atomic_wait32 withing the emscripten codebase.

// can be sure cleanup has finished first.

// clang-format off
EM_ASM({setTimeout(() => Atomics.store(HEAP32, $0 >> 2, 1))}, flag);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be EM_ASM or can you just use emscripten_set_timeout?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be an EM_ASM because we need to avoid callUserCallback. This code will run after the thread runtime has exited.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, maybe a comment then?

Base automatically changed from proxying-zombie-tasks to main February 28, 2023 23:25
Threads were previously notified of new work via postMessage messages that
carried pointers to the task queues to execute. There was no way to synchronously
pump or inspect these pending messages however, and there is no central registry
of all task queues for a thread, so this mechanism afforded no way to discover
or cancel pending work when a thread dies.

In preparation for implementing work cancellation, move the pending messages
into userspace by giving each thread a "mailbox", which is an `em_task_queue` in
the pthread struct. Instead of using `postMessage`, proxying queues now use the
thread mailbox API to notify threads of new work.

Internally, thread mailboxes still use postMessage to schedule work to be
executed when a thread returns to its event loop. Since the only task queues
involved in postMessages are now at known locations relative to the pthread
struct, there is no longer any need to store pointers to them in the postMessage
messages themselves. Removing these pointers works around tricky notification
and lifetime management edge cases that would have caused problems such as
dropped work or use-after-free bugs in future PRs.

When a thread dies because it exits or is canceled, it "closes" its mailbox by
decrementing a refcount and waiting to observe a refcount of 0. At this point,
the thread mailbox API ensures that no new messages will be enqueued on the
mailbox. Because the postMessage messages no longer contain task queue pointers,
it is safe to destroy the mailbox immediately after it is closed.

A user-visible behavior change this introduces is that proxied work is more
frequently completed _before_ a thread's main function begins running, since it
no longer gets ordered behind the `run` message in the JS postMessage queue. A
few tests are updated accordingly.
@tlively
tlively force-pushed the proxying-mailbox branch from 2f288d3 to bc9b5ad Compare March 1, 2023 01:09
@tlively
tlively merged commit 3442cda into main Mar 1, 2023
@tlively
tlively deleted the proxying-mailbox branch March 1, 2023 16:30
impact-maker pushed a commit to impact-maker/emscripten that referenced this pull request Mar 17, 2023
…e#18852)

Threads were previously notified of new work via postMessage messages that
carried pointers to the task queues to execute. There was no way to synchronously
pump or inspect these pending messages however, and there is no central registry
of all task queues for a thread, so this mechanism afforded no way to discover
or cancel pending work when a thread dies.

In preparation for implementing work cancellation, move the pending messages
into userspace by giving each thread a "mailbox", which is an `em_task_queue` in
the pthread struct. Instead of using `postMessage`, proxying queues now use the
thread mailbox API to notify threads of new work.

Internally, thread mailboxes still use postMessage to schedule work to be
executed when a thread returns to its event loop. Since the only task queues
involved in postMessages are now at known locations relative to the pthread
struct, there is no longer any need to store pointers to them in the postMessage
messages themselves. Removing these pointers works around tricky notification
and lifetime management edge cases that would have caused problems such as
dropped work or use-after-free bugs in future PRs.

When a thread dies because it exits or is canceled, it "closes" its mailbox by
decrementing a refcount and waiting to observe a refcount of 0. At this point,
the thread mailbox API ensures that no new messages will be enqueued on the
mailbox. Because the postMessage messages no longer contain task queue pointers,
it is safe to destroy the mailbox immediately after it is closed.
impact-maker pushed a commit to impact-maker/emscripten that referenced this pull request Mar 17, 2023
…e#18852)

Threads were previously notified of new work via postMessage messages that
carried pointers to the task queues to execute. There was no way to synchronously
pump or inspect these pending messages however, and there is no central registry
of all task queues for a thread, so this mechanism afforded no way to discover
or cancel pending work when a thread dies.

In preparation for implementing work cancellation, move the pending messages
into userspace by giving each thread a "mailbox", which is an `em_task_queue` in
the pthread struct. Instead of using `postMessage`, proxying queues now use the
thread mailbox API to notify threads of new work.

Internally, thread mailboxes still use postMessage to schedule work to be
executed when a thread returns to its event loop. Since the only task queues
involved in postMessages are now at known locations relative to the pthread
struct, there is no longer any need to store pointers to them in the postMessage
messages themselves. Removing these pointers works around tricky notification
and lifetime management edge cases that would have caused problems such as
dropped work or use-after-free bugs in future PRs.

When a thread dies because it exits or is canceled, it "closes" its mailbox by
decrementing a refcount and waiting to observe a refcount of 0. At this point,
the thread mailbox API ensures that no new messages will be enqueued on the
mailbox. Because the postMessage messages no longer contain task queue pointers,
it is safe to destroy the mailbox immediately after it is closed.
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 26, 2025
sbc100 added a commit that referenced this pull request Aug 27, 2025
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 27, 2025
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in emscripten-core#18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in emscripten-core#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes case of checking if the runtime is alive before calling into
native code.

Fixes: emscripten-core#20067
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 27, 2025
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in emscripten-core#18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in emscripten-core#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes case of checking if the runtime is alive before calling into
native code.

Fixes: emscripten-core#20067
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 27, 2025
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in emscripten-core#18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in emscripten-core#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes case of checking if the runtime is alive before calling into
native code.

Fixes: emscripten-core#20067
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 27, 2025
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in emscripten-core#18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in emscripten-core#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes case of checking if the runtime is alive before calling into
native code.

Fixes: emscripten-core#20067
sbc100 added a commit to sbc100/emscripten that referenced this pull request Aug 27, 2025
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in emscripten-core#18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in emscripten-core#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes case of checking if the runtime is alive before calling into
native code.

Fixes: emscripten-core#20067
sbc100 added a commit that referenced this pull request Aug 28, 2025
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in #18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in
#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes care of checking if the runtime is alive before calling into
native code.

Fixes: #20067
inolen pushed a commit to inolen/emscripten that referenced this pull request Feb 13, 2026
inolen pushed a commit to inolen/emscripten that referenced this pull request Feb 13, 2026
The `checkMailbox` callback can occur after the thread has terminated.
In this case calling into native code can trigger the `makeAbortWrapper`
wrapper that is put around each native function which then results in a
"program has already aborted!" error being thrown.

Once solution to this is to make sure that the function which are called
do not have `makeAbortWrapper` applied to them.

This was the technique I used in emscripten-core#18754, but the list of functions
became stale when emscripten_proxy_execute_task_queue was removed in
emscripten-core#18852.

A better solution is to wrap to whole function in callUserCallback,
which takes care of checking if the runtime is alive before calling into
native code.

Fixes: emscripten-core#20067
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants