Currently, Blazor WebAssembly's internals make use of the historical JS/WASM single-threadedness guarantees as a performance optimization. For example:
- Render batches are communicated to JS via zero-copy shared memory rather than by serialization
- Events triggered from JS are always processed synchronously by .NET, so there's no need to queue them
At first glance, it appears to work with <WasmEnableThreads>true</WasmEnableThreads>, but that's a mirage: it does not guarantee correctness in that mode, because the core single-threadedness assumption is violated. For example, JS might raise event 2 before event 1 finished processing, and by the time event 1 is done, there might not even be an event 2 handler any more and then you have an error.
Why we're only just encountering this now
- We didn't have multithreading before
- Even to the extent that we did have an early preview of multithreading, it worked by running .NET on the browser UI thread, so we could define that as the sync context and then all the same assumptions would remain valid within that sync context. But now .NET WebAssembly has just moved to the new "deputy thread" multithreading model (i.e., all .NET code is on a background worker), these assumptions are no longer valid (e.g., because the UI thread's JS can no longer communicate with .NET in a synchronous, blocking manner).
Ensuring correctness
We've already solved all these problems in our other hosting environments, Server and WebView, because they've always been innately asynchronous from the beginning. We would need to generalize/port all these mechanisms so they are shared with or copied by the WebAssembly renderer, JS interop, eventing system, etc.:
Approach
As a broad strategy, I think we can:
- Factor out some new
WebRenderer subclass called something like AsyncWebRenderer that holds the common logic around serializing renderbatches and accepting the async renderbatch acknowledgements from JS. This can then be used by all of Server, WebView, and multithreaded-WebAssembly.
- Use our WebView implementation as the reference for how a complete, async-safe rendering and eventing model should work, because this was more recently implemented than Server and is generally simpler and cleaner. There's not a huge amount of code in it, really. If some extra work is needed to queue event notifications, for example, we should see that clearly from the WebView code.
I suspect it's not necessary to have two different builds of Microsoft.AspNetCore.Components.WebAssembly. The total amount of code that should differ between the threaded and not-threaded won't be very much. I expect we can just have two different Renderer subclasses and some if/else branches in the JS code.
Currently, Blazor WebAssembly's internals make use of the historical JS/WASM single-threadedness guarantees as a performance optimization. For example:
At first glance, it appears to work with
<WasmEnableThreads>true</WasmEnableThreads>, but that's a mirage: it does not guarantee correctness in that mode, because the core single-threadedness assumption is violated. For example, JS might raise event 2 before event 1 finished processing, and by the time event 1 is done, there might not even be an event 2 handler any more and then you have an error.Why we're only just encountering this now
Ensuring correctness
We've already solved all these problems in our other hosting environments, Server and WebView, because they've always been innately asynchronous from the beginning. We would need to generalize/port all these mechanisms so they are shared with or copied by the WebAssembly renderer, JS interop, eventing system, etc.:
UpdateDisplayAsyncreturns aTaskthat doesn't complete until the JS side sends back an explicit acknowledgement of that renderbatchApproach
As a broad strategy, I think we can:
WebRenderersubclass called something likeAsyncWebRendererthat holds the common logic around serializing renderbatches and accepting the async renderbatch acknowledgements from JS. This can then be used by all of Server, WebView, and multithreaded-WebAssembly.I suspect it's not necessary to have two different builds of
Microsoft.AspNetCore.Components.WebAssembly. The total amount of code that should differ between the threaded and not-threaded won't be very much. I expect we can just have two differentRenderersubclasses and some if/else branches in the JS code.