-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Conversation
Tagging subscribers to 'arch-wasm': @lewing Issue DetailsThis is (anternative 10. like) proposal on how to enable threads on WASM, while allowing for blocking Feedback and questions are welcome. Contributes to #85592 Alternative to #91696
|
|
||
## C# Thread | ||
- could block on synchronization primitives | ||
- without JS interop. calling JSImport will PNSE. |
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
- Which means we will have JS proxy in UI thread of C# proxy in side-car worker.
- Comlink style.
- We could write out own and make in also spin-blocking the UI.
- But this is double dispatch on each call. Hopping over 2 threads and their main loop. This is in cases when caller was not on side-car.
- It will be slow and difficult to GC.
C) dispatch just the mono_wasm_bind_js_function
and mono_wasm_bind_cs_function
and then do the JS side of the marshaling in the UI thread. But the implementation is heavily dependent on
- memory (that's easy one)
- shared code
invoke-cs.ts
,invoke-js.ts
etc - but that is dependent on Mono C methods and emscripten methods.
- Most of them are synchronous and need to be fast. Because some of them are called per argument.
- But this proposal assumes that emscripten is not on UI thread!
- emscripten: stack alllocation, memory views (growing)
- GC and JS handles (I guess those should be UI thread local)
- various JS helpers (logging, exception handling, asserts)
- mono: string marshaling, gc roots
- mono: call dispatch to managed code: instantiate
TaskCompletionSource
etc
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.
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
- memory view update events
- per assembly
- mono_wasm_runtime_run_module_cctor
- per method binding
- BindJSFunction
- BindCSFunction
- mono_wasm_assembly_find_class
- mono_wasm_assembly_find_method
- mono_wasm_invoke_method_ref
- free
- per method invoke
- InvokeJSFunction
- InvokeImport
- mono_wasm_invoke_method_bound
- stackalloc
- per parameter when array
- mono_wasm_deregister_root/DeregisterGCRoot
- mono_wasm_register_root/RegisterGCRoot
- malloc
- per parameter instance when proxy
- release_js_owned_object_by_gc_handle for proxy of C# object
- mono_wasm_release_cs_owned_object/ReleaseCSOwnedObject for JSObject proxy of C# object
- get_managed_stack_trace_method
- per parameter instance when string
- mono_wasm_string_get_data_ref
- mono_wasm_string_from_utf16_ref
- mono_wasm_write_managed_pointer_unsafe
- mono_wasm_deregister_root
- per parameter instance when promise/task/function/delegate
- create_task_callback_method
- complete_task_method
- call_delegate_method
- MarshalPromise
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
- INTERNAL.loadLazyAssembly - async, string
- INTERNAL.loadSatelliteAssemblies - async, string
- Blazor._internal.getApplicationEnvironment, string
- receiveHotReloadAsync - async void
This needs to hit UI thread (but the payload is string/bytes, not objects)
- Blazor._internal.endInvokeDotNetFromJS
- Blazor._internal.invokeJSJson
- Blazor._internal.receiveByteArray
This needs to hit UI thread
- Blazor._internal.getPersistedState -> sync string
Could be on sidecar
- globalThis.console.debug
- globalThis.console.error
- globalThis.console.info
- globalThis.console.warn
- Blazor._internal.dotNetCriticalError
This is related to renderBatch (which we could do the same way as Blazor server does and skip this)
- MONO.getI16
- MONO.getI32
- MONO.getF32
- BINDING.js_string_to_mono_string
- BINDING.conv_string
- Blazor._internal.renderBatch
- BINDING.unbox_mono_obj - this one could be tricky
I hope this is out of scope
- ICall InvokeJS
This is a robust proposal, I could see either this or the original one working really well. I think it ends up depending on the user scenarios each proposal is best at solving. |
@elringus thanks for chiming in, appreciated. I moved our conversation here.
Something like that is what I'm considering in this "side-car" design. The other approach describes how to do the call dispatch via C#/managed/emscripten. During this research, I realized that out of 4 combinatios [JSImport/JSExport] x [Sync/Async] the only thing which need to be spin-waiting is sync JSExport (and internal variations). The other sync scenario, sync JSImport (and internal methods) could be truly blocking via Atomics. There are hairy details of managing C# stack. Also BTW: how does the comlink deal with nested structures ? Like
This is valuable feedback. With this design, I think that
I hope that Atomic wait would be faster than I have similar concerns about dispatch between C# threads. We need to measure it.
The main motivation of moving out off the UI thread, is to allow C# to have consistent and blocking .Wait , We tried to do threads in Net8 and one of the reasons we didn't deliver that, are the the implications treating one of the threads differently. That's confusing to the developer and difficult in existing code-bases. Allowing blocking everywhere would also allow us to finally implement C# crypto via browser's
That's already possible with Net8 experimental workload. It just has some rough edges, like running out of emscripten's thread pool and maybe leaking threads. |
Iirc, it uses deep clone, so yes. Alternatively, they also use transferable for types that support it and JS Proxies for callbacks (as it's not possible to clone or transfer functions to another context/thread in JS). One more thing I've forgot to mention is the frontend behavior with this kind of setup, which was another reason I've switched back to full blocking. Imagine a button with hover and active animations. With blocking, when user clicks the button, the UI thread is blocked until the underlying code is fully executed and the button remains in active (pressed) visual state during this time. While in theory it may be even desirable to not block the UI here (to get rid of slight stutter on button click), in reality UX becomes weird when user clicks the button, sees the active state, which then immediately returns to normal and then nothing happens (while the code is executed async). This also open doors to all kind of unspecified behavior, as user may accidentally click twice or interact with something else, while the call is not finished. The solution here would be blocking the interaction and authoring special CSS styles for the time when the async calls is executing, but that's additional layers of complexity.
Oh, so that's possible without all the limitations with sync/blocking calls from JS to C# ( |
Steve already answered that for Blazor #91696 (comment)
You are on your own, we would be happy to hear about trouble when you try that, but we probably would not try to fix it.
That's actually one of the difficult problems with side-car. How to make it webpack friendly. |
Sure, but if there are no useful cases for this mode, no one will bother to solve it. Even worse, users will spend time and resources adapting to this mode only to later discard all the work. Maybe some kind of warning explaining all the inherent pitfalls and additional complexity would help here.
I mean, does it run the main/entry .NET runtime on webworker (in which case all the blocking interop limitations apply, which is not useful in my case) or it runs in the same way as ST, but has an API to dispatch tasks to worker threads? |
This is for advanced user like authors of higher level UI frameworks, like Blazor and Uno.
Net 8, no emscripten doesn't work on web worker with MT build in Net8. I'm fixing it for Net9.
You can use C# thread pool. JSImport needs thread affinity of the caller and we have not exposed API for that. JSExport has the blocking problem. |
But it only works on Firefox https://wpt.fyi/results/service-workers/service-worker/fetch-request-xhr-sync.https.html?label=experimental&label=master&aligned |
closing in favor of dotnet/designs#301 |
This is (anternative 10. like) "sidecar" proposal on how to enable threads on WASM, while allowing for blocking
.Wait
,lock() { ... }
and similar from user C# code.Feedback and questions are welcome.
Contributes to #85592
Alternative to #91696