Files
romanettandClaude Opus 5.5 bd38870cf7 perf: pump the server child's stdout on a dedicated thread (#4855)
## Summary

The 1-core client stall in the server load harness (`--client-cores 1`,
write->notify p99 0.7-1.7 s) was caused by the harness, not the OPC UA
client.

`StartServerChild` started `PumpServerOutput` with `Task.Run`. The pump
blocks in a synchronous `StreamReader.ReadLine` on the server's stdout
for the whole run, so it held one thread-pool worker permanently. With
`DOTNET_PROCESSOR_COUNT=1` the pool's minimum is one worker, so all
other work items waited for the pool's starvation injection. That is why
`DOTNET_ThreadPool_ForceMinWorkerThreads=8` and two processors both hid
the stall.

**Evidence:** 14 `dotnet-stack` samples of the constrained client during
the measurement window. In every sample, the only blocked pool worker
was in `ReadFile <- StreamReader.ReadLine <- Program.PumpServerOutput <-
StartServerChild`, and the other workers were idle. No `Opc.Ua` frame
was blocked.

**Fix:** run the pump on a dedicated background thread.

## Numbers

`run --scenario sub --sessions 25 --subs 20 --nodes 100 --pub 500
--write-interval 100 --duration 10 --warmup 6 --client-cores 1
--client-memory 512`, 5 alternating rounds (ms):

| round | base write->notify p99 | fix write->notify p99 | base write
p99 | fix write p99 |
|---|---|---|---|---|
| 1 | 659 | 61 | 619 | 12 |
| 2 | 885 | 310 | 859 | 15 |
| 3 | 306 | 303 | 52 | 23 |
| 4 | 1717 | 455 | 408 | 81 |
| 5 | 234 | 193 | 148 | 67 |

The baseline stalled (> 600 ms) in 3 of 5 runs and the fix in 0 of 5
(max 492 ms). The remaining ~300 ms p99 comes from the 500 ms publishing
interval.

## Test plan
- [x] Harness Release build (net10.0)
- [x] Alternating A/B runs above

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:46:35 +02:00
..