From 4b23e1e8eec1179212971f357fb4f2165b9364a1 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 7 Sep 2023 15:11:46 +0200 Subject: [PATCH] alternative 10 --- docs/design/mono/wasm-threads.md | 158 +++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/design/mono/wasm-threads.md diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md new file mode 100644 index 0000000000000..bcea9bbd11b3c --- /dev/null +++ b/docs/design/mono/wasm-threads.md @@ -0,0 +1,158 @@ +# Multi-threading on browser + +## Goals + - CPU intensive workloads on dotnet thread pool + - enable blocking .Wait APIs from C# user code + - Current public API throws PNSE for it + - This is core part on MT value proposition. + - If people want to use existing MT code-bases, most of the time, the code is full of locks. People want to use existing code as is. + - allow HTTP and WS C# APIs to be used from any thread + - Underlying JS object have thread affinity + - don't change/break single threaded build. † + - don't try to block on UI thread. + +† Note: all the text below discusses MT build only, unless explicit about ST build. + +## Design proposal TL;DR - Alternative 10 +20. MAUI/BlazorWebView ... is the same thing +21. execute whole WASM runtime on a worker. Blocking is fine there. +22. The UI thread will only have `blazor.server.js` with small modifications. +23. This needs new marketing name! -> `WASM server` ??? + +# Detailed design + +## Blazor - what changes vs MAUI server +- it still needs to compile for WASM target +- it will have threadpool and blocking C# `.Wait` and `lock()` +- it would not have Socket or DB connection + +## Blazor - what changes vs "Blazor WASM" +- "onClick" would be asynchronous, same way as in Blazor server +- Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) would not be available +- Blazor's [`IJSUnmarshalledRuntime`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) would not be available +- no JavaScript APIs on `globalThis.Blazor` or `globalThis.MONO` +- no runtime JS interop +- no runtime on UI thread +- Blazor united: progressive switch from remote server to WASM server, after WASM binaries arrived + +## Blazor startup +- we will need to start emscripten on a worker and `postMessage` there all the configuration. + - this is possibly the biggest effort +- probably no `Blazor.start`, `start.withResourceLoader` with returning `Response` object +- we may keep `dotnet.js` loader to run in the UI + - TODO: prototype + +### Blazor `renderBatch` +- streaming bytes - as Blazor server does + - we use [RenderBatchWriter](https://github.com/dotnet/aspnetcore/blob/045afcd68e6cab65502fa307e306d967a4d28df6/src/Components/Shared/src/RenderBatchWriter.cs) in the WASM + - we use `blazor.server.js` to render it + - preferred if fast enough + - could be ok to render batch by another thread while UI is rendering +- plan b) use existing `SharedArrayBuffer` + - WASM would: + - stop the GC + - send the "read-memory-now" + - await for "done" message + - enable GC + - UI could duplicate `conv_string` and `unbox_mono_obj` dark magic and use it + +## Blazor JS interop +- will work as usual on "Blazor server" + +## runtime JS Interop +- would be available only to workers, but it would not have access to UI thread and DOM +- could call sync and async calls both directions, promises and callbacks + +## HTTP & WS +- would be running on a worker, will be accessible from all C# threads + - we will overcome thread affinity internally + +## C# Thread +- could block on synchronization primitives +- without JS interop. calling JSImport will PNSE. + +## C# Threadpool Thread +- could block on synchronization primitives +- without JS interop. calling JSImport will PNSE. + +## Blocking problem +1. If you have multithreading, any thread might need to block while waiting for any other to release a lock. + - locks are in the user code, in nuget packages, in Mono VM itself + - there are managed and un-managed locks + - in single-threaded build of the runtime, all of this is NOOP. That's why it works on UI thread. +2. UI thread in the browser can't synchronously block + - you can spin-lock but it's bad idea. + - Deadlock: when you spin-block, the JS timer loop and any messages are not pumping. But code in other threads may be waiting for some such event to resolve. + - It eats your battery + - Browser will kill your tab at random point (Aw, snap). + - It's not deterministic and you can't really test your app to prove it harmless. + - all the other threads/workers could synchronously block +3. JavaScript engine APIs and objects have thread affinity. The DOM and few other browser APIs are only available on the main UI "thread" + - and so, you need to have some way how to talk to UI + +This is the main reason why we can't run MT dotnet also on UI thread. + +## Alternatives +- "deputy thread" proposal https://github.com/dotnet/runtime/pull/91696 + +## Debugging +- VS debugger would work as usual +- Chrome dev tools would only see the events coming from `postMessage` +- Chrome dev tools debugging C# could be bit different, it possibly works already. The C# code would be in different node of the "source" tree view + +## Non-blazor +- does Uno have similar "render from distance" architecture ? + +## Open questions +- when MT emscripten starts on a WebWorker, does it know that it doesn't have to spin-block there ? + +# Further improvements + +## JSWebWorker with JS interop +- is C# thread created and disposed by new API for it +- could block on synchronization primitives +- there is JSSynchronizationContext installed on it + - so that user code could dispatch back to it, in case that it needs to call JSObject proxy (with thread affinity) + +## Promise, Task, Task +- passing Promise should work everywhere. +- when marshaled to JS they bind to specific `Promise` and have affinity +- the `Task.SetResult` need to be marshaled on thread of the Promise. + +## JSObject proxy +- has thread affinity, marked by private ThreadId. + - in deputy worker, it will be always UI thread Id + - the JSHandle always belongs to UI thread +- `Dispose` need to be called on the right thread. + - how to do that during GC/finalizer ? + - should we run finalizer per worker ? +- is it ok for `SynchronizationContext` to be public API + - because it could contain UI thread SynchronizationContext, which user code should not be dispatched on. + +## should we hide `SynchronizationContext` inside of the interop generated code. +- needs to be also inside generated nested marshalers +- is solution for deputy's SynchronizationContext same as for JSWebWorker's SynchronizationContext, from the code-gen perspective ? +- how could "HTTP from any C# thread" redirect this to the thread of fetch JS object affinity ? +- should generated code or the SynchronizationContext detect it from passed arguments ? +- TODO: figure out backward compatibility of already generated code. Must work on single threaded +- why not make user responsible for doing it, instead of changing generator ? + - I implemented MT version of HTTP and WS by calling `SynchronizationContext.Send` and it's less than perfect. It's difficult to do it right: Error handling, asynchrony. + +## SynchronizationContext +- we will need public C# API for it, `JSHost.xxxSynchronizationContext` +- maybe `JSHost.Post(direction, lambda)` without exposing the `SynchronizationContext` would be better. + - we could avoid it by generating late bound ICall. Very ugly. +- on a JSWebWorker + - to dispatch any calls of JSObject proxy members + - to dispatch `Dispose()` of JSObject proxy + - to dispatch `TaskCompletionSource.SetResult` etc + +### dispatch alternatives +- we could use emscripten's `emscripten_dispatch_to_thread_async` or JS `postMessage` +- the details on how to interleave that with calls to `ToManaged` and `ToJS` for each argument may be tricky. + +Related Net8 tracking https://github.com/dotnet/runtime/issues/85592 + +# TODO +- [ ] experiment with `test-main.js` to do the same +- [ ] experiment with Blazor to try easy version without JS extensibility concerns \ No newline at end of file