perf(scripting): #789 Windows worker pipe I/O blocks on dedicated threads, off the completion port
A .NET pipe opened Asynchronous completes even its synchronous Read/Write through the thread pool's completion port, so every frame on Windows cost a completion packet plus a pool work item (and the pool's spin-wait). New WindowsBlockingPipeStream issues its own overlapped ReadFile/WriteFile with the OVERLAPPED.hEvent low bit set (no completion packet) and waits on that event on the calling thread; reads and writes stay concurrent, unlike a synchronous handle. ScriptWorkerFrameCodec.BlockingStream picks it on Windows and the pipe itself on Unix, so both ends now use the dedicated blocking reader + synchronous combining writer on every OS (UseBlockingIo and the async read loops are gone). Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -78,14 +78,21 @@ public static class ScriptWorkerFrameCodec
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this platform's pipe should be driven with blocking I/O (lmxopcua#789). On Unix a .NET pipe is a
|
||||
/// socket, and ONE asynchronous operation on it switches it to non-blocking for good, after which even a
|
||||
/// "blocking" read goes through the socket engine (an epoll-thread hand-off and a spinning wait per frame) —
|
||||
/// measured as most of the host's per-evaluation CPU. Kept blocking, a read is a plain kernel wait on a
|
||||
/// dedicated thread. Not on Windows, where an overlapped pipe handle is needed for full duplex and its
|
||||
/// asynchronous path is the efficient one.
|
||||
/// The stream both ends drive a connected worker pipe through, with blocking I/O on dedicated threads
|
||||
/// (lmxopcua#789). On Unix a .NET pipe is a socket, and ONE asynchronous operation on it switches it to
|
||||
/// non-blocking for good, after which even a "blocking" read goes through the socket engine (an epoll-thread
|
||||
/// hand-off and a spinning wait per frame) — measured as most of the host's per-evaluation CPU; so the pipe itself
|
||||
/// is used, only ever synchronously. On Windows an overlapped pipe completes even its synchronous calls through the
|
||||
/// thread pool's completion port, so the pipe is wrapped in a <see cref="WindowsBlockingPipeStream"/>, which waits
|
||||
/// for its own overlapped I/O without the port and keeps reads and writes concurrent.
|
||||
/// </summary>
|
||||
public static bool UseBlockingIo => !OperatingSystem.IsWindows();
|
||||
/// <param name="pipe">The connected pipe (opened with <see cref="System.IO.Pipes.PipeOptions.Asynchronous"/>).</param>
|
||||
/// <returns>The stream to read and write frames through; disposing it closes the pipe.</returns>
|
||||
public static Stream BlockingStream(System.IO.Pipes.PipeStream pipe)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pipe);
|
||||
return OperatingSystem.IsWindows() ? new WindowsBlockingPipeStream(pipe) : pipe;
|
||||
}
|
||||
|
||||
/// <summary>Writes one frame synchronously (the blocking-I/O handshake). Not thread-safe.</summary>
|
||||
/// <param name="stream">The pipe.</param>
|
||||
|
||||
Reference in New Issue
Block a user