From 739f63360ddb95bda491f6dbab69846b1b6b9ad7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 6 Sep 2023 20:50:11 +0200 Subject: [PATCH 01/11] wip --- docs/design/mono/wasm-threads.md | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 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 00000000000000..ac8fb7049eed77 --- /dev/null +++ b/docs/design/mono/wasm-threads.md @@ -0,0 +1,125 @@ +# Multi-threading on browser + +## Goals + - don't change/break single threaded build. † + - JS interop + - both sync and async calls and callbacks + - sync calls from JS to C# are part of the problem, see below + - from UI thread to "some C# main thread" and back + - from dedicated worker to it's JavaScript state and back + - CPU intensive workloads on dotnet thread pool + - enable blocking .Wait APIs from C# user code + - Current public API throws PNSE for it + - allow HTTP and WS C# APIs to be used from any thread + - Underlying JS object have thread affinity + +† Note: all the text below discusses MT build only, unless explicit about ST build. + +# Problem +1. If you have multithreading, any thread might need to block while waiting for any other to release a lock. + - in 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. +2. UI thread in the browser can't synchronously block + - you can spin-lock but it's bad idea. + - 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 C# interop with UI, but you can't block there. + +## Design proposal + +### TL;DR +4. execute C# code on "deputy worker", executing all user C# on behalf of the UI JavaScript +5. throw PNSE when UI JavaScript would call in any synchronous JSExport or callback to C#. This will prevent Mono from trying to randomly block the call. + +### Alternatives +10. create emscripten engine on worker +- this is similar to 4. but not feasible +- it would break lot of existing JavaScript APIs +- it would make startup callbacks on wrong thread (blazor JS integration) +- we would have to re-write Blazor renderBatch +11. throw PNSE any time C# code needs to block +- It's not deterministic and you can't really test your app to prove it harmless. +12. throw PNSE any time C# code or VM code needs needs to block +- Mono VM needs to hold lock while allocating memory, even on the UI thread. + +## Design proposal details + +### UI thread +- this is the main browser "thread", the one with DOM on it +- will start emscripten as usual +- will create Deputy worker as C# thread +- dispatch execution of C# `Task Main()` to the deputy worker. +- dispatch all async JSImport calls to the deputy worker. +- dispatch all async callbacks to the deputy worker. +- throw PNSE on any JSImport call from JS +- it will be valid C# thread, but not used directly by user code. + - we will try to prevent user code from running on it and from needing to do so +- we will spin lock only for Mono VM code + - we assume that Mono VM will block only shortly for operations like: + - alloc/free memory + - transform IL -> IR and update Mono VM shared state + - we will spin lock before Blazor `renderBatch` + - to wait for "pause GC" + - we will spin lock during GC, if we hit barrier + - TODO: is that short enough ? + - we should never block for file operations or for network operations + - TODO: how to prove it ? + +### Deputy worker +- executing all **user C# code** on behalf of the UI JavaScript "thread" + - that is also C# entry point `Task Main()` or `void Main()` + - `void Main()` would return promise that never resolves to UI JavaScript +- doesn't expose JavaScript state to user code. + - as optimization we could consider running HTTP and WS client here, instead of UI thread. +- has SynchronizationContext installed on it + - So that C# calls could be dispatched to it by runtime +- throw PNSE on attempt to marshal sync C# delegate to UI JavaScript +- can run C# finalizers +- will run GC + +### WebWorker with JS interop +- is C# thread created and disposed by new API for it +- 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) + +### C# Thread +- without JS interop. calling JSImport will PNSE. + +### C# Threadpool Thread +- without JS interop. calling JSImport will PNSE. + +### JSImport + +### JSExport + +### Promise + +### Task, Task + +### Blazor + - provide SynchronizationContext which would dispatch [`Component.InvokeAsync`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) to + - Blazor renderBatch will continue working even with legacy interop in place. + - Because it only reads memory and it doesn't call back to Mono VM. + - throw PNSE from Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) + - throw PNSE from Blazor's any call to [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) + +## Current state 2023 Sep + - we have pre-allocated pool of webWorker which are mapped to pthread dynamically. + - we can configure pthread to keep running after synchronous thread_main finished. That's necessary to run any async tasks involving JavaScript interop. + - GC is running on UI thread/worker. + - legacy interop has problems with GC boundaries. + - JSImport & JSExport work + - There is private JSSynchronizationContext implementation which is too synchronous + - There is draft of public C# API for creating WebWorker with JS interop. It must be dedicated un-managed resource, because we could not cleanup JS state created by user code. + - There is MT version of HTTP & WS clients, which could be called from any thread but it's also too synchronous implementation. + - Many unit tests fail on MT https://github.com/dotnet/runtime/pull/91536 + +## Task breakdown + + - [ ] make C# finalizers work + + +Tracking https://github.com/dotnet/runtime/issues/85592 \ No newline at end of file From f8e7c8cfa8d4816df28b68ddcf43f5e9a0fcff7f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 6 Sep 2023 23:30:22 +0200 Subject: [PATCH 02/11] wip --- docs/design/mono/wasm-threads.md | 184 ++++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 41 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index ac8fb7049eed77..95850e3c7e2ab4 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -1,7 +1,6 @@ # Multi-threading on browser ## Goals - - don't change/break single threaded build. † - JS interop - both sync and async calls and callbacks - sync calls from JS to C# are part of the problem, see below @@ -12,44 +11,50 @@ - Current public API throws PNSE for it - 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. -# Problem +## Problem 1. If you have multithreading, any thread might need to block while waiting for any other to release a lock. - - in 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. + - 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. - - It eats your battery + - you can spin-lock but it's bad idea. + - 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 C# interop with UI, but you can't block there. -## Design proposal - -### TL;DR +## Design proposal TL;DR 4. execute C# code on "deputy worker", executing all user C# on behalf of the UI JavaScript -5. throw PNSE when UI JavaScript would call in any synchronous JSExport or callback to C#. This will prevent Mono from trying to randomly block the call. +5. throw PNSE when UI JavaScript would call in any synchronous JSExport or callback to C#. †† -### Alternatives +†† This will prevent user C# code from trying to randomly synchronously block the caller on UI thread. + +## Alternatives 10. create emscripten engine on worker - this is similar to 4. but not feasible - it would break lot of existing JavaScript APIs - it would make startup callbacks on wrong thread (blazor JS integration) -- we would have to re-write Blazor renderBatch +- we would have to re-write Blazor's `renderBatch` to bytes streaming. 11. throw PNSE any time C# code needs to block - It's not deterministic and you can't really test your app to prove it harmless. 12. throw PNSE any time C# code or VM code needs needs to block - Mono VM needs to hold lock while allocating memory, even on the UI thread. +13. modify `ConfigureAwait()`, work queue etc, to never dispatch to another thread. +- this would probably break user code expectations about dynamic behavior of tasks and how they run in parallel with each other. -## Design proposal details +# Design proposal details -### UI thread +## UI thread - this is the main browser "thread", the one with DOM on it - will start emscripten as usual + - this includes C# which runs during mono startup - will create Deputy worker as C# thread - dispatch execution of C# `Task Main()` to the deputy worker. - dispatch all async JSImport calls to the deputy worker. @@ -67,59 +72,156 @@ - TODO: is that short enough ? - we should never block for file operations or for network operations - TODO: how to prove it ? - -### Deputy worker + - Could we unregister the thread from Mono VM ? No, because we need the C# to dispatch calls in both directions in +- it will actually execute small chunks of C# code + - the pieces which are in the generated code of the JSExport + - containing dispatch to deputy worker's synchronization context +- could sync void C# methods be dispatched as fire and forget ? + - no, because that would break the contract that they are blocking until finished. + - also the errors would not propagate +- TODO: is there anything special about error propagation over the interop/thread boundary ? + +## Deputy worker +- this is new concept introduced here. I needed some name for it 🤷‍♂️ - executing all **user C# code** on behalf of the UI JavaScript "thread" - that is also C# entry point `Task Main()` or `void Main()` - `void Main()` would return promise that never resolves to UI JavaScript +- this thread **could block** on synchronization primitives just fine! - doesn't expose JavaScript state to user code. - - as optimization we could consider running HTTP and WS client here, instead of UI thread. + - because JSHandle has thread affinity and it's unique per JS thread. + - as optimization we could consider running HTTP and WS client here, instead of UI thread. But JSHandle problem. - has SynchronizationContext installed on it - So that C# calls could be dispatched to it by runtime - throw PNSE on attempt to marshal sync C# delegate to UI JavaScript - can run C# finalizers - will run GC +- this cross-threading dispatch will have performance impact for the JS interop. + - TODO: measure how much + - this should not impact Blazor `renderBatch` perf. -### WebWorker with JS interop +## 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) -### C# Thread +## C# Thread +- could block on synchronization primitives - without JS interop. calling JSImport will PNSE. -### C# Threadpool Thread +## C# Threadpool Thread +- could block on synchronization primitives - without JS interop. calling JSImport will PNSE. -### JSImport - -### JSExport - -### Promise - -### Task, Task - -### Blazor - - provide SynchronizationContext which would dispatch [`Component.InvokeAsync`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) to - - Blazor renderBatch will continue working even with legacy interop in place. - - Because it only reads memory and it doesn't call back to Mono VM. - - throw PNSE from Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) - - throw PNSE from Blazor's any call to [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) +## JSImport and marshaled JS functions +- both sync and async could be called on all threads +- sync: when called from C# it will use `SynchronizationContext.Send` and block caller. +- async: when called from C# it will use `SynchronizationContext.Post` and marshal promise immediately. +- when this is worker -> worker, `SynchronizationContext` should invoke it inline + +## JSExport & C# delegates +- sync: will throw PNSE if called from UI JavaScript +- sync: will just work when called from JSWebWorker JavaScript +- async JSExport: will work on all threads. Will marshal promise and return immediately. +- async Delegate: are there any async callback possible yet ? The code gen doesn't support it yet in Net8. +- `getAssemblyExports` need to bind JS on UI thread, but register on deputy thread +- hide `SynchronizationContext.Send` and `SynchronizationContext.Post` inside of the generated code. + - fast on worker -> worker + +## Promise +- passing promise should work everywhere. +- from UI javaScript it would be passed as Task to deputy worker +- open question: passing JS promise to deputy should be fine. But does the `resolve()` need to block UI thread ? + +## Task, Task +- passing Task should work everywhere. +- when marshaled to JS they bind to specific Promise and have affinity + - the `SetResult` need to be marshaled on thread of the Promise. + - The proxy of the Promise knows which `SynchronizationContext` to dispatch to. + - on UI thread it's the UI thread's SynchronizationContext, not deputy's + - TODO: could same task be marshaled to multiple JS workers ? + +## 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 disposed on. + +## JSHost.GlobalThis, JSHost.DotnetInstance, JSHost.ImportAsync +- calls will be dispatched from deputy thread to UI JavaScript +- on JSWebWorker call will stay on the same thread. + +## SynchronizationContext +- we will need public C# API for it, `JSHost.xxxSynchronizationContext` + - we could avoid it by generating late bound ICall. Very ugly. +- hide `SynchronizationContext.Send` and `SynchronizationContext.Post` inside of the 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: explain why not make user responsible for doing it, instaed of changing generator + - TODO: figure out backward compatibility of already generated code. Must work on single threaded +- on a JSWebWorker + - to dispatch any calls of JSObject proxy members + - to dispatch `Dispose()` of JSObject proxy + - to dispatch `TaskCompletionSource.SetResult` etc +- on the UI thread + - same as above + - as alternative we could only have there emscripten C dispatcher + - it will need some public API any way, to be called from generated code. +- on the deputy thread + - to dispatch async calls from UI thread to it + +### Blazor - what breaks +- as compared to single threaded runtime, the major difference would be no synchronous callbacks. + - for example from DOM `onClick`. This is one of the reasons people prefer WASM over Blazor Server. + - but there is really [no way around it](#problem), because you can't have both MT and sync calls from UI. +- implement Blazor's `WebAssemblyDispatcher` to dispatch [`Component.InvokeAsync`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) to deputy thread. + - process feedback from https://github.com/dotnet/aspnetcore/pull/48991 and make more async +- Blazor renderBatch will continue working even with legacy interop in place. + - Because it only reads memory and it doesn't call back to Mono VM. +- throw PNSE from Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) +- throw PNSE from Blazor's any call to [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) ## Current state 2023 Sep - - we have pre-allocated pool of webWorker which are mapped to pthread dynamically. + - we have pre-allocated pool of browser Web Workers which are mapped to pthread dynamically. - we can configure pthread to keep running after synchronous thread_main finished. That's necessary to run any async tasks involving JavaScript interop. - GC is running on UI thread/worker. - legacy interop has problems with GC boundaries. - JSImport & JSExport work - There is private JSSynchronizationContext implementation which is too synchronous - - There is draft of public C# API for creating WebWorker with JS interop. It must be dedicated un-managed resource, because we could not cleanup JS state created by user code. + - There is draft of public C# API for creating JSWebWorker with JS interop. It must be dedicated un-managed resource, because we could not cleanup JS state created by user code. - There is MT version of HTTP & WS clients, which could be called from any thread but it's also too synchronous implementation. - Many unit tests fail on MT https://github.com/dotnet/runtime/pull/91536 + - there are MT C# ref assemblies, which don't throw PNSE for MT build of the runtime for blocking APIs. ## Task breakdown - - - [ ] make C# finalizers work - - -Tracking https://github.com/dotnet/runtime/issues/85592 \ No newline at end of file +- [ ] rename `WebWorker` API to `JSWebWorker` ? +- [ ] design details of JSImport binding, allocation, asynchrony +- [ ] design details of JSExport binding, allocation, asynchrony +- [ ] `ToManaged(out Task)` to be called before the actual JS method +- [ ] public API for `JSHost.SynchronizationContext` which could be used by code generator. +- [ ] change the code gen for JSImport +- [ ] change the code gen for JSExport +- [ ] reimplement `JSSynchronizationContext` to be more async +- [ ] implement Blazor's `WebAssemblyDispatcher` +- [ ] reimplement HTTP and WS with the new code gen without direct SynchronizationContext use + - [ ] there is synchronous callback from JS event to C# in HTTP code. +- [ ] make C# finalizers work +- [ ] throw PNSE - fail fast, so that users discover limits in the dev loop + - [ ] on any MT use of `mono_bind_static_method` from legacy interop. + - Because it's synchronous. It throws on JSWebWorker already. + - [ ] on UI synchronous JSImport + - [ ] on UI synchronous C# delegate callback + - [ ] throw fatal if somehow C# code was blocking on UI thread. +- [ ] optinal: make underlying emscripten WebWorker pool allocation dynamic +- [ ] optinal: implement async function/delegate marshaling in JSImport/JSExport parameters. +- [ ] optinal: enable blocking HTTP/WS APIs +- [ ] optinal: enable lazy DLL download by blocking the caller +- [ ] measure perf impact + +Related Net8 tracking https://github.com/dotnet/runtime/issues/85592 \ No newline at end of file From 16eda7388e0be7905b2639b1ae78e17b7752f2d4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 6 Sep 2023 23:36:56 +0200 Subject: [PATCH 03/11] wip --- docs/design/mono/wasm-threads.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index 95850e3c7e2ab4..d14dde996110f0 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -188,6 +188,10 @@ - throw PNSE from Blazor's any call to [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) ## Current state 2023 Sep + - we already ship MT version of the runtime in the wasm-tools workload. + - It's enabled by `true` and it requires COOP HTTP headers. + - It will serve extra file `dotnet.native.worker.js`. + - This will also start in Blazor project, but UI rendering would not work. - we have pre-allocated pool of browser Web Workers which are mapped to pthread dynamically. - we can configure pthread to keep running after synchronous thread_main finished. That's necessary to run any async tasks involving JavaScript interop. - GC is running on UI thread/worker. From 25c91adedd96544ad6a63584bf3850576bb5bf02 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 6 Sep 2023 23:51:29 +0200 Subject: [PATCH 04/11] wit --- docs/design/mono/wasm-threads.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index d14dde996110f0..5285d0b35b2448 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -176,9 +176,9 @@ - on the deputy thread - to dispatch async calls from UI thread to it -### Blazor - what breaks +### Blazor - what breaks when MT build - as compared to single threaded runtime, the major difference would be no synchronous callbacks. - - for example from DOM `onClick`. This is one of the reasons people prefer WASM over Blazor Server. + - for example from DOM `onClick`. This is one of the reasons people prefer ST WASM over Blazor Server. - but there is really [no way around it](#problem), because you can't have both MT and sync calls from UI. - implement Blazor's `WebAssemblyDispatcher` to dispatch [`Component.InvokeAsync`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) to deputy thread. - process feedback from https://github.com/dotnet/aspnetcore/pull/48991 and make more async From 997483102b9cae1c2ba633079998433bdc39f6d7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 6 Sep 2023 23:54:52 +0200 Subject: [PATCH 05/11] wip --- docs/design/mono/wasm-threads.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index 5285d0b35b2448..8e51b13cc5220e 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -176,7 +176,7 @@ - on the deputy thread - to dispatch async calls from UI thread to it -### Blazor - what breaks when MT build +## Blazor - what breaks when MT build - as compared to single threaded runtime, the major difference would be no synchronous callbacks. - for example from DOM `onClick`. This is one of the reasons people prefer ST WASM over Blazor Server. - but there is really [no way around it](#problem), because you can't have both MT and sync calls from UI. @@ -187,7 +187,7 @@ - throw PNSE from Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) - throw PNSE from Blazor's any call to [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) -## Current state 2023 Sep +# Current state 2023 Sep - we already ship MT version of the runtime in the wasm-tools workload. - It's enabled by `true` and it requires COOP HTTP headers. - It will serve extra file `dotnet.native.worker.js`. From e3e76d2f0e6712184e51a22c9a6bd670181bba7f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 7 Sep 2023 07:38:11 +0200 Subject: [PATCH 06/11] fix blazor --- docs/design/mono/wasm-threads.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index 8e51b13cc5220e..ec4c9dc41e04e4 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -184,8 +184,9 @@ - process feedback from https://github.com/dotnet/aspnetcore/pull/48991 and make more async - Blazor renderBatch will continue working even with legacy interop in place. - Because it only reads memory and it doesn't call back to Mono VM. -- throw PNSE from Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) -- throw PNSE from Blazor's any call to [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) +- Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) should still work, because it's C#->JS direction +- Blazor's [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) should still work, because it's C#->JS direction +- TODO: Review Blazor's JavaScript APIs! # Current state 2023 Sep - we already ship MT version of the runtime in the wasm-tools workload. From 923b6c4bcaf3b6656e23b5d3bb01291f9df34dc4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 7 Sep 2023 09:37:50 +0200 Subject: [PATCH 07/11] feedback --- docs/design/mono/wasm-threads.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index ec4c9dc41e04e4..89c1c5a6647be7 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -23,6 +23,7 @@ - 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. @@ -129,7 +130,7 @@ - fast on worker -> worker ## Promise -- passing promise should work everywhere. +- passing Promise should work everywhere. - from UI javaScript it would be passed as Task to deputy worker - open question: passing JS promise to deputy should be fine. But does the `resolve()` need to block UI thread ? @@ -157,6 +158,7 @@ ## 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. - hide `SynchronizationContext.Send` and `SynchronizationContext.Post` inside of the generated code. - needs to be also inside generated nested marshalers @@ -176,6 +178,10 @@ - on the deputy thread - to dispatch async calls from UI thread to it +### 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. + ## Blazor - what breaks when MT build - as compared to single threaded runtime, the major difference would be no synchronous callbacks. - for example from DOM `onClick`. This is one of the reasons people prefer ST WASM over Blazor Server. @@ -223,7 +229,7 @@ - [ ] on UI synchronous JSImport - [ ] on UI synchronous C# delegate callback - [ ] throw fatal if somehow C# code was blocking on UI thread. -- [ ] optinal: make underlying emscripten WebWorker pool allocation dynamic +- [ ] optinal: make underlying emscripten WebWorker pool allocation dynamic, or provide C# API for that. - [ ] optinal: implement async function/delegate marshaling in JSImport/JSExport parameters. - [ ] optinal: enable blocking HTTP/WS APIs - [ ] optinal: enable lazy DLL download by blocking the caller From 831eb063f6491959a40cee16f3341824a283a13a Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 7 Sep 2023 10:07:41 +0200 Subject: [PATCH 08/11] wip --- docs/design/mono/wasm-threads.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index 89c1c5a6647be7..020976ca724987 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -150,7 +150,7 @@ - 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 disposed on. + - because it could contain UI thread SynchronizationContext, which user code should not be dispatched on. ## JSHost.GlobalThis, JSHost.DotnetInstance, JSHost.ImportAsync - calls will be dispatched from deputy thread to UI JavaScript @@ -165,8 +165,9 @@ - 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: explain why not make user responsible for doing it, instaed of changing generator - 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. - on a JSWebWorker - to dispatch any calls of JSObject proxy members - to dispatch `Dispose()` of JSObject proxy From 455f3ab952f1a93fce7bab59ec4e8b3d10c0418f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 7 Sep 2023 10:42:19 +0200 Subject: [PATCH 09/11] feedback --- docs/design/mono/wasm-threads.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index 020976ca724987..eac334bca5b02c 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -2,8 +2,10 @@ ## Goals - JS interop - - both sync and async calls and callbacks - - sync calls from JS to C# are part of the problem, see below + - async calls and callbacks + - sync calls from C# to JS + - sync calls from JS to C# + - this proposal rejects this goal in order to solve other goals. See below. - from UI thread to "some C# main thread" and back - from dedicated worker to it's JavaScript state and back - CPU intensive workloads on dotnet thread pool @@ -44,7 +46,7 @@ - it would make startup callbacks on wrong thread (blazor JS integration) - we would have to re-write Blazor's `renderBatch` to bytes streaming. 11. throw PNSE any time C# code needs to block -- It's not deterministic and you can't really test your app to prove it harmless. +- throwing PNSE (on lock attempt or spin-block) is easy, but it doesn't solve the "test my app and prove it valid" problem. 12. throw PNSE any time C# code or VM code needs needs to block - Mono VM needs to hold lock while allocating memory, even on the UI thread. 13. modify `ConfigureAwait()`, work queue etc, to never dispatch to another thread. From 69e49b91c38a1cdb67342c36705a1abfd10b4eaf Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 7 Sep 2023 13:21:22 +0200 Subject: [PATCH 10/11] wip --- docs/design/mono/wasm-threads.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index eac334bca5b02c..af4b3ee56d1640 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -96,11 +96,13 @@ - has SynchronizationContext installed on it - So that C# calls could be dispatched to it by runtime - throw PNSE on attempt to marshal sync C# delegate to UI JavaScript + - or throw later only when you try to call the function from JS side. - can run C# finalizers - will run GC - this cross-threading dispatch will have performance impact for the JS interop. - TODO: measure how much - this should not impact Blazor `renderBatch` perf. +- VS debugger would connect to mono as usual. But chrome dev tools experience may be different, because it's will be async with the C# part. ## JSWebWorker with JS interop - is C# thread created and disposed by new API for it @@ -194,7 +196,7 @@ - Blazor renderBatch will continue working even with legacy interop in place. - Because it only reads memory and it doesn't call back to Mono VM. - Blazor's [`IJSInProcessRuntime.Invoke`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime.invoke) should still work, because it's C#->JS direction -- Blazor's [`IJSUnmarshalledRuntime `](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) should still work, because it's C#->JS direction +- Blazor's [`IJSUnmarshalledRuntime`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) should still work, because it's C#->JS direction - TODO: Review Blazor's JavaScript APIs! # Current state 2023 Sep From 14a3394952d4dbb75f7dc4ad89f9b6c7a4b3074c Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Thu, 7 Sep 2023 20:51:24 +0200 Subject: [PATCH 11/11] Update docs/design/mono/wasm-threads.md Co-authored-by: Austin Wise --- docs/design/mono/wasm-threads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/mono/wasm-threads.md b/docs/design/mono/wasm-threads.md index af4b3ee56d1640..f57f95fd92392e 100644 --- a/docs/design/mono/wasm-threads.md +++ b/docs/design/mono/wasm-threads.md @@ -138,7 +138,7 @@ - from UI javaScript it would be passed as Task to deputy worker - open question: passing JS promise to deputy should be fine. But does the `resolve()` need to block UI thread ? -## Task, Task +## Task, Task\ - passing Task should work everywhere. - when marshaled to JS they bind to specific Promise and have affinity - the `SetResult` need to be marshaled on thread of the Promise.