# WASM Threading — true pipeline streaming on Blazor-WASM

How to enable real producer/consumer pipeline streaming for the chunked
deserialize path on Blazor-WASM clients. Currently WASM falls back to a
degenerate "accumulate-then-sync-deserialize" mode because the runtime is
single-threaded by default — `ManualResetEventSlim.Wait()` throws
`PlatformNotSupportedException`.

> Companion docs:
> - [`README.md`](README.md) — protocol overview
> - [`SIGNALR_BINARY_PROTOCOL_ISSUES.md`](SIGNALR_BINARY_PROTOCOL_ISSUES.md) — known issues
> - [`SIGNALR_BINARY_PROTOCOL_TODO.md`](SIGNALR_BINARY_PROTOCOL_TODO.md) — planned work
> - [`AyCode.Core/AyCode.Core/docs/BINARY/BINARY_ASYNCPIPE_TODO.md`](../../../../AyCode.Core/docs/BINARY/BINARY_ASYNCPIPE_TODO.md) — async-pipe layer TODO

## Current state — WASM single-thread fallback

`AcBinaryHubProtocol` has a static `IsBrowser = OperatingSystem.IsBrowser()`
flag (line ~88) that gates the background deserialize task at line ~936:

```csharp
if (state.DeserTask == null && !IsBrowser)
{
    state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
}
```

**Non-browser** (server / Windows-app): the deser-task runs on a dedicated
off-pool thread via `AcBinaryDeserExecutor` (NOT `Task.Run` / the .NET thread
pool — the blocking `ManualResetEventSlim.Wait()` between chunks would starve
the pool, see `SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9`).
`AsyncPipeReaderInput.TryAdvanceSegment(...)` blocks on
`ManualResetEventSlim.Wait()` when out of bytes — true pipeline parallelism
between the producer (WebSocket-receive) and consumer (deserializer).

**Browser (WASM single-thread)**: the background deser is skipped. Chunks accumulate in
`AsyncPipeReaderInput` from the WebSocket-receive callback. On `CHUNK_END`,
`Input.Complete()` is called → `_completed = true`. The deserializer then
runs **synchronously on the current thread** over the fully-buffered payload.
`TryAdvanceSegment` short-circuits at the `_completed` check (line ~494)
before reaching the `Wait()` at line ~498, so the
`PlatformNotSupportedException` is never thrown.

### Cost of the fallback

- **Peak memory**: full payload in memory at once (~10 MB at production
  workloads vs ~4 KB chunk-bounded in true pipeline mode).
- **No pipeline parallelism**: deserialize starts only after the last chunk
  arrives. Sequential serial latency = transit + deser, not max(transit,
  deser).
- **Large-payload UX**: a ~10 MB response measured ~3.4 sec end-to-end
  (DB-fetch dominant, ~500 ms client-side deser). With true pipeline mode the
  deser could overlap with receive — wall-clock saving estimated at
  100-200 ms on the production payload.

The fallback is **functionally correct** and works on every browser without
extra configuration — it's only suboptimal on memory and parallelism.

## Goal — enable true pipeline streaming on WASM

WASM Multi-Threading (the `WasmEnableThreads` toggle) is the path forward.
The runtime spawns WebWorker-based threads that share memory via
`SharedArrayBuffer`, and `ManualResetEventSlim.Wait()` is implemented on top
of `Atomics.wait()` / `Atomics.notify()` — transparent to .NET code.

When this is on, the existing `AsyncPipeReaderInput` works **unchanged** on
WASM: the consumer-thread blocks naturally at `_dataAvailable.Wait()` and is
woken by the producer-thread (UI-thread receiving WebSocket frames).

### Three-step trial setup

#### 1. Blazor-WASM client `.csproj`

```xml
<PropertyGroup>
    <WasmEnableThreads>true</WasmEnableThreads>
</PropertyGroup>
```

Available from .NET 8 (experimental), stabilising in .NET 9-10. Triggers
WebWorker-spawning, multi-threaded WASM module build, larger bundle.

#### 2. Hosting server — `Cross-Origin-Isolation` headers (mandatory)

`SharedArrayBuffer` requires `cross-origin-isolated` context. The hosting
server (Blazor-Server, Kestrel, nginx, IIS, CDN) must send both headers on
every response that serves the WASM client:

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

Blazor-Server `Program.cs` example:

```csharp
app.Use(async (ctx, next) =>
{
    ctx.Response.Headers.Append("Cross-Origin-Opener-Policy", "same-origin");
    ctx.Response.Headers.Append("Cross-Origin-Embedder-Policy", "require-corp");
    await next();
});
```

Production caveat: every third-party resource (CDN, fonts, analytics, ads,
iframes) must serve `Cross-Origin-Resource-Policy: cross-origin` or use the
`crossorigin` attribute. Anything that doesn't will fail to load — a full
content audit is required before turning this on for production.

#### 3. Override the `IsBrowser` short-circuit in `AcBinaryHubProtocol`

Currently the flag is a static-readonly bool derived only from
`OperatingSystem.IsBrowser()` — there's no runtime override. For the trial,
add an opt-in property:

```csharp
// Replace the existing line ~88:
// private static readonly bool IsBrowser = OperatingSystem.IsBrowser();

public static bool EnableWasmThreading { get; set; } = false;
private static readonly bool IsBrowser = OperatingSystem.IsBrowser() && !EnableWasmThreading;
```

Note: `IsBrowser` is `static readonly`, evaluated once at type-init. If
`EnableWasmThreading` is set during app startup *before* any
`AcBinaryHubProtocol` instance is constructed, the gate works. For
robustness, the trial may want a non-readonly variant or instance-level
opt-in via `AcBinaryHubProtocolOptions`.

Client-side startup (Blazor `Program.cs` or a DI initialiser):

```csharp
AcBinaryHubProtocol.EnableWasmThreading = true;
```

This re-enables the `Task.Run(() => Deserialize(...))` background task on
WASM. The deser runs on a worker, `_dataAvailable.Wait()` blocks via
`Atomics.wait()`, the UI-thread continues receiving WebSocket chunks.

## Expected outcomes

- **Peak memory**: from ~payload-size down to ~chunk-size (4 KB default).
- **Latency**: deser starts as soon as the first chunk arrives. On a 10 MB
  payload with ~150 ms total deser-time, the overlap saves ~100-150 ms of
  wall-clock.
- **Throughput**: per-chunk receive→feed→consume cycle on the worker-thread.
  Comparable to a non-trivial fraction of the Windows-app pipeline (the
  Windows-app does ~63 MB/s deser; WASM-worker likely ~30-50 MB/s due to
  WASM-vs-native instruction cost, but the parallelism still wins on big
  payloads).

## Risks and known gotchas

1. **Bundle-size +30-40%** — additional WASM modules, WebWorker loaders. May
   matter for cold-start latency.

2. **Cross-origin breakage** — any third-party resource not opted-in via
   `crossorigin` attribute or `CORP: cross-origin` header fails to load.
   Common offenders: Google Fonts (works with `crossorigin="anonymous"`),
   analytics snippets, ad networks, embedded YouTube/Vimeo iframes, social
   widgets.

3. **NuGet package compatibility** — packages built without
   `WasmEnableThreads` support may throw runtime errors. Likely safe: pure
   .NET libraries (BCL, custom DTOs). Likely risky: native interop
   (SkiaSharp, image-processing, crypto with native fallbacks). Audit with a
   smoke-test of the full app before going to production.

4. **Debugger experience** — multi-threaded WASM debug is improving but still
   less smooth than single-thread: breakpoint hit on the wrong thread,
   stepping across worker boundaries can be flaky. Browser dev-tools may
   show worker stacks separately.

5. **Static-readonly initialisation order** — the `IsBrowser` field is
   evaluated at type-init. If `EnableWasmThreading = true` is set *after*
   the type is loaded (e.g. after the first `AcBinaryHubProtocol` instance
   is created), the gate stays in single-threaded mode. The trial setup
   must set the flag in `Program.cs` *before* any SignalR client is built.

6. **Atomics.wait() on the UI thread is forbidden** — only worker threads
   can call it. The .NET runtime handles this internally (it schedules
   blocking work to worker threads), but if any consumer code accidentally
   blocks on the UI thread, the browser will throw. Stick to standard
   `Task.Run` / `await` patterns; don't bypass them.

7. **Worker-cold-start cost** — spawning a new worker has a ~10-50 ms
   overhead. For very small payloads (< 64 KB), the worker-spawn cost may
   exceed the pipeline-parallelism gain. The `Bytes` mode (single-flush,
   no chunked path) is still preferable for small messages.

## When to revisit this

- **.NET 10 stable release** (Nov 2025): `WasmEnableThreads` should be
  closer to production-ready. Re-evaluate compatibility with the project's
  NuGet dependencies and CDN setup.
- **.NET 11 release** (Nov 2026): expected to make `WasmEnableThreads` the
  default for new Blazor-WASM templates (per current Microsoft roadmap
  signals — not confirmed). Likely a good moment for production rollout.
- **`WebAssembly.Suspending`** (alternative path): an experimental browser
  feature (Chrome 134+, ~Q4 2024) that allows a sync WASM call to await a
  JS promise without multi-threading. If .NET adds runtime support, this
  could be an *opt-in-free* alternative that doesn't require COOP/COEP or
  multi-threaded builds. Monitor `dotnet/runtime` issue tracker for
  `WebAssembly.Suspending` integration.

## What this doc is NOT

- Not a production-ready procedure. The trial is for measurement only.
- Not a recommendation to enable multi-threading by default — the bundle
  size, cross-origin headers, and library audit costs are significant.
- Not a substitute for the chunked-protocol design decisions in the parent
  `README.md` and `SIGNALR_BINARY_PROTOCOL_ISSUES.md`.
