-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[browser] WASM sidecar - multi-threading proposal #91731
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
||
<sub><sup>† Note: all the text below discusses MT build only, unless explicit about ST build.</sup></sub> | ||
|
||
## 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<T> | ||
- 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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think leaving out JSImport makes sense as a 1.0 version of this, but it's meaningful to support JSImport so that the "server" here can utilize browser APIs, to compensate for the lack of native APIs. Like for example if the "server" is able to use an offscreen canvas it would be able to access hardware accelerated rendering, which would enable developers to use stuff like browser machine learning APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be able to do that we need JSImport/JSExport interop on the UI thread.
There are 4 choices how to do that
A) dispatch via managed as described in deputy-worker proposal
B) dispatch via just JS - double proxy
C) dispatch just the
mono_wasm_bind_js_function
andmono_wasm_bind_cs_function
and then do the JS side of the marshaling in the UI thread. But the implementation is heavily dependent oninvoke-cs.ts
,invoke-js.ts
etcTaskCompletionSource
etcThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D) we could further move the boundary and have it between full dotnet.runtime.js on the UI thread and emscripten+MONO on the side-car. That would narrow it down to
most of the C methods above need GC boundary and Mono registered thread
perhaps we could marshal strings and other value types already in side-car
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was proposing that the sidecar worker be able to access JS inside the sidecar context, to be clear. Not JS objects from the main thread. No remoting, so it can still be synchronous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need some JS interop anyway for Blazor, the current surface is
This is about startup, loading and embedding
This needs to hit UI thread (but the payload is string/bytes, not objects)
This needs to hit UI thread
Could be on sidecar
This is related to renderBatch (which we could do the same way as Blazor server does and skip this)
I hope this is out of scope