using System.Buffers; using System.Runtime.InteropServices; namespace AyCode.Services.Server.Tests.SignalRs; /// /// Custom IBufferWriter that simulates Kestrel's PinnedBlockMemoryPool transport behavior: /// /// Production: Kestrel PipeWriter → PinnedBlockMemoryPool → MemoryManager-backed Memory /// → TryGetArray FAILS → BWO takes owned-buffer fallback path /// /// Test: SlabTransportWriter → SlabMemoryManager-backed Memory /// → TryGetArray FAILS → BWO takes owned-buffer fallback path (same as production) /// /// Key behaviors: /// - GetMemory returns MemoryManager-backed Memory (TryGetArray always fails) /// - Fixed-size slab segments with random offsets (simulates slab allocator) /// - After Advance, remaining slab space is reused (offset grows) /// - GetMemory may return less than sizeHint (remaining slab space) /// - Deterministic via seeded Random for reproducible tests /// internal sealed class SlabTransportWriter : IBufferWriter { private readonly int _slabSize; private readonly Random _rng; // Committed data tracking private readonly List _segments = new(); private int _totalCommitted; // Current slab state private byte[]? _currentSlab; private int _writePos; // current write position within slab private int _slabEnd; // end of usable area in slab public SlabTransportWriter(int slabSize = 256, int seed = 42) { _slabSize = slabSize; _rng = new Random(seed); } /// Total bytes committed via Advance. public int WrittenCount => _totalCommitted; /// Number of slab segments allocated. public int SlabCount { get; private set; } public void Advance(int count) { if (_currentSlab == null) throw new InvalidOperationException("Call GetMemory/GetSpan before Advance"); if (count < 0 || _writePos + count > _slabEnd) throw new InvalidOperationException( $"Advance({count}) invalid: writePos={_writePos}, slabEnd={_slabEnd}, remaining={_slabEnd - _writePos}"); _segments.Add(new CommittedSegment(_currentSlab, _writePos, count)); _writePos += count; _totalCommitted += count; } /// /// Returns MemoryManager-backed Memory so that TryGetArray ALWAYS fails. /// May return fewer bytes than sizeHint (remaining slab space) — legal per IBufferWriter contract. /// This forces BWO to rent from ArrayPool (owned-buffer path), matching production behavior. /// public Memory GetMemory(int sizeHint = 0) { sizeHint = Math.Max(1, sizeHint); var remaining = _currentSlab != null ? _slabEnd - _writePos : 0; if (remaining <= 0) { // Allocate new slab — at least sizeHint to avoid starving the caller AllocateNewSlab(Math.Max(sizeHint, _slabSize)); remaining = _slabEnd - _writePos; } // Return MemoryManager-backed Memory: TryGetArray will fail return new SlabMemoryManager(_currentSlab!, _writePos, remaining).Memory; } /// /// Returns Span with at least sizeHint bytes. /// Used by FlushOwnedBuffer: _writer.GetSpan(bytesInChunk) must be large enough for CopyTo. /// Allocates new slab if remaining space is insufficient. /// public Span GetSpan(int sizeHint = 0) { sizeHint = Math.Max(1, sizeHint); var remaining = _currentSlab != null ? _slabEnd - _writePos : 0; if (remaining < sizeHint) { AllocateNewSlab(Math.Max(sizeHint, _slabSize)); remaining = _slabEnd - _writePos; } return _currentSlab.AsSpan(_writePos, remaining); } private void AllocateNewSlab(int minSize) { // Random offset within slab — simulates Kestrel slab allocator non-zero offsets var offset = _rng.Next(0, Math.Max(1, _slabSize / 4)); // ±12% size jitter for variety var jitter = _rng.Next(-_slabSize / 8, _slabSize / 8 + 1); var actualSize = Math.Max(minSize, _slabSize + jitter); _currentSlab = new byte[actualSize + offset]; _writePos = offset; _slabEnd = offset + actualSize; SlabCount++; } /// /// Get all committed bytes as a contiguous array. /// public byte[] ToArray() { var result = new byte[_totalCommitted]; var pos = 0; foreach (var seg in _segments) { Buffer.BlockCopy(seg.Array, seg.Offset, result, pos, seg.Length); pos += seg.Length; } return result; } /// /// Build a multi-segment ReadOnlySequence from committed data, splitting at slab boundaries. /// Each committed segment becomes a separate ReadOnlySequence segment. /// public ReadOnlySequence ToReadOnlySequence() { if (_segments.Count == 0) return ReadOnlySequence.Empty; if (_segments.Count == 1) { var seg = _segments[0]; return new ReadOnlySequence(seg.Array, seg.Offset, seg.Length); } // Build linked segment list var first = new MemorySegment(new ReadOnlyMemory(_segments[0].Array, _segments[0].Offset, _segments[0].Length)); var current = first; for (var i = 1; i < _segments.Count; i++) { var seg = _segments[i]; current = current.Append(new ReadOnlyMemory(seg.Array, seg.Offset, seg.Length)); } return new ReadOnlySequence(first, 0, current, current.Memory.Length); } /// /// Verify that TryGetArray fails on our Memory (sanity check for test correctness). /// public static void VerifyMemoryManagerBacked() { var writer = new SlabTransportWriter(64); var mem = writer.GetMemory(16); if (MemoryMarshal.TryGetArray(mem, out ArraySegment _)) throw new InvalidOperationException( "SlabTransportWriter.GetMemory returned array-backed Memory — TryGetArray should fail!"); } private record struct CommittedSegment(byte[] Array, int Offset, int Length); /// /// MemoryManager that wraps an array region but returns Memory where TryGetArray fails. /// This is the key difference from production Kestrel PinnedBlockMemoryPool. /// private sealed class SlabMemoryManager : MemoryManager { private readonly byte[] _array; private readonly int _offset; private readonly int _length; public SlabMemoryManager(byte[] array, int offset, int length) { _array = array; _offset = offset; _length = length; } public override Span GetSpan() => _array.AsSpan(_offset, _length); public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException("SlabMemoryManager does not support pinning"); public override void Unpin() => throw new NotSupportedException("SlabMemoryManager does not support pinning"); protected override void Dispose(bool disposing) { } } /// /// ReadOnlySequenceSegment for building multi-segment sequences from committed data. /// private sealed class MemorySegment : ReadOnlySequenceSegment { public MemorySegment(ReadOnlyMemory memory) { Memory = memory; } public MemorySegment Append(ReadOnlyMemory memory) { var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length }; Next = next; return next; } } }