using System.Buffers; using System.Collections.Concurrent; namespace NATS.Server.IO; /// /// Tiered write buffer pool with broadcast drain capability. /// Maintains internal pools for common sizes (512, 4096, 65536) to reduce /// allocation overhead on the hot publish path. /// Go reference: client.go — dynamic buffer sizing and broadcast flush coalescing for fan-out. /// public sealed class OutboundBufferPool { private const int SmallSize = 512; private const int MediumSize = 4096; private const int LargeSize = 65536; private readonly ConcurrentBag _small = new(); // 512 B private readonly ConcurrentBag _medium = new(); // 4 KiB private readonly ConcurrentBag _large = new(); // 64 KiB private long _rentCount; private long _returnCount; private long _broadcastCount; /// Total buffer rent operations served by the pool. public long RentCount => Interlocked.Read(ref _rentCount); /// Total buffer return operations accepted by the pool. public long ReturnCount => Interlocked.Read(ref _returnCount); /// Total broadcast-drain operations performed. public long BroadcastCount => Interlocked.Read(ref _broadcastCount); // ----------------------------------------------------------------------- // IMemoryOwner surface (preserves existing callers) // ----------------------------------------------------------------------- /// /// Rents an whose Memory.Length is at least /// bytes. Tries the internal pool first; falls back to /// . /// /// Minimum required buffer size. public IMemoryOwner Rent(int size) { Interlocked.Increment(ref _rentCount); // Try to serve from the internal pool so that Dispose() returns the // raw buffer back to us rather than to MemoryPool.Shared. if (size <= SmallSize && _small.TryTake(out var sb)) return new PooledMemoryOwner(sb, _small); if (size <= MediumSize && _medium.TryTake(out var mb)) return new PooledMemoryOwner(mb, _medium); if (size <= LargeSize && _large.TryTake(out var lb)) return new PooledMemoryOwner(lb, _large); // Nothing cached — rent from the system pool (which may return a larger // buffer; that's fine, callers must honour Memory.Length, not the // requested size). int rounded = size <= SmallSize ? SmallSize : size <= MediumSize ? MediumSize : LargeSize; return MemoryPool.Shared.Rent(rounded); } // ----------------------------------------------------------------------- // Raw byte[] surface // ----------------------------------------------------------------------- /// /// Returns a byte[] from the internal pool whose length is at least /// bytes. The caller is responsible for calling /// when finished. /// /// Minimum required buffer size. public byte[] RentBuffer(int size) { Interlocked.Increment(ref _rentCount); if (size <= SmallSize) { if (_small.TryTake(out var b)) return b; return new byte[SmallSize]; } if (size <= MediumSize) { if (_medium.TryTake(out var b)) return b; return new byte[MediumSize]; } if (_large.TryTake(out var lb)) return lb; return new byte[LargeSize]; } /// /// Returns to the appropriate tier so it can be /// reused by a subsequent call. /// /// Buffer previously rented from this pool. public void ReturnBuffer(byte[] buffer) { Interlocked.Increment(ref _returnCount); switch (buffer.Length) { case SmallSize: _small.Add(buffer); break; case MediumSize: _medium.Add(buffer); break; case LargeSize: _large.Add(buffer); break; // Buffers of unexpected sizes are simply dropped (GC reclaims them). } } // ----------------------------------------------------------------------- // Broadcast drain // ----------------------------------------------------------------------- /// /// Coalesces multiple pending payloads into a single contiguous buffer for /// batch writing. Copies every entry in /// sequentially into and returns the total /// number of bytes written. /// /// The caller must ensure is large enough /// (use to pre-check). /// /// Go reference: client.go — broadcast flush coalescing for fan-out. /// /// Pending write segments to coalesce. /// Destination buffer receiving the concatenated payloads. public int BroadcastDrain(IReadOnlyList> pendingWrites, byte[] destination) { var offset = 0; foreach (var write in pendingWrites) { write.Span.CopyTo(destination.AsSpan(offset)); offset += write.Length; } Interlocked.Increment(ref _broadcastCount); return offset; } /// /// Returns the total number of bytes needed to coalesce all /// into a single buffer. /// /// Pending write segments to size. public static int CalculateBroadcastSize(IReadOnlyList> pendingWrites) { var total = 0; foreach (var w in pendingWrites) total += w.Length; return total; } // ----------------------------------------------------------------------- // Inner type: pooled IMemoryOwner // ----------------------------------------------------------------------- /// /// Wraps a raw byte[] rented from an internal /// and returns it to that bag on disposal. /// private sealed class PooledMemoryOwner : IMemoryOwner { private readonly ConcurrentBag _pool; private byte[]? _buffer; /// /// Creates a pooled memory owner backed by a reusable byte array. /// /// Rented backing buffer. /// Pool to return the buffer to on disposal. public PooledMemoryOwner(byte[] buffer, ConcurrentBag pool) { _buffer = buffer; _pool = pool; } /// Memory view over the currently owned buffer. public Memory Memory => _buffer is { } b ? b.AsMemory() : Memory.Empty; /// Returns the owned buffer to the originating pool. public void Dispose() { if (Interlocked.Exchange(ref _buffer, null) is { } b) _pool.Add(b); } } }