Skip to content

[wasi] Fix event loop hook duplication and idle socket spinning - #136395

Merged
lewing merged 1 commit into
dotnet:mainfrom
lewing:lewing-wasi-event-loop-fix
Oct 26, 2026
Merged

lewing merged 1 commit into
dotnet:mainfrom
lewing:lewing-wasi-event-loop-fix

Conversation

@lewing

@lewing lewing commented Oct 23, 2026

Copy link
Copy Markdown
Member

Split out of #136234 at a reviewer's request.

Two bugs in the WASI event loop's socket poll hooks:

  1. Hook duplication. WasiEventLoop.CheckPollables re-added each poll hook once per pollable it subscribed, not once per hook. A connected TCP socket subscribes two pollables (input and output), so its hook doubled on every pass. Hooks that subscribed nothing were dropped for good. The fix tracks the live hooks and re-adds each one once.
  2. Idle socket spinning. For a connected TCP socket, BeforePollHook always subscribed the output stream, which is almost always writable, so any idle connection kept poll from blocking. It now subscribes input or output only when _readQueue or _writeQueue has a waiting operation (new OperationQueue.IsWaiting). The hook runs before every poll, so operations enqueued later are picked up on the next pass.

Found running System.Net.WebSockets.Client.Tests with SocketsHttpHandler enabled on WASI (#136234). That run ran out of memory inside the event loop. With the fix, the suite ran 391 tests with no OOM, and idle CPU of the wasmtime process dropped from about 99% to 0%. Main has no HTTP/2 or WebSockets client on WASI, so that exact repro needs #136234.

Validation (CoreCLR WASI Release, wasmtime 49.0.1), System.Net.Sockets functional tests: no new failures with the fix (one extra candidate test was included in both runs and later removed).

  • with the fix: 646 run, 558 passed, 0 failed, 88 skipped
  • main: 646 run, 558 passed, 0 failed, 88 skipped

Instrumenting CheckPollables on main showed the hook count going 1 → 2 → 7 → 124 → 31,744 within 16 passes while 4 connected sockets waited on receives. The Sockets tests don't keep connections alive long enough to OOM. Two candidate regression tests (idle TCP pairs with pending receives, and a UDP receive before any datagram) both passed on main, so no new test is included.

Known gap (follow-up): the UDP BOUND_STREAMING/CONNECTED path still always subscribes the outgoing datagram stream.

Note

This PR was generated with GitHub Copilot.

CheckPollables re-registered a poll hook once per pollable it subscribed, so a
connected socket (input + output pollables) doubled its hook entries every
iteration until memory ran out, and hooks with no pollables were dropped.
Re-register each live hook once.

A connected socket also always subscribed to its output stream, which is
almost always writable, so the event loop never blocked and spun with any idle
connection. Only subscribe to directions with a waiting operation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@lewing lewing added arch-wasm WebAssembly architecture os-wasi Related to WASI variant of arch-wasm labels Oct 23, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

@lewing
lewing merged commit 427f58a into dotnet:main Oct 26, 2026
152 of 154 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Oct 27, 2026
lewing added a commit that referenced this pull request Oct 30, 2026
Enables `SocketsHttpHandler`, `System.Net.WebSockets` and
`ClientWebSocket` on WASI over wasi:sockets. WASI has no
`System.Net.Security`, so only `http://` and `ws://` work. This lets
WASI apps and tools (for example a `dotnet watch` agent) talk to a local
server without wasi:http, which can't upgrade a connection to a
WebSocket.

The main parts:

1. **Enable SocketsHttpHandler and WebSockets on WASI.**
`HttpClientHandler` on WASI now follows the mobile
`UseNativeHttpHandler` pattern. The default is still `WasiHttpHandler`,
and `WasiApp.targets` defaults `UseNativeHttpHandler=true`, so the SDK
passes the switch to ILLink and `SocketsHttpHandler` is trimmed away. A
trimmed hello-world `HttpClient` app keeps `System.Net.Http.dll` at ~289
KB by default, and ~631 KB with `UseNativeHttpHandler=false`.
2. **Tests.** Enables the `SocketsHttpHandler` functional tests,
`System.Net.WebSockets.Tests` and `System.Net.WebSockets.Client.Tests`
on CoreCLR WASI.
- TLS- and MD5-dependent tests are gated on new
`PlatformDetection.IsTlsSupported`/`IsMD5Supported` capability probes
rather than `IsWasi`, so they light up if either becomes available.
- `LoopbackProxyServer` is now async and doesn't block in `Dispose`
without threads.
- `Configuration.Certificates` no longer fails fast when TLS isn't
supported. Its PFX files can't be decrypted on WASI.
- HTTP decompression tests now run on WASI too, including Brotli and
Zstandard (#136257).

The WASI socket and event loop fixes these tests depend on were split
out and have merged: zero-byte receives (#136394), event loop hook
duplication and idle-socket spinning (#136395), and UDP `ReceiveAsync`
(#136250).

Known gaps and follow-ups:
- Mono WASI builds the same libraries but doesn't run these tests.
- The functional tests use `UseNativeHttpHandler=false`, so they no
longer exercise the default `HttpClientHandler` over `WasiHttpHandler`
path on WASI.
- A bound UDP socket still always subscribes to its outgoing stream, so
it can keep the event loop busy.
- A zero-byte receive can complete early once after its data is drained,
because wasmtime keeps a socket readable until a read would block.
wasmtime treats this as by design (bytecodealliance/wasmtime#9667); the
following real read waits correctly.
- `HttpClient.DefaultProxy` on WASI now reads the standard proxy
environment variables, as on Unix.

Validated locally on CoreCLR WASI (Release) with the `System.Net.Http`
functional, `System.Net.Sockets`, `System.Net.WebSockets` and
`System.Net.WebSockets.Client` tests, plus macOS runs of the changed
shared test infrastructure.

> [!NOTE]
> This PR description was generated with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-System.Threading os-wasi Related to WASI variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants