Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wasm-mt] Land the webworker JSImport support #84489

Closed
lambdageek opened this issue Apr 7, 2023 · 3 comments
Closed

[wasm-mt] Land the webworker JSImport support #84489

lambdageek opened this issue Apr 7, 2023 · 3 comments
Assignees
Labels
arch-wasm WebAssembly architecture area-VM-threading-mono
Milestone

Comments

@lambdageek
Copy link
Member

lambdageek commented Apr 7, 2023

Instead of landing #83838 in a single PR, we will split it up into several separate PRs

Dependencies

Work to be landed

flowchart TD
  eb("Emscripten Bump to 3.1.34")
  subgraph "first PR"
    sw("Split PortableThreadPool.WorkerThread")
  end
  subgraph "second PR"
    ln("create an unmanaged async lifo semaphore")
    lm("add async variant of LowLevelLifoSemaphore")
  end
  subgraph "third PR"
    k("create WebWorkerEventLoop utility class")
  end
  subgraph "fourth PR"
    unw("unwind threadpool worker start function to JS")
    aw("create async browser PortableThreadPool.WorkerThread")
    sm("smoke test")
  end
  eb -.-> k
  eb -.-> unw
  sw --> aw
  ln --> lm --> aw
  k --> unw --> aw
  aw --> sm
Loading

Contributes to

Contributes to #77287, #68162, #76956

@lambdageek lambdageek added arch-wasm WebAssembly architecture area-VM-threading-mono labels Apr 7, 2023
@lambdageek lambdageek added this to the 8.0.0 milestone Apr 7, 2023
@lambdageek lambdageek self-assigned this Apr 7, 2023
@ghost
Copy link

ghost commented Apr 7, 2023

Tagging subscribers to 'arch-wasm': @lewing
See info in area-owners.md if you want to be subscribed.

Issue Details

Instead of landing #83838 in a single PR, we will split it up into several separate PRs

Dependencies

Work to be landed

flowchart TD
  eb("Emscripten Bump to 3.1.34")
  subgraph "first PR"
    sw("Split PortableThreadPool.WorkerThread")
  end
  subgraph "second PR"
    ln("create an unmanaged async lifo semaphore")
    lm("add async variant of LowLevelLifoSemaphore")
  end
  subgraph "third PR"
    k("create WebWorkerEventLoop utility class")
    aw("create async browser PortableThreadPool.WorkerThread")
  end
  eb -.-> k
  sw --> aw
  ln --> lm --> aw
  k --> aw
Loading

Contributes to

Contributes to #77287, #68162, #76956

Author: lambdageek
Assignees: lambdageek
Labels:

arch-wasm, area-VM-threading-mono

Milestone: 8.0.0

lambdageek added a commit that referenced this issue Apr 18, 2023
This is Part 1 of #84489 - landing support for async JS interop on threadpool threads in multi-threaded WebAssembly.

We will need to start the threadpool worker threads on the browser in a special way, such that they can exit back to the JS event loop, and use callbacks to run the worker loop body.

The current PR splits into a separate file the logic for starting threadpool worker threads, and the outer loop that waits for the semaphore that signals that work is available for the worker. The loop body (to be shared with the callback-based approach in a future PR) remains in PortableThreadPool.WorkerThread.cs as several new toplevel functions.

Current PR is just refactoring existing code. No functional change.

* Split PortableThreadPool.WorkerThread start and loop body

   For browser-wasm we will need to start the worker thread in a special way, and use callbacks to run the loop body.

   Current PR is just refactoring existing code. No functional change.

* Change loop to use return instead of break

* rename utility method to ShouldExitWorker
lambdageek added a commit that referenced this issue Apr 20, 2023
This is part of #84489 - landing support for async JS interop on threadpool threads in multi-threaded WebAssembly.

Provides two pieces of functionality:

1. A keepalive token that can be used to prevent the current POSIX thread from terminating when it returns from its thread start function, or from an invocation from the JS event loop.  When the last keepalive token is destroyed (assuming Emscripten isn't keeping the thread alive for other reasons) it will terminate as if by calling `pthread_exit` and the webworker will be made available to other threads

2. A `HasUnsettledInteropPromises` property that peeks `_js_owned_object_table` to see if there are any promises created by the interop subsystem that have not been fulfilled or rejected yet.



* [mono] add internal WebWorkerEventLoop utility class

Provides two pieces of functionality:

1. A keepalive token that can be used to prevent the current POSIX
thread from terminating when it returns from its thread start
function, or from an invocation from the JS event loop.  When the last
keepalive token is destroyed (assuming Emscripten isn't keeping the
thread alive for other reasons) it will terminate as if by calling
`pthread_exit` and the webworker will be made available to other
threads

2. A `HasUnsettledInteropPromises` property that peeks
`_js_owned_object_table` to see if there are any promises created by
the interop subsystem that have not been fulfilled or rejected yet.

* Use a per-thread unsettled promise count

for mono_wasm_eventloop_has_unsettled_interop_promises we
can't use the _js_owned_object_table size because it contains
other interop objects, not just promises

* remove old emscripten keepalive workaround hack

* Add a more general getter for JS interop to keep a webworker alive

And link to #85052 for more details

* fixup docs
lambdageek added a commit that referenced this issue Apr 21, 2023
…he browser (#84491)

This is part of #84489 - landing support for async JS interop on threadpool threads in multi-threaded WebAssembly.

This PR adds two things:

1. A new unmanaged `LifoSemaphoreAsyncWait` semaphore that can use Emscripten's ability to push C calls from one thread to another in order to implement a callback-based semaphore - when a thread wants to wait, it sets up a success callback and a timeout callback, and then can return to the JS event loop.  When the semaphore is released, Emscripten will trigger the callback to run on the waiting thread.  If the wait times out, the timeout callback will run.
2. A new managed `LowLevelLifoAsyncWaitSemaphore` that doesn't have the normal `Wait()` function, and instead needs to use the callback-based `PrepareAsyncWait()` function.  Also refactored `LowLevelLifoSemaphore` to pull out a common `LowLevelLifoSemaphoreBase` class to share with the async wait version.

* [wasm-mt][mono] Add new LifoSemaphoreAsyncWait C primitive

Add a new kind of LifoSemaphore that has a callback-based wait
function, instead of a blocking wait using Emscripten's ability to
send work from one webworker to another in C.

This will allow us to wait for a semaphore from the JS event loop in a
web worker.

* [wasm-mt][mono] split LowLevelLifoSemaphore into two kinds

A normal LowLevelLifoSemaphore that can do a synchronous wait
and another that can do a callback-based wait from the JS event loop

* Add LowLevelLifoSemaphoreBase

Move the counts to the base class

Move Release to the base class, make ReleaseCore abstract

* make a new LowLevelLifoAsyncWaitSemaphore for wasm-mt

* Revert unintentional package-lock.json changes

* fix possible null dereference

* use a separate icall for async wait InitInternal

instead of magic constants that are otherwise not needed in managed

* remove dead code; fixup comments

* LowLevelLifoSemaphore: decrement timeoutMs if we lost InterlockedCompareExchange

When a thread wakes after waiting for a semaphore to be released, if
it raced with another thread that is also trying to update the
semaphore counts and loses, it has to go back to waiting again.

In that case, decrement the remaining timeout by the elapsed wait time
so that the next wait is shorter.

* better timeout decrement code

* move timeoutMs == 0 check to PrepareAsyncWaitCore

make PrepareAsyncWaitCore static and remove a redundant argument
lambdageek added a commit that referenced this issue Apr 28, 2023
This is the last part of #84489 - initial runtime support for async JS interop on threadpool threads in multi-threaded WebAssembly.

Conceptually there are a few things here:

* A mechanism for the native runtime to start some threads as "exitable" so that we don't clean up when they return from their thread start function. (we will clean up when the pthreads TLS dtor runs)
* A change to make JSHostImplementation.s_csOwnedObjects a thread-static in multithreaded builds. This is because the map keys on a small integer handle that is allocated per-thread on the JS side as a handle for JS objects that are to be kept alive because managed code depends on them. (This is needed in general, but also makes the new smoke test work)
* A version of the PortableThreadPool.WorkerThread that starts as an exitable thread and uses asynchronous callbacks to wait for available threadpool work items and returns to the JS event loop periodically in order to allow JS promises to settle.
* A smoke test that does a background JS fetch and some async callbacks on it.

---


* [wasm-mt] Enable JSInterop on threadpool workers

   Implement PortableThreadPool loop using semaphore callbacks

   manage emscripten event loop from PortableThreadPool.WorkerThread

   make sure to keep the thread alive after setting up the semaphore wait.
   Cleanup the thread when exiting

   minimal sample - fetch on a background thread works

   Add WebWorkerEventLoop internal class to managed event loop keepalive

   Start threadpool threads with keepalive checks

   Add a flag to mono's thread start wrappers to keep track of threads that may not want cleanup to run after the Start function returns.

   Use the flag when starting threadpool threads.

   make minimal FetchBackground sample more like a unit test

   Set WorkerThread.IsIOPending when the current thread has unsettled JS interop promises.

   When IsIOPending is true, the worker will not exit even if it has no more work to do.  Instead it will repeatedly wait for more work to arrive or for all promises to settle.

   change minimal sample's fetch helper to artificially delay

   the delay is longer that the threadpool worker's semaphore timeout, in order to validate that the worker stays alive while there are unsettled promises

* [wasm-mt] Add background interop to smoketest

* update to use the LowLevelLifoAsyncWaitSemaphore

* adjust to renamed PortableThreadPool helper methods

* adjust to renamed WebWorkerEventLoop.HasJavaScriptInteropDependents

* extend and rationalize the smoke test a bit

   Add a test that just starts a thread and asserts that it has a different thread ID than the main thread.  This should allow us to rule out accidentally having the test pass on a single-threaded runtime

* hide some debug output

* smoke test: dispose of the ImportAsync result after the task is done

* [wasm-mt] make JSHostImplementation.s_csOwnedObjects ThreadStatic

   The integer jsHandles are not global - they are essentially indices into a JS array. So the mapping from a jsHandle to a JSObject must be per-thread.

   This fixes the thread affinity assertions in the smoketest (which were false positives - we looked up a worker's jsHandle and got back the main thread's JSObject - and then asserted that it was accessed from the wrong thread)

* remove locking on JSHostImplementation.CsOwnedObjects

   In single-threaded wasm, there is no need to lock since there is only one caller at a time.

   In multi-threaded wasm, the underlying dictionary is thread-static

* [threads] make the "external eventloop" platform independent

   It only does something on WASM, but in principle if other platforms allow us to run some code after returning from a thread start function, we could do it there, too.

* Add a Thread.HasExternalEventLoop managed property

   Set it from WebWorkerEventLoop.StartExitable.
   In native code, use it to set the `MONO_THREAD_CREATE_FLAGS_EXTERNAL_EVENTLOOP` flag when starting the thread.

* rename JSHostImplementation.ThreadCsOwnedObjects

   (used to be CsOwnedObjects)

   Rename to make it clear that it's objects owned by the current thread, not the runtime globally

* [checked] assert GC Safe mode, when returning to external eventloop
@lambdageek
Copy link
Member Author

The code has landed, but I'm keeping this issue open until I create a tracking issue with the follow-up work that we've identified during the code reviews for the various constituent PRs

@lambdageek
Copy link
Member Author

Remaining work is tracked in #85592

@ghost ghost locked as resolved and limited conversation to collaborators May 31, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
arch-wasm WebAssembly architecture area-VM-threading-mono
Projects
None yet
Development

No branches or pull requests

1 participant