From 6184194465fa602ce4e7d743be83741d2a42ed08 Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Wed, 27 May 2026 16:52:12 +0100 Subject: [PATCH] part 6 --- KestrelMinima.Demo/KestrelMinima.Demo.csproj | 16 + KestrelMinima.Demo/Program.cs | 12 + .../Connection/Connection.InputPipe.cs | 33 + KestrelMinima/Connection/Connection.Read.cs | 163 +++++ KestrelMinima/Connection/Connection.Write.cs | 102 ++++ KestrelMinima/Connection/Connection.cs | 68 +++ .../Connection/ConnectionDualPipe.cs | 17 + .../Connection/ConnectionPipeWriter.cs | 63 ++ KestrelMinima/Connection/RecvSnapshot.cs | 15 + KestrelMinima/KestrelMinima.csproj | 21 + KestrelMinima/KestrelMinimaEngine.cs | 45 ++ KestrelMinima/KestrelMinimaKestrel.cs | 149 +++++ KestrelMinima/Reactor/Reactor.cs | 510 ++++++++++++++++ KestrelMinima/ServerConfig.cs | 35 ++ KestrelMinima/Utils/Mpsc.cs | 115 ++++ KestrelMinima/Utils/RingSegment.cs | 31 + KestrelMinima/Utils/SpscRecvRing.cs | 105 ++++ KestrelMinima/Utils/UnmanagedMemoryManager.cs | 32 + KestrelMinima/_usings.cs | 10 + KestrelMinima/io_uring/Native.cs | 162 +++++ KestrelMinima/io_uring/Ring.cs | 179 ++++++ Minima/Program.cs | 16 +- Minima/Reactor/Reactor.cs | 2 +- MinimaSQPoll/Connection/Connection.Read.cs | 167 ++++++ MinimaSQPoll/Connection/Connection.Write.cs | 185 ++++++ MinimaSQPoll/Connection/Connection.cs | 111 ++++ MinimaSQPoll/Connection/ConnectionDualPipe.cs | 16 + .../Connection/ConnectionPipeReader.cs | 181 ++++++ .../Connection/ConnectionPipeWriter.cs | 63 ++ MinimaSQPoll/Connection/RecvSnapshot.cs | 15 + MinimaSQPoll/MinimaSQPoll.csproj | 12 + MinimaSQPoll/Program.cs | 176 ++++++ MinimaSQPoll/Reactor/Reactor.cs | 385 ++++++++++++ MinimaSQPoll/ServerConfig.cs | 35 ++ MinimaSQPoll/Utils/Mpsc.cs | 115 ++++ MinimaSQPoll/Utils/RingSegment.cs | 31 + MinimaSQPoll/Utils/SpscRecvRing.cs | 105 ++++ MinimaSQPoll/Utils/UnmanagedMemoryManager.cs | 32 + MinimaSQPoll/io_uring/Native.cs | 172 ++++++ MinimaSQPoll/io_uring/Ring.cs | 232 +++++++ MinimaTFlow/Connection/Connection.Read.cs | 168 ++++++ MinimaTFlow/Connection/Connection.Write.cs | 137 +++++ MinimaTFlow/Connection/Connection.cs | 94 +++ MinimaTFlow/Connection/ConnectionDualPipe.cs | 16 + .../Connection/ConnectionPipeReader.cs | 181 ++++++ .../Connection/ConnectionPipeWriter.cs | 63 ++ MinimaTFlow/Connection/RecvSnapshot.cs | 15 + MinimaTFlow/MinimaTFlow.csproj | 12 + MinimaTFlow/Program.cs | 178 ++++++ MinimaTFlow/Reactor/Reactor.cs | 370 ++++++++++++ MinimaTFlow/ServerConfig.cs | 35 ++ MinimaTFlow/Utils/Mpsc.cs | 115 ++++ MinimaTFlow/Utils/RingSegment.cs | 31 + MinimaTFlow/Utils/SpscRecvRing.cs | 105 ++++ MinimaTFlow/Utils/UnmanagedMemoryManager.cs | 32 + MinimaTFlow/io_uring/Native.cs | 170 ++++++ MinimaTFlow/io_uring/Ring.cs | 179 ++++++ .../Connection/Connection.Incremental.cs | 61 ++ MinimaTPool/Connection/Connection.Read.cs | 168 ++++++ MinimaTPool/Connection/Connection.Write.cs | 187 ++++++ MinimaTPool/Connection/Connection.cs | 108 ++++ MinimaTPool/Connection/ConnectionDualPipe.cs | 16 + .../Connection/ConnectionPipeReader.cs | 181 ++++++ .../Connection/ConnectionPipeWriter.cs | 63 ++ MinimaTPool/Connection/RecvSnapshot.cs | 15 + MinimaTPool/MinimaTPool.csproj | 12 + MinimaTPool/Program.cs | 178 ++++++ MinimaTPool/Reactor/Reactor.Incremental.cs | 306 ++++++++++ MinimaTPool/Reactor/Reactor.cs | 564 ++++++++++++++++++ MinimaTPool/ServerConfig.cs | 35 ++ MinimaTPool/Utils/Mpsc.cs | 115 ++++ MinimaTPool/Utils/RingSegment.cs | 31 + MinimaTPool/Utils/SpscRecvRing.cs | 105 ++++ MinimaTPool/Utils/UnmanagedMemoryManager.cs | 32 + MinimaTPool/io_uring/Native.cs | 162 +++++ MinimaTPool/io_uring/Ring.cs | 179 ++++++ Shrike.Playground/Program.cs | 23 +- Shrike/Engine/Connection.cs | 4 +- Shrike/Writers/FixedBufferWriter.cs | 116 +--- SocketBaseline/Program.cs | 8 +- Spring.Demo/Program.cs | 8 +- docs/blog/blog.css | 119 +++- docs/blog/images/part-6-hero.png | Bin 0 -> 241845 bytes docs/blog/index.html | 7 + docs/blog/io-uring-minima-part-6.html | 470 +++++++++++++++ docs/index.html | 48 +- zerg.sln | 70 +++ 87 files changed, 9052 insertions(+), 174 deletions(-) create mode 100644 KestrelMinima.Demo/KestrelMinima.Demo.csproj create mode 100644 KestrelMinima.Demo/Program.cs create mode 100644 KestrelMinima/Connection/Connection.InputPipe.cs create mode 100644 KestrelMinima/Connection/Connection.Read.cs create mode 100644 KestrelMinima/Connection/Connection.Write.cs create mode 100644 KestrelMinima/Connection/Connection.cs create mode 100644 KestrelMinima/Connection/ConnectionDualPipe.cs create mode 100644 KestrelMinima/Connection/ConnectionPipeWriter.cs create mode 100644 KestrelMinima/Connection/RecvSnapshot.cs create mode 100644 KestrelMinima/KestrelMinima.csproj create mode 100644 KestrelMinima/KestrelMinimaEngine.cs create mode 100644 KestrelMinima/KestrelMinimaKestrel.cs create mode 100644 KestrelMinima/Reactor/Reactor.cs create mode 100644 KestrelMinima/ServerConfig.cs create mode 100644 KestrelMinima/Utils/Mpsc.cs create mode 100644 KestrelMinima/Utils/RingSegment.cs create mode 100644 KestrelMinima/Utils/SpscRecvRing.cs create mode 100644 KestrelMinima/Utils/UnmanagedMemoryManager.cs create mode 100644 KestrelMinima/_usings.cs create mode 100644 KestrelMinima/io_uring/Native.cs create mode 100644 KestrelMinima/io_uring/Ring.cs create mode 100644 MinimaSQPoll/Connection/Connection.Read.cs create mode 100644 MinimaSQPoll/Connection/Connection.Write.cs create mode 100644 MinimaSQPoll/Connection/Connection.cs create mode 100644 MinimaSQPoll/Connection/ConnectionDualPipe.cs create mode 100644 MinimaSQPoll/Connection/ConnectionPipeReader.cs create mode 100644 MinimaSQPoll/Connection/ConnectionPipeWriter.cs create mode 100644 MinimaSQPoll/Connection/RecvSnapshot.cs create mode 100644 MinimaSQPoll/MinimaSQPoll.csproj create mode 100644 MinimaSQPoll/Program.cs create mode 100644 MinimaSQPoll/Reactor/Reactor.cs create mode 100644 MinimaSQPoll/ServerConfig.cs create mode 100644 MinimaSQPoll/Utils/Mpsc.cs create mode 100644 MinimaSQPoll/Utils/RingSegment.cs create mode 100644 MinimaSQPoll/Utils/SpscRecvRing.cs create mode 100644 MinimaSQPoll/Utils/UnmanagedMemoryManager.cs create mode 100644 MinimaSQPoll/io_uring/Native.cs create mode 100644 MinimaSQPoll/io_uring/Ring.cs create mode 100644 MinimaTFlow/Connection/Connection.Read.cs create mode 100644 MinimaTFlow/Connection/Connection.Write.cs create mode 100644 MinimaTFlow/Connection/Connection.cs create mode 100644 MinimaTFlow/Connection/ConnectionDualPipe.cs create mode 100644 MinimaTFlow/Connection/ConnectionPipeReader.cs create mode 100644 MinimaTFlow/Connection/ConnectionPipeWriter.cs create mode 100644 MinimaTFlow/Connection/RecvSnapshot.cs create mode 100644 MinimaTFlow/MinimaTFlow.csproj create mode 100644 MinimaTFlow/Program.cs create mode 100644 MinimaTFlow/Reactor/Reactor.cs create mode 100644 MinimaTFlow/ServerConfig.cs create mode 100644 MinimaTFlow/Utils/Mpsc.cs create mode 100644 MinimaTFlow/Utils/RingSegment.cs create mode 100644 MinimaTFlow/Utils/SpscRecvRing.cs create mode 100644 MinimaTFlow/Utils/UnmanagedMemoryManager.cs create mode 100644 MinimaTFlow/io_uring/Native.cs create mode 100644 MinimaTFlow/io_uring/Ring.cs create mode 100644 MinimaTPool/Connection/Connection.Incremental.cs create mode 100644 MinimaTPool/Connection/Connection.Read.cs create mode 100644 MinimaTPool/Connection/Connection.Write.cs create mode 100644 MinimaTPool/Connection/Connection.cs create mode 100644 MinimaTPool/Connection/ConnectionDualPipe.cs create mode 100644 MinimaTPool/Connection/ConnectionPipeReader.cs create mode 100644 MinimaTPool/Connection/ConnectionPipeWriter.cs create mode 100644 MinimaTPool/Connection/RecvSnapshot.cs create mode 100644 MinimaTPool/MinimaTPool.csproj create mode 100644 MinimaTPool/Program.cs create mode 100644 MinimaTPool/Reactor/Reactor.Incremental.cs create mode 100644 MinimaTPool/Reactor/Reactor.cs create mode 100644 MinimaTPool/ServerConfig.cs create mode 100644 MinimaTPool/Utils/Mpsc.cs create mode 100644 MinimaTPool/Utils/RingSegment.cs create mode 100644 MinimaTPool/Utils/SpscRecvRing.cs create mode 100644 MinimaTPool/Utils/UnmanagedMemoryManager.cs create mode 100644 MinimaTPool/io_uring/Native.cs create mode 100644 MinimaTPool/io_uring/Ring.cs create mode 100644 docs/blog/images/part-6-hero.png create mode 100644 docs/blog/io-uring-minima-part-6.html diff --git a/KestrelMinima.Demo/KestrelMinima.Demo.csproj b/KestrelMinima.Demo/KestrelMinima.Demo.csproj new file mode 100644 index 0000000..0179cd3 --- /dev/null +++ b/KestrelMinima.Demo/KestrelMinima.Demo.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + true + false + true + + + + + + + diff --git a/KestrelMinima.Demo/Program.cs b/KestrelMinima.Demo/Program.cs new file mode 100644 index 0000000..4620407 --- /dev/null +++ b/KestrelMinima.Demo/Program.cs @@ -0,0 +1,12 @@ +using KestrelMinima; + +var builder = WebApplication.CreateSlimBuilder(args); +builder.Logging.SetMinimumLevel(LogLevel.Warning); + +builder.WebHost + .UseKestrelMinima(o => o.ReactorCount = 8) + .ConfigureKestrel(o => o.ListenAnyIP(8080)); + +var app = builder.Build(); +app.MapGet("/", () => "Hello World!"); +app.Run(); diff --git a/KestrelMinima/Connection/Connection.InputPipe.cs b/KestrelMinima/Connection/Connection.InputPipe.cs new file mode 100644 index 0000000..4b8b2de --- /dev/null +++ b/KestrelMinima/Connection/Connection.InputPipe.cs @@ -0,0 +1,33 @@ +using System.IO.Pipelines; + +namespace KestrelMinima; + +/// +/// Kestrel-mode input path. The reactor copies recv bytes into a BCL +/// and Kestrel reads InputPipe.Reader — bypassing the +/// hand-rolled read IVTS, which can't take Kestrel's concurrent off-reactor +/// access. Output uses the write slab + a fire-and-forget FlushAsync (no IVTS). +/// +public sealed unsafe partial class Connection +{ + internal Pipe? InputPipe; + + internal void InitInputPipe() + => InputPipe = new Pipe(new PipeOptions( + pauseWriterThreshold: 0, + resumeWriterThreshold: 0, + useSynchronizationContext: false)); + + /// Reactor-thread: copy recv bytes into the pipe and publish. + internal void FeedInput(byte* ptr, int len) + { + Span dst = InputPipe!.Writer.GetSpan(len); + new ReadOnlySpan(ptr, len).CopyTo(dst); + InputPipe.Writer.Advance(len); + _ = InputPipe.Writer.FlushAsync(); // no backpressure → completes synchronously + } + + /// Reactor-thread: signal EOF to Kestrel's reader. + internal void CompleteInput(Exception? error = null) + => InputPipe?.Writer.Complete(error); +} diff --git a/KestrelMinima/Connection/Connection.Read.cs b/KestrelMinima/Connection/Connection.Read.cs new file mode 100644 index 0000000..6ade2ab --- /dev/null +++ b/KestrelMinima/Connection/Connection.Read.cs @@ -0,0 +1,163 @@ +using System.Threading.Tasks.Sources; +using KestrelMinima.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace KestrelMinima; + +/// +/// Per-connection state. The handler may run on any thread (e.g. resumed by +/// a thread-pool timer); reactor-only side effects are funnelled through the +/// MPSC queues on `Reactor`. Coordination uses Interlocked.Exchange on the +/// arm flags and a sticky `_pending` to close the lost-wakeup race. +/// +/// Lifetime is pool-managed: the reactor pops a Connection on accept (or new +/// one if pool is empty), and pushes it back on teardown after `Clear()`. The +/// `_generation` field is bumped on each `Clear` so stale `ValueTask` tokens +/// from a previous connection life are detectable and return `Closed()` +/// instead of leaking the new tenant's state. +/// +public sealed unsafe partial class Connection : IValueTaskSource +{ + internal Connection SetFd(int fd) + { + ClientFd = fd; + return this; + } + + private ManualResetValueTaskSourceCore _readSignal; + private int _armed; + private int _pending; + private int _closed; + + private readonly SpscRecvRing _recv = new(capacityPow2: 16); + + public ValueTask ReadAsync() + { + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1) + { + Volatile.Write(ref _pending, 0); + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + if (Volatile.Read(ref _closed) != 0) + { + return new ValueTask(RecvSnapshot.Closed()); + } + + if (Interlocked.Exchange(ref _armed, 1) == 1) + { + throw new InvalidOperationException("ReadAsync already armed."); + } + + // Snapshot the generation as the IVTS token so a future Clear() can + // invalidate this awaiter if the connection gets pool-recycled. + int gen = Volatile.Read(ref _generation); + + // Race recovery: re-check between arming and returning the IVTS task. + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1 || Volatile.Read(ref _closed) != 0) + { + Volatile.Write(ref _pending, 0); + Interlocked.Exchange(ref _armed, 0); + + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + return new ValueTask(this, (short)gen); + } + + public bool TryGetItem(in RecvSnapshot snap, out SpscRecvRing.Item item) + => _recv.TryDequeueUntil(snap.Tail, out item); + + public void ResetRead() => _readSignal.Reset(); + + public void Complete(int res, ushort bid, bool hasBuffer, byte* ptr) + { + if (!_recv.TryEnqueue(new SpscRecvRing.Item + { + Ptr = ptr, + Bid = bid, + Len = res, + HasBuffer = hasBuffer, + Gen = (ushort)Volatile.Read(ref _generation) + })) + { + Console.Error.WriteLine("[conn] recv queue overflow."); + if (hasBuffer) + { + _reactor.ReturnBufferDirect(bid); + } + Volatile.Write(ref _closed, 1); + } + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + else + { + Volatile.Write(ref _pending, 1); + } + } + + internal void DrainRecv() + { + // Return any buffer IDs still sitting in the SPSC ring (handler exited + // before draining them, or a recv arrived after _closed was set). + while (_recv.TryDequeue(out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + _reactor.ReturnBufferDirect(item.Bid); + } + } + } + + // ========================================================================= + // IValueTaskSource plumbing — token (= snapshot of `_generation` at await + // time) is compared against the current `_generation` to detect stale + // awaiters from before a Clear()/pool reuse. Stale awaiters get a + // sentinel result rather than the new tenant's state. + // + // For the actual IVTS dispatch we pass `_readSignal.Version` / + // `_flushSignal.Version` to the underlying core (not `token`) because the + // core's version is bumped by ResetRead/CompleteFlush mid-life and is + // unrelated to the cross-life generation guard. + // ========================================================================= + + RecvSnapshot IValueTaskSource.GetResult(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return RecvSnapshot.Closed(); + } + + return _readSignal.GetResult(_readSignal.Version); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return ValueTaskSourceStatus.Succeeded; + } + + return _readSignal.GetStatus(_readSignal.Version); + } + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (token != (short)Volatile.Read(ref _generation)) + { + // Stale — run the continuation now so the awaiter unblocks and + // gets RecvSnapshot.Closed() from GetResult. + continuation(state); + + return; + } + + _readSignal.OnCompleted(continuation, state, _readSignal.Version, flags); + } +} diff --git a/KestrelMinima/Connection/Connection.Write.cs b/KestrelMinima/Connection/Connection.Write.cs new file mode 100644 index 0000000..8a95d08 --- /dev/null +++ b/KestrelMinima/Connection/Connection.Write.cs @@ -0,0 +1,102 @@ +using System.Buffers; +using KestrelMinima.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace KestrelMinima; + +/// +/// Fire-and-forget write path: FlushAsync hands the slab to the reactor (an io_uring +/// send SQE + eventfd wake) and returns synchronously. No IValueTaskSource, no +/// awaiter scheduling, no continuation hop. Safe for HTTP/1.1 plaintext because the +/// client cannot send the next request until it receives the previous response — +/// which in turn cannot happen until the kernel finishes our send (and the reactor +/// has processed the resulting send CQE, which is what resets WriteHead/WriteTail). +/// So by the time Kestrel produces the next response into this slab, the previous +/// send is fully ack'd and the slab is free for reuse. +/// +public sealed unsafe partial class Connection : IBufferWriter +{ + private readonly int _writeSlabSize; + internal byte* WriteBuffer; + // WriteHead — bytes ack'd by the kernel (reactor thread mutates). + // WriteSubmitted — bytes queued to the kernel via SubmitSend (reactor thread mutates). + // WriteTail — bytes produced by Kestrel into the slab (Kestrel thread mutates). + internal int WriteHead; + internal int WriteSubmitted; + internal int WriteTail; + + private readonly UnmanagedMemoryManager _manager; + + // IBufferWriter +#region IBufferWriter + + public Memory GetMemory(int sizeHint = 0) + { + int remaining = _writeSlabSize - WriteTail; + if (sizeHint > remaining) + { + throw new InvalidOperationException( + $"GetMemory: sizeHint={sizeHint} > remaining={remaining} (slab={_writeSlabSize}, WriteTail={WriteTail}, WriteSubmitted={WriteSubmitted}, WriteHead={WriteHead}, closed={Volatile.Read(ref _closed)})"); + } + + return _manager.Memory.Slice(WriteTail, remaining); + } + + public Span GetSpan(int sizeHint = 0) + { + if (WriteTail + sizeHint > _writeSlabSize) + { + throw new InvalidOperationException( + $"GetSpan: sizeHint={sizeHint}, WriteTail={WriteTail}, slab={_writeSlabSize}, WriteSubmitted={WriteSubmitted}, WriteHead={WriteHead}, closed={Volatile.Read(ref _closed)}"); + } + + return new Span(WriteBuffer + WriteTail, _writeSlabSize - WriteTail); + } + + public void Advance(int count) => WriteTail += count; + +#endregion + + // Write to the inner buffer + public void Write(ReadOnlySpan source) + { + int len = source.Length; + if (WriteTail + len > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + + source.CopyTo(new Span(WriteBuffer + WriteTail, len)); + WriteTail += len; + } + + // Fire-and-forget: hand the fd to the reactor and return. The reactor reads + // [WriteSubmitted, WriteTail) on drain and submits an SQE. Multi-flush within + // one response is handled naturally — the MPSC may have the fd queued multiple + // times, but the second drain finds end <= begin and no-ops. + public ValueTask FlushAsync() + { + if (Volatile.Read(ref _closed) == 1) + { + return default; + } + + if (WriteTail == 0) + { + return default; + } + + _reactor.EnqueueFlush(ClientFd); + + return default; + } + + // Reactor-thread: all submitted bytes ack'd AND no new bytes pending — reset. + internal void CompleteFlush() + { + WriteHead = 0; + WriteSubmitted = 0; + WriteTail = 0; + } +} diff --git a/KestrelMinima/Connection/Connection.cs b/KestrelMinima/Connection/Connection.cs new file mode 100644 index 0000000..3b394c1 --- /dev/null +++ b/KestrelMinima/Connection/Connection.cs @@ -0,0 +1,68 @@ +using System.Runtime.InteropServices; +using KestrelMinima.Utils; + +namespace KestrelMinima; + +public sealed unsafe partial class Connection +{ + private readonly Reactor _reactor; + + public int ClientFd { get; private set; } + + // Bumped on Clear(); the low 16 bits are used as the read IVTS token so + // stale awaiters can be detected after pool reuse. (The Kestrel path never + // touches the read IVTS, but it's reused by MarkClosed's `_readSignal` + // SetResult — harmless when nobody awaits.) + private int _generation; + + public Connection(Reactor reactor, int fd, int writeSlabSize = 256 * 1024) + { + _reactor = reactor; + ClientFd = fd; + _writeSlabSize = writeSlabSize; + WriteBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)writeSlabSize, 64); + + _manager = new UnmanagedMemoryManager(WriteBuffer, writeSlabSize); + } + + // Reactor-thread only — called from Recycle in the reactor's recv/send error paths. + public void MarkClosed() + { + Volatile.Write(ref _closed, 1); + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), isClosed: true)); + } + else + { + Volatile.Write(ref _pending, 1); + } + } + + internal void Clear() + { + Interlocked.Increment(ref _generation); + + Volatile.Write(ref _armed, 0); + Volatile.Write(ref _pending, 0); + Volatile.Write(ref _closed, 0); + + WriteHead = 0; + WriteSubmitted = 0; + WriteTail = 0; + + _readSignal.Reset(); + + _recv.Reset(); // discard any leftover SPSC items + } + + public void Dispose() + { + if (WriteBuffer != null) + { + NativeMemory.AlignedFree(WriteBuffer); + WriteBuffer = null; + } + } +} diff --git a/KestrelMinima/Connection/ConnectionDualPipe.cs b/KestrelMinima/Connection/ConnectionDualPipe.cs new file mode 100644 index 0000000..62b518c --- /dev/null +++ b/KestrelMinima/Connection/ConnectionDualPipe.cs @@ -0,0 +1,17 @@ +using System.IO.Pipelines; + +namespace KestrelMinima; + +public sealed class ConnectionDualPipe : IDuplexPipe +{ + public PipeReader Input { get; } + public PipeWriter Output { get; } + + public ConnectionDualPipe(Connection connection) + { + ArgumentNullException.ThrowIfNull(connection); + // Kestrel mode only — InitInputPipe is always called on accept. + Input = connection.InputPipe!.Reader; + Output = new ConnectionPipeWriter(connection); + } +} \ No newline at end of file diff --git a/KestrelMinima/Connection/ConnectionPipeWriter.cs b/KestrelMinima/Connection/ConnectionPipeWriter.cs new file mode 100644 index 0000000..120db4a --- /dev/null +++ b/KestrelMinima/Connection/ConnectionPipeWriter.cs @@ -0,0 +1,63 @@ +using System.IO.Pipelines; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace KestrelMinima; + +/// +/// Adapts Minima's write API (GetMemory/GetSpan/Advance/ +/// FlushAsync) to a standard , so PipeWriter-based code +/// can write responses through the connection's per-connection slab. +/// A thin wrapper — all the work lives in Connection. +/// +public sealed class ConnectionPipeWriter : PipeWriter +{ + private readonly Connection _conn; + private bool _completed; + private bool _cancelRequested; + private long _unflushed; + + public ConnectionPipeWriter(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override bool CanGetUnflushedBytes => true; + public override long UnflushedBytes => _unflushed; + + public override Memory GetMemory(int sizeHint = 0) => _conn.GetMemory(sizeHint); + + public override Span GetSpan(int sizeHint = 0) => _conn.GetSpan(sizeHint); + + public override void Advance(int bytes) + { + _unflushed += bytes; + _conn.Advance(bytes); + } + + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_cancelRequested) + { + _cancelRequested = false; + return new ValueTask(new FlushResult(isCanceled: true, isCompleted: _completed)); + } + + _unflushed = 0; + ValueTask inner = _conn.FlushAsync(); + + if (inner.IsCompletedSuccessfully) + return new ValueTask(new FlushResult(isCanceled: false, isCompleted: _completed)); + + return AwaitFlush(inner); + } + + private async ValueTask AwaitFlush(ValueTask inner) + { + await inner; + return new FlushResult(isCanceled: false, isCompleted: _completed); + } + + public override void CancelPendingFlush() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) => _completed = true; +} diff --git a/KestrelMinima/Connection/RecvSnapshot.cs b/KestrelMinima/Connection/RecvSnapshot.cs new file mode 100644 index 0000000..809a4b1 --- /dev/null +++ b/KestrelMinima/Connection/RecvSnapshot.cs @@ -0,0 +1,15 @@ +namespace KestrelMinima; + +public readonly struct RecvSnapshot +{ + public readonly long Tail; + public readonly bool IsClosed; + + public RecvSnapshot(long tail, bool isClosed) + { + Tail = tail; + IsClosed = isClosed; + } + + public static RecvSnapshot Closed() => new(0, isClosed: true); +} \ No newline at end of file diff --git a/KestrelMinima/KestrelMinima.csproj b/KestrelMinima/KestrelMinima.csproj new file mode 100644 index 0000000..61a0661 --- /dev/null +++ b/KestrelMinima/KestrelMinima.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + disable + true + + KestrelMinima + 0.1.0 + Diogo Martins + Minima's io_uring engine as a Kestrel transport. Per-core reactor drives accept and recv (multishot + provided buffers). Writes are fire-and-forget: FlushAsync enqueues an io_uring send SQE and returns synchronously — no IValueTaskSource, no awaiter scheduling. Safe for HTTP/1.1 plaintext because clients send the next request only after receiving the response. Self-contained — no external dependencies. + io_uring;kestrel;aspnetcore;transport;linux;networking;performance + MIT + + + + + + + diff --git a/KestrelMinima/KestrelMinimaEngine.cs b/KestrelMinima/KestrelMinimaEngine.cs new file mode 100644 index 0000000..ffda90c --- /dev/null +++ b/KestrelMinima/KestrelMinimaEngine.cs @@ -0,0 +1,45 @@ +namespace KestrelMinima; + +/// +/// Owns N io_uring reactors (each its own SO_REUSEPORT listener) and funnels +/// accepted connections to the Kestrel transport via a channel. +/// +public sealed class KestrelMinimaEngine +{ + private readonly Reactor[] _reactors; + private readonly Channel _accepted = + Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = false, + SingleWriter = false, + }); + + public KestrelMinimaEngine(ServerConfig config) + { + _reactors = new Reactor[config.ReactorCount]; + for (int i = 0; i < config.ReactorCount; i++) + { + _reactors[i] = new Reactor(i, config) { OnAccept = OnReactorAccept }; + } + } + + private void OnReactorAccept(Connection conn) => _accepted.Writer.TryWrite(conn); + + public void Start() + { + for (int i = 0; i < _reactors.Length; i++) + { + int idx = i; + var t = new Thread(() => _reactors[idx].Run()) + { + IsBackground = true, + Name = $"kestrel-minima-r{idx}", + }; + t.Start(); + } + } + + public ValueTask AcceptAsync(CancellationToken ct) => _accepted.Reader.ReadAsync(ct); + + public void Stop() => _accepted.Writer.TryComplete(); +} diff --git a/KestrelMinima/KestrelMinimaKestrel.cs b/KestrelMinima/KestrelMinimaKestrel.cs new file mode 100644 index 0000000..67b7d60 --- /dev/null +++ b/KestrelMinima/KestrelMinimaKestrel.cs @@ -0,0 +1,149 @@ +using System.Net; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KestrelMinima; + +internal sealed class KestrelMinimaConnectionContext : ConnectionContext, + IConnectionIdFeature, IConnectionTransportFeature, IConnectionItemsFeature, + IConnectionLifetimeFeature, IConnectionEndPointFeature +{ + private static long s_id; + + private readonly Connection _conn; + private readonly ConnectionDualPipe _pipe; + private readonly CancellationTokenSource _closedCts = new(); + private readonly FeatureCollection _features = new(); + private bool _disposed; + + public KestrelMinimaConnectionContext(Connection conn, EndPoint? localEndPoint) + { + _conn = conn; + _pipe = new ConnectionDualPipe(conn); + + ConnectionId = $"kestrel-minima-{Interlocked.Increment(ref s_id):x}"; + LocalEndPoint = localEndPoint; + Items = new ConnectionItems(); + ConnectionClosed = _closedCts.Token; + + _features.Set(this); + _features.Set(this); + _features.Set(this); + _features.Set(this); + _features.Set(this); + } + + public override string ConnectionId { get; set; } + public override IFeatureCollection Features => _features; + public override IDictionary Items { get; set; } + public override IDuplexPipe Transport + { + get => _pipe; + set => throw new NotSupportedException("Transport is owned by the KestrelMinima transport."); + } + public override CancellationToken ConnectionClosed { get; set; } + public override EndPoint? LocalEndPoint { get; set; } + public override EndPoint? RemoteEndPoint { get; set; } + + public override void Abort(ConnectionAbortedException abortReason) + { + try { _closedCts.Cancel(); } catch { } + try { _pipe.Input.Complete(abortReason); } catch { } + try { _pipe.Output.Complete(abortReason); } catch { } + } + + public override ValueTask DisposeAsync() + { + if (_disposed) return ValueTask.CompletedTask; + _disposed = true; + try { _closedCts.Cancel(); } catch { } + try { _pipe.Input.Complete(); } catch { } + try { _pipe.Output.Complete(); } catch { } + _closedCts.Dispose(); + return ValueTask.CompletedTask; + } +} + +internal sealed class KestrelMinimaConnectionListener : IConnectionListener +{ + private readonly KestrelMinimaEngine _engine; + + public KestrelMinimaConnectionListener(KestrelMinimaEngine engine, EndPoint endpoint) + { + _engine = engine; + EndPoint = endpoint; + } + + public EndPoint EndPoint { get; } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + try + { + Connection conn = await _engine.AcceptAsync(cancellationToken).ConfigureAwait(false); + return new KestrelMinimaConnectionContext(conn, EndPoint); + } + catch (OperationCanceledException) { return null; } + catch (ChannelClosedException) { return null; } + } + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) { _engine.Stop(); return ValueTask.CompletedTask; } + public ValueTask DisposeAsync() { _engine.Stop(); return ValueTask.CompletedTask; } +} + +public sealed class KestrelMinimaTransportOptions +{ + public int ReactorCount { get; set; } = Math.Max(1, Environment.ProcessorCount); +} + +public sealed class KestrelMinimaTransportFactory : IConnectionListenerFactory +{ + private readonly KestrelMinimaTransportOptions _options; + private readonly ILogger _logger; + + public KestrelMinimaTransportFactory(IOptions options, ILoggerFactory loggerFactory) + { + _options = options.Value; + _logger = loggerFactory.CreateLogger(); + } + + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + { + if (endpoint is not IPEndPoint ip) + { + throw new NotSupportedException( + $"KestrelMinima only supports {nameof(IPEndPoint)} (got {endpoint.GetType().Name})."); + } + + var config = new ServerConfig { Port = (ushort)ip.Port, ReactorCount = _options.ReactorCount, Incremental = false }; + var engine = new KestrelMinimaEngine(config); + engine.Start(); + _logger.LogInformation("[kestrel-minima] Bound :{Port} with {ReactorCount} io_uring reactor(s) (fire-and-forget send)", ip.Port, _options.ReactorCount); + + IConnectionListener listener = new KestrelMinimaConnectionListener(engine, ip); + return ValueTask.FromResult(listener); + } +} + +public static class KestrelMinimaKestrelExtensions +{ + /// + /// Replace Kestrel's socket transport with KestrelMinima: a per-core io_uring reactor for + /// accept/recv and a fire-and-forget io_uring send (FlushAsync enqueues an SQE and returns + /// synchronously — no IValueTaskSource awaiter scheduling). Linux only, HTTP/1.1 plaintext. + /// + public static IWebHostBuilder UseKestrelMinima(this IWebHostBuilder builder, Action? configure = null) + { + builder.ConfigureServices(services => + { + if (configure is not null) services.Configure(configure); + services.AddSingleton(); + }); + return builder; + } +} diff --git a/KestrelMinima/Reactor/Reactor.cs b/KestrelMinima/Reactor/Reactor.cs new file mode 100644 index 0000000..9f74037 --- /dev/null +++ b/KestrelMinima/Reactor/Reactor.cs @@ -0,0 +1,510 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using KestrelMinima.Utils; +using static KestrelMinima.Native; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace KestrelMinima; + +/// +/// One reactor = one thread + one io_uring + one listening socket (SO_REUSEPORT) +/// + one connection map. The reactor thread is the sole writer of the SQ ring, +/// the kernel-shared buf_ring, and the connection map. Handlers may run on any +/// thread (e.g. resumed by a thread-pool timer after `await Task.Delay(1)`); +/// they reach the reactor only through two MPSC queues (`_returnQ`, `_flushQ`) +/// woken by an `eventfd` registered as a multishot poll in the ring. +/// +public sealed unsafe partial class Reactor +{ + public readonly int Id; + public Ring Ring = null!; // created on the reactor's own thread (DEFER_TASKRUN requires same-thread setup+enter) + public readonly Dictionary Connections = new(); + + /// Set by the Kestrel transport: vend each accepted connection instead of running an inline handler. + public Action? OnAccept; + + private int _listenFd; + private readonly ServerConfig _config; + private readonly ushort _port; + private readonly uint _ringEntries; + private readonly uint RecvBufferSize; + + // CQE user_data layout: kind tag in the high 32 bits, fd in the low 32. + private const ulong KindAccept = 1UL << 32; + private const ulong KindRecv = 2UL << 32; + private const ulong KindSend = 3UL << 32; + private const ulong KindWake = 4UL << 32; // eventfd-based cross-thread wake + + // Provided-buffer ring (one per reactor, shared by all its connections). + private const ushort BgId = 1; + private readonly uint BufferRingEntries; // power of two + private byte* _bufRing; // io_uring_buf_ring (kernel-shared) + private byte* _bufSlab; // contiguous slab of recv buffers + private uint _bufRingMask; + private ushort _bufRingTail; + + // Cross-thread wake mechanism: handlers running off-reactor enqueue work + // into these MPSC queues and `eventfd_write` _wakeFd; a multishot poll on + // _wakeFd registered with the ring delivers a CQE that wakes the reactor. + // When the caller is already the reactor thread (the common case — handler + // resumed inline from an IVTS SetResult), the Enqueue* methods bypass + // the queue and call the direct op, avoiding 2 syscalls per request. + private int _wakeFd; + private int _reactorThreadId; + private readonly Mpsc _returnQ = new(1 << 14); // 16384 slots + private readonly Mpsc _flushQ = new(1 << 12); // 4096 slots (fire-and-forget hand-off from off-reactor FlushAsync) + + // Connection pool. Reactor-thread-only — accept and teardown both run on + // this reactor, so a plain Stack is sufficient (no MPMC primitive + // needed). PoolMax caps the slab footprint per reactor: + // PoolMax × WriteSlabSize × ReactorCount = total reserved native memory. + private readonly int PoolMax; + private readonly Stack _pool; + + // Transient io_uring_enter errnos (Linux): interrupted, would-block, busy. + private const int EINTR = 4; + private const int EAGAIN = 11; + private const int EBUSY = 16; + + public Reactor(int id, ServerConfig config) + { + Id = id; + _config = config; + _port = config.Port; + _ringEntries = config.RingEntries; + RecvBufferSize = (uint)config.RecvBufferSize; + BufferRingEntries = (uint)config.BufferRingEntries; + PoolMax = config.PoolMax; + _pool = new Stack(config.PoolMax); + } + + // ========================================================================= + // Buffer ring + // ========================================================================= + + private void InitBufferRing() + { + nuint ringBytes = (nuint)BufferRingEntries * 16; + _bufRing = (byte*)NativeMemory.AlignedAlloc(ringBytes, 4096); + NativeMemory.Clear(_bufRing, ringBytes); + + nuint slabBytes = BufferRingEntries * (nuint)RecvBufferSize; + _bufSlab = (byte*)NativeMemory.AlignedAlloc(slabBytes, 64); + + _bufRingMask = BufferRingEntries - 1; + + var reg = new io_uring_buf_reg { + ring_addr = (ulong)_bufRing, + ring_entries = BufferRingEntries, + bgid = BgId, + }; + + int ret = io_uring_register(Ring.Fd, IORING_REGISTER_PBUF_RING, ®, 1); + if (ret < 0) + { + int err = Marshal.GetLastPInvokeError(); + + throw new InvalidOperationException($"register pbuf_ring failed: ret={ret} errno={err}"); + } + + // Populate every slot once. Slot 0 overlaps with the ring's tail field + // at offset 14, but we only write addr/len/bid (offsets 0..13) so tail + // stays at zero until we set it explicitly. + for (ushort bid = 0; bid < BufferRingEntries; bid++) { + byte* slot = _bufRing + (uint)bid * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + } + _bufRingTail = (ushort)BufferRingEntries; + + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + // Reactor-thread-only: writes the kernel-shared buf_ring tail directly. + // Off-reactor callers must use EnqueueReturnQ instead. + internal void ReturnBufferDirect(ushort bid) + { + byte* slot = _bufRing + (_bufRingTail & _bufRingMask) * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + _bufRingTail++; + + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + // ========================================================================= + // Cross-thread entry points (safe to call from any thread) + // ========================================================================= + + public void EnqueueReturnQ(ushort bid) + { + // Fast path: caller is the reactor thread (handler running inline from + // an IVTS SetResult). Go straight to the buf_ring — no queue, no syscall. + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + ReturnBufferDirect(bid); + return; + } + SpinWait sw = default; + while (!_returnQ.TryEnqueue(bid)) + { + sw.SpinOnce(); + } + WakeFdWrite(); + } + + internal void EnqueueFlush(int fd) + { + // Fast path: caller is the reactor thread; submit the SQE for the new + // bytes [WriteSubmitted, WriteTail) directly. + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + if (Connections.TryGetValue(fd, out var conn)) + { + SubmitSendRange(fd, conn); + } + return; + } + SpinWait sw = default; + while (!_flushQ.TryEnqueue(fd)) + { + sw.SpinOnce(); + } + WakeFdWrite(); + } + + private void WakeFdWrite() + { + ulong v = 1; + // 8-byte write to eventfd increments its counter; the kernel marks the + // fd readable, which fires our registered multishot poll's next CQE. + write(_wakeFd, &v, 8); + } + + private void DrainReturnQ() + { + while (_returnQ.TryDequeue(out ushort bid)) + { + ReturnBufferDirect(bid); + } + } + + private void DrainFlushQ() + { + while (_flushQ.TryDequeue(out int fd)) + { + if (!Connections.TryGetValue(fd, out var conn)) + { + continue; + } + // Mpsc Enqueue/Dequeue establishes happens-before, so WriteTail is + // visible here. + SubmitSendRange(fd, conn); + } + } + + // Submit a send for the new bytes the connection has produced since the last + // SubmitSend. Reactor-thread only — mutates WriteSubmitted. + private void SubmitSendRange(int fd, Connection conn) + { + int begin = conn.WriteSubmitted; + int end = conn.WriteTail; + if (end <= begin) + { + return; + } + SubmitSend(fd, conn.WriteBuffer + begin, (uint)(end - begin)); + conn.WriteSubmitted = end; + } + + private void ArmWakePoll() + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_POLL_ADD; + sqe->fd = _wakeFd; + sqe->op_flags = POLLIN; // poll32_events lives at this offset + sqe->len = IORING_POLL_ADD_MULTI; // multishot — stays armed across CQEs + sqe->user_data = KindWake | (uint)_wakeFd; + } + + // ========================================================================= + // Main loop + // ========================================================================= + + public void Run() + { + _reactorThreadId = Environment.CurrentManagedThreadId; + + Ring = Ring.Create(_ringEntries); + _listenFd = OpenReusePortListener(_port); + + InitBufferRing(); + + _wakeFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (_wakeFd < 0) + { + throw new InvalidOperationException("eventfd failed"); + } + + Console.WriteLine($"[kestrel-minima r{Id}] listening on 0.0.0.0:{_port}"); + SubmitAcceptMultishot(); + ArmWakePoll(); + + LoopShared(); + + close(_listenFd); + close(_wakeFd); + Ring.Dispose(); + } + + private void LoopShared() + { + while (true) + { + // Drain MPSC queues from off-reactor handlers. Cheap when empty. + DrainReturnQ(); + DrainFlushQ(); + + int rc = Ring.SubmitAndWait(1); + if (rc < 0 && rc != -EINTR && rc != -EAGAIN && rc != -EBUSY) + { + Console.Error.WriteLine($"[r{Id}] io_uring_enter failed: {rc}"); + break; + } + + uint ready = Ring.CqReady(); + for (uint i = 0; i < ready; i++) + { + Dispatch(in Ring.CqeAt(i)); + } + Ring.CqAdvance(ready); + } + } + + private void Dispatch(in IoUringCqe cqe) + { + ulong kind = cqe.user_data & 0xffffffff_00000000UL; + int fd = (int)(cqe.user_data & 0xffffffffUL); + bool more = (cqe.flags & IORING_CQE_F_MORE) != 0; + + if (kind == KindWake) + { + // Drain the eventfd counter so the next write re-triggers POLLIN + // (multishot poll is edge-triggered on the user_space side). + ulong drain; + read(_wakeFd, &drain, 8); + // The actual queue drains happen at the top of the next loop + // iteration — nothing else to do here. + if (!more) + { + ArmWakePoll(); + } + return; + } + + if (kind == KindAccept) + { + if (cqe.res >= 0) + { + int clientFd = cqe.res; + SetNoDelay(clientFd); + Connection conn = _pool.TryPop(out var pooled) + ? pooled.SetFd(clientFd) + : new Connection(this, clientFd, _config.WriteSlabSize); + Connections[clientFd] = conn; + conn.InitInputPipe(); // recv lands in a BCL pipe Kestrel reads + SubmitRecvMultishot(clientFd); + OnAccept?.Invoke(conn); // vend to the Kestrel transport + } + else + { + Console.Error.WriteLine($"[r{Id}] accept error: {cqe.res}"); + } + // Multishot accept stays armed; only re-arm if the kernel terminated it. + if (!more) + { + SubmitAcceptMultishot(); + } + } + else if (kind == KindRecv) + { + bool hasBuf = (cqe.flags & IORING_CQE_F_BUFFER) != 0; + ushort bid = hasBuf ? (ushort)(cqe.flags >> IORING_CQE_BUFFER_SHIFT) : (ushort)0; + + if (cqe.res <= 0) + { + // Peer EOF or recv error — reactor owns teardown. + if (hasBuf) + { + ReturnBufferDirect(bid); + } + if (Connections.Remove(fd, out var dyingConn)) + { + Recycle(dyingConn, fd); + } + return; + } + + if (!Connections.TryGetValue(fd, out var conn)) + { + // Straggler buffer for an already-closed connection. + if (hasBuf) + { + ReturnBufferDirect(bid); + } + return; + } + + // Kestrel: copy recv bytes into the BCL pipe, return the buffer. + if (hasBuf) + { + conn.FeedInput(_bufSlab + (nuint)bid * (nuint)RecvBufferSize, cqe.res); + ReturnBufferDirect(bid); + } + + if (!more) + { + SubmitRecvMultishot(fd); + } + } + else if (kind == KindSend) + { + if (!Connections.TryGetValue(fd, out var conn)) + { + return; + } + if (cqe.res <= 0) + { + // Send error — reactor owns teardown. + Connections.Remove(fd); + Recycle(conn, fd); + return; + } + conn.WriteHead += cqe.res; + if (conn.WriteHead < conn.WriteSubmitted) + { + // Partial send: resubmit the remainder of the most recent batch. + SubmitSend(fd, conn.WriteBuffer + conn.WriteHead, (uint)(conn.WriteSubmitted - conn.WriteHead)); + return; + } + // All submitted bytes ack'd. Reset only when Kestrel has produced + // nothing further; otherwise the next DrainFlushQ pass picks up the + // pending bytes. + if (conn.WriteHead == conn.WriteTail) + { + conn.CompleteFlush(); + } + } + } + + // ========================================================================= + // SQE producers (reactor-thread-only — Connection.FlushAsync hands off via + // EnqueueFlush, which DrainFlushQ turns into SubmitSend on this thread) + // ========================================================================= + + private IoUringSqe* GetSqeOrFlush() + { + IoUringSqe* sqe = Ring.GetSqe(); + if (sqe != null) + { + return sqe; + } + + Ring.SubmitAndWait(0); + sqe = Ring.GetSqe(); + + if (sqe == null) + { + throw new InvalidOperationException("SQ full after flush"); + } + + return sqe; + } + + private void SubmitAcceptMultishot() + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_ACCEPT; + sqe->ioprio = IORING_ACCEPT_MULTISHOT; + sqe->fd = _listenFd; + sqe->user_data = KindAccept | (uint)_listenFd; + } + + private void SubmitRecvMultishot(int fd) => SubmitRecvMultishot(fd, BgId); + + private void SubmitRecvMultishot(int fd, ushort bgid) + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_RECV; + sqe->flags = IOSQE_BUFFER_SELECT; + sqe->ioprio = IORING_RECV_MULTISHOT; + sqe->fd = fd; + sqe->buf_index = bgid; // buffer-group id (shared BgId, or per-conn in incremental) + sqe->user_data = KindRecv | (uint)fd; + } + + private void SubmitSend(int fd, byte* buf, uint len) + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_SEND; + sqe->fd = fd; + sqe->addr = (ulong)buf; + sqe->len = len; + sqe->user_data = KindSend | (uint)fd; + } + + private void Recycle(Connection conn, int fd) + { + // Kestrel transport: the connection is owned by the off-reactor consumer. + // Signal EOF to its input pipe, close the fd, and drop it. Do NOT Clear/pool — + // a recycled connection would race the consumer. + conn.CompleteInput(); + conn.MarkClosed(); + close(fd); + } + + // Disable Nagle on an accepted connection. Must be set per-accepted-socket, + // not on the listener — TCP_NODELAY doesn't reliably inherit across accept, + // which is why zerg/terraform/rtr all set it on the client fd, not the listener. + private static void SetNoDelay(int fd) + { + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(int)); + } + + private static int OpenReusePortListener(ushort port) + { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + throw new InvalidOperationException($"socket failed: {fd}"); + } + + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(int)); + + sockaddr_in addr = default; + addr.sin_family = AF_INET; + addr.sin_port = Htons(port); + addr.sin_addr.s_addr = 0; // 0.0.0.0 + + if (bind(fd, &addr, (uint)sizeof(sockaddr_in)) < 0) + { + throw new InvalidOperationException("bind failed"); + } + + if (listen(fd, 128) < 0) + { + throw new InvalidOperationException("listen failed"); + } + + return fd; + } +} diff --git a/KestrelMinima/ServerConfig.cs b/KestrelMinima/ServerConfig.cs new file mode 100644 index 0000000..561b040 --- /dev/null +++ b/KestrelMinima/ServerConfig.cs @@ -0,0 +1,35 @@ +namespace KestrelMinima; + +/// +/// All server tunables in one place — replaces the consts that used to be +/// scattered across Program.cs and Reactor.cs. Defaults match the previous +/// hardcoded values; override via object initializer in Main, e.g.: +/// new ServerConfig { Port = 9000, ReactorCount = 8, Incremental = true }. +/// +public sealed record ServerConfig +{ + // Server-level. + public ushort Port { get; init; } = 8080; + public int ReactorCount { get; init; } = 12; + + // Handler style: false = raw ReadAsync/TryGetItem loop; true = PipeReader/PipeWriter. + public bool UsePipe { get; init; } = false; + + // io_uring SQ/CQ depth. + public uint RingEntries { get; init; } = 8192; + + // Shared buffer ring (used when Incremental == false). + public int RecvBufferSize { get; init; } = 32 * 1024; + public int BufferRingEntries { get; init; } = 4096; + + // Per-connection write slab + connection pool cap. + public int WriteSlabSize { get; init; } = 256 * 1024; + public int PoolMax { get; init; } = 1024; + + // Incremental mode (IOU_PBUF_RING_INC) — per-connection rings. + // reserved native memory ≈ PoolMax × ConnBufRingEntries × IncRecvBufferSize × ReactorCount. + public bool Incremental { get; init; } = false; + public int MaxConnections { get; init; } = 4096; // GID cap (one bgid per active connection) + public int ConnBufRingEntries { get; init; } = 16; // buffers per connection ring + public int IncRecvBufferSize { get; init; } = 4096; // bytes per buffer (filled incrementally) +} diff --git a/KestrelMinima/Utils/Mpsc.cs b/KestrelMinima/Utils/Mpsc.cs new file mode 100644 index 0000000..3724976 --- /dev/null +++ b/KestrelMinima/Utils/Mpsc.cs @@ -0,0 +1,115 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace KestrelMinima.Utils; + +/// +/// Bounded lock-free multi-producer / single-consumer queue. +/// +/// Dmitry Vyukov's bounded MPMC algorithm, specialised to one consumer. +/// Power-of-two capacity, zero-allocation after construction. Producers claim a +/// slot via CAS on the enqueue position (a failed TryEnqueue on a full queue +/// leaves the position untouched — no burned tickets); the single consumer +/// advances the dequeue position with a plain write. Each slot carries a +/// sequence number that coordinates ownership between producers and consumer. +/// +/// One generic queue serves every reactor handoff: Mpsc<ushort> for buffer +/// returns, Mpsc<int> for flush fds, Mpsc<ulong> for packed incremental +/// returns. T is unmanaged so each Cell is a blittable value type with no GC refs. +/// +internal sealed class Mpsc where T : unmanaged +{ + private struct Cell + { + public long Sequence; + public T Value; + } + + private readonly Cell[] _buffer; + private readonly int _mask; + + // PaddedLong is a top-level struct (not nested here) because the CLR forbids + // explicit layout on a type nested inside a generic. + private PaddedLong _enqueuePos; + private PaddedLong _dequeuePos; + + public Mpsc(int capacityPow2) + { + if (capacityPow2 < 2 || (capacityPow2 & (capacityPow2 - 1)) != 0) + throw new ArgumentException("Capacity must be a power of two >= 2.", nameof(capacityPow2)); + + _buffer = new Cell[capacityPow2]; + _mask = capacityPow2 - 1; + + for (int i = 0; i < capacityPow2; i++) + _buffer[i].Sequence = i; + } + + /// Multi-producer safe. Returns false if the queue is full. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + while (true) + { + long pos = Volatile.Read(ref _enqueuePos.Value); + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - pos; + + if (dif == 0) + { + if (Interlocked.CompareExchange(ref _enqueuePos.Value, pos + 1, pos) == pos) + { + cell.Value = item; + Volatile.Write(ref cell.Sequence, pos + 1); + return true; + } + continue; // lost the race; reload and retry + } + + if (dif < 0) + return false; // slot not yet consumed → full + } + } + + /// Single-consumer only. Returns false if empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + long pos = _dequeuePos.Value; // single consumer: plain read + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - (pos + 1); + + if (dif == 0) + { + item = cell.Value; + _dequeuePos.Value = pos + 1; // single consumer: plain write + Volatile.Write(ref cell.Sequence, pos + mask + 1); // free slot for producers + return true; + } + + item = default; + return false; + } +} + +/// +/// A single long padded to a 64-byte cache line so the producer and consumer +/// positions never share a line (no false sharing). Top-level and non-generic +/// so it can legally use explicit layout. +/// +[StructLayout(LayoutKind.Explicit, Size = 64)] +internal struct PaddedLong +{ + [FieldOffset(0)] public long Value; +} diff --git a/KestrelMinima/Utils/RingSegment.cs b/KestrelMinima/Utils/RingSegment.cs new file mode 100644 index 0000000..2802b7a --- /dev/null +++ b/KestrelMinima/Utils/RingSegment.cs @@ -0,0 +1,31 @@ +using System.Buffers; + +namespace KestrelMinima.Utils; + +/// +/// One segment of a multi-buffer ReadOnlySequence<byte> built by the +/// ConnectionPipeReader when a single read spans more than one recv buffer. +/// BufferId is carried for debugging; buffer return is driven off the held +/// item list, not the segments. +/// +public sealed class RingSegment : ReadOnlySequenceSegment +{ + public ushort BufferId { get; } + + public RingSegment(ReadOnlyMemory memory, ushort bufferId) + { + Memory = memory; + BufferId = bufferId; + } + + public RingSegment Append(ReadOnlyMemory memory, ushort bufferId) + { + var next = new RingSegment(memory, bufferId) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = next; + return next; + } +} diff --git a/KestrelMinima/Utils/SpscRecvRing.cs b/KestrelMinima/Utils/SpscRecvRing.cs new file mode 100644 index 0000000..4eec97a --- /dev/null +++ b/KestrelMinima/Utils/SpscRecvRing.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace KestrelMinima.Utils; + +public sealed unsafe class SpscRecvRing +{ + public struct Item + { + public byte* Ptr; + public ushort Bid; + public int Len; + public bool HasBuffer; + public ushort Gen; // connection generation when enqueued (incremental return guard) + + public ReadOnlySpan AsSpan() => new(Ptr, Len); + + public UnmanagedMemoryManager AsMemoryManager() => new(Ptr, Len, Bid); + } + + private readonly Item[] _items; + private readonly int _mask; + private long _tail; + private long _head; + + public SpscRecvRing(int capacityPow2) + { + if (capacityPow2 <= 0 || (capacityPow2 & (capacityPow2 - 1)) != 0) + { + throw new ArgumentException("capacity must be a power of two", nameof(capacityPow2)); + } + + _items = new Item[capacityPow2]; + _mask = capacityPow2 - 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(in Item item) + { + long head = Volatile.Read(ref _head); + long tail = _tail; + + if ((ulong)(tail - head) >= (ulong)_items.Length) + { + return false; + } + + _items[(int)(tail & _mask)] = item; + Volatile.Write(ref _tail, tail + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out Item item) + { + long head = _head; + long tail = Volatile.Read(ref _tail); + + if (head >= tail) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long SnapshotTail() => Volatile.Read(ref _tail); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeueUntil(long tailSnapshot, out Item item) + { + long head = _head; + + if (head >= tailSnapshot) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEmpty() => Volatile.Read(ref _head) >= Volatile.Read(ref _tail); + + // Reactor-thread-only, called during connection teardown (Clear) when no + // handler is consuming. Discards any leftover items so the recycled + // connection starts empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _head = 0; + _tail = 0; + } +} diff --git a/KestrelMinima/Utils/UnmanagedMemoryManager.cs b/KestrelMinima/Utils/UnmanagedMemoryManager.cs new file mode 100644 index 0000000..794f389 --- /dev/null +++ b/KestrelMinima/Utils/UnmanagedMemoryManager.cs @@ -0,0 +1,32 @@ +using System.Buffers; + +namespace KestrelMinima.Utils; + +public sealed unsafe class UnmanagedMemoryManager : MemoryManager +{ + private readonly byte* _ptr; + private readonly int _length; + + public ushort BufferId { get; } + + public UnmanagedMemoryManager(byte* ptr, int length) + { + _ptr = ptr; + _length = length; + } + + public UnmanagedMemoryManager(byte* ptr, int length, ushort bufferId) + { + _ptr = ptr; + _length = length; + BufferId = bufferId; + } + + public override Span GetSpan() => new(_ptr, _length); + + public override MemoryHandle Pin(int elementIndex = 0) => new(_ptr + elementIndex); + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } +} diff --git a/KestrelMinima/_usings.cs b/KestrelMinima/_usings.cs new file mode 100644 index 0000000..085b05b --- /dev/null +++ b/KestrelMinima/_usings.cs @@ -0,0 +1,10 @@ +global using System; +global using System.Buffers; +global using System.Collections.Generic; +global using System.IO.Pipelines; +global using System.Runtime.CompilerServices; +global using System.Runtime.InteropServices; +global using System.Threading; +global using System.Threading.Channels; +global using System.Threading.Tasks; +global using System.Threading.Tasks.Sources; diff --git a/KestrelMinima/io_uring/Native.cs b/KestrelMinima/io_uring/Native.cs new file mode 100644 index 0000000..ca19e4c --- /dev/null +++ b/KestrelMinima/io_uring/Native.cs @@ -0,0 +1,162 @@ +using System.Runtime.InteropServices; + +namespace KestrelMinima; + +/// +/// All native interop in one file: io_uring syscalls, libc socket calls, +/// the kernel struct layouts they expect, and the constants needed to +/// drive a minimal io_uring loop. +/// +public static unsafe class Native { + private const long SYS_IO_URING_SETUP = 425; + private const long SYS_IO_URING_ENTER = 426; + private const long SYS_IO_URING_REGISTER = 427; + + public const byte IORING_OP_POLL_ADD = 6; + public const byte IORING_OP_ACCEPT = 13; + public const byte IORING_OP_SEND = 26; + public const byte IORING_OP_RECV = 27; + public const uint IORING_ENTER_GETEVENTS = 1u << 0; + public const long IORING_OFF_SQ_RING = 0; + public const long IORING_OFF_SQES = 0x10000000; + + // Multishot / buffer-ring goodies. + public const ushort IORING_ACCEPT_MULTISHOT = 1 << 0; + public const ushort IORING_RECV_MULTISHOT = 1 << 1; + public const byte IOSQE_BUFFER_SELECT = 1 << 5; + public const uint IORING_CQE_F_BUFFER = 1u << 0; + public const uint IORING_CQE_F_MORE = 1u << 1; + public const int IORING_CQE_BUFFER_SHIFT = 16; + public const uint IORING_REGISTER_PBUF_RING = 22; + public const uint IORING_UNREGISTER_PBUF_RING = 23; + public const uint IORING_POLL_ADD_MULTI = 1u << 0; + + // Incremental provided-buffer consumption (kernel 6.12+). IOU_PBUF_RING_INC + // is set in io_uring_buf_reg.flags at registration; IORING_CQE_F_BUF_MORE is + // set on recv CQEs while the kernel will keep appending to the same buffer. + public const ushort IOU_PBUF_RING_INC = 2; + public const uint IORING_CQE_F_BUF_MORE = 1u << 4; + + // eventfd flags + poll mask (used for the cross-thread wake mechanism). + public const int EFD_CLOEXEC = 0x80000; + public const int EFD_NONBLOCK = 0x800; + public const uint POLLIN = 0x0001; + + // Setup flags. SINGLE_ISSUER tells the kernel only one thread will submit + // to this ring (skips locking on the SQ). DEFER_TASKRUN defers completion + // processing until io_uring_enter(GETEVENTS), which lets the kernel batch + // work and avoids interrupting the reactor with task_work mid-flight. + public const uint IORING_SETUP_SINGLE_ISSUER = 1u << 12; + public const uint IORING_SETUP_DEFER_TASKRUN = 1u << 13; + + public const int PROT_READ = 1; + public const int PROT_WRITE = 2; + public const int MAP_SHARED = 1; + public const int MAP_POPULATE = 0x8000; + + public const int AF_INET = 2; + public const int SOCK_STREAM = 1; + public const int SOL_SOCKET = 1; + public const int SO_REUSEADDR = 2; + public const int SO_REUSEPORT = 15; + public const int IPPROTO_TCP = 6; + public const int TCP_NODELAY = 1; + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall3(long nr, uint a1, IoUringParams* a2); + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall6(long nr, uint a1, uint a2, uint a3, uint a4, void* a5, nuint a6); + + [DllImport("libc", EntryPoint = "syscall", SetLastError = true)] + private static extern long syscall4(long nr, uint a1, uint a2, void* a3, uint a4); + + public static int io_uring_setup(uint entries, IoUringParams* p) => + (int)syscall3(SYS_IO_URING_SETUP, entries, p); + + public static int io_uring_enter(int fd, uint toSubmit, uint minComplete, uint flags) => + (int)syscall6(SYS_IO_URING_ENTER, (uint)fd, toSubmit, minComplete, flags, null, 0); + + public static int io_uring_register(int fd, uint opcode, void* arg, uint nrArgs) => + (int)syscall4(SYS_IO_URING_REGISTER, (uint)fd, opcode, arg, nrArgs); + + [DllImport("libc")] public static extern void* mmap(void* addr, nuint length, int prot, int flags, int fd, long offset); + [DllImport("libc")] public static extern int munmap(void* addr, nuint length); + [DllImport("libc")] public static extern int close(int fd); + [DllImport("libc")] public static extern int socket(int domain, int type, int proto); + [DllImport("libc")] public static extern int bind(int fd, sockaddr_in* addr, uint len); + [DllImport("libc")] public static extern int listen(int fd, int backlog); + [DllImport("libc")] public static extern int setsockopt(int fd, int level, int optname, void* optval, uint optlen); + [DllImport("libc")] public static extern int eventfd(uint initval, int flags); + [DllImport("libc")] public static extern long write(int fd, void* buf, nuint count); + [DllImport("libc")] public static extern long read(int fd, void* buf, nuint count); + + public static ushort Htons(ushort x) => (ushort)((x << 8) | (x >> 8)); + + // Kernel struct layouts (must match include/uapi/linux/io_uring.h) + [StructLayout(LayoutKind.Sequential)] + public struct SqRingOffsets { + public uint head, tail, ring_mask, ring_entries, flags, dropped, array, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CqRingOffsets { + public uint head, tail, ring_mask, ring_entries, overflow, cqes, flags, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringParams { + public uint sq_entries, cq_entries, flags, sq_thread_cpu, sq_thread_idle; + public uint features, wq_fd, resv0, resv1, resv2; + public SqRingOffsets sq_off; + public CqRingOffsets cq_off; + } + + [StructLayout(LayoutKind.Explicit, Size = 64)] + public struct IoUringSqe { + [FieldOffset(0)] public byte opcode; + [FieldOffset(1)] public byte flags; + [FieldOffset(2)] public ushort ioprio; + [FieldOffset(4)] public int fd; + [FieldOffset(8)] public ulong off; + [FieldOffset(16)] public ulong addr; + [FieldOffset(24)] public uint len; + [FieldOffset(28)] public uint op_flags; + [FieldOffset(32)] public ulong user_data; + [FieldOffset(40)] public ushort buf_index; + [FieldOffset(42)] public ushort personality; + [FieldOffset(44)] public int splice_fd_in; + [FieldOffset(48)] public ulong addr3; + [FieldOffset(56)] public ulong __pad2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringCqe { + public ulong user_data; + public int res; + public uint flags; + } + + // Argument struct for IORING_REGISTER_PBUF_RING. + [StructLayout(LayoutKind.Sequential)] + public struct io_uring_buf_reg { + public ulong ring_addr; + public uint ring_entries; + public ushort bgid; + public ushort flags; + public ulong resv1, resv2, resv3; + } + + [StructLayout(LayoutKind.Sequential)] + public struct in_addr { public uint s_addr; } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct sockaddr_in { + public ushort sin_family; + public ushort sin_port; + public in_addr sin_addr; + public fixed byte sin_zero[8]; + } +} diff --git a/KestrelMinima/io_uring/Ring.cs b/KestrelMinima/io_uring/Ring.cs new file mode 100644 index 0000000..240c423 --- /dev/null +++ b/KestrelMinima/io_uring/Ring.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +using static KestrelMinima.Native; + +// ReSharper disable SuggestVarOrType_BuiltInTypes +// ReSharper disable SuggestVarOrType_Elsewhere +#pragma warning disable CA1806 + +namespace KestrelMinima; + +public sealed unsafe class Ring : IDisposable +{ + private int _fd; + + public int Fd => _fd; + + private uint* _sqHead; + private uint* _sqTail; + private uint* _sqArray; + private uint _sqMask; + private uint _sqEntries; + private IoUringSqe* _sqes; + + private uint* _cqHead; + private uint* _cqTail; + private IoUringCqe* _cqes; + private uint _cqMask; + + private uint _sqeTail; + + private byte* _ringPtr; + private nuint _ringSize; + private byte* _sqePtr; + private nuint _sqeSize; + + public static Ring Create(uint entries) + { + IoUringParams ioUringParams = default; + ioUringParams.flags = IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN; + int fd = io_uring_setup(entries, &ioUringParams); + if (fd < 0) + { + throw new InvalidOperationException($"io_uring_setup failed: {fd}"); + } + + var ring = new Ring + { + _fd = fd, + _sqEntries = ioUringParams.sq_entries + }; + + nuint sqRingBytes = ioUringParams.sq_off.array + ioUringParams.sq_entries * sizeof(uint); + nuint cqRingBytes = ioUringParams.cq_off.cqes + ioUringParams.cq_entries * (nuint)sizeof(IoUringCqe); + nuint ringBytes = sqRingBytes > cqRingBytes ? sqRingBytes : cqRingBytes; + + void* ringMem = mmap(null, ringBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING); + if (ringMem == (void*)-1) + { + close(fd); + + throw new InvalidOperationException("mmap(SQ_RING) failed"); + } + ring._ringPtr = (byte*)ringMem; + ring._ringSize = ringBytes; + + nuint sqeBytes = ioUringParams.sq_entries * (nuint)sizeof(IoUringSqe); + void* sqeMem = mmap(null, sqeBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQES); + if (sqeMem == (void*)-1) + { + munmap(ringMem, ringBytes); + close(fd); + + throw new InvalidOperationException("mmap(SQES) failed"); + } + ring._sqes = (IoUringSqe*)sqeMem; + ring._sqePtr = (byte*)sqeMem; + ring._sqeSize = sqeBytes; + + byte* ringPointer = (byte*)ringMem; + ring._sqHead = (uint*)(ringPointer + ioUringParams.sq_off.head); + ring._sqTail = (uint*)(ringPointer + ioUringParams.sq_off.tail); + ring._sqArray = (uint*)(ringPointer + ioUringParams.sq_off.array); + ring._sqMask = *(uint*)(ringPointer + ioUringParams.sq_off.ring_mask); + + ring._cqHead = (uint*)(ringPointer + ioUringParams.cq_off.head); + ring._cqTail = (uint*)(ringPointer + ioUringParams.cq_off.tail); + ring._cqes = (IoUringCqe*)(ringPointer + ioUringParams.cq_off.cqes); + ring._cqMask = *(uint*)(ringPointer + ioUringParams.cq_off.ring_mask); + + return ring; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IoUringSqe* GetSqe() + { + uint head = Volatile.Read(ref *_sqHead); + + if (_sqeTail - head >= _sqEntries) + { + return null; + } + + uint slot = _sqeTail & _sqMask; + _sqArray[slot] = slot; + _sqeTail++; + + return &_sqes[slot]; + } + + public int SubmitAndWait(uint waitFor) + { + uint published = *_sqTail; + uint toSubmit = _sqeTail - published; + + if (toSubmit > 0) + { + Volatile.Write(ref *_sqTail, _sqeTail); + } + + if (toSubmit == 0 && waitFor == 0) return 0; + + uint flags = waitFor > 0 ? IORING_ENTER_GETEVENTS : 0; + + return io_uring_enter(_fd, toSubmit, waitFor, flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetCqe(out IoUringCqe cqe) + { + uint head = *_cqHead; + uint tail = Volatile.Read(ref *_cqTail); + + if (head == tail) + { + cqe = default; + + return false; + } + + cqe = _cqes[head & _cqMask]; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqeSeen() => Volatile.Write(ref *_cqHead, *_cqHead + 1); + + // Batched CQ drain (liburing io_uring_for_each_cqe + io_uring_cq_advance): + // read the kernel-written tail once (acquire), process the whole batch, + // then publish the consumed head once (release) instead of once per CQE. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint CqReady() => Volatile.Read(ref *_cqTail) - *_cqHead; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref readonly IoUringCqe CqeAt(uint i) => ref _cqes[(*_cqHead + i) & _cqMask]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqAdvance(uint n) => Volatile.Write(ref *_cqHead, *_cqHead + n); + + public void Dispose() + { + if (_ringPtr != null) + { + munmap(_ringPtr, _ringSize); _ringPtr = null; + } + + if (_sqePtr != null) + { + munmap(_sqePtr, _sqeSize); _sqePtr = null; + } + + if (_fd > 0) + { + close(_fd); _fd = 0; + } + } +} + +#pragma warning restore CA1806 diff --git a/Minima/Program.cs b/Minima/Program.cs index c4e38a0..241abaa 100644 --- a/Minima/Program.cs +++ b/Minima/Program.cs @@ -23,7 +23,7 @@ private static int Main() var config = new ServerConfig() { UsePipe = false, - ReactorCount = 24 + ReactorCount = 12 }; Console.WriteLine($"[Minima] starting {config.ReactorCount} reactors on port {config.Port} (incremental={config.Incremental})"); @@ -55,7 +55,7 @@ internal static class Handler // Real async-work knob: serialize an in-memory object of WORK_ITEMS elements to JSON // on the THREAD POOL (via Task.Run) per request. 0 / unset = disabled (pure inline // reactor path). Genuine CPU + allocation, not a busy-spin. - private static readonly int WorkItems = 50; + private static readonly int WorkItems = 1; private static readonly Payload LargeObject = BuildPayload(Math.Max(WorkItems, 1)); @@ -91,16 +91,18 @@ public static async Task HandleAsync(Reactor reactor, Connection conn) conn.ReturnBuffer(in item); } } - + + _ = await Task.Run(static () => JsonSerializer.Serialize("Hello World!")); + // Real async work: serialize a large object to JSON on the THREAD POOL. // The handler resumes OFF-REACTOR, so the FlushAsync below pays the eventfd // handoff the pure-inline path avoids — and the serialization is genuine // CPU + GC pressure on the pool, not a busy-spin. - if (WorkItems > 0) + /*if (WorkItems > 0) { - //_ = await Task.Run(static () => JsonSerializer.SerializeToUtf8Bytes(LargeObject)); - JsonSerializer.SerializeToUtf8Bytes(LargeObject); - } + _ = await Task.Run(static () => JsonSerializer.SerializeToUtf8Bytes(LargeObject)); + //JsonSerializer.SerializeToUtf8Bytes(LargeObject); + }*/ // One response per recv burst — accumulate in the connection's // per-connection write slab, then submit and await ack. diff --git a/Minima/Reactor/Reactor.cs b/Minima/Reactor/Reactor.cs index 0346f62..7f51e32 100644 --- a/Minima/Reactor/Reactor.cs +++ b/Minima/Reactor/Reactor.cs @@ -170,7 +170,7 @@ public void EnqueueReturnQ(ushort bid) { sw.SpinOnce(); } - WakeFdWrite(); + //WakeFdWrite(); } internal void EnqueueFlush(int fd) diff --git a/MinimaSQPoll/Connection/Connection.Read.cs b/MinimaSQPoll/Connection/Connection.Read.cs new file mode 100644 index 0000000..444e1ae --- /dev/null +++ b/MinimaSQPoll/Connection/Connection.Read.cs @@ -0,0 +1,167 @@ +using System.Threading.Tasks.Sources; +using MinimaSQPoll.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll; + +/// +/// Per-connection state. The handler may run on any thread (e.g. resumed by +/// a thread-pool timer); reactor-only side effects are funnelled through the +/// MPSC queues on `Reactor`. Coordination uses Interlocked.Exchange on the +/// arm flags and a sticky `_pending` to close the lost-wakeup race. +/// +/// Lifetime is pool-managed: the reactor pops a Connection on accept (or new +/// one if pool is empty), and pushes it back on teardown after `Clear()`. The +/// `_generation` field is bumped on each `Clear` so stale `ValueTask` tokens +/// from a previous connection life are detectable and return `Closed()` +/// instead of leaking the new tenant's state. +/// +public sealed unsafe partial class Connection : IValueTaskSource +{ + internal Connection SetFd(int fd) + { + ClientFd = fd; + return this; + } + + private ManualResetValueTaskSourceCore _readSignal = new() + { + RunContinuationsAsynchronously = true, + }; + + private int _armed; + private int _pending; + private int _closed; + + private readonly SpscRecvRing _recv = new(capacityPow2: 16); + + public ValueTask ReadAsync() + { + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1) + { + Volatile.Write(ref _pending, 0); + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + if (Volatile.Read(ref _closed) != 0) + { + return new ValueTask(RecvSnapshot.Closed()); + } + + if (Interlocked.Exchange(ref _armed, 1) == 1) + { + throw new InvalidOperationException("ReadAsync already armed."); + } + + // Snapshot the generation as the IVTS token so a future Clear() can + // invalidate this awaiter if the connection gets pool-recycled. + int gen = Volatile.Read(ref _generation); + + // Race recovery: re-check between arming and returning the IVTS task. + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1 || Volatile.Read(ref _closed) != 0) + { + Volatile.Write(ref _pending, 0); + Interlocked.Exchange(ref _armed, 0); + + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + return new ValueTask(this, (short)gen); + } + + public bool TryGetItem(in RecvSnapshot snap, out SpscRecvRing.Item item) + => _recv.TryDequeueUntil(snap.Tail, out item); + + public void ResetRead() => _readSignal.Reset(); + + public void Complete(int res, ushort bid, bool hasBuffer, byte* ptr) + { + if (!_recv.TryEnqueue(new SpscRecvRing.Item + { + Ptr = ptr, + Bid = bid, + Len = res, + HasBuffer = hasBuffer, + Gen = (ushort)Volatile.Read(ref _generation) + })) + { + Console.Error.WriteLine("[conn] recv queue overflow."); + if (hasBuffer) + { + _reactor.ReturnBufferDirect(bid); + } + Volatile.Write(ref _closed, 1); + } + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + else + { + Volatile.Write(ref _pending, 1); + } + } + + internal void DrainRecv() + { + // Return any buffer IDs still sitting in the SPSC ring (handler exited + // before draining them, or a recv arrived after _closed was set). + while (_recv.TryDequeue(out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + _reactor.ReturnBufferDirect(item.Bid); + } + } + } + + // ========================================================================= + // IValueTaskSource plumbing — token (= snapshot of `_generation` at await + // time) is compared against the current `_generation` to detect stale + // awaiters from before a Clear()/pool reuse. Stale awaiters get a + // sentinel result rather than the new tenant's state. + // + // For the actual IVTS dispatch we pass `_readSignal.Version` / + // `_flushSignal.Version` to the underlying core (not `token`) because the + // core's version is bumped by ResetRead/CompleteFlush mid-life and is + // unrelated to the cross-life generation guard. + // ========================================================================= + + RecvSnapshot IValueTaskSource.GetResult(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return RecvSnapshot.Closed(); + } + + return _readSignal.GetResult(_readSignal.Version); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return ValueTaskSourceStatus.Succeeded; + } + + return _readSignal.GetStatus(_readSignal.Version); + } + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (token != (short)Volatile.Read(ref _generation)) + { + // Stale — run the continuation now so the awaiter unblocks and + // gets RecvSnapshot.Closed() from GetResult. + continuation(state); + + return; + } + + _readSignal.OnCompleted(continuation, state, _readSignal.Version, flags); + } +} diff --git a/MinimaSQPoll/Connection/Connection.Write.cs b/MinimaSQPoll/Connection/Connection.Write.cs new file mode 100644 index 0000000..3ea08b4 --- /dev/null +++ b/MinimaSQPoll/Connection/Connection.Write.cs @@ -0,0 +1,185 @@ +using System.Buffers; +using System.Threading.Tasks.Sources; +using MinimaSQPoll.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll; + +public sealed unsafe partial class Connection : IValueTaskSource, IBufferWriter +{ + private readonly int _writeSlabSize; + internal byte* WriteBuffer; + internal int WriteHead; + internal int WriteTail; + internal int WriteInFlight; + + private readonly UnmanagedMemoryManager _manager; + + private ManualResetValueTaskSourceCore _flushSignal = new() + { + RunContinuationsAsynchronously = true, + }; + private int _flushArmed; + private int _flushInProgress; + + // IBufferWrite +#region IBufferWrite + + public Memory GetMemory(int sizeHint = 0) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + int remaining = _writeSlabSize - WriteTail; + if (sizeHint > remaining) + { + throw new InvalidOperationException("Buffer too small."); + } + + return _manager.Memory.Slice(WriteTail, remaining); + } + + public Span GetSpan(int sizeHint = 0) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + if (WriteTail + sizeHint > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + + return new Span(WriteBuffer + WriteTail, _writeSlabSize - WriteTail); + } + + public void Advance(int count) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + WriteTail += count; + } + +#endregion + + // Write to the inner buffer + public void Write(ReadOnlySpan source) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + int len = source.Length; + if (WriteTail + len > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + + source.CopyTo(new Span(WriteBuffer + WriteTail, len)); + WriteTail += len; + } + + // Flush inner buffer data to the kernel + public ValueTask FlushAsync() + { + // Connection already torn down (reactor saw EOF/error → MarkClosed): don't flush + // a removed connection — the handoff would reach a reactor that no longer knows + // this fd and the awaiter would hang. Return completed so the handler unwinds to + // its next ReadAsync, sees IsClosed, and exits. + if (Volatile.Read(ref _closed) == 1) + { + return default; + } + + if (Interlocked.Exchange(ref _flushInProgress, 1) == 1) + { + throw new InvalidOperationException("FlushAsync already in progress."); + } + + int target = WriteTail; + if (target == 0) + { + Volatile.Write(ref _flushInProgress, 0); + + return default; + } + + if (Interlocked.Exchange(ref _flushArmed, 1) == 1) + { + throw new InvalidOperationException("FlushAsync already armed."); + } + + _flushSignal.Reset(); + WriteInFlight = target; + + int gen = Volatile.Read(ref _generation); + + _reactor.EnqueueFlush(this); + + // Race recovery (mirrors ReadAsync): if close raced in after the guard above, + // self-complete so we don't hang waiting on a send the reactor will never make. + if (Volatile.Read(ref _closed) == 1 && Interlocked.Exchange(ref _flushArmed, 0) == 1) + { + Volatile.Write(ref _flushInProgress, 0); + _flushSignal.SetResult(true); + } + + return new ValueTask(this, (short)gen); + } + + // Signal the FlushAsync was completed, called by the reactor's dispatcher send branch + internal void CompleteFlush() + { + WriteHead = 0; + WriteTail = 0; + WriteInFlight = 0; + Volatile.Write(ref _flushInProgress, 0); + Interlocked.Exchange(ref _flushArmed, 0); + + _flushSignal.SetResult(true); + } + + // IValueTaskSource +#region IValueTaskSource + + void IValueTaskSource.GetResult(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return; + } + + _flushSignal.GetResult(_flushSignal.Version); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return ValueTaskSourceStatus.Succeeded; + } + + return _flushSignal.GetStatus(_flushSignal.Version); + } + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (token != (short)Volatile.Read(ref _generation)) + { + continuation(state); + + return; + } + _flushSignal.OnCompleted(continuation, state, _flushSignal.Version, flags); + } + +#endregion +} \ No newline at end of file diff --git a/MinimaSQPoll/Connection/Connection.cs b/MinimaSQPoll/Connection/Connection.cs new file mode 100644 index 0000000..a2fa671 --- /dev/null +++ b/MinimaSQPoll/Connection/Connection.cs @@ -0,0 +1,111 @@ +using System.Runtime.InteropServices; +using MinimaSQPoll.Utils; + +namespace MinimaSQPoll; + +public sealed unsafe partial class Connection +{ + private readonly Reactor _reactor; + + public int ClientFd { get; private set; } + + // Bumped on Clear(); the low 16 bits are used as the IVTS token so stale + // awaiters can be detected after pool reuse. + private int _generation; + + // Refcount: the connection has two owners — the reactor (recv side) and the + // handler (which may run off-reactor). Init to 2 on accept; each owner DecRef's + // when done; teardown (Recycle) runs only at refs==0, so a connection is never + // recycled or pool-reused while a handler is still in flight on another thread. + private int _refs; + + public Connection(Reactor reactor, int fd, int writeSlabSize = 1024 * 16) + { + _reactor = reactor; + ClientFd = fd; + _writeSlabSize = writeSlabSize; + WriteBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)writeSlabSize, 64); + + _manager = new UnmanagedMemoryManager(WriteBuffer, writeSlabSize); + } + + // ========================================================================= + // Pool lifecycle — invoked from Reactor.Dispatch's recv/send error paths. + // Reactor-thread only. + // + // teardown: MarkClosed() → wake awaiters with closed=1 + // DrainRecv() → return any in-flight buf_ring items + // close(fd) + // Clear() → reset state, bump _generation + // push to pool, OR Dispose() if pool is full + // ========================================================================= + + public void MarkClosed() + { + Volatile.Write(ref _closed, 1); + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), isClosed: true)); + } + else + { + Volatile.Write(ref _pending, 1); + } + + if (Interlocked.Exchange(ref _flushArmed, 0) == 1) + { + Volatile.Write(ref _flushInProgress, 0); + _flushSignal.SetResult(true); + } + } + + // Init to 2 (reactor + handler) at accept. + internal void InitRefs() => Volatile.Write(ref _refs, 2); + + // Release one owner's ref. Whoever drives it to 0 hands the connection to the + // reactor for teardown (close + Clear + pool) — never recycled before both done. + internal void DecRef() + { + if (Interlocked.Decrement(ref _refs) == 0) + { + _reactor.EnqueueRecycle(this); + } + } + + internal void Clear() + { + // Bump generation first — readers of IVTS plumbing observe this via + // Volatile.Read and stale tokens get RecvSnapshot.Closed() / no-op. + Interlocked.Increment(ref _generation); + + Volatile.Write(ref _armed, 0); + Volatile.Write(ref _pending, 0); + Volatile.Write(ref _closed, 0); + Volatile.Write(ref _flushArmed, 0); + Volatile.Write(ref _flushInProgress, 0); + + WriteHead = 0; + WriteTail = 0; + WriteInFlight = 0; + + _readSignal.Reset(); + _flushSignal.Reset(); + + _recv.Reset(); // discard any leftover SPSC items + } + + public void Dispose() + { + if (WriteBuffer != null) + { + NativeMemory.AlignedFree(WriteBuffer); + WriteBuffer = null; + } + } + + // Convenience: hand a buffer back to the reactor's shared buf_ring. Lives + // here (instead of the deleted Connection.Incremental.cs) because the raw + // handler API in Program.cs and ConnectionPipeReader both call it. + public void ReturnBuffer(in SpscRecvRing.Item item) => _reactor.EnqueueReturnQ(item.Bid); +} \ No newline at end of file diff --git a/MinimaSQPoll/Connection/ConnectionDualPipe.cs b/MinimaSQPoll/Connection/ConnectionDualPipe.cs new file mode 100644 index 0000000..3063eeb --- /dev/null +++ b/MinimaSQPoll/Connection/ConnectionDualPipe.cs @@ -0,0 +1,16 @@ +using System.IO.Pipelines; + +namespace MinimaSQPoll; + +public sealed class ConnectionDualPipe : IDuplexPipe +{ + public PipeReader Input { get; } + public PipeWriter Output { get; } + + public ConnectionDualPipe(Connection connection) + { + ArgumentNullException.ThrowIfNull(connection); + Input = new ConnectionPipeReader(connection); + Output = new ConnectionPipeWriter(connection); + } +} \ No newline at end of file diff --git a/MinimaSQPoll/Connection/ConnectionPipeReader.cs b/MinimaSQPoll/Connection/ConnectionPipeReader.cs new file mode 100644 index 0000000..8a9de71 --- /dev/null +++ b/MinimaSQPoll/Connection/ConnectionPipeReader.cs @@ -0,0 +1,181 @@ +using System.Buffers; +using System.IO.Pipelines; +using MinimaSQPoll.Utils; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll; + +/// +/// Adapts Minima's raw read API (ReadAsync + TryGetItem +/// + ReturnBuffer) to a standard . Recv buffers are +/// exposed zero-copy as a ReadOnlySequence<byte> (one segment per buffer) +/// and held until AdvanceTo consumes them, at which point fully-consumed buffers +/// are returned to the reactor. +/// +/// Convenience/compat layer for PipeReader consumers — the raw ReadAsync/ +/// TryGetItem path stays the faster one (this adds held-buffer + sequence +/// bookkeeping per read). +/// +public sealed class ConnectionPipeReader : PipeReader +{ + private readonly Connection _conn; + private readonly List _held = new(16); + private ReadOnlySequence _lastSequence; + + private bool _completed; + private bool _cancelRequested; + private bool _connectionClosed; + + private readonly struct Held + { + public readonly ReadOnlyMemory Memory; + public readonly SpscRecvRing.Item Item; + + public Held(ReadOnlyMemory memory, SpscRecvRing.Item item) + { + Memory = memory; + Item = item; + } + + public Held WithMemory(ReadOnlyMemory memory) => new(memory, Item); + } + + public ConnectionPipeReader(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + ThrowIfCompleted(); + + if (_cancelRequested) + { + _cancelRequested = false; + return new ReadResult(BuildSequence(), isCanceled: true, isCompleted: _connectionClosed); + } + + // Anything still held from a previous read that wasn't fully consumed. + if (_held.Count > 0) + return new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + + if (_connectionClosed) + return new ReadResult(default, isCanceled: false, isCompleted: true); + + RecvSnapshot snap = await _conn.ReadAsync(); + + while (_conn.TryGetItem(snap, out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + _held.Add(new Held(item.AsMemoryManager().Memory, item)); + } + + _conn.ResetRead(); + + if (snap.IsClosed) + _connectionClosed = true; + + if (_cancelRequested) + { + _cancelRequested = false; + return new ReadResult(BuildSequence(), isCanceled: true, isCompleted: _connectionClosed); + } + + return new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + } + + public override bool TryRead(out ReadResult result) + { + ThrowIfCompleted(); + + if (_held.Count > 0) + { + result = new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + return true; + } + + if (_connectionClosed) + { + result = new ReadResult(default, isCanceled: false, isCompleted: true); + return true; + } + + result = default; + return false; + } + + public override void AdvanceTo(SequencePosition consumed) => AdvanceTo(consumed, consumed); + + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + if (_held.Count == 0) + return; + + long consumedBytes = _lastSequence.Slice(0, consumed).Length; + + while (_held.Count > 0 && consumedBytes > 0) + { + Held seg = _held[0]; + int available = seg.Memory.Length; + + if (consumedBytes >= available) + { + // Whole buffer consumed — return it to the reactor. + _conn.ReturnBuffer(seg.Item); + _held.RemoveAt(0); + consumedBytes -= available; + } + else + { + // Partial — keep the unconsumed tail of this buffer. + _held[0] = seg.WithMemory(seg.Memory[(int)consumedBytes..]); + consumedBytes = 0; + } + } + } + + public override void CancelPendingRead() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) + { + if (_completed) + return; + + _completed = true; + + for (int i = 0; i < _held.Count; i++) + _conn.ReturnBuffer(_held[i].Item); + + _held.Clear(); + } + + private ReadOnlySequence BuildSequence() + { + if (_held.Count == 0) + { + _lastSequence = default; + return _lastSequence; + } + + if (_held.Count == 1) + { + _lastSequence = new ReadOnlySequence(_held[0].Memory); + return _lastSequence; + } + + var head = new RingSegment(_held[0].Memory, _held[0].Item.Bid); + RingSegment tail = head; + + for (int i = 1; i < _held.Count; i++) + tail = tail.Append(_held[i].Memory, _held[i].Item.Bid); + + _lastSequence = new ReadOnlySequence(head, 0, tail, tail.Memory.Length); + return _lastSequence; + } + + private void ThrowIfCompleted() + { + if (_completed) + throw new InvalidOperationException("Reading is not allowed after the reader was completed."); + } +} diff --git a/MinimaSQPoll/Connection/ConnectionPipeWriter.cs b/MinimaSQPoll/Connection/ConnectionPipeWriter.cs new file mode 100644 index 0000000..8a6a4c4 --- /dev/null +++ b/MinimaSQPoll/Connection/ConnectionPipeWriter.cs @@ -0,0 +1,63 @@ +using System.IO.Pipelines; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll; + +/// +/// Adapts Minima's write API (GetMemory/GetSpan/Advance/ +/// FlushAsync) to a standard , so PipeWriter-based code +/// can write responses through the connection's per-connection slab. +/// A thin wrapper — all the work lives in Connection. +/// +public sealed class ConnectionPipeWriter : PipeWriter +{ + private readonly Connection _conn; + private bool _completed; + private bool _cancelRequested; + private long _unflushed; + + public ConnectionPipeWriter(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override bool CanGetUnflushedBytes => true; + public override long UnflushedBytes => _unflushed; + + public override Memory GetMemory(int sizeHint = 0) => _conn.GetMemory(sizeHint); + + public override Span GetSpan(int sizeHint = 0) => _conn.GetSpan(sizeHint); + + public override void Advance(int bytes) + { + _unflushed += bytes; + _conn.Advance(bytes); + } + + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_cancelRequested) + { + _cancelRequested = false; + return new ValueTask(new FlushResult(isCanceled: true, isCompleted: _completed)); + } + + _unflushed = 0; + ValueTask inner = _conn.FlushAsync(); + + if (inner.IsCompletedSuccessfully) + return new ValueTask(new FlushResult(isCanceled: false, isCompleted: _completed)); + + return AwaitFlush(inner); + } + + private async ValueTask AwaitFlush(ValueTask inner) + { + await inner; + return new FlushResult(isCanceled: false, isCompleted: _completed); + } + + public override void CancelPendingFlush() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) => _completed = true; +} diff --git a/MinimaSQPoll/Connection/RecvSnapshot.cs b/MinimaSQPoll/Connection/RecvSnapshot.cs new file mode 100644 index 0000000..ceaec51 --- /dev/null +++ b/MinimaSQPoll/Connection/RecvSnapshot.cs @@ -0,0 +1,15 @@ +namespace MinimaSQPoll; + +public readonly struct RecvSnapshot +{ + public readonly long Tail; + public readonly bool IsClosed; + + public RecvSnapshot(long tail, bool isClosed) + { + Tail = tail; + IsClosed = isClosed; + } + + public static RecvSnapshot Closed() => new(0, isClosed: true); +} \ No newline at end of file diff --git a/MinimaSQPoll/MinimaSQPoll.csproj b/MinimaSQPoll/MinimaSQPoll.csproj new file mode 100644 index 0000000..a38f6fa --- /dev/null +++ b/MinimaSQPoll/MinimaSQPoll.csproj @@ -0,0 +1,12 @@ + + + + Exe + net10.0 + enable + enable + true + MinimaSQPoll + + + diff --git a/MinimaSQPoll/Program.cs b/MinimaSQPoll/Program.cs new file mode 100644 index 0000000..f4ab432 --- /dev/null +++ b/MinimaSQPoll/Program.cs @@ -0,0 +1,176 @@ +using System.Buffers; +using System.IO.Pipelines; +using System.Text.Json; +using MinimaSQPoll.Utils; + +namespace MinimaSQPoll; + +/// +/// Multi-reactor HTTP/1.1 server using io_uring directly. Spawns N reactor +/// threads (one per CPU); each opens its own SO_REUSEPORT listener, runs its +/// own io_uring, owns its own connection map. The kernel load-balances new +/// connections across reactors. Per-connection state never crosses threads, +/// so no synchronization is needed on the hot path. +/// +internal static unsafe class Program +{ + internal static ReadOnlySpan Response => + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok"u8; + + private static int Main() + { + // All tunables live in ServerConfig — override the defaults here. + var config = new ServerConfig() + { + UsePipe = false, + ReactorCount = 6 + }; + + Console.WriteLine($"[Minima] starting {config.ReactorCount} reactors on port {config.Port} (incremental={config.Incremental})"); + + var threads = new Thread[config.ReactorCount]; + for (var i = 0; i < config.ReactorCount; i++) + { + var reactor = new Reactor(i, config); + + threads[i] = new Thread(reactor.Run) + { + Name = $"reactor-{i}", + IsBackground = false + }; + threads[i].Start(); + } + + foreach (var t in threads) + { + t.Join(); + } + + return 0; + } +} + +internal static class Handler +{ + // Real async-work knob: serialize an in-memory object of WORK_ITEMS elements to JSON + // on the THREAD POOL (via Task.Run) per request. 0 / unset = disabled (pure inline + // reactor path). Genuine CPU + allocation, not a busy-spin. + private static readonly int WorkItems = 50; + + private static readonly Payload LargeObject = BuildPayload(Math.Max(WorkItems, 1)); + + private static Payload BuildPayload(int count) + { + var items = new Item[count]; + for (int i = 0; i < count; i++) + { + items[i] = new Item(i, $"item-{i}", i * 1.5, (i & 1) == 0, $"category-{i % 8}"); + } + return new Payload(DateTime.UtcNow.ToString("O"), count, items); + } + + public static async Task HandleAsync(Reactor reactor, Connection conn) + { + try + { + while (true) + { + RecvSnapshot snap = await conn.ReadAsync(); + + while (conn.TryGetItem(snap, out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + UnmanagedMemoryManager mem = item.AsMemoryManager(); + ReadOnlyMemory data = mem.Memory; + // data is now usable with any BCL Memory/async API + _ = data.Length; + + // Cross-thread safe and mode-agnostic: routes to the + // shared-ring return or the incremental refcounted return. + conn.ReturnBuffer(in item); + } + } + + // Real async work: serialize a large object to JSON on the THREAD POOL. + // The handler resumes OFF-REACTOR, so the FlushAsync below pays the eventfd + // handoff the pure-inline path avoids — and the serialization is genuine + // CPU + GC pressure on the pool, not a busy-spin. + /*if (WorkItems > 0) + { + //_ = await Task.Run(static () => JsonSerializer.SerializeToUtf8Bytes(LargeObject)); + JsonSerializer.SerializeToUtf8Bytes(LargeObject); + }*/ + + // One response per recv burst — accumulate in the connection's + // per-connection write slab, then submit and await ack. + conn.Write(Program.Response); + await conn.FlushAsync(); + + if (snap.IsClosed) + { + // Reactor already owns teardown (Connections.Remove + close + // happens in Dispatch's recv-error branch); we just exit. + return; + } + + conn.ResetRead(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[r{reactor.Id}] handler crash on fd={conn.ClientFd}: {ex}"); + // Reactor will clean the connection up via the recv-error path + // (or SPSC overflow) on the next CQE for this fd. + } + finally + { + conn.DecRef(); // release the handler's ref; teardown runs once the reactor releases too + } + } + + // PipeReader/PipeWriter variant — same behavior, driven through the BCL + // pipe adapters instead of the raw ReadAsync/TryGetItem/Write API. + public static async Task HandlePipeAsync(Reactor reactor, Connection conn) + { + var reader = new ConnectionPipeReader(conn); + var writer = new ConnectionPipeWriter(conn); + + try + { + while (true) + { + ReadResult read = await reader.ReadAsync(); + ReadOnlySequence buffer = read.Buffer; + + if (!buffer.IsEmpty) + { + // A real server would parse requests out of `buffer` here. + writer.Write(Program.Response); + await writer.FlushAsync(); + } + + // Consume everything we got; AdvanceTo returns the recv buffers. + reader.AdvanceTo(buffer.End); + + if (read.IsCompleted) + { + break; + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[r{reactor.Id}] pipe handler crash on fd={conn.ClientFd}: {ex}"); + } + finally + { + reader.Complete(); + writer.Complete(); + conn.DecRef(); + } + } +} + +internal sealed record Item(int Id, string Name, double Value, bool Active, string Category); +internal sealed record Payload(string Generated, int Count, Item[] Items); diff --git a/MinimaSQPoll/Reactor/Reactor.cs b/MinimaSQPoll/Reactor/Reactor.cs new file mode 100644 index 0000000..8a3bef5 --- /dev/null +++ b/MinimaSQPoll/Reactor/Reactor.cs @@ -0,0 +1,385 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using MinimaSQPoll.Utils; +using static MinimaSQPoll.Native; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll; + +/// +/// SQPOLL reactor: the kernel polls the SQ for us, so handler threads write +/// SQEs directly into ring memory via + +/// (SpinLock-guarded). The reactor's only job is +/// to wait for CQEs and dispatch them — no MPSC queues, no eventfd wake, no +/// drain phase. +/// +public sealed unsafe partial class Reactor +{ + public readonly int Id; + public Ring Ring = null!; // created on the reactor's own thread + public readonly ConcurrentDictionary Connections = new(); + + private int _listenFd; + private readonly ServerConfig _config; + private readonly ushort _port; + private readonly uint _ringEntries; + private readonly bool _incremental; + private readonly uint RecvBufferSize; + + // CQE user_data layout: kind tag in the high 32 bits, fd in the low 32. + private const ulong KindAccept = 1UL << 32; + private const ulong KindRecv = 2UL << 32; + private const ulong KindSend = 3UL << 32; + + // Provided-buffer ring (one per reactor, shared by all its connections). + private const ushort BgId = 1; + private readonly uint BufferRingEntries; // power of two + private byte* _bufRing; // io_uring_buf_ring (kernel-shared) + private byte* _bufSlab; // contiguous slab of recv buffers + private uint _bufRingMask; + private ushort _bufRingTail; + + // Guards multi-threaded updates to the buf_ring tail. Critical section is a + // 16-byte write + a ushort tail bump, so SpinLock is the right primitive. + private SpinLock _bufRingLock = new SpinLock(false); + + // Connection pool: accept runs on the reactor, recycle can run on any + // thread (handler refcount → 0 off-reactor), so use the MPMC variant. + private readonly int PoolMax; + private readonly ConcurrentStack _pool = new(); + + // Incremental-mode (IOU_PBUF_RING_INC) sizing. + private readonly int MaxConnections; + private readonly int ConnBufRingEntries; + private readonly uint IncRecvBufferSize; + + // Transient io_uring_enter errnos (Linux): interrupted, would-block, busy. + private const int EINTR = 4; + private const int EAGAIN = 11; + private const int EBUSY = 16; + + public Reactor(int id, ServerConfig config) + { + Id = id; + _config = config; + _port = config.Port; + _ringEntries = config.RingEntries; + _incremental = config.Incremental; + RecvBufferSize = (uint)config.RecvBufferSize; + BufferRingEntries = (uint)config.BufferRingEntries; + PoolMax = config.PoolMax; + MaxConnections = config.MaxConnections; + ConnBufRingEntries = config.ConnBufRingEntries; + IncRecvBufferSize = (uint)config.IncRecvBufferSize; + } + + // ========================================================================= + // Buffer ring + // ========================================================================= + + private void InitBufferRing() + { + nuint ringBytes = (nuint)BufferRingEntries * 16; + _bufRing = (byte*)NativeMemory.AlignedAlloc(ringBytes, 4096); + NativeMemory.Clear(_bufRing, ringBytes); + + nuint slabBytes = BufferRingEntries * (nuint)RecvBufferSize; + _bufSlab = (byte*)NativeMemory.AlignedAlloc(slabBytes, 64); + + _bufRingMask = BufferRingEntries - 1; + + var reg = new io_uring_buf_reg { + ring_addr = (ulong)_bufRing, + ring_entries = BufferRingEntries, + bgid = BgId, + }; + + int ret = io_uring_register(Ring.Fd, IORING_REGISTER_PBUF_RING, ®, 1); + if (ret < 0) + { + int err = Marshal.GetLastPInvokeError(); + throw new InvalidOperationException($"register pbuf_ring failed: ret={ret} errno={err}"); + } + + for (ushort bid = 0; bid < BufferRingEntries; bid++) { + byte* slot = _bufRing + (uint)bid * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + } + _bufRingTail = (ushort)BufferRingEntries; + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + // Thread-safe buf_ring return — callable from any handler thread. + internal void ReturnBufferDirect(ushort bid) + { + bool taken = false; + _bufRingLock.Enter(ref taken); + try + { + byte* slot = _bufRing + (_bufRingTail & _bufRingMask) * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + _bufRingTail++; + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + finally { _bufRingLock.Exit(); } + } + + // ========================================================================= + // Cross-thread entry points — all run directly on the calling thread now, + // no MPSC handoff, no eventfd wake. Synchronisation is via the SpinLocks + // inside Ring (for SQ submit) and on _bufRingLock (for buf_ring return). + // ========================================================================= + + public void EnqueueReturnQ(ushort bid) => ReturnBufferDirect(bid); + + internal void EnqueueFlush(Connection conn) + { + SubmitSend(conn.ClientFd, conn.WriteBuffer, (uint)conn.WriteInFlight); + } + + internal void EnqueueRecycle(Connection conn) => Recycle(conn, conn.ClientFd); + + // ========================================================================= + // Main loop + // ========================================================================= + + public void Run() + { + Ring = Ring.Create(_ringEntries); + _listenFd = OpenReusePortListener(_port); + + InitBufferRing(); + + Console.WriteLine($"[r{Id}] listening on 0.0.0.0:{_port}"); + SubmitAcceptMultishot(); + + LoopShared(); + + close(_listenFd); + Ring.Dispose(); + } + + private void LoopShared() + { + while (true) + { + int rc = Ring.WaitForCqe(1); + if (rc < 0 && rc != -EINTR && rc != -EAGAIN && rc != -EBUSY) + { + Console.Error.WriteLine($"[r{Id}] io_uring_enter failed: {rc}"); + break; + } + + uint ready = Ring.CqReady(); + for (uint i = 0; i < ready; i++) + { + Dispatch(in Ring.CqeAt(i)); + } + Ring.CqAdvance(ready); + } + } + + private void Dispatch(in IoUringCqe cqe) + { + ulong kind = cqe.user_data & 0xffffffff_00000000UL; + int fd = (int)(cqe.user_data & 0xffffffffUL); + bool more = (cqe.flags & IORING_CQE_F_MORE) != 0; + + if (kind == KindAccept) + { + if (cqe.res >= 0) + { + int clientFd = cqe.res; + SetNoDelay(clientFd); + Connection conn = _pool.TryPop(out var pooled) + ? pooled.SetFd(clientFd) + : new Connection(this, clientFd, _config.WriteSlabSize); + Connections[clientFd] = conn; + conn.InitRefs(); + SubmitRecvMultishot(clientFd); + + _ = _config.UsePipe + ? Handler.HandlePipeAsync(this, conn) + : Handler.HandleAsync(this, conn); + } + else + { + Console.Error.WriteLine($"[r{Id}] accept error: {cqe.res}"); + } + if (!more) + { + SubmitAcceptMultishot(); + } + } + else if (kind == KindRecv) + { + bool hasBuf = (cqe.flags & IORING_CQE_F_BUFFER) != 0; + ushort bid = hasBuf ? (ushort)(cqe.flags >> IORING_CQE_BUFFER_SHIFT) : (ushort)0; + + if (cqe.res <= 0) + { + if (hasBuf) + { + ReturnBufferDirect(bid); + } + if (Connections.TryRemove(fd, out var dyingConn)) + { + dyingConn.MarkClosed(); + dyingConn.DecRef(); + } + return; + } + + if (!Connections.TryGetValue(fd, out var conn)) + { + if (hasBuf) + { + ReturnBufferDirect(bid); + } + return; + } + + byte* ptr = hasBuf ? _bufSlab + (nuint)bid * (nuint)RecvBufferSize : null; + conn.Complete(cqe.res, bid, hasBuf, ptr); + + if (!more) + { + SubmitRecvMultishot(fd); + } + } + else if (kind == KindSend) + { + if (!Connections.TryGetValue(fd, out var conn)) + { + return; + } + if (cqe.res <= 0) + { + if (Connections.TryRemove(fd, out _)) + { + conn.MarkClosed(); + conn.DecRef(); + } + return; + } + conn.WriteHead += cqe.res; + if (conn.WriteHead < conn.WriteInFlight) + { + SubmitSend(fd, conn.WriteBuffer + conn.WriteHead, (uint)(conn.WriteInFlight - conn.WriteHead)); + return; + } + conn.CompleteFlush(); + } + } + + // ========================================================================= + // SQE producers — thread-safe via Ring's submit SpinLock. Each call is + // one allocate-write-publish cycle, so any thread can submit directly. + // ========================================================================= + + private IoUringSqe* GetSqe() + { + IoUringSqe* sqe = Ring.TryGetSqe(); + if (sqe == null) + { + throw new InvalidOperationException("SQ full"); + } + return sqe; + } + + private void SubmitAcceptMultishot() + { + IoUringSqe* sqe = GetSqe(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_ACCEPT; + sqe->ioprio = IORING_ACCEPT_MULTISHOT; + sqe->fd = _listenFd; + sqe->user_data = KindAccept | (uint)_listenFd; + Ring.PublishSqe(); + } + + private void SubmitRecvMultishot(int fd) => SubmitRecvMultishot(fd, BgId); + + private void SubmitRecvMultishot(int fd, ushort bgid) + { + IoUringSqe* sqe = GetSqe(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_RECV; + sqe->flags = IOSQE_BUFFER_SELECT; + sqe->ioprio = IORING_RECV_MULTISHOT; + sqe->fd = fd; + sqe->buf_index = bgid; + sqe->user_data = KindRecv | (uint)fd; + Ring.PublishSqe(); + } + + private void SubmitSend(int fd, byte* buf, uint len) + { + IoUringSqe* sqe = GetSqe(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_SEND; + sqe->fd = fd; + sqe->addr = (ulong)buf; + sqe->len = len; + sqe->user_data = KindSend | (uint)fd; + Ring.PublishSqe(); + } + + private void Recycle(Connection conn, int fd) + { + conn.MarkClosed(); + conn.DrainRecv(); + close(fd); + conn.Clear(); + + if (_pool.Count < PoolMax) + { + _pool.Push(conn); + } + else + { + conn.Dispose(); + } + } + + private static void SetNoDelay(int fd) + { + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(int)); + } + + private static int OpenReusePortListener(ushort port) + { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + throw new InvalidOperationException($"socket failed: {fd}"); + } + + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(int)); + + sockaddr_in addr = default; + addr.sin_family = AF_INET; + addr.sin_port = Htons(port); + addr.sin_addr.s_addr = 0; + + if (bind(fd, &addr, (uint)sizeof(sockaddr_in)) < 0) + { + throw new InvalidOperationException("bind failed"); + } + + if (listen(fd, 128) < 0) + { + throw new InvalidOperationException("listen failed"); + } + + return fd; + } +} diff --git a/MinimaSQPoll/ServerConfig.cs b/MinimaSQPoll/ServerConfig.cs new file mode 100644 index 0000000..668dc7a --- /dev/null +++ b/MinimaSQPoll/ServerConfig.cs @@ -0,0 +1,35 @@ +namespace MinimaSQPoll; + +/// +/// All server tunables in one place — replaces the consts that used to be +/// scattered across Program.cs and Reactor.cs. Defaults match the previous +/// hardcoded values; override via object initializer in Main, e.g.: +/// new ServerConfig { Port = 9000, ReactorCount = 8, Incremental = true }. +/// +public sealed record ServerConfig +{ + // Server-level. + public ushort Port { get; init; } = 8080; + public int ReactorCount { get; init; } = 12; + + // Handler style: false = raw ReadAsync/TryGetItem loop; true = PipeReader/PipeWriter. + public bool UsePipe { get; init; } = false; + + // io_uring SQ/CQ depth. + public uint RingEntries { get; init; } = 8192; + + // Shared buffer ring (used when Incremental == false). + public int RecvBufferSize { get; init; } = 32 * 1024; + public int BufferRingEntries { get; init; } = 4096; + + // Per-connection write slab + connection pool cap. + public int WriteSlabSize { get; init; } = 16 * 1024; + public int PoolMax { get; init; } = 1024; + + // Incremental mode (IOU_PBUF_RING_INC) — per-connection rings. + // reserved native memory ≈ PoolMax × ConnBufRingEntries × IncRecvBufferSize × ReactorCount. + public bool Incremental { get; init; } = false; + public int MaxConnections { get; init; } = 4096; // GID cap (one bgid per active connection) + public int ConnBufRingEntries { get; init; } = 16; // buffers per connection ring + public int IncRecvBufferSize { get; init; } = 4096; // bytes per buffer (filled incrementally) +} diff --git a/MinimaSQPoll/Utils/Mpsc.cs b/MinimaSQPoll/Utils/Mpsc.cs new file mode 100644 index 0000000..ece5563 --- /dev/null +++ b/MinimaSQPoll/Utils/Mpsc.cs @@ -0,0 +1,115 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll.Utils; + +/// +/// Bounded lock-free multi-producer / single-consumer queue. +/// +/// Dmitry Vyukov's bounded MPMC algorithm, specialised to one consumer. +/// Power-of-two capacity, zero-allocation after construction. Producers claim a +/// slot via CAS on the enqueue position (a failed TryEnqueue on a full queue +/// leaves the position untouched — no burned tickets); the single consumer +/// advances the dequeue position with a plain write. Each slot carries a +/// sequence number that coordinates ownership between producers and consumer. +/// +/// One generic queue serves every reactor handoff: Mpsc<ushort> for buffer +/// returns, Mpsc<int> for flush fds, Mpsc<ulong> for packed incremental +/// returns. T is unmanaged so each Cell is a blittable value type with no GC refs. +/// +internal sealed class Mpsc where T : unmanaged +{ + private struct Cell + { + public long Sequence; + public T Value; + } + + private readonly Cell[] _buffer; + private readonly int _mask; + + // PaddedLong is a top-level struct (not nested here) because the CLR forbids + // explicit layout on a type nested inside a generic. + private PaddedLong _enqueuePos; + private PaddedLong _dequeuePos; + + public Mpsc(int capacityPow2) + { + if (capacityPow2 < 2 || (capacityPow2 & (capacityPow2 - 1)) != 0) + throw new ArgumentException("Capacity must be a power of two >= 2.", nameof(capacityPow2)); + + _buffer = new Cell[capacityPow2]; + _mask = capacityPow2 - 1; + + for (int i = 0; i < capacityPow2; i++) + _buffer[i].Sequence = i; + } + + /// Multi-producer safe. Returns false if the queue is full. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + while (true) + { + long pos = Volatile.Read(ref _enqueuePos.Value); + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - pos; + + if (dif == 0) + { + if (Interlocked.CompareExchange(ref _enqueuePos.Value, pos + 1, pos) == pos) + { + cell.Value = item; + Volatile.Write(ref cell.Sequence, pos + 1); + return true; + } + continue; // lost the race; reload and retry + } + + if (dif < 0) + return false; // slot not yet consumed → full + } + } + + /// Single-consumer only. Returns false if empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + long pos = _dequeuePos.Value; // single consumer: plain read + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - (pos + 1); + + if (dif == 0) + { + item = cell.Value; + _dequeuePos.Value = pos + 1; // single consumer: plain write + Volatile.Write(ref cell.Sequence, pos + mask + 1); // free slot for producers + return true; + } + + item = default; + return false; + } +} + +/// +/// A single long padded to a 64-byte cache line so the producer and consumer +/// positions never share a line (no false sharing). Top-level and non-generic +/// so it can legally use explicit layout. +/// +[StructLayout(LayoutKind.Explicit, Size = 64)] +internal struct PaddedLong +{ + [FieldOffset(0)] public long Value; +} diff --git a/MinimaSQPoll/Utils/RingSegment.cs b/MinimaSQPoll/Utils/RingSegment.cs new file mode 100644 index 0000000..034dfdd --- /dev/null +++ b/MinimaSQPoll/Utils/RingSegment.cs @@ -0,0 +1,31 @@ +using System.Buffers; + +namespace MinimaSQPoll.Utils; + +/// +/// One segment of a multi-buffer ReadOnlySequence<byte> built by the +/// ConnectionPipeReader when a single read spans more than one recv buffer. +/// BufferId is carried for debugging; buffer return is driven off the held +/// item list, not the segments. +/// +public sealed class RingSegment : ReadOnlySequenceSegment +{ + public ushort BufferId { get; } + + public RingSegment(ReadOnlyMemory memory, ushort bufferId) + { + Memory = memory; + BufferId = bufferId; + } + + public RingSegment Append(ReadOnlyMemory memory, ushort bufferId) + { + var next = new RingSegment(memory, bufferId) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = next; + return next; + } +} diff --git a/MinimaSQPoll/Utils/SpscRecvRing.cs b/MinimaSQPoll/Utils/SpscRecvRing.cs new file mode 100644 index 0000000..376657b --- /dev/null +++ b/MinimaSQPoll/Utils/SpscRecvRing.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaSQPoll.Utils; + +public sealed unsafe class SpscRecvRing +{ + public struct Item + { + public byte* Ptr; + public ushort Bid; + public int Len; + public bool HasBuffer; + public ushort Gen; // connection generation when enqueued (incremental return guard) + + public ReadOnlySpan AsSpan() => new(Ptr, Len); + + public UnmanagedMemoryManager AsMemoryManager() => new(Ptr, Len, Bid); + } + + private readonly Item[] _items; + private readonly int _mask; + private long _tail; + private long _head; + + public SpscRecvRing(int capacityPow2) + { + if (capacityPow2 <= 0 || (capacityPow2 & (capacityPow2 - 1)) != 0) + { + throw new ArgumentException("capacity must be a power of two", nameof(capacityPow2)); + } + + _items = new Item[capacityPow2]; + _mask = capacityPow2 - 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(in Item item) + { + long head = Volatile.Read(ref _head); + long tail = _tail; + + if ((ulong)(tail - head) >= (ulong)_items.Length) + { + return false; + } + + _items[(int)(tail & _mask)] = item; + Volatile.Write(ref _tail, tail + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out Item item) + { + long head = _head; + long tail = Volatile.Read(ref _tail); + + if (head >= tail) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long SnapshotTail() => Volatile.Read(ref _tail); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeueUntil(long tailSnapshot, out Item item) + { + long head = _head; + + if (head >= tailSnapshot) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEmpty() => Volatile.Read(ref _head) >= Volatile.Read(ref _tail); + + // Reactor-thread-only, called during connection teardown (Clear) when no + // handler is consuming. Discards any leftover items so the recycled + // connection starts empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _head = 0; + _tail = 0; + } +} diff --git a/MinimaSQPoll/Utils/UnmanagedMemoryManager.cs b/MinimaSQPoll/Utils/UnmanagedMemoryManager.cs new file mode 100644 index 0000000..af4f39b --- /dev/null +++ b/MinimaSQPoll/Utils/UnmanagedMemoryManager.cs @@ -0,0 +1,32 @@ +using System.Buffers; + +namespace MinimaSQPoll.Utils; + +public sealed unsafe class UnmanagedMemoryManager : MemoryManager +{ + private readonly byte* _ptr; + private readonly int _length; + + public ushort BufferId { get; } + + public UnmanagedMemoryManager(byte* ptr, int length) + { + _ptr = ptr; + _length = length; + } + + public UnmanagedMemoryManager(byte* ptr, int length, ushort bufferId) + { + _ptr = ptr; + _length = length; + BufferId = bufferId; + } + + public override Span GetSpan() => new(_ptr, _length); + + public override MemoryHandle Pin(int elementIndex = 0) => new(_ptr + elementIndex); + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } +} diff --git a/MinimaSQPoll/io_uring/Native.cs b/MinimaSQPoll/io_uring/Native.cs new file mode 100644 index 0000000..76e8e77 --- /dev/null +++ b/MinimaSQPoll/io_uring/Native.cs @@ -0,0 +1,172 @@ +using System.Runtime.InteropServices; + +namespace MinimaSQPoll; + +/// +/// All native interop in one file: io_uring syscalls, libc socket calls, +/// the kernel struct layouts they expect, and the constants needed to +/// drive a minimal io_uring loop. +/// +public static unsafe class Native { + private const long SYS_IO_URING_SETUP = 425; + private const long SYS_IO_URING_ENTER = 426; + private const long SYS_IO_URING_REGISTER = 427; + + public const byte IORING_OP_POLL_ADD = 6; + public const byte IORING_OP_ACCEPT = 13; + public const byte IORING_OP_SEND = 26; + public const byte IORING_OP_RECV = 27; + public const uint IORING_ENTER_GETEVENTS = 1u << 0; + public const long IORING_OFF_SQ_RING = 0; + public const long IORING_OFF_SQES = 0x10000000; + + // Multishot / buffer-ring goodies. + public const ushort IORING_ACCEPT_MULTISHOT = 1 << 0; + public const ushort IORING_RECV_MULTISHOT = 1 << 1; + public const byte IOSQE_BUFFER_SELECT = 1 << 5; + public const uint IORING_CQE_F_BUFFER = 1u << 0; + public const uint IORING_CQE_F_MORE = 1u << 1; + public const int IORING_CQE_BUFFER_SHIFT = 16; + public const uint IORING_REGISTER_PBUF_RING = 22; + public const uint IORING_UNREGISTER_PBUF_RING = 23; + public const uint IORING_POLL_ADD_MULTI = 1u << 0; + + // Incremental provided-buffer consumption (kernel 6.12+). IOU_PBUF_RING_INC + // is set in io_uring_buf_reg.flags at registration; IORING_CQE_F_BUF_MORE is + // set on recv CQEs while the kernel will keep appending to the same buffer. + public const ushort IOU_PBUF_RING_INC = 2; + public const uint IORING_CQE_F_BUF_MORE = 1u << 4; + + // eventfd flags + poll mask (used for the cross-thread wake mechanism). + public const int EFD_CLOEXEC = 0x80000; + public const int EFD_NONBLOCK = 0x800; + public const uint POLLIN = 0x0001; + + // Setup flags. SINGLE_ISSUER tells the kernel only one thread will submit + // to this ring (skips locking on the SQ). DEFER_TASKRUN defers completion + // processing until io_uring_enter(GETEVENTS), which lets the kernel batch + // work and avoids interrupting the reactor with task_work mid-flight. + public const uint IORING_SETUP_SINGLE_ISSUER = 1u << 12; + public const uint IORING_SETUP_DEFER_TASKRUN = 1u << 13; + + // SQPOLL: kernel spawns a poller thread that reads SQEs from shared memory + // and submits them without us calling io_uring_enter. SQ_AFF pins the poller + // to a specific CPU. After sq_thread_idle ms of inactivity, the poller + // parks and sets SQ_NEED_WAKEUP in sq_flags; we must then call + // io_uring_enter(IORING_ENTER_SQ_WAKEUP) to revive it. + public const uint IORING_SETUP_SQPOLL = 1u << 1; + public const uint IORING_SETUP_SQ_AFF = 1u << 2; + public const uint IORING_SQ_NEED_WAKEUP = 1u << 0; + public const uint IORING_ENTER_SQ_WAKEUP = 1u << 1; + + public const int PROT_READ = 1; + public const int PROT_WRITE = 2; + public const int MAP_SHARED = 1; + public const int MAP_POPULATE = 0x8000; + + public const int AF_INET = 2; + public const int SOCK_STREAM = 1; + public const int SOL_SOCKET = 1; + public const int SO_REUSEADDR = 2; + public const int SO_REUSEPORT = 15; + public const int IPPROTO_TCP = 6; + public const int TCP_NODELAY = 1; + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall3(long nr, uint a1, IoUringParams* a2); + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall6(long nr, uint a1, uint a2, uint a3, uint a4, void* a5, nuint a6); + + [DllImport("libc", EntryPoint = "syscall", SetLastError = true)] + private static extern long syscall4(long nr, uint a1, uint a2, void* a3, uint a4); + + public static int io_uring_setup(uint entries, IoUringParams* p) => + (int)syscall3(SYS_IO_URING_SETUP, entries, p); + + public static int io_uring_enter(int fd, uint toSubmit, uint minComplete, uint flags) => + (int)syscall6(SYS_IO_URING_ENTER, (uint)fd, toSubmit, minComplete, flags, null, 0); + + public static int io_uring_register(int fd, uint opcode, void* arg, uint nrArgs) => + (int)syscall4(SYS_IO_URING_REGISTER, (uint)fd, opcode, arg, nrArgs); + + [DllImport("libc")] public static extern void* mmap(void* addr, nuint length, int prot, int flags, int fd, long offset); + [DllImport("libc")] public static extern int munmap(void* addr, nuint length); + [DllImport("libc")] public static extern int close(int fd); + [DllImport("libc")] public static extern int socket(int domain, int type, int proto); + [DllImport("libc")] public static extern int bind(int fd, sockaddr_in* addr, uint len); + [DllImport("libc")] public static extern int listen(int fd, int backlog); + [DllImport("libc")] public static extern int setsockopt(int fd, int level, int optname, void* optval, uint optlen); + [DllImport("libc")] public static extern int eventfd(uint initval, int flags); + [DllImport("libc")] public static extern long write(int fd, void* buf, nuint count); + [DllImport("libc")] public static extern long read(int fd, void* buf, nuint count); + + public static ushort Htons(ushort x) => (ushort)((x << 8) | (x >> 8)); + + // Kernel struct layouts (must match include/uapi/linux/io_uring.h) + [StructLayout(LayoutKind.Sequential)] + public struct SqRingOffsets { + public uint head, tail, ring_mask, ring_entries, flags, dropped, array, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CqRingOffsets { + public uint head, tail, ring_mask, ring_entries, overflow, cqes, flags, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringParams { + public uint sq_entries, cq_entries, flags, sq_thread_cpu, sq_thread_idle; + public uint features, wq_fd, resv0, resv1, resv2; + public SqRingOffsets sq_off; + public CqRingOffsets cq_off; + } + + [StructLayout(LayoutKind.Explicit, Size = 64)] + public struct IoUringSqe { + [FieldOffset(0)] public byte opcode; + [FieldOffset(1)] public byte flags; + [FieldOffset(2)] public ushort ioprio; + [FieldOffset(4)] public int fd; + [FieldOffset(8)] public ulong off; + [FieldOffset(16)] public ulong addr; + [FieldOffset(24)] public uint len; + [FieldOffset(28)] public uint op_flags; + [FieldOffset(32)] public ulong user_data; + [FieldOffset(40)] public ushort buf_index; + [FieldOffset(42)] public ushort personality; + [FieldOffset(44)] public int splice_fd_in; + [FieldOffset(48)] public ulong addr3; + [FieldOffset(56)] public ulong __pad2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringCqe { + public ulong user_data; + public int res; + public uint flags; + } + + // Argument struct for IORING_REGISTER_PBUF_RING. + [StructLayout(LayoutKind.Sequential)] + public struct io_uring_buf_reg { + public ulong ring_addr; + public uint ring_entries; + public ushort bgid; + public ushort flags; + public ulong resv1, resv2, resv3; + } + + [StructLayout(LayoutKind.Sequential)] + public struct in_addr { public uint s_addr; } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct sockaddr_in { + public ushort sin_family; + public ushort sin_port; + public in_addr sin_addr; + public fixed byte sin_zero[8]; + } +} diff --git a/MinimaSQPoll/io_uring/Ring.cs b/MinimaSQPoll/io_uring/Ring.cs new file mode 100644 index 0000000..5e9f38c --- /dev/null +++ b/MinimaSQPoll/io_uring/Ring.cs @@ -0,0 +1,232 @@ +using System.Runtime.CompilerServices; +using static MinimaSQPoll.Native; + +// ReSharper disable SuggestVarOrType_BuiltInTypes +// ReSharper disable SuggestVarOrType_Elsewhere +#pragma warning disable CA1806 + +namespace MinimaSQPoll; + +public sealed unsafe class Ring : IDisposable +{ + private int _fd; + + public int Fd => _fd; + + private uint* _sqHead; + private uint* _sqTail; + private uint* _sqArray; + private uint* _sqFlags; // kernel-shared SQ flags (carries IORING_SQ_NEED_WAKEUP under SQPOLL) + private uint _sqMask; + private uint _sqEntries; + private IoUringSqe* _sqes; + private bool _sqPoll; + + private uint* _cqHead; + private uint* _cqTail; + private IoUringCqe* _cqes; + private uint _cqMask; + + private uint _sqeTail; + + // Guards SQE allocation + publish so any thread can submit. Critical section + // is tiny (write 64 B, advance tail), so a SpinLock comfortably outperforms + // Monitor and there is no scheduler interaction. + private SpinLock _submitLock = new SpinLock(false); + + private byte* _ringPtr; + private nuint _ringSize; + private byte* _sqePtr; + private nuint _sqeSize; + + public static Ring Create(uint entries, bool sqPoll = true, uint sqIdleMs = 1000, int sqCpu = -1) + { + IoUringParams ioUringParams = default; + if (sqPoll) + { + // SQPOLL: kernel poller thread reads SQEs from shared memory and + // submits them without us calling io_uring_enter. Incompatible with + // SINGLE_ISSUER/DEFER_TASKRUN (the poller is the "submitter" from + // the kernel's perspective). + ioUringParams.flags = IORING_SETUP_SQPOLL; + ioUringParams.sq_thread_idle = sqIdleMs; + if (sqCpu >= 0) + { + ioUringParams.flags |= IORING_SETUP_SQ_AFF; + ioUringParams.sq_thread_cpu = (uint)sqCpu; + } + } + else + { + ioUringParams.flags = IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN; + } + int fd = io_uring_setup(entries, &ioUringParams); + if (fd < 0) + { + throw new InvalidOperationException($"io_uring_setup failed: {fd}"); + } + + var ring = new Ring + { + _fd = fd, + _sqEntries = ioUringParams.sq_entries, + _sqPoll = sqPoll, + }; + + nuint sqRingBytes = ioUringParams.sq_off.array + ioUringParams.sq_entries * sizeof(uint); + nuint cqRingBytes = ioUringParams.cq_off.cqes + ioUringParams.cq_entries * (nuint)sizeof(IoUringCqe); + nuint ringBytes = sqRingBytes > cqRingBytes ? sqRingBytes : cqRingBytes; + + void* ringMem = mmap(null, ringBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING); + if (ringMem == (void*)-1) + { + close(fd); + + throw new InvalidOperationException("mmap(SQ_RING) failed"); + } + ring._ringPtr = (byte*)ringMem; + ring._ringSize = ringBytes; + + nuint sqeBytes = ioUringParams.sq_entries * (nuint)sizeof(IoUringSqe); + void* sqeMem = mmap(null, sqeBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQES); + if (sqeMem == (void*)-1) + { + munmap(ringMem, ringBytes); + close(fd); + + throw new InvalidOperationException("mmap(SQES) failed"); + } + ring._sqes = (IoUringSqe*)sqeMem; + ring._sqePtr = (byte*)sqeMem; + ring._sqeSize = sqeBytes; + + byte* ringPointer = (byte*)ringMem; + ring._sqHead = (uint*)(ringPointer + ioUringParams.sq_off.head); + ring._sqTail = (uint*)(ringPointer + ioUringParams.sq_off.tail); + ring._sqArray = (uint*)(ringPointer + ioUringParams.sq_off.array); + ring._sqFlags = (uint*)(ringPointer + ioUringParams.sq_off.flags); + ring._sqMask = *(uint*)(ringPointer + ioUringParams.sq_off.ring_mask); + + ring._cqHead = (uint*)(ringPointer + ioUringParams.cq_off.head); + ring._cqTail = (uint*)(ringPointer + ioUringParams.cq_off.tail); + ring._cqes = (IoUringCqe*)(ringPointer + ioUringParams.cq_off.cqes); + ring._cqMask = *(uint*)(ringPointer + ioUringParams.cq_off.ring_mask); + + return ring; + } + + // Thread-safe SQE allocation. The lock is held until PublishSqe() is called, + // so callers must always call PublishSqe (or be on the reactor with no other + // threads submitting). Pattern is: sqe = TryGetSqe(); write fields; PublishSqe(). + public IoUringSqe* TryGetSqe() + { + bool taken = false; + _submitLock.Enter(ref taken); + + uint head = Volatile.Read(ref *_sqHead); + if (_sqeTail - head >= _sqEntries) + { + _submitLock.Exit(); + return null; + } + + uint slot = _sqeTail & _sqMask; + _sqArray[slot] = slot; + _sqeTail++; + + return &_sqes[slot]; + } + + // Publishes the SQE the caller just wrote to the kernel-visible tail and + // releases the submit lock. Under SQPOLL this also wakes the poller if it + // has parked (SQ_NEED_WAKEUP set). + public void PublishSqe() + { + Volatile.Write(ref *_sqTail, _sqeTail); + + if (_sqPoll && (Volatile.Read(ref *_sqFlags) & IORING_SQ_NEED_WAKEUP) != 0) + { + io_uring_enter(_fd, 0, 0, IORING_ENTER_SQ_WAKEUP); + } + + _submitLock.Exit(); + } + + // Block waiting for at least waitFor CQEs. With direct submission (handlers + // call TryGetSqe/PublishSqe), the reactor only ever needs to wait here — + // no submit work to coordinate. Under non-SQPOLL we still need to submit + // any pending SQEs along with the wait. + public int WaitForCqe(uint waitFor) + { + if (_sqPoll) + { + if (waitFor == 0) return 0; + return io_uring_enter(_fd, 0, waitFor, IORING_ENTER_GETEVENTS); + } + + // Non-SQPOLL fallback: submit + wait in one syscall. + uint published = *_sqTail; + uint toSubmit = _sqeTail - published; + if (toSubmit > 0) + { + Volatile.Write(ref *_sqTail, _sqeTail); + } + if (toSubmit == 0 && waitFor == 0) return 0; + uint enterFlags = waitFor > 0 ? IORING_ENTER_GETEVENTS : 0; + return io_uring_enter(_fd, toSubmit, waitFor, enterFlags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetCqe(out IoUringCqe cqe) + { + uint head = *_cqHead; + uint tail = Volatile.Read(ref *_cqTail); + + if (head == tail) + { + cqe = default; + + return false; + } + + cqe = _cqes[head & _cqMask]; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqeSeen() => Volatile.Write(ref *_cqHead, *_cqHead + 1); + + // Batched CQ drain (liburing io_uring_for_each_cqe + io_uring_cq_advance): + // read the kernel-written tail once (acquire), process the whole batch, + // then publish the consumed head once (release) instead of once per CQE. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint CqReady() => Volatile.Read(ref *_cqTail) - *_cqHead; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref readonly IoUringCqe CqeAt(uint i) => ref _cqes[(*_cqHead + i) & _cqMask]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqAdvance(uint n) => Volatile.Write(ref *_cqHead, *_cqHead + n); + + public void Dispose() + { + if (_ringPtr != null) + { + munmap(_ringPtr, _ringSize); _ringPtr = null; + } + + if (_sqePtr != null) + { + munmap(_sqePtr, _sqeSize); _sqePtr = null; + } + + if (_fd > 0) + { + close(_fd); _fd = 0; + } + } +} + +#pragma warning restore CA1806 diff --git a/MinimaTFlow/Connection/Connection.Read.cs b/MinimaTFlow/Connection/Connection.Read.cs new file mode 100644 index 0000000..c27f1e2 --- /dev/null +++ b/MinimaTFlow/Connection/Connection.Read.cs @@ -0,0 +1,168 @@ +using System.Threading.Tasks.Sources; +using MinimaTFlow.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow; + +/// +/// Per-connection state. The handler may run on any thread (e.g. resumed by +/// a thread-pool timer); reactor-only side effects are funnelled through the +/// MPSC queues on `Reactor`. Coordination uses Interlocked.Exchange on the +/// arm flags and a sticky `_pending` to close the lost-wakeup race. +/// +/// Lifetime is pool-managed: the reactor pops a Connection on accept (or new +/// one if pool is empty), and pushes it back on teardown after `Clear()`. The +/// `_generation` field is bumped on each `Clear` so stale `ValueTask` tokens +/// from a previous connection life are detectable and return `Closed()` +/// instead of leaking the new tenant's state. +/// +public sealed unsafe partial class Connection : IValueTaskSource +{ + internal Connection SetFd(int fd) + { + ClientFd = fd; + return this; + } + + // Async continuations: handler resumes on the thread pool so libc send() + // in FlushAsync never blocks the reactor thread. + private ManualResetValueTaskSourceCore _readSignal = new() + { + RunContinuationsAsynchronously = true, + }; + private int _armed; + private int _pending; + private int _closed; + + private readonly SpscRecvRing _recv = new(capacityPow2: 16); + + public ValueTask ReadAsync() + { + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1) + { + Volatile.Write(ref _pending, 0); + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + if (Volatile.Read(ref _closed) != 0) + { + return new ValueTask(RecvSnapshot.Closed()); + } + + if (Interlocked.Exchange(ref _armed, 1) == 1) + { + throw new InvalidOperationException("ReadAsync already armed."); + } + + // Snapshot the generation as the IVTS token so a future Clear() can + // invalidate this awaiter if the connection gets pool-recycled. + int gen = Volatile.Read(ref _generation); + + // Race recovery: re-check between arming and returning the IVTS task. + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1 || Volatile.Read(ref _closed) != 0) + { + Volatile.Write(ref _pending, 0); + Interlocked.Exchange(ref _armed, 0); + + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + return new ValueTask(this, (short)gen); + } + + public bool TryGetItem(in RecvSnapshot snap, out SpscRecvRing.Item item) + => _recv.TryDequeueUntil(snap.Tail, out item); + + public void ResetRead() => _readSignal.Reset(); + + public void Complete(int res, ushort bid, bool hasBuffer, byte* ptr) + { + if (!_recv.TryEnqueue(new SpscRecvRing.Item + { + Ptr = ptr, + Bid = bid, + Len = res, + HasBuffer = hasBuffer, + Gen = (ushort)Volatile.Read(ref _generation) + })) + { + Console.Error.WriteLine("[conn] recv queue overflow."); + if (hasBuffer) + { + _reactor.ReturnBufferDirect(bid); + } + Volatile.Write(ref _closed, 1); + } + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + else + { + Volatile.Write(ref _pending, 1); + } + } + + internal void DrainRecv() + { + // Return any buffer IDs still sitting in the SPSC ring (handler exited + // before draining them, or a recv arrived after _closed was set). + while (_recv.TryDequeue(out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + _reactor.ReturnBufferDirect(item.Bid); + } + } + } + + // ========================================================================= + // IValueTaskSource plumbing — token (= snapshot of `_generation` at await + // time) is compared against the current `_generation` to detect stale + // awaiters from before a Clear()/pool reuse. Stale awaiters get a + // sentinel result rather than the new tenant's state. + // + // For the actual IVTS dispatch we pass `_readSignal.Version` / + // `_flushSignal.Version` to the underlying core (not `token`) because the + // core's version is bumped by ResetRead/CompleteFlush mid-life and is + // unrelated to the cross-life generation guard. + // ========================================================================= + + RecvSnapshot IValueTaskSource.GetResult(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return RecvSnapshot.Closed(); + } + + return _readSignal.GetResult(_readSignal.Version); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return ValueTaskSourceStatus.Succeeded; + } + + return _readSignal.GetStatus(_readSignal.Version); + } + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (token != (short)Volatile.Read(ref _generation)) + { + // Stale — run the continuation now so the awaiter unblocks and + // gets RecvSnapshot.Closed() from GetResult. + continuation(state); + + return; + } + + _readSignal.OnCompleted(continuation, state, _readSignal.Version, flags); + } +} diff --git a/MinimaTFlow/Connection/Connection.Write.cs b/MinimaTFlow/Connection/Connection.Write.cs new file mode 100644 index 0000000..5ebf966 --- /dev/null +++ b/MinimaTFlow/Connection/Connection.Write.cs @@ -0,0 +1,137 @@ +using System.Buffers; +using System.Runtime.InteropServices; +using MinimaTFlow.Utils; +using static MinimaTFlow.Native; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow; + +/// +/// Twinflow-style write path: handler thread calls libc send() directly, +/// keeping the io_uring reactor on the recv side only. No flush IVTS, no MPSC +/// hand-off, no send CQE — the response goes straight to the kernel via a +/// single syscall on whichever thread the handler is running on. +/// +public sealed unsafe partial class Connection : IBufferWriter +{ + private readonly int _writeSlabSize; + internal byte* WriteBuffer; + internal int WriteTail; + + private readonly UnmanagedMemoryManager _manager; + + // IBufferWriter +#region IBufferWriter + + public Memory GetMemory(int sizeHint = 0) + { + int remaining = _writeSlabSize - WriteTail; + if (sizeHint > remaining) + { + throw new InvalidOperationException("Buffer too small."); + } + return _manager.Memory.Slice(WriteTail, remaining); + } + + public Span GetSpan(int sizeHint = 0) + { + if (WriteTail + sizeHint > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + return new Span(WriteBuffer + WriteTail, _writeSlabSize - WriteTail); + } + + public void Advance(int count) => WriteTail += count; + +#endregion + + public void Write(ReadOnlySpan source) + { + int len = source.Length; + if (WriteTail + len > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + source.CopyTo(new Span(WriteBuffer + WriteTail, len)); + WriteTail += len; + } + + /// + /// Synchronously send everything we've buffered via libc send(). + /// Returns a completed ValueTask in the common case; on EAGAIN, spin-yields + /// the thread until the kernel send buffer drains. No reactor handoff, no + /// IVTS — the syscall happens on the handler thread. + /// + /// Async fallback for EAGAIN is omitted because the class is `unsafe` and + /// C# disallows `await` in unsafe context. For HTTP/1.1 plaintext on + /// loopback EAGAIN is essentially never hit; if you serve large bodies, + /// extract the slow path to a non-unsafe helper. + /// + public ValueTask FlushAsync() + { + if (Volatile.Read(ref _closed) == 1) + { + return default; + } + + int target = WriteTail; + if (target == 0) + { + return default; + } + + int off = 0; + while (off < target) + { + int sent = TrySend(WriteBuffer + off, (uint)(target - off), out bool wouldBlock, out bool closed); + if (closed) + { + MarkClosed(); + WriteTail = 0; + return default; + } + if (sent > 0) + { + off += sent; + continue; + } + if (wouldBlock) + { + if (Volatile.Read(ref _closed) == 1) + { + WriteTail = 0; + return default; + } + Thread.Yield(); + } + } + + WriteTail = 0; + return default; + } + + private int TrySend(byte* buf, uint len, out bool wouldBlock, out bool closed) + { + wouldBlock = false; + closed = false; + long n = send(ClientFd, buf, len, MSG_NOSIGNAL); + if (n > 0) + { + return (int)n; + } + int err = (n == 0) ? EAGAIN : Marshal.GetLastPInvokeError(); + if (err is EAGAIN or EWOULDBLOCK) + { + wouldBlock = true; + return 0; + } + if (err == EINTR) + { + return 0; + } + closed = true; + return 0; + } +} diff --git a/MinimaTFlow/Connection/Connection.cs b/MinimaTFlow/Connection/Connection.cs new file mode 100644 index 0000000..fe5b112 --- /dev/null +++ b/MinimaTFlow/Connection/Connection.cs @@ -0,0 +1,94 @@ +using System.Runtime.InteropServices; +using MinimaTFlow.Utils; + +namespace MinimaTFlow; + +public sealed unsafe partial class Connection +{ + private readonly Reactor _reactor; + + public int ClientFd { get; private set; } + + // Bumped on Clear(); the low 16 bits are used as the IVTS token so stale + // awaiters can be detected after pool reuse. + private int _generation; + + // Refcount: the connection has two owners — the reactor (recv side) and the + // handler (which may run off-reactor). Init to 2 on accept; each owner DecRef's + // when done; teardown (Recycle) runs only at refs==0, so a connection is never + // recycled or pool-reused while a handler is still in flight on another thread. + private int _refs; + + public Connection(Reactor reactor, int fd, int writeSlabSize = 1024 * 16) + { + _reactor = reactor; + ClientFd = fd; + _writeSlabSize = writeSlabSize; + WriteBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)writeSlabSize, 64); + + _manager = new UnmanagedMemoryManager(WriteBuffer, writeSlabSize); + } + + // ========================================================================= + // Pool lifecycle — invoked from Reactor.Dispatch's recv/send error paths. + // Reactor-thread only. + // + // teardown: MarkClosed() → wake awaiters with closed=1 + // DrainRecv() → return any in-flight buf_ring items + // close(fd) + // Clear() → reset state, bump _generation + // push to pool, OR Dispose() if pool is full + // ========================================================================= + + public void MarkClosed() + { + Volatile.Write(ref _closed, 1); + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), isClosed: true)); + } + else + { + Volatile.Write(ref _pending, 1); + } + } + + // Init to 2 (reactor + handler) at accept. + internal void InitRefs() => Volatile.Write(ref _refs, 2); + + // Release one owner's ref. Whoever drives it to 0 hands the connection to the + // reactor for teardown (close + Clear + pool) — never recycled before both done. + internal void DecRef() + { + if (Interlocked.Decrement(ref _refs) == 0) + { + _reactor.EnqueueRecycle(this); + } + } + + internal void Clear() + { + Interlocked.Increment(ref _generation); + + Volatile.Write(ref _armed, 0); + Volatile.Write(ref _pending, 0); + Volatile.Write(ref _closed, 0); + + WriteTail = 0; + + _readSignal.Reset(); + _recv.Reset(); + } + + public void Dispose() + { + if (WriteBuffer != null) + { + NativeMemory.AlignedFree(WriteBuffer); + WriteBuffer = null; + } + } + + public void ReturnBuffer(in SpscRecvRing.Item item) => _reactor.EnqueueReturnQ(item.Bid); +} \ No newline at end of file diff --git a/MinimaTFlow/Connection/ConnectionDualPipe.cs b/MinimaTFlow/Connection/ConnectionDualPipe.cs new file mode 100644 index 0000000..90682b5 --- /dev/null +++ b/MinimaTFlow/Connection/ConnectionDualPipe.cs @@ -0,0 +1,16 @@ +using System.IO.Pipelines; + +namespace MinimaTFlow; + +public sealed class ConnectionDualPipe : IDuplexPipe +{ + public PipeReader Input { get; } + public PipeWriter Output { get; } + + public ConnectionDualPipe(Connection connection) + { + ArgumentNullException.ThrowIfNull(connection); + Input = new ConnectionPipeReader(connection); + Output = new ConnectionPipeWriter(connection); + } +} \ No newline at end of file diff --git a/MinimaTFlow/Connection/ConnectionPipeReader.cs b/MinimaTFlow/Connection/ConnectionPipeReader.cs new file mode 100644 index 0000000..ed744ec --- /dev/null +++ b/MinimaTFlow/Connection/ConnectionPipeReader.cs @@ -0,0 +1,181 @@ +using System.Buffers; +using System.IO.Pipelines; +using MinimaTFlow.Utils; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow; + +/// +/// Adapts Minima's raw read API (ReadAsync + TryGetItem +/// + ReturnBuffer) to a standard . Recv buffers are +/// exposed zero-copy as a ReadOnlySequence<byte> (one segment per buffer) +/// and held until AdvanceTo consumes them, at which point fully-consumed buffers +/// are returned to the reactor. +/// +/// Convenience/compat layer for PipeReader consumers — the raw ReadAsync/ +/// TryGetItem path stays the faster one (this adds held-buffer + sequence +/// bookkeeping per read). +/// +public sealed class ConnectionPipeReader : PipeReader +{ + private readonly Connection _conn; + private readonly List _held = new(16); + private ReadOnlySequence _lastSequence; + + private bool _completed; + private bool _cancelRequested; + private bool _connectionClosed; + + private readonly struct Held + { + public readonly ReadOnlyMemory Memory; + public readonly SpscRecvRing.Item Item; + + public Held(ReadOnlyMemory memory, SpscRecvRing.Item item) + { + Memory = memory; + Item = item; + } + + public Held WithMemory(ReadOnlyMemory memory) => new(memory, Item); + } + + public ConnectionPipeReader(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + ThrowIfCompleted(); + + if (_cancelRequested) + { + _cancelRequested = false; + return new ReadResult(BuildSequence(), isCanceled: true, isCompleted: _connectionClosed); + } + + // Anything still held from a previous read that wasn't fully consumed. + if (_held.Count > 0) + return new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + + if (_connectionClosed) + return new ReadResult(default, isCanceled: false, isCompleted: true); + + RecvSnapshot snap = await _conn.ReadAsync(); + + while (_conn.TryGetItem(snap, out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + _held.Add(new Held(item.AsMemoryManager().Memory, item)); + } + + _conn.ResetRead(); + + if (snap.IsClosed) + _connectionClosed = true; + + if (_cancelRequested) + { + _cancelRequested = false; + return new ReadResult(BuildSequence(), isCanceled: true, isCompleted: _connectionClosed); + } + + return new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + } + + public override bool TryRead(out ReadResult result) + { + ThrowIfCompleted(); + + if (_held.Count > 0) + { + result = new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + return true; + } + + if (_connectionClosed) + { + result = new ReadResult(default, isCanceled: false, isCompleted: true); + return true; + } + + result = default; + return false; + } + + public override void AdvanceTo(SequencePosition consumed) => AdvanceTo(consumed, consumed); + + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + if (_held.Count == 0) + return; + + long consumedBytes = _lastSequence.Slice(0, consumed).Length; + + while (_held.Count > 0 && consumedBytes > 0) + { + Held seg = _held[0]; + int available = seg.Memory.Length; + + if (consumedBytes >= available) + { + // Whole buffer consumed — return it to the reactor. + _conn.ReturnBuffer(seg.Item); + _held.RemoveAt(0); + consumedBytes -= available; + } + else + { + // Partial — keep the unconsumed tail of this buffer. + _held[0] = seg.WithMemory(seg.Memory[(int)consumedBytes..]); + consumedBytes = 0; + } + } + } + + public override void CancelPendingRead() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) + { + if (_completed) + return; + + _completed = true; + + for (int i = 0; i < _held.Count; i++) + _conn.ReturnBuffer(_held[i].Item); + + _held.Clear(); + } + + private ReadOnlySequence BuildSequence() + { + if (_held.Count == 0) + { + _lastSequence = default; + return _lastSequence; + } + + if (_held.Count == 1) + { + _lastSequence = new ReadOnlySequence(_held[0].Memory); + return _lastSequence; + } + + var head = new RingSegment(_held[0].Memory, _held[0].Item.Bid); + RingSegment tail = head; + + for (int i = 1; i < _held.Count; i++) + tail = tail.Append(_held[i].Memory, _held[i].Item.Bid); + + _lastSequence = new ReadOnlySequence(head, 0, tail, tail.Memory.Length); + return _lastSequence; + } + + private void ThrowIfCompleted() + { + if (_completed) + throw new InvalidOperationException("Reading is not allowed after the reader was completed."); + } +} diff --git a/MinimaTFlow/Connection/ConnectionPipeWriter.cs b/MinimaTFlow/Connection/ConnectionPipeWriter.cs new file mode 100644 index 0000000..a6a41eb --- /dev/null +++ b/MinimaTFlow/Connection/ConnectionPipeWriter.cs @@ -0,0 +1,63 @@ +using System.IO.Pipelines; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow; + +/// +/// Adapts Minima's write API (GetMemory/GetSpan/Advance/ +/// FlushAsync) to a standard , so PipeWriter-based code +/// can write responses through the connection's per-connection slab. +/// A thin wrapper — all the work lives in Connection. +/// +public sealed class ConnectionPipeWriter : PipeWriter +{ + private readonly Connection _conn; + private bool _completed; + private bool _cancelRequested; + private long _unflushed; + + public ConnectionPipeWriter(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override bool CanGetUnflushedBytes => true; + public override long UnflushedBytes => _unflushed; + + public override Memory GetMemory(int sizeHint = 0) => _conn.GetMemory(sizeHint); + + public override Span GetSpan(int sizeHint = 0) => _conn.GetSpan(sizeHint); + + public override void Advance(int bytes) + { + _unflushed += bytes; + _conn.Advance(bytes); + } + + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_cancelRequested) + { + _cancelRequested = false; + return new ValueTask(new FlushResult(isCanceled: true, isCompleted: _completed)); + } + + _unflushed = 0; + ValueTask inner = _conn.FlushAsync(); + + if (inner.IsCompletedSuccessfully) + return new ValueTask(new FlushResult(isCanceled: false, isCompleted: _completed)); + + return AwaitFlush(inner); + } + + private async ValueTask AwaitFlush(ValueTask inner) + { + await inner; + return new FlushResult(isCanceled: false, isCompleted: _completed); + } + + public override void CancelPendingFlush() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) => _completed = true; +} diff --git a/MinimaTFlow/Connection/RecvSnapshot.cs b/MinimaTFlow/Connection/RecvSnapshot.cs new file mode 100644 index 0000000..e6daeea --- /dev/null +++ b/MinimaTFlow/Connection/RecvSnapshot.cs @@ -0,0 +1,15 @@ +namespace MinimaTFlow; + +public readonly struct RecvSnapshot +{ + public readonly long Tail; + public readonly bool IsClosed; + + public RecvSnapshot(long tail, bool isClosed) + { + Tail = tail; + IsClosed = isClosed; + } + + public static RecvSnapshot Closed() => new(0, isClosed: true); +} \ No newline at end of file diff --git a/MinimaTFlow/MinimaTFlow.csproj b/MinimaTFlow/MinimaTFlow.csproj new file mode 100644 index 0000000..e6699cd --- /dev/null +++ b/MinimaTFlow/MinimaTFlow.csproj @@ -0,0 +1,12 @@ + + + + Exe + net10.0 + enable + enable + true + MinimaTFlow + + + diff --git a/MinimaTFlow/Program.cs b/MinimaTFlow/Program.cs new file mode 100644 index 0000000..78b987e --- /dev/null +++ b/MinimaTFlow/Program.cs @@ -0,0 +1,178 @@ +using System.Buffers; +using System.IO.Pipelines; +using System.Text.Json; +using MinimaTFlow.Utils; + +namespace MinimaTFlow; + +/// +/// Multi-reactor HTTP/1.1 server using io_uring directly. Spawns N reactor +/// threads (one per CPU); each opens its own SO_REUSEPORT listener, runs its +/// own io_uring, owns its own connection map. The kernel load-balances new +/// connections across reactors. Per-connection state never crosses threads, +/// so no synchronization is needed on the hot path. +/// +internal static unsafe class Program +{ + internal static ReadOnlySpan Response => + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok"u8; + + private static int Main() + { + // All tunables live in ServerConfig — override the defaults here. + var config = new ServerConfig() + { + UsePipe = false, + ReactorCount = 8 + }; + + Console.WriteLine($"[Minima] starting {config.ReactorCount} reactors on port {config.Port} (incremental={config.Incremental})"); + + var threads = new Thread[config.ReactorCount]; + for (var i = 0; i < config.ReactorCount; i++) + { + var reactor = new Reactor(i, config); + + threads[i] = new Thread(reactor.Run) + { + Name = $"reactor-{i}", + IsBackground = false + }; + threads[i].Start(); + } + + foreach (var t in threads) + { + t.Join(); + } + + return 0; + } +} + +internal static class Handler +{ + // Real async-work knob: serialize an in-memory object of WORK_ITEMS elements to JSON + // on the THREAD POOL (via Task.Run) per request. 0 / unset = disabled (pure inline + // reactor path). Genuine CPU + allocation, not a busy-spin. + private static readonly int WorkItems = 50; + + private static readonly Payload LargeObject = BuildPayload(Math.Max(WorkItems, 1)); + + private static Payload BuildPayload(int count) + { + var items = new Item[count]; + for (int i = 0; i < count; i++) + { + items[i] = new Item(i, $"item-{i}", i * 1.5, (i & 1) == 0, $"category-{i % 8}"); + } + return new Payload(DateTime.UtcNow.ToString("O"), count, items); + } + + public static async Task HandleAsync(Reactor reactor, Connection conn) + { + try + { + while (true) + { + RecvSnapshot snap = await conn.ReadAsync(); + + while (conn.TryGetItem(snap, out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + UnmanagedMemoryManager mem = item.AsMemoryManager(); + ReadOnlyMemory data = mem.Memory; + // data is now usable with any BCL Memory/async API + _ = data.Length; + + // Cross-thread safe and mode-agnostic: routes to the + // shared-ring return or the incremental refcounted return. + conn.ReturnBuffer(in item); + } + } + + _ = await Task.Run(static () => JsonSerializer.Serialize("Hello World!")); + + // Real async work: serialize a large object to JSON on the THREAD POOL. + // The handler resumes OFF-REACTOR, so the FlushAsync below pays the eventfd + // handoff the pure-inline path avoids — and the serialization is genuine + // CPU + GC pressure on the pool, not a busy-spin. + /*if (WorkItems > 0) + { + //_ = await Task.Run(static () => JsonSerializer.SerializeToUtf8Bytes(LargeObject)); + JsonSerializer.SerializeToUtf8Bytes(LargeObject); + }*/ + + // One response per recv burst — accumulate in the connection's + // per-connection write slab, then submit and await ack. + conn.Write(Program.Response); + await conn.FlushAsync(); + + if (snap.IsClosed) + { + // Reactor already owns teardown (Connections.Remove + close + // happens in Dispatch's recv-error branch); we just exit. + return; + } + + conn.ResetRead(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[r{reactor.Id}] handler crash on fd={conn.ClientFd}: {ex}"); + // Reactor will clean the connection up via the recv-error path + // (or SPSC overflow) on the next CQE for this fd. + } + finally + { + conn.DecRef(); // release the handler's ref; teardown runs once the reactor releases too + } + } + + // PipeReader/PipeWriter variant — same behavior, driven through the BCL + // pipe adapters instead of the raw ReadAsync/TryGetItem/Write API. + public static async Task HandlePipeAsync(Reactor reactor, Connection conn) + { + var reader = new ConnectionPipeReader(conn); + var writer = new ConnectionPipeWriter(conn); + + try + { + while (true) + { + ReadResult read = await reader.ReadAsync(); + ReadOnlySequence buffer = read.Buffer; + + if (!buffer.IsEmpty) + { + // A real server would parse requests out of `buffer` here. + writer.Write(Program.Response); + await writer.FlushAsync(); + } + + // Consume everything we got; AdvanceTo returns the recv buffers. + reader.AdvanceTo(buffer.End); + + if (read.IsCompleted) + { + break; + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[r{reactor.Id}] pipe handler crash on fd={conn.ClientFd}: {ex}"); + } + finally + { + reader.Complete(); + writer.Complete(); + conn.DecRef(); + } + } +} + +internal sealed record Item(int Id, string Name, double Value, bool Active, string Category); +internal sealed record Payload(string Generated, int Count, Item[] Items); diff --git a/MinimaTFlow/Reactor/Reactor.cs b/MinimaTFlow/Reactor/Reactor.cs new file mode 100644 index 0000000..7af8ce7 --- /dev/null +++ b/MinimaTFlow/Reactor/Reactor.cs @@ -0,0 +1,370 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using MinimaTFlow.Utils; +using static MinimaTFlow.Native; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow; + +/// +/// Recv-only io_uring reactor. Accept + multishot recv flow through the ring; +/// the response goes out via libc send() on the handler thread +/// (Twinflow-style), so the reactor never deals with sends. Cross-thread paths +/// (buffer-return, recycle) still go through the MPSC + eventfd-wake pattern. +/// +public sealed unsafe partial class Reactor +{ + public readonly int Id; + public Ring Ring = null!; + public readonly Dictionary Connections = new(); + + private int _listenFd; + private readonly ServerConfig _config; + private readonly ushort _port; + private readonly uint _ringEntries; + private readonly uint RecvBufferSize; + + private const ulong KindAccept = 1UL << 32; + private const ulong KindRecv = 2UL << 32; + private const ulong KindWake = 4UL << 32; + + private const ushort BgId = 1; + private readonly uint BufferRingEntries; + private byte* _bufRing; + private byte* _bufSlab; + private uint _bufRingMask; + private ushort _bufRingTail; + + private int _wakeFd; + private int _reactorThreadId; + private readonly Mpsc _returnQ = new(1 << 14); + private readonly ConcurrentQueue _recycleQ = new(); + + private readonly int PoolMax; + private readonly Stack _pool; + + private const int EINTR = 4; + private const int EAGAIN = 11; + private const int EBUSY = 16; + + public Reactor(int id, ServerConfig config) + { + Id = id; + _config = config; + _port = config.Port; + _ringEntries = config.RingEntries; + RecvBufferSize = (uint)config.RecvBufferSize; + BufferRingEntries = (uint)config.BufferRingEntries; + PoolMax = config.PoolMax; + _pool = new Stack(config.PoolMax); + } + + // ========================================================================= + // Buffer ring + // ========================================================================= + + private void InitBufferRing() + { + nuint ringBytes = (nuint)BufferRingEntries * 16; + _bufRing = (byte*)NativeMemory.AlignedAlloc(ringBytes, 4096); + NativeMemory.Clear(_bufRing, ringBytes); + + nuint slabBytes = BufferRingEntries * (nuint)RecvBufferSize; + _bufSlab = (byte*)NativeMemory.AlignedAlloc(slabBytes, 64); + + _bufRingMask = BufferRingEntries - 1; + + var reg = new io_uring_buf_reg { + ring_addr = (ulong)_bufRing, + ring_entries = BufferRingEntries, + bgid = BgId, + }; + + int ret = io_uring_register(Ring.Fd, IORING_REGISTER_PBUF_RING, ®, 1); + if (ret < 0) + { + int err = Marshal.GetLastPInvokeError(); + throw new InvalidOperationException($"register pbuf_ring failed: ret={ret} errno={err}"); + } + + for (ushort bid = 0; bid < BufferRingEntries; bid++) { + byte* slot = _bufRing + (uint)bid * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + } + _bufRingTail = (ushort)BufferRingEntries; + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + internal void ReturnBufferDirect(ushort bid) + { + byte* slot = _bufRing + (_bufRingTail & _bufRingMask) * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + _bufRingTail++; + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + // ========================================================================= + // Cross-thread entry points + // ========================================================================= + + public void EnqueueReturnQ(ushort bid) + { + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + ReturnBufferDirect(bid); + return; + } + SpinWait sw = default; + while (!_returnQ.TryEnqueue(bid)) sw.SpinOnce(); + WakeFdWrite(); + } + + internal void EnqueueRecycle(Connection conn) + { + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + Recycle(conn, conn.ClientFd); + return; + } + _recycleQ.Enqueue(conn); + WakeFdWrite(); + } + + private void WakeFdWrite() + { + ulong v = 1; + write(_wakeFd, &v, 8); + } + + private void DrainReturnQ() + { + while (_returnQ.TryDequeue(out ushort bid)) + { + ReturnBufferDirect(bid); + } + } + + private void DrainRecycleQ() + { + while (_recycleQ.TryDequeue(out Connection? conn)) + { + Recycle(conn, conn.ClientFd); + } + } + + private void ArmWakePoll() + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_POLL_ADD; + sqe->fd = _wakeFd; + sqe->op_flags = POLLIN; + sqe->len = IORING_POLL_ADD_MULTI; + sqe->user_data = KindWake | (uint)_wakeFd; + } + + // ========================================================================= + // Main loop + // ========================================================================= + + public void Run() + { + _reactorThreadId = Environment.CurrentManagedThreadId; + + Ring = Ring.Create(_ringEntries); + _listenFd = OpenReusePortListener(_port); + + InitBufferRing(); + + _wakeFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (_wakeFd < 0) + { + throw new InvalidOperationException("eventfd failed"); + } + + Console.WriteLine($"[r{Id}] listening on 0.0.0.0:{_port}"); + SubmitAcceptMultishot(); + ArmWakePoll(); + + LoopShared(); + + close(_listenFd); + close(_wakeFd); + Ring.Dispose(); + } + + private void LoopShared() + { + while (true) + { + DrainReturnQ(); + DrainRecycleQ(); + + int rc = Ring.SubmitAndWait(1); + if (rc < 0 && rc != -EINTR && rc != -EAGAIN && rc != -EBUSY) + { + Console.Error.WriteLine($"[r{Id}] io_uring_enter failed: {rc}"); + break; + } + + uint ready = Ring.CqReady(); + for (uint i = 0; i < ready; i++) + { + Dispatch(in Ring.CqeAt(i)); + } + Ring.CqAdvance(ready); + } + } + + private void Dispatch(in IoUringCqe cqe) + { + ulong kind = cqe.user_data & 0xffffffff_00000000UL; + int fd = (int)(cqe.user_data & 0xffffffffUL); + bool more = (cqe.flags & IORING_CQE_F_MORE) != 0; + + if (kind == KindWake) + { + ulong drain; + read(_wakeFd, &drain, 8); + if (!more) ArmWakePoll(); + return; + } + + if (kind == KindAccept) + { + if (cqe.res >= 0) + { + int clientFd = cqe.res; + SetNoDelay(clientFd); + Connection conn = _pool.TryPop(out var pooled) + ? pooled.SetFd(clientFd) + : new Connection(this, clientFd, _config.WriteSlabSize); + Connections[clientFd] = conn; + conn.InitRefs(); + SubmitRecvMultishot(clientFd); + + _ = _config.UsePipe + ? Handler.HandlePipeAsync(this, conn) + : Handler.HandleAsync(this, conn); + } + else + { + Console.Error.WriteLine($"[r{Id}] accept error: {cqe.res}"); + } + if (!more) SubmitAcceptMultishot(); + } + else if (kind == KindRecv) + { + bool hasBuf = (cqe.flags & IORING_CQE_F_BUFFER) != 0; + ushort bid = hasBuf ? (ushort)(cqe.flags >> IORING_CQE_BUFFER_SHIFT) : (ushort)0; + + if (cqe.res <= 0) + { + if (hasBuf) ReturnBufferDirect(bid); + if (Connections.Remove(fd, out var dyingConn)) + { + dyingConn.MarkClosed(); + dyingConn.DecRef(); + } + return; + } + + if (!Connections.TryGetValue(fd, out var conn)) + { + if (hasBuf) ReturnBufferDirect(bid); + return; + } + + byte* ptr = hasBuf ? _bufSlab + (nuint)bid * (nuint)RecvBufferSize : null; + conn.Complete(cqe.res, bid, hasBuf, ptr); + + if (!more) SubmitRecvMultishot(fd); + } + } + + // ========================================================================= + // SQE producers — reactor-thread only (no send op; that's libc send() in + // the handler). + // ========================================================================= + + private IoUringSqe* GetSqeOrFlush() + { + IoUringSqe* sqe = Ring.GetSqe(); + if (sqe != null) return sqe; + Ring.SubmitAndWait(0); + sqe = Ring.GetSqe(); + if (sqe == null) throw new InvalidOperationException("SQ full after flush"); + return sqe; + } + + private void SubmitAcceptMultishot() + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_ACCEPT; + sqe->ioprio = IORING_ACCEPT_MULTISHOT; + sqe->fd = _listenFd; + sqe->user_data = KindAccept | (uint)_listenFd; + } + + private void SubmitRecvMultishot(int fd) + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_RECV; + sqe->flags = IOSQE_BUFFER_SELECT; + sqe->ioprio = IORING_RECV_MULTISHOT; + sqe->fd = fd; + sqe->buf_index = BgId; + sqe->user_data = KindRecv | (uint)fd; + } + + private void Recycle(Connection conn, int fd) + { + conn.MarkClosed(); + conn.DrainRecv(); + close(fd); + conn.Clear(); + + if (_pool.Count < PoolMax) + { + _pool.Push(conn); + } + else + { + conn.Dispose(); + } + } + + private static void SetNoDelay(int fd) + { + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(int)); + } + + private static int OpenReusePortListener(ushort port) + { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) throw new InvalidOperationException($"socket failed: {fd}"); + + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(int)); + + sockaddr_in addr = default; + addr.sin_family = AF_INET; + addr.sin_port = Htons(port); + addr.sin_addr.s_addr = 0; + + if (bind(fd, &addr, (uint)sizeof(sockaddr_in)) < 0) + throw new InvalidOperationException("bind failed"); + if (listen(fd, 128) < 0) + throw new InvalidOperationException("listen failed"); + return fd; + } +} diff --git a/MinimaTFlow/ServerConfig.cs b/MinimaTFlow/ServerConfig.cs new file mode 100644 index 0000000..8e09ae3 --- /dev/null +++ b/MinimaTFlow/ServerConfig.cs @@ -0,0 +1,35 @@ +namespace MinimaTFlow; + +/// +/// All server tunables in one place — replaces the consts that used to be +/// scattered across Program.cs and Reactor.cs. Defaults match the previous +/// hardcoded values; override via object initializer in Main, e.g.: +/// new ServerConfig { Port = 9000, ReactorCount = 8, Incremental = true }. +/// +public sealed record ServerConfig +{ + // Server-level. + public ushort Port { get; init; } = 8080; + public int ReactorCount { get; init; } = 12; + + // Handler style: false = raw ReadAsync/TryGetItem loop; true = PipeReader/PipeWriter. + public bool UsePipe { get; init; } = false; + + // io_uring SQ/CQ depth. + public uint RingEntries { get; init; } = 8192; + + // Shared buffer ring (used when Incremental == false). + public int RecvBufferSize { get; init; } = 32 * 1024; + public int BufferRingEntries { get; init; } = 4096; + + // Per-connection write slab + connection pool cap. + public int WriteSlabSize { get; init; } = 16 * 1024; + public int PoolMax { get; init; } = 1024; + + // Incremental mode (IOU_PBUF_RING_INC) — per-connection rings. + // reserved native memory ≈ PoolMax × ConnBufRingEntries × IncRecvBufferSize × ReactorCount. + public bool Incremental { get; init; } = false; + public int MaxConnections { get; init; } = 4096; // GID cap (one bgid per active connection) + public int ConnBufRingEntries { get; init; } = 16; // buffers per connection ring + public int IncRecvBufferSize { get; init; } = 4096; // bytes per buffer (filled incrementally) +} diff --git a/MinimaTFlow/Utils/Mpsc.cs b/MinimaTFlow/Utils/Mpsc.cs new file mode 100644 index 0000000..0575711 --- /dev/null +++ b/MinimaTFlow/Utils/Mpsc.cs @@ -0,0 +1,115 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow.Utils; + +/// +/// Bounded lock-free multi-producer / single-consumer queue. +/// +/// Dmitry Vyukov's bounded MPMC algorithm, specialised to one consumer. +/// Power-of-two capacity, zero-allocation after construction. Producers claim a +/// slot via CAS on the enqueue position (a failed TryEnqueue on a full queue +/// leaves the position untouched — no burned tickets); the single consumer +/// advances the dequeue position with a plain write. Each slot carries a +/// sequence number that coordinates ownership between producers and consumer. +/// +/// One generic queue serves every reactor handoff: Mpsc<ushort> for buffer +/// returns, Mpsc<int> for flush fds, Mpsc<ulong> for packed incremental +/// returns. T is unmanaged so each Cell is a blittable value type with no GC refs. +/// +internal sealed class Mpsc where T : unmanaged +{ + private struct Cell + { + public long Sequence; + public T Value; + } + + private readonly Cell[] _buffer; + private readonly int _mask; + + // PaddedLong is a top-level struct (not nested here) because the CLR forbids + // explicit layout on a type nested inside a generic. + private PaddedLong _enqueuePos; + private PaddedLong _dequeuePos; + + public Mpsc(int capacityPow2) + { + if (capacityPow2 < 2 || (capacityPow2 & (capacityPow2 - 1)) != 0) + throw new ArgumentException("Capacity must be a power of two >= 2.", nameof(capacityPow2)); + + _buffer = new Cell[capacityPow2]; + _mask = capacityPow2 - 1; + + for (int i = 0; i < capacityPow2; i++) + _buffer[i].Sequence = i; + } + + /// Multi-producer safe. Returns false if the queue is full. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + while (true) + { + long pos = Volatile.Read(ref _enqueuePos.Value); + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - pos; + + if (dif == 0) + { + if (Interlocked.CompareExchange(ref _enqueuePos.Value, pos + 1, pos) == pos) + { + cell.Value = item; + Volatile.Write(ref cell.Sequence, pos + 1); + return true; + } + continue; // lost the race; reload and retry + } + + if (dif < 0) + return false; // slot not yet consumed → full + } + } + + /// Single-consumer only. Returns false if empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + long pos = _dequeuePos.Value; // single consumer: plain read + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - (pos + 1); + + if (dif == 0) + { + item = cell.Value; + _dequeuePos.Value = pos + 1; // single consumer: plain write + Volatile.Write(ref cell.Sequence, pos + mask + 1); // free slot for producers + return true; + } + + item = default; + return false; + } +} + +/// +/// A single long padded to a 64-byte cache line so the producer and consumer +/// positions never share a line (no false sharing). Top-level and non-generic +/// so it can legally use explicit layout. +/// +[StructLayout(LayoutKind.Explicit, Size = 64)] +internal struct PaddedLong +{ + [FieldOffset(0)] public long Value; +} diff --git a/MinimaTFlow/Utils/RingSegment.cs b/MinimaTFlow/Utils/RingSegment.cs new file mode 100644 index 0000000..758f975 --- /dev/null +++ b/MinimaTFlow/Utils/RingSegment.cs @@ -0,0 +1,31 @@ +using System.Buffers; + +namespace MinimaTFlow.Utils; + +/// +/// One segment of a multi-buffer ReadOnlySequence<byte> built by the +/// ConnectionPipeReader when a single read spans more than one recv buffer. +/// BufferId is carried for debugging; buffer return is driven off the held +/// item list, not the segments. +/// +public sealed class RingSegment : ReadOnlySequenceSegment +{ + public ushort BufferId { get; } + + public RingSegment(ReadOnlyMemory memory, ushort bufferId) + { + Memory = memory; + BufferId = bufferId; + } + + public RingSegment Append(ReadOnlyMemory memory, ushort bufferId) + { + var next = new RingSegment(memory, bufferId) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = next; + return next; + } +} diff --git a/MinimaTFlow/Utils/SpscRecvRing.cs b/MinimaTFlow/Utils/SpscRecvRing.cs new file mode 100644 index 0000000..13d434a --- /dev/null +++ b/MinimaTFlow/Utils/SpscRecvRing.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTFlow.Utils; + +public sealed unsafe class SpscRecvRing +{ + public struct Item + { + public byte* Ptr; + public ushort Bid; + public int Len; + public bool HasBuffer; + public ushort Gen; // connection generation when enqueued (incremental return guard) + + public ReadOnlySpan AsSpan() => new(Ptr, Len); + + public UnmanagedMemoryManager AsMemoryManager() => new(Ptr, Len, Bid); + } + + private readonly Item[] _items; + private readonly int _mask; + private long _tail; + private long _head; + + public SpscRecvRing(int capacityPow2) + { + if (capacityPow2 <= 0 || (capacityPow2 & (capacityPow2 - 1)) != 0) + { + throw new ArgumentException("capacity must be a power of two", nameof(capacityPow2)); + } + + _items = new Item[capacityPow2]; + _mask = capacityPow2 - 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(in Item item) + { + long head = Volatile.Read(ref _head); + long tail = _tail; + + if ((ulong)(tail - head) >= (ulong)_items.Length) + { + return false; + } + + _items[(int)(tail & _mask)] = item; + Volatile.Write(ref _tail, tail + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out Item item) + { + long head = _head; + long tail = Volatile.Read(ref _tail); + + if (head >= tail) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long SnapshotTail() => Volatile.Read(ref _tail); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeueUntil(long tailSnapshot, out Item item) + { + long head = _head; + + if (head >= tailSnapshot) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEmpty() => Volatile.Read(ref _head) >= Volatile.Read(ref _tail); + + // Reactor-thread-only, called during connection teardown (Clear) when no + // handler is consuming. Discards any leftover items so the recycled + // connection starts empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _head = 0; + _tail = 0; + } +} diff --git a/MinimaTFlow/Utils/UnmanagedMemoryManager.cs b/MinimaTFlow/Utils/UnmanagedMemoryManager.cs new file mode 100644 index 0000000..ad7a2f1 --- /dev/null +++ b/MinimaTFlow/Utils/UnmanagedMemoryManager.cs @@ -0,0 +1,32 @@ +using System.Buffers; + +namespace MinimaTFlow.Utils; + +public sealed unsafe class UnmanagedMemoryManager : MemoryManager +{ + private readonly byte* _ptr; + private readonly int _length; + + public ushort BufferId { get; } + + public UnmanagedMemoryManager(byte* ptr, int length) + { + _ptr = ptr; + _length = length; + } + + public UnmanagedMemoryManager(byte* ptr, int length, ushort bufferId) + { + _ptr = ptr; + _length = length; + BufferId = bufferId; + } + + public override Span GetSpan() => new(_ptr, _length); + + public override MemoryHandle Pin(int elementIndex = 0) => new(_ptr + elementIndex); + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } +} diff --git a/MinimaTFlow/io_uring/Native.cs b/MinimaTFlow/io_uring/Native.cs new file mode 100644 index 0000000..71e79b6 --- /dev/null +++ b/MinimaTFlow/io_uring/Native.cs @@ -0,0 +1,170 @@ +using System.Runtime.InteropServices; + +namespace MinimaTFlow; + +/// +/// All native interop in one file: io_uring syscalls, libc socket calls, +/// the kernel struct layouts they expect, and the constants needed to +/// drive a minimal io_uring loop. +/// +public static unsafe class Native { + private const long SYS_IO_URING_SETUP = 425; + private const long SYS_IO_URING_ENTER = 426; + private const long SYS_IO_URING_REGISTER = 427; + + public const byte IORING_OP_POLL_ADD = 6; + public const byte IORING_OP_ACCEPT = 13; + public const byte IORING_OP_SEND = 26; + public const byte IORING_OP_RECV = 27; + public const uint IORING_ENTER_GETEVENTS = 1u << 0; + public const long IORING_OFF_SQ_RING = 0; + public const long IORING_OFF_SQES = 0x10000000; + + // Multishot / buffer-ring goodies. + public const ushort IORING_ACCEPT_MULTISHOT = 1 << 0; + public const ushort IORING_RECV_MULTISHOT = 1 << 1; + public const byte IOSQE_BUFFER_SELECT = 1 << 5; + public const uint IORING_CQE_F_BUFFER = 1u << 0; + public const uint IORING_CQE_F_MORE = 1u << 1; + public const int IORING_CQE_BUFFER_SHIFT = 16; + public const uint IORING_REGISTER_PBUF_RING = 22; + public const uint IORING_UNREGISTER_PBUF_RING = 23; + public const uint IORING_POLL_ADD_MULTI = 1u << 0; + + // Incremental provided-buffer consumption (kernel 6.12+). IOU_PBUF_RING_INC + // is set in io_uring_buf_reg.flags at registration; IORING_CQE_F_BUF_MORE is + // set on recv CQEs while the kernel will keep appending to the same buffer. + public const ushort IOU_PBUF_RING_INC = 2; + public const uint IORING_CQE_F_BUF_MORE = 1u << 4; + + // eventfd flags + poll mask (used for the cross-thread wake mechanism). + public const int EFD_CLOEXEC = 0x80000; + public const int EFD_NONBLOCK = 0x800; + public const uint POLLIN = 0x0001; + + // Setup flags. SINGLE_ISSUER tells the kernel only one thread will submit + // to this ring (skips locking on the SQ). DEFER_TASKRUN defers completion + // processing until io_uring_enter(GETEVENTS), which lets the kernel batch + // work and avoids interrupting the reactor with task_work mid-flight. + public const uint IORING_SETUP_SINGLE_ISSUER = 1u << 12; + public const uint IORING_SETUP_DEFER_TASKRUN = 1u << 13; + + public const int PROT_READ = 1; + public const int PROT_WRITE = 2; + public const int MAP_SHARED = 1; + public const int MAP_POPULATE = 0x8000; + + public const int AF_INET = 2; + public const int SOCK_STREAM = 1; + public const int SOL_SOCKET = 1; + public const int SO_REUSEADDR = 2; + public const int SO_REUSEPORT = 15; + public const int IPPROTO_TCP = 6; + public const int TCP_NODELAY = 1; + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall3(long nr, uint a1, IoUringParams* a2); + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall6(long nr, uint a1, uint a2, uint a3, uint a4, void* a5, nuint a6); + + [DllImport("libc", EntryPoint = "syscall", SetLastError = true)] + private static extern long syscall4(long nr, uint a1, uint a2, void* a3, uint a4); + + public static int io_uring_setup(uint entries, IoUringParams* p) => + (int)syscall3(SYS_IO_URING_SETUP, entries, p); + + public static int io_uring_enter(int fd, uint toSubmit, uint minComplete, uint flags) => + (int)syscall6(SYS_IO_URING_ENTER, (uint)fd, toSubmit, minComplete, flags, null, 0); + + public static int io_uring_register(int fd, uint opcode, void* arg, uint nrArgs) => + (int)syscall4(SYS_IO_URING_REGISTER, (uint)fd, opcode, arg, nrArgs); + + [DllImport("libc")] public static extern void* mmap(void* addr, nuint length, int prot, int flags, int fd, long offset); + [DllImport("libc")] public static extern int munmap(void* addr, nuint length); + [DllImport("libc")] public static extern int close(int fd); + [DllImport("libc")] public static extern int socket(int domain, int type, int proto); + [DllImport("libc")] public static extern int bind(int fd, sockaddr_in* addr, uint len); + [DllImport("libc")] public static extern int listen(int fd, int backlog); + [DllImport("libc")] public static extern int setsockopt(int fd, int level, int optname, void* optval, uint optlen); + [DllImport("libc")] public static extern int eventfd(uint initval, int flags); + [DllImport("libc")] public static extern long write(int fd, void* buf, nuint count); + [DllImport("libc")] public static extern long read(int fd, void* buf, nuint count); + // Inline send used by the handler thread for the response path — keeps the + // send off the io_uring reactor entirely (Twinflow-style). + [DllImport("libc", SetLastError = true)] public static extern long send(int fd, byte* buf, nuint len, int flags); + + public const int MSG_NOSIGNAL = 0x4000; + public const int EAGAIN = 11; + public const int EWOULDBLOCK = 11; + public const int EINTR = 4; + + public static ushort Htons(ushort x) => (ushort)((x << 8) | (x >> 8)); + + // Kernel struct layouts (must match include/uapi/linux/io_uring.h) + [StructLayout(LayoutKind.Sequential)] + public struct SqRingOffsets { + public uint head, tail, ring_mask, ring_entries, flags, dropped, array, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CqRingOffsets { + public uint head, tail, ring_mask, ring_entries, overflow, cqes, flags, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringParams { + public uint sq_entries, cq_entries, flags, sq_thread_cpu, sq_thread_idle; + public uint features, wq_fd, resv0, resv1, resv2; + public SqRingOffsets sq_off; + public CqRingOffsets cq_off; + } + + [StructLayout(LayoutKind.Explicit, Size = 64)] + public struct IoUringSqe { + [FieldOffset(0)] public byte opcode; + [FieldOffset(1)] public byte flags; + [FieldOffset(2)] public ushort ioprio; + [FieldOffset(4)] public int fd; + [FieldOffset(8)] public ulong off; + [FieldOffset(16)] public ulong addr; + [FieldOffset(24)] public uint len; + [FieldOffset(28)] public uint op_flags; + [FieldOffset(32)] public ulong user_data; + [FieldOffset(40)] public ushort buf_index; + [FieldOffset(42)] public ushort personality; + [FieldOffset(44)] public int splice_fd_in; + [FieldOffset(48)] public ulong addr3; + [FieldOffset(56)] public ulong __pad2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringCqe { + public ulong user_data; + public int res; + public uint flags; + } + + // Argument struct for IORING_REGISTER_PBUF_RING. + [StructLayout(LayoutKind.Sequential)] + public struct io_uring_buf_reg { + public ulong ring_addr; + public uint ring_entries; + public ushort bgid; + public ushort flags; + public ulong resv1, resv2, resv3; + } + + [StructLayout(LayoutKind.Sequential)] + public struct in_addr { public uint s_addr; } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct sockaddr_in { + public ushort sin_family; + public ushort sin_port; + public in_addr sin_addr; + public fixed byte sin_zero[8]; + } +} diff --git a/MinimaTFlow/io_uring/Ring.cs b/MinimaTFlow/io_uring/Ring.cs new file mode 100644 index 0000000..2d4837f --- /dev/null +++ b/MinimaTFlow/io_uring/Ring.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +using static MinimaTFlow.Native; + +// ReSharper disable SuggestVarOrType_BuiltInTypes +// ReSharper disable SuggestVarOrType_Elsewhere +#pragma warning disable CA1806 + +namespace MinimaTFlow; + +public sealed unsafe class Ring : IDisposable +{ + private int _fd; + + public int Fd => _fd; + + private uint* _sqHead; + private uint* _sqTail; + private uint* _sqArray; + private uint _sqMask; + private uint _sqEntries; + private IoUringSqe* _sqes; + + private uint* _cqHead; + private uint* _cqTail; + private IoUringCqe* _cqes; + private uint _cqMask; + + private uint _sqeTail; + + private byte* _ringPtr; + private nuint _ringSize; + private byte* _sqePtr; + private nuint _sqeSize; + + public static Ring Create(uint entries) + { + IoUringParams ioUringParams = default; + ioUringParams.flags = IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN; + int fd = io_uring_setup(entries, &ioUringParams); + if (fd < 0) + { + throw new InvalidOperationException($"io_uring_setup failed: {fd}"); + } + + var ring = new Ring + { + _fd = fd, + _sqEntries = ioUringParams.sq_entries + }; + + nuint sqRingBytes = ioUringParams.sq_off.array + ioUringParams.sq_entries * sizeof(uint); + nuint cqRingBytes = ioUringParams.cq_off.cqes + ioUringParams.cq_entries * (nuint)sizeof(IoUringCqe); + nuint ringBytes = sqRingBytes > cqRingBytes ? sqRingBytes : cqRingBytes; + + void* ringMem = mmap(null, ringBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING); + if (ringMem == (void*)-1) + { + close(fd); + + throw new InvalidOperationException("mmap(SQ_RING) failed"); + } + ring._ringPtr = (byte*)ringMem; + ring._ringSize = ringBytes; + + nuint sqeBytes = ioUringParams.sq_entries * (nuint)sizeof(IoUringSqe); + void* sqeMem = mmap(null, sqeBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQES); + if (sqeMem == (void*)-1) + { + munmap(ringMem, ringBytes); + close(fd); + + throw new InvalidOperationException("mmap(SQES) failed"); + } + ring._sqes = (IoUringSqe*)sqeMem; + ring._sqePtr = (byte*)sqeMem; + ring._sqeSize = sqeBytes; + + byte* ringPointer = (byte*)ringMem; + ring._sqHead = (uint*)(ringPointer + ioUringParams.sq_off.head); + ring._sqTail = (uint*)(ringPointer + ioUringParams.sq_off.tail); + ring._sqArray = (uint*)(ringPointer + ioUringParams.sq_off.array); + ring._sqMask = *(uint*)(ringPointer + ioUringParams.sq_off.ring_mask); + + ring._cqHead = (uint*)(ringPointer + ioUringParams.cq_off.head); + ring._cqTail = (uint*)(ringPointer + ioUringParams.cq_off.tail); + ring._cqes = (IoUringCqe*)(ringPointer + ioUringParams.cq_off.cqes); + ring._cqMask = *(uint*)(ringPointer + ioUringParams.cq_off.ring_mask); + + return ring; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IoUringSqe* GetSqe() + { + uint head = Volatile.Read(ref *_sqHead); + + if (_sqeTail - head >= _sqEntries) + { + return null; + } + + uint slot = _sqeTail & _sqMask; + _sqArray[slot] = slot; + _sqeTail++; + + return &_sqes[slot]; + } + + public int SubmitAndWait(uint waitFor) + { + uint published = *_sqTail; + uint toSubmit = _sqeTail - published; + + if (toSubmit > 0) + { + Volatile.Write(ref *_sqTail, _sqeTail); + } + + if (toSubmit == 0 && waitFor == 0) return 0; + + uint flags = waitFor > 0 ? IORING_ENTER_GETEVENTS : 0; + + return io_uring_enter(_fd, toSubmit, waitFor, flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetCqe(out IoUringCqe cqe) + { + uint head = *_cqHead; + uint tail = Volatile.Read(ref *_cqTail); + + if (head == tail) + { + cqe = default; + + return false; + } + + cqe = _cqes[head & _cqMask]; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqeSeen() => Volatile.Write(ref *_cqHead, *_cqHead + 1); + + // Batched CQ drain (liburing io_uring_for_each_cqe + io_uring_cq_advance): + // read the kernel-written tail once (acquire), process the whole batch, + // then publish the consumed head once (release) instead of once per CQE. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint CqReady() => Volatile.Read(ref *_cqTail) - *_cqHead; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref readonly IoUringCqe CqeAt(uint i) => ref _cqes[(*_cqHead + i) & _cqMask]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqAdvance(uint n) => Volatile.Write(ref *_cqHead, *_cqHead + n); + + public void Dispose() + { + if (_ringPtr != null) + { + munmap(_ringPtr, _ringSize); _ringPtr = null; + } + + if (_sqePtr != null) + { + munmap(_sqePtr, _sqeSize); _sqePtr = null; + } + + if (_fd > 0) + { + close(_fd); _fd = 0; + } + } +} + +#pragma warning restore CA1806 diff --git a/MinimaTPool/Connection/Connection.Incremental.cs b/MinimaTPool/Connection/Connection.Incremental.cs new file mode 100644 index 0000000..d6aaa20 --- /dev/null +++ b/MinimaTPool/Connection/Connection.Incremental.cs @@ -0,0 +1,61 @@ +using System.Runtime.InteropServices; +using MinimaTPool.Utils; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +/// +/// Incremental-mode (IOU_PBUF_RING_INC) per-connection buffer-ring state. +/// Each connection owns its own ring + slab; one buffer accumulates this +/// connection's byte stream across many recvs. The reactor (Reactor.Incremental) +/// drives setup/teardown and the refcounted recycle; this partial just holds the +/// state and routes a handler return to the right reactor entry point. +/// +/// All of these stay allocated across pool reuse and are freed in Dispose(). +/// +public sealed unsafe partial class Connection +{ + internal byte* BufRing; // kernel-shared ring control area + internal byte* BufSlab; // this connection's recv slab + internal ushort Bgid; + internal uint BufRingMask; + internal int BufRingEntries; + internal bool IncrementalMode; + + internal int[]? CumOffset; // per-bid: byte offset where the next slice begins + internal int[]? RefCount; // per-bid: outstanding handler refs + internal bool[]? KernelDone; // per-bid: kernel finished appending (no F_BUF_MORE) + + internal int Generation => Volatile.Read(ref _generation); + + /// + /// Called by the handler to hand a consumed recv buffer back. Routes by mode: + /// incremental returns carry (fd, gen, bid) for refcounted recycle; the shared + /// path returns the bare bid to the reactor's single buf_ring. + /// + public void ReturnBuffer(in SpscRecvRing.Item item) + { + if (IncrementalMode) + { + _reactor.EnqueueReturnQIncremental(ClientFd, item.Gen, item.Bid); + } + else + { + _reactor.EnqueueReturnQ(item.Bid); + } + } + + private void DisposeIncremental() + { + if (BufRing != null) + { + NativeMemory.AlignedFree(BufRing); + BufRing = null; + } + if (BufSlab != null) + { + NativeMemory.AlignedFree(BufSlab); + BufSlab = null; + } + } +} diff --git a/MinimaTPool/Connection/Connection.Read.cs b/MinimaTPool/Connection/Connection.Read.cs new file mode 100644 index 0000000..bf2aced --- /dev/null +++ b/MinimaTPool/Connection/Connection.Read.cs @@ -0,0 +1,168 @@ +using System.Threading.Tasks.Sources; +using MinimaTPool.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +/// +/// Per-connection state. The handler may run on any thread (e.g. resumed by +/// a thread-pool timer); reactor-only side effects are funnelled through the +/// MPSC queues on `Reactor`. Coordination uses Interlocked.Exchange on the +/// arm flags and a sticky `_pending` to close the lost-wakeup race. +/// +/// Lifetime is pool-managed: the reactor pops a Connection on accept (or new +/// one if pool is empty), and pushes it back on teardown after `Clear()`. The +/// `_generation` field is bumped on each `Clear` so stale `ValueTask` tokens +/// from a previous connection life are detectable and return `Closed()` +/// instead of leaking the new tenant's state. +/// +public sealed unsafe partial class Connection : IValueTaskSource +{ + internal Connection SetFd(int fd) + { + ClientFd = fd; + return this; + } + + private ManualResetValueTaskSourceCore _readSignal = new() + { + // Always resume the handler on the thread pool — never inline on the reactor. + // This is the key knob that distinguishes MinimaTPool from Minima. + RunContinuationsAsynchronously = true, + }; + private int _armed; + private int _pending; + private int _closed; + + private readonly SpscRecvRing _recv = new(capacityPow2: 16); + + public ValueTask ReadAsync() + { + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1) + { + Volatile.Write(ref _pending, 0); + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + if (Volatile.Read(ref _closed) != 0) + { + return new ValueTask(RecvSnapshot.Closed()); + } + + if (Interlocked.Exchange(ref _armed, 1) == 1) + { + throw new InvalidOperationException("ReadAsync already armed."); + } + + // Snapshot the generation as the IVTS token so a future Clear() can + // invalidate this awaiter if the connection gets pool-recycled. + int gen = Volatile.Read(ref _generation); + + // Race recovery: re-check between arming and returning the IVTS task. + if (!_recv.IsEmpty() || Volatile.Read(ref _pending) == 1 || Volatile.Read(ref _closed) != 0) + { + Volatile.Write(ref _pending, 0); + Interlocked.Exchange(ref _armed, 0); + + return new ValueTask( + new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + + return new ValueTask(this, (short)gen); + } + + public bool TryGetItem(in RecvSnapshot snap, out SpscRecvRing.Item item) + => _recv.TryDequeueUntil(snap.Tail, out item); + + public void ResetRead() => _readSignal.Reset(); + + public void Complete(int res, ushort bid, bool hasBuffer, byte* ptr) + { + if (!_recv.TryEnqueue(new SpscRecvRing.Item + { + Ptr = ptr, + Bid = bid, + Len = res, + HasBuffer = hasBuffer, + Gen = (ushort)Volatile.Read(ref _generation) + })) + { + Console.Error.WriteLine("[conn] recv queue overflow."); + if (hasBuffer) + { + _reactor.ReturnBufferDirect(bid); + } + Volatile.Write(ref _closed, 1); + } + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), Volatile.Read(ref _closed) != 0)); + } + else + { + Volatile.Write(ref _pending, 1); + } + } + + internal void DrainRecv() + { + // Return any buffer IDs still sitting in the SPSC ring (handler exited + // before draining them, or a recv arrived after _closed was set). + while (_recv.TryDequeue(out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + _reactor.ReturnBufferDirect(item.Bid); + } + } + } + + // ========================================================================= + // IValueTaskSource plumbing — token (= snapshot of `_generation` at await + // time) is compared against the current `_generation` to detect stale + // awaiters from before a Clear()/pool reuse. Stale awaiters get a + // sentinel result rather than the new tenant's state. + // + // For the actual IVTS dispatch we pass `_readSignal.Version` / + // `_flushSignal.Version` to the underlying core (not `token`) because the + // core's version is bumped by ResetRead/CompleteFlush mid-life and is + // unrelated to the cross-life generation guard. + // ========================================================================= + + RecvSnapshot IValueTaskSource.GetResult(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return RecvSnapshot.Closed(); + } + + return _readSignal.GetResult(_readSignal.Version); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return ValueTaskSourceStatus.Succeeded; + } + + return _readSignal.GetStatus(_readSignal.Version); + } + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (token != (short)Volatile.Read(ref _generation)) + { + // Stale — run the continuation now so the awaiter unblocks and + // gets RecvSnapshot.Closed() from GetResult. + continuation(state); + + return; + } + + _readSignal.OnCompleted(continuation, state, _readSignal.Version, flags); + } +} diff --git a/MinimaTPool/Connection/Connection.Write.cs b/MinimaTPool/Connection/Connection.Write.cs new file mode 100644 index 0000000..79eb82d --- /dev/null +++ b/MinimaTPool/Connection/Connection.Write.cs @@ -0,0 +1,187 @@ +using System.Buffers; +using System.Threading.Tasks.Sources; +using MinimaTPool.Utils; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +public sealed unsafe partial class Connection : IValueTaskSource, IBufferWriter +{ + private readonly int _writeSlabSize; + internal byte* WriteBuffer; + internal int WriteHead; + internal int WriteTail; + internal int WriteInFlight; + + private readonly UnmanagedMemoryManager _manager; + + private ManualResetValueTaskSourceCore _flushSignal = new() + { + // Always resume the handler on the thread pool — never inline on the reactor. + // This is the key knob that distinguishes MinimaTPool from Minima. + RunContinuationsAsynchronously = true, + }; + private int _flushArmed; + private int _flushInProgress; + + // IBufferWrite +#region IBufferWrite + + public Memory GetMemory(int sizeHint = 0) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + int remaining = _writeSlabSize - WriteTail; + if (sizeHint > remaining) + { + throw new InvalidOperationException("Buffer too small."); + } + + return _manager.Memory.Slice(WriteTail, remaining); + } + + public Span GetSpan(int sizeHint = 0) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + if (WriteTail + sizeHint > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + + return new Span(WriteBuffer + WriteTail, _writeSlabSize - WriteTail); + } + + public void Advance(int count) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + WriteTail += count; + } + +#endregion + + // Write to the inner buffer + public void Write(ReadOnlySpan source) + { + if (Volatile.Read(ref _flushInProgress) != 0) + { + throw new InvalidOperationException("Cannot write while flush is in progress."); + } + + int len = source.Length; + if (WriteTail + len > _writeSlabSize) + { + throw new InvalidOperationException("Write buffer too small."); + } + + source.CopyTo(new Span(WriteBuffer + WriteTail, len)); + WriteTail += len; + } + + // Flush inner buffer data to the kernel + public ValueTask FlushAsync() + { + // Connection already torn down (reactor saw EOF/error → MarkClosed): don't flush + // a removed connection — the handoff would reach a reactor that no longer knows + // this fd and the awaiter would hang. Return completed so the handler unwinds to + // its next ReadAsync, sees IsClosed, and exits. + if (Volatile.Read(ref _closed) == 1) + { + return default; + } + + if (Interlocked.Exchange(ref _flushInProgress, 1) == 1) + { + throw new InvalidOperationException("FlushAsync already in progress."); + } + + int target = WriteTail; + if (target == 0) + { + Volatile.Write(ref _flushInProgress, 0); + + return default; + } + + if (Interlocked.Exchange(ref _flushArmed, 1) == 1) + { + throw new InvalidOperationException("FlushAsync already armed."); + } + + _flushSignal.Reset(); + WriteInFlight = target; + + int gen = Volatile.Read(ref _generation); + + _reactor.EnqueueFlush(ClientFd); + + // Race recovery (mirrors ReadAsync): if close raced in after the guard above, + // self-complete so we don't hang waiting on a send the reactor will never make. + if (Volatile.Read(ref _closed) == 1 && Interlocked.Exchange(ref _flushArmed, 0) == 1) + { + Volatile.Write(ref _flushInProgress, 0); + _flushSignal.SetResult(true); + } + + return new ValueTask(this, (short)gen); + } + + // Signal the FlushAsync was completed, called by the reactor's dispatcher send branch + internal void CompleteFlush() + { + WriteHead = 0; + WriteTail = 0; + WriteInFlight = 0; + Volatile.Write(ref _flushInProgress, 0); + Interlocked.Exchange(ref _flushArmed, 0); + + _flushSignal.SetResult(true); + } + + // IValueTaskSource +#region IValueTaskSource + + void IValueTaskSource.GetResult(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return; + } + + _flushSignal.GetResult(_flushSignal.Version); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + { + if (token != (short)Volatile.Read(ref _generation)) + { + return ValueTaskSourceStatus.Succeeded; + } + + return _flushSignal.GetStatus(_flushSignal.Version); + } + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (token != (short)Volatile.Read(ref _generation)) + { + continuation(state); + + return; + } + _flushSignal.OnCompleted(continuation, state, _flushSignal.Version, flags); + } + +#endregion +} \ No newline at end of file diff --git a/MinimaTPool/Connection/Connection.cs b/MinimaTPool/Connection/Connection.cs new file mode 100644 index 0000000..16eb291 --- /dev/null +++ b/MinimaTPool/Connection/Connection.cs @@ -0,0 +1,108 @@ +using System.Runtime.InteropServices; +using MinimaTPool.Utils; + +namespace MinimaTPool; + +public sealed unsafe partial class Connection +{ + private readonly Reactor _reactor; + + public int ClientFd { get; private set; } + + // Bumped on Clear(); the low 16 bits are used as the IVTS token so stale + // awaiters can be detected after pool reuse. + private int _generation; + + // Refcount: the connection has two owners — the reactor (recv side) and the + // handler (which may run off-reactor). Init to 2 on accept; each owner DecRef's + // when done; teardown (Recycle) runs only at refs==0, so a connection is never + // recycled or pool-reused while a handler is still in flight on another thread. + private int _refs; + + public Connection(Reactor reactor, int fd, int writeSlabSize = 1024 * 16) + { + _reactor = reactor; + ClientFd = fd; + _writeSlabSize = writeSlabSize; + WriteBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)writeSlabSize, 64); + + _manager = new UnmanagedMemoryManager(WriteBuffer, writeSlabSize); + } + + // ========================================================================= + // Pool lifecycle — invoked from Reactor.Dispatch's recv/send error paths. + // Reactor-thread only. + // + // teardown: MarkClosed() → wake awaiters with closed=1 + // DrainRecv() → return any in-flight buf_ring items + // close(fd) + // Clear() → reset state, bump _generation + // push to pool, OR Dispose() if pool is full + // ========================================================================= + + public void MarkClosed() + { + Volatile.Write(ref _closed, 1); + + if (Interlocked.Exchange(ref _armed, 0) == 1) + { + _readSignal.SetResult(new RecvSnapshot(_recv.SnapshotTail(), isClosed: true)); + } + else + { + Volatile.Write(ref _pending, 1); + } + + if (Interlocked.Exchange(ref _flushArmed, 0) == 1) + { + Volatile.Write(ref _flushInProgress, 0); + _flushSignal.SetResult(true); + } + } + + // Init to 2 (reactor + handler) at accept. + internal void InitRefs() => Volatile.Write(ref _refs, 2); + + // Release one owner's ref. Whoever drives it to 0 hands the connection to the + // reactor for teardown (close + Clear + pool) — never recycled before both done. + internal void DecRef() + { + if (Interlocked.Decrement(ref _refs) == 0) + { + _reactor.EnqueueRecycle(this); + } + } + + internal void Clear() + { + // Bump generation first — readers of IVTS plumbing observe this via + // Volatile.Read and stale tokens get RecvSnapshot.Closed() / no-op. + Interlocked.Increment(ref _generation); + + Volatile.Write(ref _armed, 0); + Volatile.Write(ref _pending, 0); + Volatile.Write(ref _closed, 0); + Volatile.Write(ref _flushArmed, 0); + Volatile.Write(ref _flushInProgress, 0); + + WriteHead = 0; + WriteTail = 0; + WriteInFlight = 0; + + _readSignal.Reset(); + _flushSignal.Reset(); + + _recv.Reset(); // discard any leftover SPSC items + IncrementalMode = false; // per-conn ring (if any) was torn down before Clear + } + + public void Dispose() + { + if (WriteBuffer != null) + { + NativeMemory.AlignedFree(WriteBuffer); + WriteBuffer = null; + } + DisposeIncremental(); + } +} \ No newline at end of file diff --git a/MinimaTPool/Connection/ConnectionDualPipe.cs b/MinimaTPool/Connection/ConnectionDualPipe.cs new file mode 100644 index 0000000..1bd16fd --- /dev/null +++ b/MinimaTPool/Connection/ConnectionDualPipe.cs @@ -0,0 +1,16 @@ +using System.IO.Pipelines; + +namespace MinimaTPool; + +public sealed class ConnectionDualPipe : IDuplexPipe +{ + public PipeReader Input { get; } + public PipeWriter Output { get; } + + public ConnectionDualPipe(Connection connection) + { + ArgumentNullException.ThrowIfNull(connection); + Input = new ConnectionPipeReader(connection); + Output = new ConnectionPipeWriter(connection); + } +} \ No newline at end of file diff --git a/MinimaTPool/Connection/ConnectionPipeReader.cs b/MinimaTPool/Connection/ConnectionPipeReader.cs new file mode 100644 index 0000000..8bdce9c --- /dev/null +++ b/MinimaTPool/Connection/ConnectionPipeReader.cs @@ -0,0 +1,181 @@ +using System.Buffers; +using System.IO.Pipelines; +using MinimaTPool.Utils; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +/// +/// Adapts Minima's raw read API (ReadAsync + TryGetItem +/// + ReturnBuffer) to a standard . Recv buffers are +/// exposed zero-copy as a ReadOnlySequence<byte> (one segment per buffer) +/// and held until AdvanceTo consumes them, at which point fully-consumed buffers +/// are returned to the reactor. +/// +/// Convenience/compat layer for PipeReader consumers — the raw ReadAsync/ +/// TryGetItem path stays the faster one (this adds held-buffer + sequence +/// bookkeeping per read). +/// +public sealed class ConnectionPipeReader : PipeReader +{ + private readonly Connection _conn; + private readonly List _held = new(16); + private ReadOnlySequence _lastSequence; + + private bool _completed; + private bool _cancelRequested; + private bool _connectionClosed; + + private readonly struct Held + { + public readonly ReadOnlyMemory Memory; + public readonly SpscRecvRing.Item Item; + + public Held(ReadOnlyMemory memory, SpscRecvRing.Item item) + { + Memory = memory; + Item = item; + } + + public Held WithMemory(ReadOnlyMemory memory) => new(memory, Item); + } + + public ConnectionPipeReader(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + ThrowIfCompleted(); + + if (_cancelRequested) + { + _cancelRequested = false; + return new ReadResult(BuildSequence(), isCanceled: true, isCompleted: _connectionClosed); + } + + // Anything still held from a previous read that wasn't fully consumed. + if (_held.Count > 0) + return new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + + if (_connectionClosed) + return new ReadResult(default, isCanceled: false, isCompleted: true); + + RecvSnapshot snap = await _conn.ReadAsync(); + + while (_conn.TryGetItem(snap, out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + _held.Add(new Held(item.AsMemoryManager().Memory, item)); + } + + _conn.ResetRead(); + + if (snap.IsClosed) + _connectionClosed = true; + + if (_cancelRequested) + { + _cancelRequested = false; + return new ReadResult(BuildSequence(), isCanceled: true, isCompleted: _connectionClosed); + } + + return new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + } + + public override bool TryRead(out ReadResult result) + { + ThrowIfCompleted(); + + if (_held.Count > 0) + { + result = new ReadResult(BuildSequence(), isCanceled: false, isCompleted: _connectionClosed); + return true; + } + + if (_connectionClosed) + { + result = new ReadResult(default, isCanceled: false, isCompleted: true); + return true; + } + + result = default; + return false; + } + + public override void AdvanceTo(SequencePosition consumed) => AdvanceTo(consumed, consumed); + + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + if (_held.Count == 0) + return; + + long consumedBytes = _lastSequence.Slice(0, consumed).Length; + + while (_held.Count > 0 && consumedBytes > 0) + { + Held seg = _held[0]; + int available = seg.Memory.Length; + + if (consumedBytes >= available) + { + // Whole buffer consumed — return it to the reactor. + _conn.ReturnBuffer(seg.Item); + _held.RemoveAt(0); + consumedBytes -= available; + } + else + { + // Partial — keep the unconsumed tail of this buffer. + _held[0] = seg.WithMemory(seg.Memory[(int)consumedBytes..]); + consumedBytes = 0; + } + } + } + + public override void CancelPendingRead() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) + { + if (_completed) + return; + + _completed = true; + + for (int i = 0; i < _held.Count; i++) + _conn.ReturnBuffer(_held[i].Item); + + _held.Clear(); + } + + private ReadOnlySequence BuildSequence() + { + if (_held.Count == 0) + { + _lastSequence = default; + return _lastSequence; + } + + if (_held.Count == 1) + { + _lastSequence = new ReadOnlySequence(_held[0].Memory); + return _lastSequence; + } + + var head = new RingSegment(_held[0].Memory, _held[0].Item.Bid); + RingSegment tail = head; + + for (int i = 1; i < _held.Count; i++) + tail = tail.Append(_held[i].Memory, _held[i].Item.Bid); + + _lastSequence = new ReadOnlySequence(head, 0, tail, tail.Memory.Length); + return _lastSequence; + } + + private void ThrowIfCompleted() + { + if (_completed) + throw new InvalidOperationException("Reading is not allowed after the reader was completed."); + } +} diff --git a/MinimaTPool/Connection/ConnectionPipeWriter.cs b/MinimaTPool/Connection/ConnectionPipeWriter.cs new file mode 100644 index 0000000..1598e2e --- /dev/null +++ b/MinimaTPool/Connection/ConnectionPipeWriter.cs @@ -0,0 +1,63 @@ +using System.IO.Pipelines; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +/// +/// Adapts Minima's write API (GetMemory/GetSpan/Advance/ +/// FlushAsync) to a standard , so PipeWriter-based code +/// can write responses through the connection's per-connection slab. +/// A thin wrapper — all the work lives in Connection. +/// +public sealed class ConnectionPipeWriter : PipeWriter +{ + private readonly Connection _conn; + private bool _completed; + private bool _cancelRequested; + private long _unflushed; + + public ConnectionPipeWriter(Connection connection) + { + _conn = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public override bool CanGetUnflushedBytes => true; + public override long UnflushedBytes => _unflushed; + + public override Memory GetMemory(int sizeHint = 0) => _conn.GetMemory(sizeHint); + + public override Span GetSpan(int sizeHint = 0) => _conn.GetSpan(sizeHint); + + public override void Advance(int bytes) + { + _unflushed += bytes; + _conn.Advance(bytes); + } + + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_cancelRequested) + { + _cancelRequested = false; + return new ValueTask(new FlushResult(isCanceled: true, isCompleted: _completed)); + } + + _unflushed = 0; + ValueTask inner = _conn.FlushAsync(); + + if (inner.IsCompletedSuccessfully) + return new ValueTask(new FlushResult(isCanceled: false, isCompleted: _completed)); + + return AwaitFlush(inner); + } + + private async ValueTask AwaitFlush(ValueTask inner) + { + await inner; + return new FlushResult(isCanceled: false, isCompleted: _completed); + } + + public override void CancelPendingFlush() => _cancelRequested = true; + + public override void Complete(Exception? exception = null) => _completed = true; +} diff --git a/MinimaTPool/Connection/RecvSnapshot.cs b/MinimaTPool/Connection/RecvSnapshot.cs new file mode 100644 index 0000000..58cf47c --- /dev/null +++ b/MinimaTPool/Connection/RecvSnapshot.cs @@ -0,0 +1,15 @@ +namespace MinimaTPool; + +public readonly struct RecvSnapshot +{ + public readonly long Tail; + public readonly bool IsClosed; + + public RecvSnapshot(long tail, bool isClosed) + { + Tail = tail; + IsClosed = isClosed; + } + + public static RecvSnapshot Closed() => new(0, isClosed: true); +} \ No newline at end of file diff --git a/MinimaTPool/MinimaTPool.csproj b/MinimaTPool/MinimaTPool.csproj new file mode 100644 index 0000000..1ec9d30 --- /dev/null +++ b/MinimaTPool/MinimaTPool.csproj @@ -0,0 +1,12 @@ + + + + Exe + net10.0 + enable + enable + true + MinimaTPool + + + diff --git a/MinimaTPool/Program.cs b/MinimaTPool/Program.cs new file mode 100644 index 0000000..5b44b1a --- /dev/null +++ b/MinimaTPool/Program.cs @@ -0,0 +1,178 @@ +using System.Buffers; +using System.IO.Pipelines; +using System.Text.Json; +using MinimaTPool.Utils; + +namespace MinimaTPool; + +/// +/// Multi-reactor HTTP/1.1 server using io_uring directly. Spawns N reactor +/// threads (one per CPU); each opens its own SO_REUSEPORT listener, runs its +/// own io_uring, owns its own connection map. The kernel load-balances new +/// connections across reactors. Per-connection state never crosses threads, +/// so no synchronization is needed on the hot path. +/// +internal static unsafe class Program +{ + internal static ReadOnlySpan Response => + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok"u8; + + private static int Main() + { + // All tunables live in ServerConfig — override the defaults here. + var config = new ServerConfig() + { + UsePipe = false, + ReactorCount = 10 + }; + + Console.WriteLine($"[Minima] starting {config.ReactorCount} reactors on port {config.Port} (incremental={config.Incremental})"); + + var threads = new Thread[config.ReactorCount]; + for (var i = 0; i < config.ReactorCount; i++) + { + var reactor = new Reactor(i, config); + + threads[i] = new Thread(reactor.Run) + { + Name = $"reactor-{i}", + IsBackground = false + }; + threads[i].Start(); + } + + foreach (var t in threads) + { + t.Join(); + } + + return 0; + } +} + +internal static class Handler +{ + // Real async-work knob: serialize an in-memory object of WORK_ITEMS elements to JSON + // on the THREAD POOL (via Task.Run) per request. 0 / unset = disabled (pure inline + // reactor path). Genuine CPU + allocation, not a busy-spin. + private static readonly int WorkItems = 1000; + + private static readonly Payload LargeObject = BuildPayload(Math.Max(WorkItems, 1)); + + private static Payload BuildPayload(int count) + { + var items = new Item[count]; + for (int i = 0; i < count; i++) + { + items[i] = new Item(i, $"item-{i}", i * 1.5, (i & 1) == 0, $"category-{i % 8}"); + } + return new Payload(DateTime.UtcNow.ToString("O"), count, items); + } + + public static async Task HandleAsync(Reactor reactor, Connection conn) + { + try + { + while (true) + { + RecvSnapshot snap = await conn.ReadAsync(); + + while (conn.TryGetItem(snap, out SpscRecvRing.Item item)) + { + if (item.HasBuffer) + { + UnmanagedMemoryManager mem = item.AsMemoryManager(); + ReadOnlyMemory data = mem.Memory; + // data is now usable with any BCL Memory/async API + _ = data.Length; + + // Cross-thread safe and mode-agnostic: routes to the + // shared-ring return or the incremental refcounted return. + conn.ReturnBuffer(in item); + } + } + + _ = await Task.Run(static () => JsonSerializer.Serialize("Hello World!")); + + // Real async work: serialize a large object to JSON on the THREAD POOL. + // The handler resumes OFF-REACTOR, so the FlushAsync below pays the eventfd + // handoff the pure-inline path avoids — and the serialization is genuine + // CPU + GC pressure on the pool, not a busy-spin. + /*if (WorkItems > 0) + { + _ = await Task.Run(static () => JsonSerializer.SerializeToUtf8Bytes(LargeObject)); + //JsonSerializer.SerializeToUtf8Bytes(LargeObject); + }*/ + + // One response per recv burst — accumulate in the connection's + // per-connection write slab, then submit and await ack. + conn.Write(Program.Response); + await conn.FlushAsync(); + + if (snap.IsClosed) + { + // Reactor already owns teardown (Connections.Remove + close + // happens in Dispatch's recv-error branch); we just exit. + return; + } + + conn.ResetRead(); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[r{reactor.Id}] handler crash on fd={conn.ClientFd}: {ex}"); + // Reactor will clean the connection up via the recv-error path + // (or SPSC overflow) on the next CQE for this fd. + } + finally + { + conn.DecRef(); // release the handler's ref; teardown runs once the reactor releases too + } + } + + // PipeReader/PipeWriter variant — same behavior, driven through the BCL + // pipe adapters instead of the raw ReadAsync/TryGetItem/Write API. + public static async Task HandlePipeAsync(Reactor reactor, Connection conn) + { + var reader = new ConnectionPipeReader(conn); + var writer = new ConnectionPipeWriter(conn); + + try + { + while (true) + { + ReadResult read = await reader.ReadAsync(); + ReadOnlySequence buffer = read.Buffer; + + if (!buffer.IsEmpty) + { + // A real server would parse requests out of `buffer` here. + writer.Write(Program.Response); + await writer.FlushAsync(); + } + + // Consume everything we got; AdvanceTo returns the recv buffers. + reader.AdvanceTo(buffer.End); + + if (read.IsCompleted) + { + break; + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[r{reactor.Id}] pipe handler crash on fd={conn.ClientFd}: {ex}"); + } + finally + { + reader.Complete(); + writer.Complete(); + conn.DecRef(); + } + } +} + +internal sealed record Item(int Id, string Name, double Value, bool Active, string Category); +internal sealed record Payload(string Generated, int Count, Item[] Items); diff --git a/MinimaTPool/Reactor/Reactor.Incremental.cs b/MinimaTPool/Reactor/Reactor.Incremental.cs new file mode 100644 index 0000000..b0dfbe2 --- /dev/null +++ b/MinimaTPool/Reactor/Reactor.Incremental.cs @@ -0,0 +1,306 @@ +using System.Runtime.InteropServices; +using MinimaTPool.Utils; +using static MinimaTPool.Native; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +/// +/// Incremental-buffer (IOU_PBUF_RING_INC) path. Each connection gets its own +/// buffer ring: one buffer accumulates that connection's byte stream across many +/// recvs, so buffers are recycled only when the kernel is done appending AND the +/// handler has returned every slice it was handed. Selected per reactor by the +/// `_incremental` flag; the shared-ring path in Reactor.cs is untouched. +/// +public sealed unsafe partial class Reactor +{ + private Stack? _freeGids; + private Mpsc? _returnQInc; + + private void InitIncremental() + { + // Per-connection rings; no shared ring. GID 1 reserved; per-conn GIDs 2..MaxConnections+1. + _freeGids = new Stack(MaxConnections); + for (int g = MaxConnections + 1; g >= 2; g--) + _freeGids.Push((ushort)g); + + _returnQInc = new Mpsc(1 << 16); + } + + private ushort AllocGid() => _freeGids!.Pop(); + private void FreeGid(ushort gid) => _freeGids!.Push(gid); + + // ========================================================================= + // Per-connection ring lifecycle + // ========================================================================= + + private void SetupConnectionBufRing(Connection conn) + { + ushort gid = AllocGid(); + int entries = ConnBufRingEntries; + + // Ring control area + slab + tracking arrays are allocated once and + // reused across pool lives; only the kernel registration is per-life. + if (conn.BufRing == null) + conn.BufRing = (byte*)NativeMemory.AlignedAlloc((nuint)entries * 16, 4096); + NativeMemory.Clear(conn.BufRing, (nuint)entries * 16); + + if (conn.BufSlab == null) + conn.BufSlab = (byte*)NativeMemory.AlignedAlloc((nuint)entries * (nuint)IncRecvBufferSize, 64); + + conn.CumOffset ??= new int[entries]; + conn.RefCount ??= new int[entries]; + conn.KernelDone ??= new bool[entries]; + Array.Clear(conn.CumOffset, 0, entries); + Array.Clear(conn.RefCount, 0, entries); + Array.Clear(conn.KernelDone, 0, entries); + + var reg = new io_uring_buf_reg + { + ring_addr = (ulong)conn.BufRing, + ring_entries = (uint)entries, + bgid = gid, + flags = IOU_PBUF_RING_INC, + }; + int ret = io_uring_register(Ring.Fd, IORING_REGISTER_PBUF_RING, ®, 1); + if (ret < 0) + throw new InvalidOperationException($"register pbuf_ring (inc) failed: ret={ret} gid={gid}"); + + conn.Bgid = gid; + conn.BufRingEntries = entries; + conn.BufRingMask = (uint)(entries - 1); + conn.IncrementalMode = true; + + for (ushort bid = 0; bid < entries; bid++) + { + byte* slot = conn.BufRing + (uint)bid * 16; + *(ulong*)(slot + 0) = (ulong)(conn.BufSlab + bid * (nuint)IncRecvBufferSize); + *(uint*)(slot + 8) = IncRecvBufferSize; + *(ushort*)(slot + 12) = bid; + } + Volatile.Write(ref *(ushort*)(conn.BufRing + 14), (ushort)entries); + } + + private void TeardownConnectionBufRing(Connection conn) + { + if (conn.IncrementalMode) + { + var reg = new io_uring_buf_reg { bgid = conn.Bgid }; + io_uring_register(Ring.Fd, IORING_UNREGISTER_PBUF_RING, ®, 1); + FreeGid(conn.Bgid); + } + // BufRing / BufSlab / arrays stay allocated for pool reuse. + } + + // Re-add a fully-consumed buffer to its connection's ring (reactor-thread only). + private void ReturnConnectionBuffer(Connection conn, ushort bid) + { + conn.CumOffset![bid] = 0; + conn.RefCount![bid] = 0; + conn.KernelDone![bid] = false; + + ushort tail = Volatile.Read(ref *(ushort*)(conn.BufRing + 14)); + byte* slot = conn.BufRing + (tail & conn.BufRingMask) * 16; + *(ulong*)(slot + 0) = (ulong)(conn.BufSlab + bid * (nuint)IncRecvBufferSize); + *(uint*)(slot + 8) = IncRecvBufferSize; + *(ushort*)(slot + 12) = bid; + Volatile.Write(ref *(ushort*)(conn.BufRing + 14), (ushort)(tail + 1)); + } + + // ========================================================================= + // Refcounted return path (handler → reactor), carrying (fd, gen, bid) + // ========================================================================= + + // (fd, gen, bid) packed into one ulong for the incremental return queue: + // fd in the high 32 bits, gen in the next 16, bid in the low 16. + private static ulong PackReturn(int fd, ushort gen, ushort bid) + => ((ulong)(uint)fd << 32) | ((ulong)gen << 16) | bid; + + private static void UnpackReturn(ulong packed, out int fd, out ushort gen, out ushort bid) + { + fd = (int)(packed >> 32); + gen = (ushort)((packed >> 16) & 0xFFFF); + bid = (ushort)(packed & 0xFFFF); + } + + public void EnqueueReturnQIncremental(int fd, ushort gen, ushort bid) + { + // Fast path: caller is the reactor thread (handler resumed inline). + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + ApplyReturnIncremental(fd, gen, bid); + return; + } + ulong packed = PackReturn(fd, gen, bid); + SpinWait sw = default; + while (!_returnQInc!.TryEnqueue(packed)) + sw.SpinOnce(); + WakeFdWrite(); + } + + private void DrainReturnQIncremental() + { + while (_returnQInc!.TryDequeue(out ulong packed)) + { + UnpackReturn(packed, out int fd, out ushort gen, out ushort bid); + ApplyReturnIncremental(fd, gen, bid); + } + } + + private void ApplyReturnIncremental(int fd, ushort gen, ushort bid) + { + if (!Connections.TryGetValue(fd, out var conn) || !conn.IncrementalMode) + { + return; // fd gone / ring already torn down + } + if ((ushort)conn.Generation != gen) + { + return; // stale return from a previous life (fd reused) + } + + conn.RefCount![bid]--; + if (conn.RefCount[bid] <= 0 && conn.KernelDone![bid]) + { + ReturnConnectionBuffer(conn, bid); + } + } + + // ========================================================================= + // Incremental reactor loop + // ========================================================================= + + private void LoopIncremental() + { + while (true) + { + DrainReturnQIncremental(); + DrainFlushQ(); + DrainRecycleQ(); + + int rc = Ring.SubmitAndWait(1); + if (rc < 0 && rc != -EINTR && rc != -EAGAIN && rc != -EBUSY) + { + Console.Error.WriteLine($"[r{Id}] io_uring_enter failed: {rc}"); + + break; + } + + uint ready = Ring.CqReady(); + for (uint i = 0; i < ready; i++) + { + DispatchIncremental(in Ring.CqeAt(i)); + } + Ring.CqAdvance(ready); + } + } + + private void DispatchIncremental(in IoUringCqe cqe) + { + ulong kind = cqe.user_data & 0xffffffff_00000000UL; + int fd = (int)(cqe.user_data & 0xffffffffUL); + bool more = (cqe.flags & IORING_CQE_F_MORE) != 0; + + if (kind == KindWake) + { + ulong drain; + read(_wakeFd, &drain, 8); + if (!more) + { + ArmWakePoll(); + } + return; + } + + if (kind == KindAccept) + { + if (cqe.res >= 0) + { + int clientFd = cqe.res; + SetNoDelay(clientFd); + Connection conn = _pool.TryPop(out var pooled) + ? pooled.SetFd(clientFd) + : new Connection(this, clientFd, _config.WriteSlabSize); + Connections[clientFd] = conn; + conn.InitRefs(); + SetupConnectionBufRing(conn); + SubmitRecvMultishot(clientFd, conn.Bgid); + + _ = _config.UsePipe + ? Handler.HandlePipeAsync(this, conn) + : Handler.HandleAsync(this, conn); + } + else + { + Console.Error.WriteLine($"[r{Id}] accept error: {cqe.res}"); + } + if (!more) + { + SubmitAcceptMultishot(); + } + } + else if (kind == KindRecv) + { + bool hasBuf = (cqe.flags & IORING_CQE_F_BUFFER) != 0; + bool bufMore = (cqe.flags & IORING_CQE_F_BUF_MORE) != 0; + ushort bid = hasBuf ? (ushort)(cqe.flags >> IORING_CQE_BUFFER_SHIFT) : (ushort)0; + + if (cqe.res <= 0) + { + // Peer EOF / recv error — the whole per-conn ring is freed in Recycle. + if (Connections.Remove(fd, out var dyingConn)) + { + dyingConn.MarkClosed(); + dyingConn.DecRef(); + } + + return; + } + + if (!Connections.TryGetValue(fd, out var conn)) + { + return; // straggler for a connection whose ring is already gone + } + + // Data lands at the buffer's running offset; the kernel keeps + // appending to this bid until the buffer is full (F_BUF_MORE clear). + byte* ptr = conn.BufSlab + (nuint)bid * (nuint)IncRecvBufferSize + (nuint)conn.CumOffset![bid]; + conn.CumOffset[bid] += cqe.res; + conn.RefCount![bid]++; + if (!bufMore || !more) + { + conn.KernelDone![bid] = true; + } + + conn.Complete(cqe.res, bid, hasBuffer: true, ptr); + + if (!more) + { + SubmitRecvMultishot(fd, conn.Bgid); + } + } + else if (kind == KindSend) + { + if (!Connections.TryGetValue(fd, out var conn)) + { + return; + } + if (cqe.res <= 0) + { + Connections.Remove(fd); + conn.MarkClosed(); + conn.DecRef(); + + return; + } + conn.WriteHead += cqe.res; + if (conn.WriteHead < conn.WriteInFlight) + { + SubmitSend(fd, conn.WriteBuffer + conn.WriteHead, (uint)(conn.WriteInFlight - conn.WriteHead)); + + return; + } + + conn.CompleteFlush(); + } + } +} diff --git a/MinimaTPool/Reactor/Reactor.cs b/MinimaTPool/Reactor/Reactor.cs new file mode 100644 index 0000000..a392198 --- /dev/null +++ b/MinimaTPool/Reactor/Reactor.cs @@ -0,0 +1,564 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using MinimaTPool.Utils; +using static MinimaTPool.Native; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool; + +/// +/// One reactor = one thread + one io_uring + one listening socket (SO_REUSEPORT) +/// + one connection map. The reactor thread is the sole writer of the SQ ring, +/// the kernel-shared buf_ring, and the connection map. Handlers may run on any +/// thread (e.g. resumed by a thread-pool timer after `await Task.Delay(1)`); +/// they reach the reactor only through two MPSC queues (`_returnQ`, `_flushQ`) +/// woken by an `eventfd` registered as a multishot poll in the ring. +/// +public sealed unsafe partial class Reactor +{ + public readonly int Id; + public Ring Ring = null!; // created on the reactor's own thread (DEFER_TASKRUN requires same-thread setup+enter) + public readonly Dictionary Connections = new(); + + private int _listenFd; + private readonly ServerConfig _config; + private readonly ushort _port; + private readonly uint _ringEntries; + private readonly bool _incremental; + private readonly uint RecvBufferSize; + + // CQE user_data layout: kind tag in the high 32 bits, fd in the low 32. + private const ulong KindAccept = 1UL << 32; + private const ulong KindRecv = 2UL << 32; + private const ulong KindSend = 3UL << 32; + private const ulong KindWake = 4UL << 32; // eventfd-based cross-thread wake + + // Provided-buffer ring (one per reactor, shared by all its connections). + private const ushort BgId = 1; + private readonly uint BufferRingEntries; // power of two + private byte* _bufRing; // io_uring_buf_ring (kernel-shared) + private byte* _bufSlab; // contiguous slab of recv buffers + private uint _bufRingMask; + private ushort _bufRingTail; + + // Cross-thread wake mechanism: handlers running off-reactor enqueue work + // into these MPSC queues and `eventfd_write` _wakeFd; a multishot poll on + // _wakeFd registered with the ring delivers a CQE that wakes the reactor. + // When the caller is already the reactor thread (the common case — handler + // resumed inline from an IVTS SetResult), the Enqueue* methods bypass + // the queue and call the direct op, avoiding 2 syscalls per request. + private int _wakeFd; + private int _reactorThreadId; + private readonly Mpsc _returnQ = new(1 << 14); // 16384 slots + private readonly Mpsc _flushQ = new(1 << 12); // 4096 slots + + // Teardown handoff: when a connection's refcount hits 0 off-reactor (handler exited + // on the thread pool), the recycle must run on the reactor (it touches the buf_ring + // and the reactor-only pool). Connection is a ref type, so this is a ConcurrentQueue + // rather than the unmanaged Mpsc. + private readonly ConcurrentQueue _recycleQ = new(); + + // Connection pool. Reactor-thread-only — accept and teardown both run on + // this reactor, so a plain Stack is sufficient (no MPMC primitive + // needed). PoolMax caps the slab footprint per reactor: + // PoolMax × WriteSlabSize × ReactorCount = total reserved native memory. + private readonly int PoolMax; + private readonly Stack _pool; + + // Incremental-mode (IOU_PBUF_RING_INC) sizing. Each connection gets its own + // ring, so reserved native memory is bounded by: + // PoolMax × ConnBufRingEntries × IncRecvBufferSize × ReactorCount. + // Keep entries small — the point of incremental is that one buffer holds + // many reads, so you need few of them per connection. + private readonly int MaxConnections; // GID cap (one bgid per active connection) + private readonly int ConnBufRingEntries; // buffers per connection ring + private readonly uint IncRecvBufferSize; // bytes per buffer (filled incrementally) + + // Transient io_uring_enter errnos (Linux): interrupted, would-block, busy. + private const int EINTR = 4; + private const int EAGAIN = 11; + private const int EBUSY = 16; + + public Reactor(int id, ServerConfig config) + { + Id = id; + _config = config; + _port = config.Port; + _ringEntries = config.RingEntries; + _incremental = config.Incremental; + RecvBufferSize = (uint)config.RecvBufferSize; + BufferRingEntries = (uint)config.BufferRingEntries; + PoolMax = config.PoolMax; + MaxConnections = config.MaxConnections; + ConnBufRingEntries = config.ConnBufRingEntries; + IncRecvBufferSize = (uint)config.IncRecvBufferSize; + _pool = new Stack(config.PoolMax); + } + + // ========================================================================= + // Buffer ring + // ========================================================================= + + private void InitBufferRing() + { + nuint ringBytes = (nuint)BufferRingEntries * 16; + _bufRing = (byte*)NativeMemory.AlignedAlloc(ringBytes, 4096); + NativeMemory.Clear(_bufRing, ringBytes); + + nuint slabBytes = BufferRingEntries * (nuint)RecvBufferSize; + _bufSlab = (byte*)NativeMemory.AlignedAlloc(slabBytes, 64); + + _bufRingMask = BufferRingEntries - 1; + + var reg = new io_uring_buf_reg { + ring_addr = (ulong)_bufRing, + ring_entries = BufferRingEntries, + bgid = BgId, + }; + + int ret = io_uring_register(Ring.Fd, IORING_REGISTER_PBUF_RING, ®, 1); + if (ret < 0) + { + int err = Marshal.GetLastPInvokeError(); + + throw new InvalidOperationException($"register pbuf_ring failed: ret={ret} errno={err}"); + } + + // Populate every slot once. Slot 0 overlaps with the ring's tail field + // at offset 14, but we only write addr/len/bid (offsets 0..13) so tail + // stays at zero until we set it explicitly. + for (ushort bid = 0; bid < BufferRingEntries; bid++) { + byte* slot = _bufRing + (uint)bid * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + } + _bufRingTail = (ushort)BufferRingEntries; + + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + // Reactor-thread-only: writes the kernel-shared buf_ring tail directly. + // Off-reactor callers must use EnqueueReturnQ instead. + internal void ReturnBufferDirect(ushort bid) + { + byte* slot = _bufRing + (_bufRingTail & _bufRingMask) * 16; + *(ulong*)(slot + 0) = (ulong)(_bufSlab + bid * (nuint)RecvBufferSize); + *(uint*)(slot + 8) = RecvBufferSize; + *(ushort*)(slot + 12) = bid; + _bufRingTail++; + + Volatile.Write(ref *(ushort*)(_bufRing + 14), _bufRingTail); + } + + // ========================================================================= + // Cross-thread entry points (safe to call from any thread) + // ========================================================================= + + public void EnqueueReturnQ(ushort bid) + { + // Fast path: caller is the reactor thread (handler running inline from + // an IVTS SetResult). Go straight to the buf_ring — no queue, no syscall. + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + ReturnBufferDirect(bid); + return; + } + SpinWait sw = default; + while (!_returnQ.TryEnqueue(bid)) + { + sw.SpinOnce(); + } + //WakeFdWrite(); + } + + internal void EnqueueFlush(int fd) + { + // Fast path: caller is the reactor thread; write the SQE directly. + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + if (Connections.TryGetValue(fd, out var conn)) + { + SubmitSend(fd, conn.WriteBuffer, (uint)conn.WriteInFlight); + } + return; + } + SpinWait sw = default; + while (!_flushQ.TryEnqueue(fd)) + { + sw.SpinOnce(); + } + WakeFdWrite(); + } + + private void WakeFdWrite() + { + ulong v = 1; + // 8-byte write to eventfd increments its counter; the kernel marks the + // fd readable, which fires our registered multishot poll's next CQE. + write(_wakeFd, &v, 8); + } + + private void DrainReturnQ() + { + while (_returnQ.TryDequeue(out ushort bid)) + { + ReturnBufferDirect(bid); + } + } + + private void DrainFlushQ() + { + while (_flushQ.TryDequeue(out int fd)) + { + if (!Connections.TryGetValue(fd, out var conn)) + { + continue; + } + // Connection state was set by FlushAsync; the Enqueue/Dequeue pair + // establishes the happens-before so WriteInFlight is visible here. + SubmitSend(fd, conn.WriteBuffer, (uint)conn.WriteInFlight); + } + } + + // Called by Connection.DecRef when the refcount hits 0. Teardown must run on the + // reactor (buf_ring + pool are reactor-owned), so off-reactor callers hand off. + internal void EnqueueRecycle(Connection conn) + { + if (Environment.CurrentManagedThreadId == _reactorThreadId) + { + Recycle(conn, conn.ClientFd); + return; + } + _recycleQ.Enqueue(conn); + WakeFdWrite(); + } + + private void DrainRecycleQ() + { + while (_recycleQ.TryDequeue(out Connection? conn)) + { + Recycle(conn, conn.ClientFd); + } + } + + private void ArmWakePoll() + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_POLL_ADD; + sqe->fd = _wakeFd; + sqe->op_flags = POLLIN; // poll32_events lives at this offset + sqe->len = IORING_POLL_ADD_MULTI; // multishot — stays armed across CQEs + sqe->user_data = KindWake | (uint)_wakeFd; + } + + // ========================================================================= + // Main loop + // ========================================================================= + + public void Run() + { + _reactorThreadId = Environment.CurrentManagedThreadId; + + Ring = Ring.Create(_ringEntries); + _listenFd = OpenReusePortListener(_port); + + if (_incremental) + { + InitIncremental(); + } + else + { + InitBufferRing(); + } + + _wakeFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (_wakeFd < 0) + { + throw new InvalidOperationException("eventfd failed"); + } + + Console.WriteLine($"[r{Id}] listening on 0.0.0.0:{_port} (incremental={_incremental})"); + SubmitAcceptMultishot(); + ArmWakePoll(); + + if (_incremental) + { + LoopIncremental(); + } + else + { + LoopShared(); + } + + close(_listenFd); + close(_wakeFd); + Ring.Dispose(); + } + + private void LoopShared() + { + while (true) + { + // Drain MPSC queues from off-reactor handlers. Cheap when empty. + DrainReturnQ(); + DrainFlushQ(); + DrainRecycleQ(); + + int rc = Ring.SubmitAndWait(1); + if (rc < 0 && rc != -EINTR && rc != -EAGAIN && rc != -EBUSY) + { + Console.Error.WriteLine($"[r{Id}] io_uring_enter failed: {rc}"); + break; + } + + uint ready = Ring.CqReady(); + for (uint i = 0; i < ready; i++) + { + Dispatch(in Ring.CqeAt(i)); + } + Ring.CqAdvance(ready); + } + } + + private void Dispatch(in IoUringCqe cqe) + { + ulong kind = cqe.user_data & 0xffffffff_00000000UL; + int fd = (int)(cqe.user_data & 0xffffffffUL); + bool more = (cqe.flags & IORING_CQE_F_MORE) != 0; + + if (kind == KindWake) + { + // Drain the eventfd counter so the next write re-triggers POLLIN + // (multishot poll is edge-triggered on the user_space side). + ulong drain; + read(_wakeFd, &drain, 8); + // The actual queue drains happen at the top of the next loop + // iteration — nothing else to do here. + if (!more) + { + ArmWakePoll(); + } + return; + } + + if (kind == KindAccept) + { + if (cqe.res >= 0) + { + int clientFd = cqe.res; + SetNoDelay(clientFd); + Connection conn = _pool.TryPop(out var pooled) + ? pooled.SetFd(clientFd) + : new Connection(this, clientFd, _config.WriteSlabSize); + Connections[clientFd] = conn; + conn.InitRefs(); + SubmitRecvMultishot(clientFd); + + _ = _config.UsePipe + ? Handler.HandlePipeAsync(this, conn) + : Handler.HandleAsync(this, conn); + } + else + { + Console.Error.WriteLine($"[r{Id}] accept error: {cqe.res}"); + } + // Multishot accept stays armed; only re-arm if the kernel terminated it. + if (!more) + { + SubmitAcceptMultishot(); + } + } + else if (kind == KindRecv) + { + bool hasBuf = (cqe.flags & IORING_CQE_F_BUFFER) != 0; + ushort bid = hasBuf ? (ushort)(cqe.flags >> IORING_CQE_BUFFER_SHIFT) : (ushort)0; + + if (cqe.res <= 0) + { + // Peer EOF or recv error — reactor owns teardown. + if (hasBuf) + { + ReturnBufferDirect(bid); + } + if (Connections.Remove(fd, out var dyingConn)) + { + dyingConn.MarkClosed(); // signal the handler to exit + dyingConn.DecRef(); // release the reactor's ref; teardown at refs==0 + } + return; + } + + if (!Connections.TryGetValue(fd, out var conn)) + { + // Straggler buffer for an already-closed connection. + if (hasBuf) + { + ReturnBufferDirect(bid); + } + return; + } + + byte* ptr = hasBuf ? _bufSlab + (nuint)bid * (nuint)RecvBufferSize : null; + conn.Complete(cqe.res, bid, hasBuf, ptr); + + if (!more) + { + SubmitRecvMultishot(fd); + } + } + else if (kind == KindSend) + { + if (!Connections.TryGetValue(fd, out var conn)) + { + return; + } + if (cqe.res <= 0) + { + // Send error — release the reactor's ref; teardown when the handler exits too. + Connections.Remove(fd); + conn.MarkClosed(); + conn.DecRef(); + return; + } + conn.WriteHead += cqe.res; + if (conn.WriteHead < conn.WriteInFlight) + { + // Partial send: resubmit the remainder. + SubmitSend(fd, conn.WriteBuffer + conn.WriteHead, (uint)(conn.WriteInFlight - conn.WriteHead)); + return; + } + // Full target ack'd — resets buffer state and signals the awaiter. + conn.CompleteFlush(); + } + } + + // ========================================================================= + // SQE producers (reactor-thread-only — Connection.FlushAsync hands off via + // EnqueueFlush, which DrainFlushQ turns into SubmitSend on this thread) + // ========================================================================= + + private IoUringSqe* GetSqeOrFlush() + { + IoUringSqe* sqe = Ring.GetSqe(); + if (sqe != null) + { + return sqe; + } + + Ring.SubmitAndWait(0); + sqe = Ring.GetSqe(); + + if (sqe == null) + { + throw new InvalidOperationException("SQ full after flush"); + } + + return sqe; + } + + private void SubmitAcceptMultishot() + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_ACCEPT; + sqe->ioprio = IORING_ACCEPT_MULTISHOT; + sqe->fd = _listenFd; + sqe->user_data = KindAccept | (uint)_listenFd; + } + + private void SubmitRecvMultishot(int fd) => SubmitRecvMultishot(fd, BgId); + + private void SubmitRecvMultishot(int fd, ushort bgid) + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_RECV; + sqe->flags = IOSQE_BUFFER_SELECT; + sqe->ioprio = IORING_RECV_MULTISHOT; + sqe->fd = fd; + sqe->buf_index = bgid; // buffer-group id (shared BgId, or per-conn in incremental) + sqe->user_data = KindRecv | (uint)fd; + } + + private void SubmitSend(int fd, byte* buf, uint len) + { + IoUringSqe* sqe = GetSqeOrFlush(); + Unsafe.InitBlockUnaligned(sqe, 0, 64); + sqe->opcode = IORING_OP_SEND; + sqe->fd = fd; + sqe->addr = (ulong)buf; + sqe->len = len; + sqe->user_data = KindSend | (uint)fd; + } + + private void Recycle(Connection conn, int fd) + { + // Wake awaiters, drain in-flight buffers, close the fd, reset state, + // and either push the Connection back to the pool or free its native + // WriteBuffer if the pool is full. + conn.MarkClosed(); + if (_incremental) + { + // The per-connection ring is freed wholesale; no per-buffer return. + // Clear() empties the SPSC ring (leftover slices discarded). + TeardownConnectionBufRing(conn); + } + else + { + conn.DrainRecv(); // return leftover buffers to the shared ring + } + close(fd); + conn.Clear(); + + if (_pool.Count < PoolMax) + { + _pool.Push(conn); + } + else + { + conn.Dispose(); + } + } + + // Disable Nagle on an accepted connection. Must be set per-accepted-socket, + // not on the listener — TCP_NODELAY doesn't reliably inherit across accept, + // which is why zerg/terraform/rtr all set it on the client fd, not the listener. + private static void SetNoDelay(int fd) + { + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(int)); + } + + private static int OpenReusePortListener(ushort port) + { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + throw new InvalidOperationException($"socket failed: {fd}"); + } + + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)); + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(int)); + + sockaddr_in addr = default; + addr.sin_family = AF_INET; + addr.sin_port = Htons(port); + addr.sin_addr.s_addr = 0; // 0.0.0.0 + + if (bind(fd, &addr, (uint)sizeof(sockaddr_in)) < 0) + { + throw new InvalidOperationException("bind failed"); + } + + if (listen(fd, 128) < 0) + { + throw new InvalidOperationException("listen failed"); + } + + return fd; + } +} diff --git a/MinimaTPool/ServerConfig.cs b/MinimaTPool/ServerConfig.cs new file mode 100644 index 0000000..d6dd3db --- /dev/null +++ b/MinimaTPool/ServerConfig.cs @@ -0,0 +1,35 @@ +namespace MinimaTPool; + +/// +/// All server tunables in one place — replaces the consts that used to be +/// scattered across Program.cs and Reactor.cs. Defaults match the previous +/// hardcoded values; override via object initializer in Main, e.g.: +/// new ServerConfig { Port = 9000, ReactorCount = 8, Incremental = true }. +/// +public sealed record ServerConfig +{ + // Server-level. + public ushort Port { get; init; } = 8080; + public int ReactorCount { get; init; } = 12; + + // Handler style: false = raw ReadAsync/TryGetItem loop; true = PipeReader/PipeWriter. + public bool UsePipe { get; init; } = false; + + // io_uring SQ/CQ depth. + public uint RingEntries { get; init; } = 8192; + + // Shared buffer ring (used when Incremental == false). + public int RecvBufferSize { get; init; } = 32 * 1024; + public int BufferRingEntries { get; init; } = 4096; + + // Per-connection write slab + connection pool cap. + public int WriteSlabSize { get; init; } = 16 * 1024; + public int PoolMax { get; init; } = 1024; + + // Incremental mode (IOU_PBUF_RING_INC) — per-connection rings. + // reserved native memory ≈ PoolMax × ConnBufRingEntries × IncRecvBufferSize × ReactorCount. + public bool Incremental { get; init; } = false; + public int MaxConnections { get; init; } = 4096; // GID cap (one bgid per active connection) + public int ConnBufRingEntries { get; init; } = 16; // buffers per connection ring + public int IncRecvBufferSize { get; init; } = 4096; // bytes per buffer (filled incrementally) +} diff --git a/MinimaTPool/Utils/Mpsc.cs b/MinimaTPool/Utils/Mpsc.cs new file mode 100644 index 0000000..62dbeab --- /dev/null +++ b/MinimaTPool/Utils/Mpsc.cs @@ -0,0 +1,115 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool.Utils; + +/// +/// Bounded lock-free multi-producer / single-consumer queue. +/// +/// Dmitry Vyukov's bounded MPMC algorithm, specialised to one consumer. +/// Power-of-two capacity, zero-allocation after construction. Producers claim a +/// slot via CAS on the enqueue position (a failed TryEnqueue on a full queue +/// leaves the position untouched — no burned tickets); the single consumer +/// advances the dequeue position with a plain write. Each slot carries a +/// sequence number that coordinates ownership between producers and consumer. +/// +/// One generic queue serves every reactor handoff: Mpsc<ushort> for buffer +/// returns, Mpsc<int> for flush fds, Mpsc<ulong> for packed incremental +/// returns. T is unmanaged so each Cell is a blittable value type with no GC refs. +/// +internal sealed class Mpsc where T : unmanaged +{ + private struct Cell + { + public long Sequence; + public T Value; + } + + private readonly Cell[] _buffer; + private readonly int _mask; + + // PaddedLong is a top-level struct (not nested here) because the CLR forbids + // explicit layout on a type nested inside a generic. + private PaddedLong _enqueuePos; + private PaddedLong _dequeuePos; + + public Mpsc(int capacityPow2) + { + if (capacityPow2 < 2 || (capacityPow2 & (capacityPow2 - 1)) != 0) + throw new ArgumentException("Capacity must be a power of two >= 2.", nameof(capacityPow2)); + + _buffer = new Cell[capacityPow2]; + _mask = capacityPow2 - 1; + + for (int i = 0; i < capacityPow2; i++) + _buffer[i].Sequence = i; + } + + /// Multi-producer safe. Returns false if the queue is full. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + while (true) + { + long pos = Volatile.Read(ref _enqueuePos.Value); + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - pos; + + if (dif == 0) + { + if (Interlocked.CompareExchange(ref _enqueuePos.Value, pos + 1, pos) == pos) + { + cell.Value = item; + Volatile.Write(ref cell.Sequence, pos + 1); + return true; + } + continue; // lost the race; reload and retry + } + + if (dif < 0) + return false; // slot not yet consumed → full + } + } + + /// Single-consumer only. Returns false if empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out T item) + { + Cell[] buffer = _buffer; + int mask = _mask; + + long pos = _dequeuePos.Value; // single consumer: plain read + ref Cell cell = ref buffer[(int)pos & mask]; + + long seq = Volatile.Read(ref cell.Sequence); + long dif = seq - (pos + 1); + + if (dif == 0) + { + item = cell.Value; + _dequeuePos.Value = pos + 1; // single consumer: plain write + Volatile.Write(ref cell.Sequence, pos + mask + 1); // free slot for producers + return true; + } + + item = default; + return false; + } +} + +/// +/// A single long padded to a 64-byte cache line so the producer and consumer +/// positions never share a line (no false sharing). Top-level and non-generic +/// so it can legally use explicit layout. +/// +[StructLayout(LayoutKind.Explicit, Size = 64)] +internal struct PaddedLong +{ + [FieldOffset(0)] public long Value; +} diff --git a/MinimaTPool/Utils/RingSegment.cs b/MinimaTPool/Utils/RingSegment.cs new file mode 100644 index 0000000..a797b3c --- /dev/null +++ b/MinimaTPool/Utils/RingSegment.cs @@ -0,0 +1,31 @@ +using System.Buffers; + +namespace MinimaTPool.Utils; + +/// +/// One segment of a multi-buffer ReadOnlySequence<byte> built by the +/// ConnectionPipeReader when a single read spans more than one recv buffer. +/// BufferId is carried for debugging; buffer return is driven off the held +/// item list, not the segments. +/// +public sealed class RingSegment : ReadOnlySequenceSegment +{ + public ushort BufferId { get; } + + public RingSegment(ReadOnlyMemory memory, ushort bufferId) + { + Memory = memory; + BufferId = bufferId; + } + + public RingSegment Append(ReadOnlyMemory memory, ushort bufferId) + { + var next = new RingSegment(memory, bufferId) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = next; + return next; + } +} diff --git a/MinimaTPool/Utils/SpscRecvRing.cs b/MinimaTPool/Utils/SpscRecvRing.cs new file mode 100644 index 0000000..288df28 --- /dev/null +++ b/MinimaTPool/Utils/SpscRecvRing.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace MinimaTPool.Utils; + +public sealed unsafe class SpscRecvRing +{ + public struct Item + { + public byte* Ptr; + public ushort Bid; + public int Len; + public bool HasBuffer; + public ushort Gen; // connection generation when enqueued (incremental return guard) + + public ReadOnlySpan AsSpan() => new(Ptr, Len); + + public UnmanagedMemoryManager AsMemoryManager() => new(Ptr, Len, Bid); + } + + private readonly Item[] _items; + private readonly int _mask; + private long _tail; + private long _head; + + public SpscRecvRing(int capacityPow2) + { + if (capacityPow2 <= 0 || (capacityPow2 & (capacityPow2 - 1)) != 0) + { + throw new ArgumentException("capacity must be a power of two", nameof(capacityPow2)); + } + + _items = new Item[capacityPow2]; + _mask = capacityPow2 - 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryEnqueue(in Item item) + { + long head = Volatile.Read(ref _head); + long tail = _tail; + + if ((ulong)(tail - head) >= (ulong)_items.Length) + { + return false; + } + + _items[(int)(tail & _mask)] = item; + Volatile.Write(ref _tail, tail + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out Item item) + { + long head = _head; + long tail = Volatile.Read(ref _tail); + + if (head >= tail) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long SnapshotTail() => Volatile.Read(ref _tail); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeueUntil(long tailSnapshot, out Item item) + { + long head = _head; + + if (head >= tailSnapshot) + { + item = default; + return false; + } + + item = _items[(int)(head & _mask)]; + Volatile.Write(ref _head, head + 1); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEmpty() => Volatile.Read(ref _head) >= Volatile.Read(ref _tail); + + // Reactor-thread-only, called during connection teardown (Clear) when no + // handler is consuming. Discards any leftover items so the recycled + // connection starts empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _head = 0; + _tail = 0; + } +} diff --git a/MinimaTPool/Utils/UnmanagedMemoryManager.cs b/MinimaTPool/Utils/UnmanagedMemoryManager.cs new file mode 100644 index 0000000..0c03a26 --- /dev/null +++ b/MinimaTPool/Utils/UnmanagedMemoryManager.cs @@ -0,0 +1,32 @@ +using System.Buffers; + +namespace MinimaTPool.Utils; + +public sealed unsafe class UnmanagedMemoryManager : MemoryManager +{ + private readonly byte* _ptr; + private readonly int _length; + + public ushort BufferId { get; } + + public UnmanagedMemoryManager(byte* ptr, int length) + { + _ptr = ptr; + _length = length; + } + + public UnmanagedMemoryManager(byte* ptr, int length, ushort bufferId) + { + _ptr = ptr; + _length = length; + BufferId = bufferId; + } + + public override Span GetSpan() => new(_ptr, _length); + + public override MemoryHandle Pin(int elementIndex = 0) => new(_ptr + elementIndex); + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } +} diff --git a/MinimaTPool/io_uring/Native.cs b/MinimaTPool/io_uring/Native.cs new file mode 100644 index 0000000..f6780e3 --- /dev/null +++ b/MinimaTPool/io_uring/Native.cs @@ -0,0 +1,162 @@ +using System.Runtime.InteropServices; + +namespace MinimaTPool; + +/// +/// All native interop in one file: io_uring syscalls, libc socket calls, +/// the kernel struct layouts they expect, and the constants needed to +/// drive a minimal io_uring loop. +/// +public static unsafe class Native { + private const long SYS_IO_URING_SETUP = 425; + private const long SYS_IO_URING_ENTER = 426; + private const long SYS_IO_URING_REGISTER = 427; + + public const byte IORING_OP_POLL_ADD = 6; + public const byte IORING_OP_ACCEPT = 13; + public const byte IORING_OP_SEND = 26; + public const byte IORING_OP_RECV = 27; + public const uint IORING_ENTER_GETEVENTS = 1u << 0; + public const long IORING_OFF_SQ_RING = 0; + public const long IORING_OFF_SQES = 0x10000000; + + // Multishot / buffer-ring goodies. + public const ushort IORING_ACCEPT_MULTISHOT = 1 << 0; + public const ushort IORING_RECV_MULTISHOT = 1 << 1; + public const byte IOSQE_BUFFER_SELECT = 1 << 5; + public const uint IORING_CQE_F_BUFFER = 1u << 0; + public const uint IORING_CQE_F_MORE = 1u << 1; + public const int IORING_CQE_BUFFER_SHIFT = 16; + public const uint IORING_REGISTER_PBUF_RING = 22; + public const uint IORING_UNREGISTER_PBUF_RING = 23; + public const uint IORING_POLL_ADD_MULTI = 1u << 0; + + // Incremental provided-buffer consumption (kernel 6.12+). IOU_PBUF_RING_INC + // is set in io_uring_buf_reg.flags at registration; IORING_CQE_F_BUF_MORE is + // set on recv CQEs while the kernel will keep appending to the same buffer. + public const ushort IOU_PBUF_RING_INC = 2; + public const uint IORING_CQE_F_BUF_MORE = 1u << 4; + + // eventfd flags + poll mask (used for the cross-thread wake mechanism). + public const int EFD_CLOEXEC = 0x80000; + public const int EFD_NONBLOCK = 0x800; + public const uint POLLIN = 0x0001; + + // Setup flags. SINGLE_ISSUER tells the kernel only one thread will submit + // to this ring (skips locking on the SQ). DEFER_TASKRUN defers completion + // processing until io_uring_enter(GETEVENTS), which lets the kernel batch + // work and avoids interrupting the reactor with task_work mid-flight. + public const uint IORING_SETUP_SINGLE_ISSUER = 1u << 12; + public const uint IORING_SETUP_DEFER_TASKRUN = 1u << 13; + + public const int PROT_READ = 1; + public const int PROT_WRITE = 2; + public const int MAP_SHARED = 1; + public const int MAP_POPULATE = 0x8000; + + public const int AF_INET = 2; + public const int SOCK_STREAM = 1; + public const int SOL_SOCKET = 1; + public const int SO_REUSEADDR = 2; + public const int SO_REUSEPORT = 15; + public const int IPPROTO_TCP = 6; + public const int TCP_NODELAY = 1; + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall3(long nr, uint a1, IoUringParams* a2); + + [DllImport("libc", EntryPoint = "syscall")] + private static extern long syscall6(long nr, uint a1, uint a2, uint a3, uint a4, void* a5, nuint a6); + + [DllImport("libc", EntryPoint = "syscall", SetLastError = true)] + private static extern long syscall4(long nr, uint a1, uint a2, void* a3, uint a4); + + public static int io_uring_setup(uint entries, IoUringParams* p) => + (int)syscall3(SYS_IO_URING_SETUP, entries, p); + + public static int io_uring_enter(int fd, uint toSubmit, uint minComplete, uint flags) => + (int)syscall6(SYS_IO_URING_ENTER, (uint)fd, toSubmit, minComplete, flags, null, 0); + + public static int io_uring_register(int fd, uint opcode, void* arg, uint nrArgs) => + (int)syscall4(SYS_IO_URING_REGISTER, (uint)fd, opcode, arg, nrArgs); + + [DllImport("libc")] public static extern void* mmap(void* addr, nuint length, int prot, int flags, int fd, long offset); + [DllImport("libc")] public static extern int munmap(void* addr, nuint length); + [DllImport("libc")] public static extern int close(int fd); + [DllImport("libc")] public static extern int socket(int domain, int type, int proto); + [DllImport("libc")] public static extern int bind(int fd, sockaddr_in* addr, uint len); + [DllImport("libc")] public static extern int listen(int fd, int backlog); + [DllImport("libc")] public static extern int setsockopt(int fd, int level, int optname, void* optval, uint optlen); + [DllImport("libc")] public static extern int eventfd(uint initval, int flags); + [DllImport("libc")] public static extern long write(int fd, void* buf, nuint count); + [DllImport("libc")] public static extern long read(int fd, void* buf, nuint count); + + public static ushort Htons(ushort x) => (ushort)((x << 8) | (x >> 8)); + + // Kernel struct layouts (must match include/uapi/linux/io_uring.h) + [StructLayout(LayoutKind.Sequential)] + public struct SqRingOffsets { + public uint head, tail, ring_mask, ring_entries, flags, dropped, array, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CqRingOffsets { + public uint head, tail, ring_mask, ring_entries, overflow, cqes, flags, resv1; + public ulong resv2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringParams { + public uint sq_entries, cq_entries, flags, sq_thread_cpu, sq_thread_idle; + public uint features, wq_fd, resv0, resv1, resv2; + public SqRingOffsets sq_off; + public CqRingOffsets cq_off; + } + + [StructLayout(LayoutKind.Explicit, Size = 64)] + public struct IoUringSqe { + [FieldOffset(0)] public byte opcode; + [FieldOffset(1)] public byte flags; + [FieldOffset(2)] public ushort ioprio; + [FieldOffset(4)] public int fd; + [FieldOffset(8)] public ulong off; + [FieldOffset(16)] public ulong addr; + [FieldOffset(24)] public uint len; + [FieldOffset(28)] public uint op_flags; + [FieldOffset(32)] public ulong user_data; + [FieldOffset(40)] public ushort buf_index; + [FieldOffset(42)] public ushort personality; + [FieldOffset(44)] public int splice_fd_in; + [FieldOffset(48)] public ulong addr3; + [FieldOffset(56)] public ulong __pad2; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IoUringCqe { + public ulong user_data; + public int res; + public uint flags; + } + + // Argument struct for IORING_REGISTER_PBUF_RING. + [StructLayout(LayoutKind.Sequential)] + public struct io_uring_buf_reg { + public ulong ring_addr; + public uint ring_entries; + public ushort bgid; + public ushort flags; + public ulong resv1, resv2, resv3; + } + + [StructLayout(LayoutKind.Sequential)] + public struct in_addr { public uint s_addr; } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct sockaddr_in { + public ushort sin_family; + public ushort sin_port; + public in_addr sin_addr; + public fixed byte sin_zero[8]; + } +} diff --git a/MinimaTPool/io_uring/Ring.cs b/MinimaTPool/io_uring/Ring.cs new file mode 100644 index 0000000..940a486 --- /dev/null +++ b/MinimaTPool/io_uring/Ring.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +using static MinimaTPool.Native; + +// ReSharper disable SuggestVarOrType_BuiltInTypes +// ReSharper disable SuggestVarOrType_Elsewhere +#pragma warning disable CA1806 + +namespace MinimaTPool; + +public sealed unsafe class Ring : IDisposable +{ + private int _fd; + + public int Fd => _fd; + + private uint* _sqHead; + private uint* _sqTail; + private uint* _sqArray; + private uint _sqMask; + private uint _sqEntries; + private IoUringSqe* _sqes; + + private uint* _cqHead; + private uint* _cqTail; + private IoUringCqe* _cqes; + private uint _cqMask; + + private uint _sqeTail; + + private byte* _ringPtr; + private nuint _ringSize; + private byte* _sqePtr; + private nuint _sqeSize; + + public static Ring Create(uint entries) + { + IoUringParams ioUringParams = default; + ioUringParams.flags = IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN; + int fd = io_uring_setup(entries, &ioUringParams); + if (fd < 0) + { + throw new InvalidOperationException($"io_uring_setup failed: {fd}"); + } + + var ring = new Ring + { + _fd = fd, + _sqEntries = ioUringParams.sq_entries + }; + + nuint sqRingBytes = ioUringParams.sq_off.array + ioUringParams.sq_entries * sizeof(uint); + nuint cqRingBytes = ioUringParams.cq_off.cqes + ioUringParams.cq_entries * (nuint)sizeof(IoUringCqe); + nuint ringBytes = sqRingBytes > cqRingBytes ? sqRingBytes : cqRingBytes; + + void* ringMem = mmap(null, ringBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING); + if (ringMem == (void*)-1) + { + close(fd); + + throw new InvalidOperationException("mmap(SQ_RING) failed"); + } + ring._ringPtr = (byte*)ringMem; + ring._ringSize = ringBytes; + + nuint sqeBytes = ioUringParams.sq_entries * (nuint)sizeof(IoUringSqe); + void* sqeMem = mmap(null, sqeBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQES); + if (sqeMem == (void*)-1) + { + munmap(ringMem, ringBytes); + close(fd); + + throw new InvalidOperationException("mmap(SQES) failed"); + } + ring._sqes = (IoUringSqe*)sqeMem; + ring._sqePtr = (byte*)sqeMem; + ring._sqeSize = sqeBytes; + + byte* ringPointer = (byte*)ringMem; + ring._sqHead = (uint*)(ringPointer + ioUringParams.sq_off.head); + ring._sqTail = (uint*)(ringPointer + ioUringParams.sq_off.tail); + ring._sqArray = (uint*)(ringPointer + ioUringParams.sq_off.array); + ring._sqMask = *(uint*)(ringPointer + ioUringParams.sq_off.ring_mask); + + ring._cqHead = (uint*)(ringPointer + ioUringParams.cq_off.head); + ring._cqTail = (uint*)(ringPointer + ioUringParams.cq_off.tail); + ring._cqes = (IoUringCqe*)(ringPointer + ioUringParams.cq_off.cqes); + ring._cqMask = *(uint*)(ringPointer + ioUringParams.cq_off.ring_mask); + + return ring; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IoUringSqe* GetSqe() + { + uint head = Volatile.Read(ref *_sqHead); + + if (_sqeTail - head >= _sqEntries) + { + return null; + } + + uint slot = _sqeTail & _sqMask; + _sqArray[slot] = slot; + _sqeTail++; + + return &_sqes[slot]; + } + + public int SubmitAndWait(uint waitFor) + { + uint published = *_sqTail; + uint toSubmit = _sqeTail - published; + + if (toSubmit > 0) + { + Volatile.Write(ref *_sqTail, _sqeTail); + } + + if (toSubmit == 0 && waitFor == 0) return 0; + + uint flags = waitFor > 0 ? IORING_ENTER_GETEVENTS : 0; + + return io_uring_enter(_fd, toSubmit, waitFor, flags); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetCqe(out IoUringCqe cqe) + { + uint head = *_cqHead; + uint tail = Volatile.Read(ref *_cqTail); + + if (head == tail) + { + cqe = default; + + return false; + } + + cqe = _cqes[head & _cqMask]; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqeSeen() => Volatile.Write(ref *_cqHead, *_cqHead + 1); + + // Batched CQ drain (liburing io_uring_for_each_cqe + io_uring_cq_advance): + // read the kernel-written tail once (acquire), process the whole batch, + // then publish the consumed head once (release) instead of once per CQE. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint CqReady() => Volatile.Read(ref *_cqTail) - *_cqHead; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref readonly IoUringCqe CqeAt(uint i) => ref _cqes[(*_cqHead + i) & _cqMask]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CqAdvance(uint n) => Volatile.Write(ref *_cqHead, *_cqHead + n); + + public void Dispose() + { + if (_ringPtr != null) + { + munmap(_ringPtr, _ringSize); _ringPtr = null; + } + + if (_sqePtr != null) + { + munmap(_sqePtr, _sqeSize); _sqePtr = null; + } + + if (_fd > 0) + { + close(_fd); _fd = 0; + } + } +} + +#pragma warning restore CA1806 diff --git a/Shrike.Playground/Program.cs b/Shrike.Playground/Program.cs index 070112f..6ce3468 100644 --- a/Shrike.Playground/Program.cs +++ b/Shrike.Playground/Program.cs @@ -10,7 +10,7 @@ public static void Main() { var engine = ShrikeEngine .CreateBuilder() - .SetNWorkersSolver(() => 12) + .SetNWorkersSolver(() => 6) .SetBacklog(16384) .SetMaxEventsPerWake(512) .SetMaxNumberConnectionsPerWorker(512) @@ -23,8 +23,7 @@ public static void Main() // Same knob + object as Minima / AspBaseline / SocketBaseline: serialize a // WORK_ITEMS-element object to JSON on the THREAD POOL per request. 0/unset = inline. - private static readonly int WorkItems = - int.TryParse(Environment.GetEnvironmentVariable("WORK_ITEMS"), out int n) ? n : 0; + private static readonly int WorkItems = 1000; private static readonly Payload LargeObject = BuildPayload(Math.Max(WorkItems, 1)); @@ -64,12 +63,14 @@ private static async Task HandleAsync(Connection conn) if (wrote) { + _ = await Task.Run(static () => JsonSerializer.Serialize("Hello World!")); + // Real async work on the THREAD POOL — handler resumes off-worker. Shrike's // FlushAsync does a thread-safe send() directly (epoll), so no handoff here. - if (WorkItems > 0) + /*if (WorkItems > 0) { _ = await Task.Run(static () => JsonSerializer.SerializeToUtf8Bytes(LargeObject)); - } + }*/ await conn.FlushAsync(); } @@ -80,14 +81,14 @@ private static async Task HandleAsync(Connection conn) private static unsafe void CommitPlainTextResponse(Connection connection) { - int tail = connection.WriteBuffer.Tail; - int contentLength = s_plainTextBody.Length; + //int tail = connection.WriteBuffer.Tail; + //int contentLength = s_plainTextBody.Length; connection.WriteBuffer.WriteUnmanaged("HTTP/1.1 200 OK\r\n"u8 + - "Content-Length: \r\n"u8 + + "Content-Length: 13\r\n"u8 + "Server: S\r\n"u8 + - "Content-Type: text/plain\r\n"u8); - connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes); + "Content-Type: text/plain\r\n\r\nHello, World!"u8); + /*connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes); connection.WriteBuffer.WriteUnmanaged(s_plainTextBody); // Patch the 2-digit Content-Length into the reserved spaces (offset matches the header above). @@ -95,7 +96,7 @@ private static unsafe void CommitPlainTextResponse(Connection connection) int tens = contentLength / 10; int ones = contentLength - tens * 10; dst[0] = (byte)('0' + tens); - dst[1] = (byte)('0' + ones); + dst[1] = (byte)('0' + ones);*/ } } diff --git a/Shrike/Engine/Connection.cs b/Shrike/Engine/Connection.cs index 7dfd86e..9ef4846 100644 --- a/Shrike/Engine/Connection.cs +++ b/Shrike/Engine/Connection.cs @@ -39,13 +39,13 @@ public enum FlushResult { Complete, Incomplete, Close } public int Ep; // ---- read IVTS (result = isClosed) ---- - private ManualResetValueTaskSourceCore _readSignal = new() { RunContinuationsAsynchronously = false }; + private ManualResetValueTaskSourceCore _readSignal = new() { RunContinuationsAsynchronously = true }; private int _armed; private int _pending; private int _closed; // ---- flush IVTS ---- - private ManualResetValueTaskSourceCore _flushSignal = new() { RunContinuationsAsynchronously = false }; + private ManualResetValueTaskSourceCore _flushSignal = new() { RunContinuationsAsynchronously = true }; private int _flushArmed; public Connection(int maxConnections, int inSlabSize, int outSlabSize) diff --git a/Shrike/Writers/FixedBufferWriter.cs b/Shrike/Writers/FixedBufferWriter.cs index efdf9e2..dd83397 100644 --- a/Shrike/Writers/FixedBufferWriter.cs +++ b/Shrike/Writers/FixedBufferWriter.cs @@ -26,47 +26,13 @@ namespace Shrike; [SkipLocalsInit] public unsafe class FixedBufferWriter : IUnmanagedBufferWriter, IBufferWriter, IDisposable { - // ========================================================================= - // Fields - // ========================================================================= - - /// - /// The total capacity (in bytes) of the memory region represented by this writer. - /// private readonly int _capacity; - private readonly UnmanagedMemoryManager _manager; - - /// - /// The current read position (if the buffer is also reused for reads). - /// Not used by the writer itself, but exposed for external control. - /// public int Head; - - /// - /// The current write position. Bytes have been written in [0 .. Tail). - /// public int Tail; - - /// - /// Pointer to the beginning of the unmanaged buffer. - /// + public byte* Ptr { get; } - - // ========================================================================= - // Constructor - // ========================================================================= - - /// - /// Creates a new instance over an unmanaged - /// memory region. - /// - /// must point to a memory block of at least - /// bytes that remains valid for the lifetime - /// of this struct. - /// - /// Pointer to the start of the unmanaged buffer. - /// Maximum number of bytes writable to the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public FixedBufferWriter(byte* ptr, int capacity) { @@ -77,28 +43,14 @@ public FixedBufferWriter(byte* ptr, int capacity) _manager = new UnmanagedMemoryManager(ptr, capacity); } - - // ========================================================================= - // Core Methods - // ========================================================================= - - /// - /// Resets both read () and write () - /// indices to zero, effectively clearing the buffer (logically). - /// - /// Does not modify the underlying memory — only the pointers. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { Head = 0; Tail = 0; } - - /// - /// Advances the write pointer by bytes after data - /// has been written directly into the memory region. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Advance(int count) { @@ -110,55 +62,39 @@ public Memory GetMemory(int sizeHint = 0) { int remaining = _capacity - Tail; if (sizeHint > remaining) + { throw new InvalidOperationException("Buffer too small."); + } return _manager.Memory.Slice(Tail, remaining); } - /// - /// Gets a raw unmanaged pointer to the start of the buffer. - /// This is mainly for interop or direct native I/O operations (e.g. send()). - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte* GetPointer() => Ptr; - - /// - /// Returns a writable over the remaining space in - /// the buffer, starting at the current position. - /// - /// Throws if the requested - /// would exceed the buffer capacity. - /// - /// The minimum required size for the writable region. + public Span GetSpan(int sizeHint = 0) { if (Tail + sizeHint > _capacity) + { throw new InvalidOperationException("Buffer too small."); + } return new Span(Ptr + Tail, _capacity - Tail); } - - // ========================================================================= - // Write Helpers - // ========================================================================= - - /// - /// Copies unmanaged data directly into the buffer using a raw pointer copy. - /// Slightly faster than for large spans because it avoids - /// intermediate range checks. - /// - /// The caller must ensure does not overlap the target region. - /// - /// Data to copy into the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteUnmanaged(ReadOnlySpan source) { int len = source.Length; if (Tail + len > _capacity) + { throw new InvalidOperationException("Buffer too small."); + } fixed (byte* src = source) + { Buffer.MemoryCopy(src, Ptr + Tail, _capacity - Tail, len); + } Tail += len; } @@ -170,40 +106,24 @@ public void WriteUnmanaged(string source) Tail += bytesWritten; } - /// - /// Copies data from a managed into the unmanaged buffer. - /// This version uses which performs bounds checks - /// and is safe for managed callers. - /// - /// The data to copy into the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ReadOnlySpan source) { int len = source.Length; if (Tail + len > _capacity) + { throw new InvalidOperationException("Buffer too small."); + } source.CopyTo(new Span(Ptr + Tail, _capacity - Tail)); Tail += len; } - // ========================================================================= - // Disposal - // ========================================================================= - - /// - /// Releases the unmanaged memory associated with this writer if it owns the pointer. - /// - /// If points to a shared memory region (e.g. part of a - /// connection pool or slab allocator), calling this will free that memory - /// globally — causing use-after-free crashes for other users. - /// - /// Only call this when you know this instance *owns* the buffer and no one else - /// references it. - /// public void Dispose() { if (Ptr != null) + { NativeMemory.AlignedFree(Ptr); + } } } \ No newline at end of file diff --git a/SocketBaseline/Program.cs b/SocketBaseline/Program.cs index cd20f3d..35ee6b6 100644 --- a/SocketBaseline/Program.cs +++ b/SocketBaseline/Program.cs @@ -5,7 +5,7 @@ // Raw System.Net.Sockets HTTP/1.1 server — NO ASP.NET, NO Kestrel. A single async accept // loop; each connection is handled on the thread pool via the runtime's async socket engine // (epoll-backed on Linux). Same WORK_ITEMS knob + same object as Minima / AspBaseline. -int workItems = 50; +int workItems = 1000; Payload largeObject = BuildPayload(Math.Max(workItems, 1)); byte[] response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok"u8.ToArray(); @@ -32,14 +32,16 @@ async Task HandleAsync(Socket client) { int read = await client.ReceiveAsync(buf.AsMemory(), SocketFlags.None); if (read <= 0) break; // peer closed + + _ = await Task.Run(static () => JsonSerializer.Serialize("Hello World!")); // Same work as Minima/AspBaseline: serialize the object on the thread pool // (the handler already runs there) and discard. WORK_ITEMS=0 → plain "ok". - if (workItems > 0) + /*if (workItems > 0) { // Force threadpool _ = await Task.Run(() => JsonSerializer.SerializeToUtf8Bytes(largeObject)); - } + }*/ int sent = 0; while (sent < response.Length) diff --git a/Spring.Demo/Program.cs b/Spring.Demo/Program.cs index 16ff3d1..cf0f74e 100644 --- a/Spring.Demo/Program.cs +++ b/Spring.Demo/Program.cs @@ -10,14 +10,10 @@ kestrel.ListenAnyIP(8080); }); -// SPRING=0 → Kestrel's default Socket transport (baseline). Otherwise the io_uring Spring transport. -if (Environment.GetEnvironmentVariable("SPRING") != "0") -{ - builder.WebHost.UseSpring(opts => opts.ReactorCount = Math.Max(1, 12)); -} +builder.WebHost.UseSpring(opts => opts.ReactorCount = Math.Max(1, 12)); var app = builder.Build(); -app.MapGet("/", () => "Hello from Spring + Kestrel\n"); +app.MapGet("/", () => "Hello, World!\n"); app.Run(); diff --git a/docs/blog/blog.css b/docs/blog/blog.css index 4c77021..566414a 100644 --- a/docs/blog/blog.css +++ b/docs/blog/blog.css @@ -1,14 +1,14 @@ :root { - --bg: #0a0a0f; - --bg-card: #12121a; - --bg-code: #1a1a2e; - --border: #1e1e30; - --text: #e0e0e8; - --text-muted: #8888a0; - --accent-zerg: #7c5cfc; - --accent-zerg-dim: #5a3ed8; - --accent-terraform: #00c896; - --accent-hot: #ff6b6b; + --bg: #eef1f5; + --bg-card: #ffffff; + --bg-code: #f4f7fb; + --border: #d4dbe5; + --text: #1a2540; + --text-muted: #5b6679; + --accent-zerg: #1e40af; + --accent-zerg-dim: #2563eb; + --accent-terraform: #0d9488; + --accent-hot: #b91c1c; --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace; --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } @@ -29,7 +29,7 @@ nav { top: 0; width: 100%; z-index: 100; - background: rgba(10, 10, 15, 0.85); + background: rgba(238, 241, 245, 0.85); backdrop-filter: blur(16px); border-bottom: 1px solid var(--border); padding: 0 2rem; @@ -186,7 +186,7 @@ nav .links a.active { color: var(--accent-zerg); } .post-body a { color: var(--accent-zerg); text-decoration: none; - border-bottom: 1px solid rgba(124, 92, 252, 0.3); + border-bottom: 1px solid rgba(30, 64, 175, 0.3); transition: border-color 0.2s; } .post-body a:hover { border-bottom-color: var(--accent-zerg); } @@ -194,7 +194,7 @@ nav .links a.active { color: var(--accent-zerg); } font-family: var(--font-mono); font-size: 0.88em; color: var(--accent-zerg); - background: rgba(124, 92, 252, 0.1); + background: rgba(30, 64, 175, 0.1); padding: 0.1em 0.35em; border-radius: 4px; } @@ -228,28 +228,28 @@ nav .links a.active { color: var(--accent-zerg); } .token.comment, .token.prolog, .token.doctype, -.token.cdata { color: #546e7a; font-style: italic; } +.token.cdata { color: #7d8590; font-style: italic; } .token.punctuation { color: var(--text); } .token.namespace { opacity: 0.75; } .token.keyword, .token.boolean, .token.constant, -.token.symbol { color: #c792ea; } -.token.number { color: #f78c6c; } +.token.symbol { color: #1d4ed8; } +.token.number { color: #9a3412; } .token.string, .token.char, .token.attr-value, -.token.regex { color: #c3e88d; } +.token.regex { color: #1a7f37; } .token.class-name, -.token.builtin { color: #82aaff; } -.token.function { color: #f0c674; } +.token.builtin { color: #0550ae; } +.token.function { color: #1e40af; } .token.operator, .token.entity, -.token.url { color: #89ddff; } +.token.url { color: #0e7490; } .token.variable, .token.parameter { color: var(--text); } -.token.attr-name { color: #82aaff; } -.token.preprocessor { color: #c792ea; opacity: 0.85; } +.token.attr-name { color: #0550ae; } +.token.preprocessor { color: #1d4ed8; opacity: 0.9; } .post-body ul { margin: 1.25rem 0; padding-left: 0.5rem; @@ -270,6 +270,81 @@ nav .links a.active { color: var(--accent-zerg); } font-weight: bold; } .post-body strong { color: var(--text); font-weight: 700; } +.post-body .note { + border-left: 3px solid var(--accent-zerg); + background: rgba(30, 64, 175, 0.04); + padding: 0.75rem 1rem; + margin: 1.25rem 0; + color: var(--text-muted); + font-style: italic; + border-radius: 0 6px 6px 0; +} + +/* TABLES */ +.post-body .table-scroll { + overflow-x: auto; + margin: 1.5rem 0; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card); +} +.post-body .table-scroll table { + width: 100%; + border-collapse: collapse; + font-family: var(--font-mono); + font-size: 0.85rem; +} +.post-body table th, +.post-body table td { + text-align: left; + padding: 0.6rem 0.9rem; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} +.post-body table thead th { + color: var(--accent-zerg); + font-weight: 700; + background: rgba(30, 64, 175, 0.08); + border-bottom: 1px solid var(--border); +} +.post-body table tbody tr:last-child td { border-bottom: none; } +.post-body table tbody tr:hover { background: rgba(30, 64, 175, 0.04); } + +/* Wide table variant: bleeds beyond the 760px article column on large screens */ +.post-body .table-scroll.table-wide { + margin-left: -260px; + margin-right: -260px; +} +.post-body .table-scroll.table-wide table th, +.post-body .table-scroll.table-wide table td { + padding: 0.5rem 0.55rem; + font-size: 0.8rem; +} +.post-body table th.col-group { + text-align: center; + border-right: 1px solid var(--border); + border-left: 1px solid var(--border); +} +.post-body table th.col-group-end, +.post-body table td.col-group-end { border-right: 1px solid var(--border); } +@media (max-width: 1400px) { + .post-body .table-scroll.table-wide { + margin-left: -120px; + margin-right: -120px; + } +} +@media (max-width: 1024px) { + .post-body .table-scroll.table-wide { + margin-left: -40px; + margin-right: -40px; + } +} +@media (max-width: 768px) { + .post-body .table-scroll.table-wide { + margin-left: 0; + margin-right: 0; + } +} /* GLOSSARY */ .glossary { diff --git a/docs/blog/images/part-6-hero.png b/docs/blog/images/part-6-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..74d4cc261fc8b58945fc1e0e0f6aa551e0608029 GIT binary patch literal 241845 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μ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%0ABMW&m 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 JEKs+-{(l&Blog

Notes on io_uring, low-level networking and the internals of zerg.

    +
  • + + +
    C# Networking Deep Dive With io_uring — Part 6: Numbers
    +
    Six implementations on the same i9 14900k box, two workloads (sync plaintext "OK" and an async Task.Run doing a trivial JsonSerializer.Serialize), measured with wrk and gcannon. io_uring r+w with IVTS inline tops the sync chart at 3.95M req/s but turns very unstable under async; once the handler leaves the reactor, the io_uring/threadpool variant drops to ~2.4M while epoll/threadpool and Kestrel's stock socket both hold ~3.6M sync / ~3.0M async. A hybrid io_uring recv + libc send lands in the middle. Includes a comparison-at-a-glance table.
    +
    +
  • diff --git a/docs/blog/io-uring-minima-part-6.html b/docs/blog/io-uring-minima-part-6.html new file mode 100644 index 0000000..a45d52e --- /dev/null +++ b/docs/blog/io-uring-minima-part-6.html @@ -0,0 +1,470 @@ + + + + + + C# Networking Deep Dive With io_uring — Part 6 — zerg + + + + + +
    + ← All posts + +
    + A dense, chaotic scattering of black numerals on a white background, overlapping at every angle. + +
    +

    C# Networking Deep Dive With io_uring — Part 6: Numbers

    + +
    + +
    +

    For part 6 let's do some benchmarks;

    + +

    These values are not "scientific", just a ballpark estimate.

    + +

    What is going to be benchmarked

    + +
      +
    • io_uring read+write with IVTS reactor inline continuations (RunAsynchrounousContinuation = false)
    • +
    • io_uring read+write without IVTS reactor inline continuations (threadpool) (RunAsynchrounousContinuation = true)
    • +
    • io_uring read + libc send write without IVTS reactor inline continuations (threadpool) (RunAsynchrounousContinuation = true)
    • +
    • epoll read+write with IVTS reactor inline continuations
    • +
    • epoll read+write without IVTS reactor inline continuations
    • +
    • System.Net.Socket (Kestrel stock) - epoll threadpool
    • +
    + +

    Tests

    + +

    (No pipelining)

    + +
      +
    • Synchronous lightweight plaintext "OK" response.
    • +
    • Asynchronous workload: _ = await Task.Run(static () => JsonSerializer.Serialize("Hello World!"));
    • +
    + +

    The purpose of the async workload is to force the continuation onto the threadpool, not to model a heavy async workload.

    + +

    Hardware

    + +

    i9 14900k
    + 64GB DDR5 6400MHz
    + Linux Kernel 6.17.0-22-generic

    + +

    Tests are done through localhost loopback (no NIC influence)
    + MTU 1500

    + +

    Load generators

    + +

    Http/1.1 no TLS

    + +

    + wrk (epoll)
    + gcannon (io_uring) +

    + +

    io_uring read+write with IVTS reactor inline continuations

    + +

    This is the exact model explored throughout the series, expected to deliver high performance on synchronous test.

    + +

    Reactor count: 12

    + +

    Sync workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg121.45us129us
    Latency Stdev178.81us
    Latency Max8.32ms
    Latency p50125us
    Latency p90185us
    Latency p99245us
    Latency p99.9317us
    Req/Sec Avg201.31k3.95M
    Requests Total18,299,27819,735,722
    Duration5.10s5.00s
    Transfer/Bandwidth225.84MB/s248.42MB/s
    + +

    Async workload (very unstable)

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg435.74us185us
    Latency Stdev795.84us
    Latency Max12.73ms
    Latency p50135us
    Latency p90229us
    Latency p991.84ms
    Latency p99.94.10ms
    Req/Sec Avg142.93k2.76M
    Requests Total12,883,29413,797,048
    Duration5.10s5.00s
    Transfer/Bandwidth159.05MB/s173.67MB/s
    + +

    io_uring read+write without IVTS reactor inline

    + +

    Similar model explored throughout the series but with RunAsynchronousContinuation set to true on both IVTS, expected to deliver close results on both tests.

    + +

    Reactor count: 12

    + +

    Sync workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg515.72us211us
    Latency Stdev821.99us
    Latency Max12.67ms
    Latency p50164us
    Latency p90273us
    Latency p991.55ms
    Latency p99.93.79ms
    Req/Sec Avg110.03k2.41M
    Requests Total9,946,28212,080,236
    Duration5.10s5.00s
    Transfer/Bandwidth122.80MB/s151.97MB/s
    + +

    Async workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg530.17us213us
    Latency Stdev842.05us
    Latency Max13.37ms
    Latency p50146us
    Latency p90265us
    Latency p992.27ms
    Latency p99.94.38ms
    Req/Sec Avg108.43k2.39M
    Requests Total9,726,08311,952,675
    Duration5.03s5.00s
    Transfer/Bandwidth121.82MB/s150.45MB/s
    + +

    io_uring read + libc send write without IVTS reactor inline continuations

    + +

    Similar model explored throughout the series but with RunAsynchronousContinuation set to true on both IVTS and the write branch is not io_uring, instead we use the libc's send, expected to deliver close results on both tests. This is an hybrid approach and should be the middle ground between the first two models.

    + +

    Reactor count: 12

    + +

    Sync workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg410.23us154us
    Latency Stdev782.03us
    Latency Max12.08ms
    Latency p5084us
    Latency p90176us
    Latency p992.68ms
    Latency p99.94.32ms
    Req/Sec Avg158.40k3.31M
    Requests Total14,361,23916,551,871
    Duration5.10s5.00s
    Transfer/Bandwidth0.88GB read208.27MB/s
    + +

    Async workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg418.96us159us
    Latency Stdev824.32us
    Latency Max17.51ms
    Latency p5085us
    Latency p90198us
    Latency p991.99ms
    Latency p99.94.41ms
    Req/Sec Avg154.72k3.20M
    Requests Total13,955,37115,997,491
    Duration5.09s5.00s
    Transfer/Bandwidth172.59MB/s201.18MB/s
    + +

    epoll read+write with IVTS reactor inline continuations

    + +

    Pure epoll approach with same reactor threading architecture. Inline handler continuation for both IVTS.

    + +

    Reactor count: 12

    + +

    Sync workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg284.42us160us
    Latency Stdev610.90us
    Latency Max11.06ms
    Latency p5086us
    Latency p90194us
    Latency p992.07ms
    Latency p99.94.39ms
    Req/Sec Avg188.08k3.17M
    Requests Total17,141,22515,856,691
    Duration5.10s5.00s
    Transfer/Bandwidth403.61MB/s199.56MB/s
    + +

    Async workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg458.63us159us
    Latency Stdev0.90ms
    Latency Max15.96ms
    Latency p5074us
    Latency p90185us
    Latency p992.68ms
    Latency p99.95.32ms
    Req/Sec Avg150.84k3.08M
    Requests Total13,670,69715,386,279
    Duration5.10s5.00s
    Transfer/Bandwidth322.12MB/s369.72MB/s
    + +

    epoll read+write without IVTS reactor inline continuations

    + +

    Pure epoll approach with same reactor threading architecture. Threadpool handler continuation for both IVTS.

    + +

    Reactor count: 6

    + +

    Sync workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg391.31us140us
    Latency Stdev764.42us
    Latency Max13.71ms
    Latency p5096us
    Latency p90150us
    Latency p992.06ms
    Latency p99.94.15ms
    Req/Sec Avg167.26k3.60M
    Requests Total15,179,06618,019,801
    Duration5.10s5.00s
    Transfer/Bandwidth357.60MB/s432.83MB/s
    + +

    Async workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg464.15us154us
    Latency Stdev838.78us
    Latency Max10.74ms
    Latency p5096us
    Latency p90154us
    Latency p992.22ms
    Latency p99.94.48ms
    Req/Sec Avg158.12k3.27M
    Requests Total14,231,17616,342,325
    Duration5.10s5.00s
    Transfer/Bandwidth236.89MB/s277.35MB/s
    + +

    System.Net.Socket (Kestrel stock) - epoll threadpool

    + +

    Kestrel's stock network I/O with some tunning

    + +

    Sync workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg156.79us141us
    Latency Stdev342.31us
    Latency Max6.98ms
    Latency p50129us
    Latency p90176us
    Latency p99305us
    Latency p99.93.17ms
    Req/Sec Avg174.25k3.60M
    Requests Total15,748,22318,024,579
    Duration5.10s5.00s
    Transfer/Bandwidth194.39MB/s226.84MB/s
    + +

    Async workload

    + +
    + + + + + + + + + + + + + + +
    Metricwrkgcannon
    Latency Avg255.07us169us
    Latency Stdev507.29us
    Latency Max12.53ms
    Latency p50123us
    Latency p90237us
    Latency p991.25ms
    Latency p99.93.89ms
    Req/Sec Avg150.64k3.01M
    Requests Total13,618,90615,043,820
    Duration5.10s5.00s
    Transfer/Bandwidth168.14MB/s189.25MB/s
    + +

    Comparison at a glance

    + +

    wrk and gcannon req/s and avg latency for every model, side by side.

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ImplementationReactorsSyncAsync
    wrk req/swrk avggcannon req/sgcannon avgwrk req/swrk avggcannon req/sgcannon avg
    io_uring r+w, IVTS inline123.59M121.45us3.95M129us2.53M*435.74us*2.76M*185us*
    io_uring r+w, threadpool121.95M515.72us2.41M211us1.93M530.17us2.39M213us
    io_uring recv + libc send122.82M410.23us3.31M154us2.74M418.96us3.20M159us
    epoll r+w, IVTS inline123.36M284.42us3.17M160us2.68M458.63us3.08M159us
    epoll r+w, threadpool62.98M391.31us3.60M140us2.79M464.15us3.27M154us
    System.Net.Socket (Kestrel stock)3.09M156.79us3.60M141us2.67M255.07us3.01M169us
    + +

    CPU usage: the inline-IVTS cases (io_uring r+w IVTS, epoll r+w IVTS) cap at around 1200% max, while every other model averages ~1600%.

    + +

    * Async run flagged as very unstable in the original write-up.

    + +

    Conclusion

    + +

    The numbers are aligned with part 5's rant. On a fully synchronous benchmark, io_uring with the reactor inline continuation rides ahead, no cross thread hand offs.

    + +

    Force the continuation on the threadpool (async workload) and that lead evaporates. The hybrid approach reclaims most of it and is a serious contender for further tests with Kestrel integration.

    + +

    A little note on the load generators, quite interesting results, gcannon seems a lot more stable on latency values while wrk is all over the place.

    + +

    Important to highlight that the reactor inline sync models consume in average 20% less CPU as they are bounded to 12 reactor CPU threads. On the other hand, solutions that allow threadpool continuation will use as much CPU is available. For example, epoll r+w IVTS inline can actually yield 3.9M rps if we increase the reactor count to 16, surpassing System.Net.Socket performance for same CPU usage.

    + +

    Very surprising result on epoll r+w threadpool, was expecting the performance to be equal to System.Net.Socket, this will be quite interesting for part 7.

    + +

    On part 7 some of these models will be integrated on Kestrel/ASP.NET for direct benchmark comparison.

    +
    +
    +
    + + + + + + + + diff --git a/docs/index.html b/docs/index.html index c1ecf6c..94e7dbd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,17 +6,17 @@ zerg - High-Performance TCP Server Framework for C#