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:
@@ -22,9 +22,8 @@ internal static class Program
|
||||
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
|
||||
try
|
||||
{
|
||||
// Synchronous on Unix so the socket under the pipe stays blocking (ScriptWorkerFrameCodec.UseBlockingIo).
|
||||
if (ScriptWorkerFrameCodec.UseBlockingIo) pipe.Connect(TimeSpan.FromSeconds(10));
|
||||
else await pipe.ConnectAsync(TimeSpan.FromSeconds(10), CancellationToken.None);
|
||||
// Synchronous, so the socket under a Unix pipe stays blocking (ScriptWorkerFrameCodec.BlockingStream).
|
||||
pipe.Connect(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -32,7 +31,7 @@ internal static class Program
|
||||
return ScriptWorkerEnvironment.ExitProtocolFault;
|
||||
}
|
||||
|
||||
var host = new WorkerHost(pipe, nonce, parentPid);
|
||||
var host = new WorkerHost(ScriptWorkerFrameCodec.BlockingStream(pipe), nonce, parentPid);
|
||||
return await host.RunAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ internal sealed class WorkerHost
|
||||
/// <summary>The watchdog's longest sleep between passes.</summary>
|
||||
private static readonly TimeSpan WatchdogPoll = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
private readonly NamedPipeClientStream _pipe;
|
||||
/// <summary>The host pipe, driven blocking (<see cref="ScriptWorkerFrameCodec.BlockingStream"/>).</summary>
|
||||
private readonly Stream _pipe;
|
||||
private readonly string _nonce;
|
||||
private readonly int _parentPid;
|
||||
private readonly ConcurrentDictionary<string, LoadedScript> _scripts = new(StringComparer.Ordinal);
|
||||
@@ -93,7 +94,7 @@ internal sealed class WorkerHost
|
||||
private long _watchdogNextPass;
|
||||
private long _seq;
|
||||
|
||||
public WorkerHost(NamedPipeClientStream pipe, string nonce, int parentPid)
|
||||
public WorkerHost(Stream pipe, string nonce, int parentPid)
|
||||
{
|
||||
_pipe = pipe;
|
||||
_nonce = nonce;
|
||||
@@ -106,11 +107,10 @@ internal sealed class WorkerHost
|
||||
{
|
||||
try
|
||||
{
|
||||
// Blocking I/O on Unix from the first byte (lmxopcua#789): one async call would turn the socket under the
|
||||
// pipe non-blocking for good, and every later read would go through the socket engine.
|
||||
var hello = ScriptWorkerFrameCodec.UseBlockingIo
|
||||
? ScriptWorkerFrameCodec.Read(_pipe, ScriptWorkerFrameCodec.DefaultMaxFrameBytes)
|
||||
: await ScriptWorkerFrameCodec.ReadAsync(_pipe, ScriptWorkerFrameCodec.DefaultMaxFrameBytes);
|
||||
// Blocking I/O from the first byte (lmxopcua#789): on Unix one async call would turn the socket under the
|
||||
// pipe non-blocking for good, and every later read would go through the socket engine; on Windows the
|
||||
// stream waits for its own overlapped I/O, off the completion port.
|
||||
var hello = ScriptWorkerFrameCodec.Read(_pipe, ScriptWorkerFrameCodec.DefaultMaxFrameBytes);
|
||||
if (hello is null) return ScriptWorkerEnvironment.ExitPipeClosed;
|
||||
if (hello.BodyCase != Envelope.BodyOneofCase.Hello)
|
||||
throw new ScriptWorkerProtocolException($"expected Hello, got {hello.BodyCase}");
|
||||
@@ -128,14 +128,13 @@ internal sealed class WorkerHost
|
||||
WorkerSoftClock = _workerSoftClock,
|
||||
},
|
||||
});
|
||||
if (ScriptWorkerFrameCodec.UseBlockingIo) ScriptWorkerFrameCodec.Write(_pipe, ready, _maxFrameBytes);
|
||||
else await ScriptWorkerFrameCodec.WriteAsync(_pipe, ready, _maxFrameBytes);
|
||||
ScriptWorkerFrameCodec.Write(_pipe, ready, _maxFrameBytes);
|
||||
|
||||
_logSink.Capacity = (int)Math.Max(1, hello.Hello.MaxLogLines == 0 ? 1000 : hello.Hello.MaxLogLines);
|
||||
_writer = new ScriptWorkerFrameWriter(_pipe, OnWriteFault, _maxFrameBytes, firstSeq: Interlocked.Read(ref _seq),
|
||||
synchronous: ScriptWorkerFrameCodec.UseBlockingIo);
|
||||
synchronous: true);
|
||||
StartBackground(Math.Max(1, (int)hello.Hello.EvalThreads));
|
||||
var exit = ScriptWorkerFrameCodec.UseBlockingIo ? await ReadOnDedicatedThread() : await ReadLoopAsync();
|
||||
var exit = await ReadOnDedicatedThread();
|
||||
_writer.Complete();
|
||||
return exit;
|
||||
}
|
||||
@@ -181,7 +180,7 @@ internal sealed class WorkerHost
|
||||
if (_parentPid > 0) _ = Task.Run(ParentWatchLoopAsync);
|
||||
}
|
||||
|
||||
/// <summary>The blocking read loop on its own thread (Unix, lmxopcua#789); completes with the exit code.</summary>
|
||||
/// <summary>The blocking read loop on its own thread (lmxopcua#789); completes with the exit code.</summary>
|
||||
private Task<int> ReadOnDedicatedThread()
|
||||
{
|
||||
var done = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
@@ -216,29 +215,6 @@ internal sealed class WorkerHost
|
||||
return done.Task;
|
||||
}
|
||||
|
||||
private async Task<int> ReadLoopAsync()
|
||||
{
|
||||
var reader = new ScriptWorkerFrameReader(_pipe, _maxFrameBytes);
|
||||
while (true)
|
||||
{
|
||||
Envelope? frame;
|
||||
try
|
||||
{
|
||||
frame = await reader.ReadAsync();
|
||||
}
|
||||
catch (ScriptWorkerProtocolException ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"ScriptWorker: protocol fault: {ex.Message}");
|
||||
return ScriptWorkerEnvironment.ExitProtocolFault;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return ScriptWorkerEnvironment.ExitPipeClosed;
|
||||
}
|
||||
if (HandleFrame(frame) is { } exit) return exit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Acts on one frame from the host.</summary>
|
||||
/// <param name="frame">The frame; null = end of stream.</param>
|
||||
/// <returns>The exit code when the worker must end, otherwise null.</returns>
|
||||
|
||||
Reference in New Issue
Block a user