216 lines
7.8 KiB
C#
216 lines
7.8 KiB
C#
using System.Buffers;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace AyCode.Services.Server.Tests.SignalRs;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
internal sealed class SlabTransportWriter : IBufferWriter<byte>
|
|
{
|
|
private readonly int _slabSize;
|
|
private readonly Random _rng;
|
|
|
|
// Committed data tracking
|
|
private readonly List<CommittedSegment> _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);
|
|
}
|
|
|
|
/// <summary>Total bytes committed via Advance.</summary>
|
|
public int WrittenCount => _totalCommitted;
|
|
|
|
/// <summary>Number of slab segments allocated.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public Memory<byte> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public Span<byte> 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++;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all committed bytes as a contiguous array.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a multi-segment ReadOnlySequence from committed data, splitting at slab boundaries.
|
|
/// Each committed segment becomes a separate ReadOnlySequence segment.
|
|
/// </summary>
|
|
public ReadOnlySequence<byte> ToReadOnlySequence()
|
|
{
|
|
if (_segments.Count == 0)
|
|
return ReadOnlySequence<byte>.Empty;
|
|
|
|
if (_segments.Count == 1)
|
|
{
|
|
var seg = _segments[0];
|
|
return new ReadOnlySequence<byte>(seg.Array, seg.Offset, seg.Length);
|
|
}
|
|
|
|
// Build linked segment list
|
|
var first = new MemorySegment(new ReadOnlyMemory<byte>(_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<byte>(seg.Array, seg.Offset, seg.Length));
|
|
}
|
|
|
|
return new ReadOnlySequence<byte>(first, 0, current, current.Memory.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that TryGetArray fails on our Memory (sanity check for test correctness).
|
|
/// </summary>
|
|
public static void VerifyMemoryManagerBacked()
|
|
{
|
|
var writer = new SlabTransportWriter(64);
|
|
var mem = writer.GetMemory(16);
|
|
if (MemoryMarshal.TryGetArray(mem, out ArraySegment<byte> _))
|
|
throw new InvalidOperationException(
|
|
"SlabTransportWriter.GetMemory returned array-backed Memory — TryGetArray should fail!");
|
|
}
|
|
|
|
private record struct CommittedSegment(byte[] Array, int Offset, int Length);
|
|
|
|
/// <summary>
|
|
/// MemoryManager that wraps an array region but returns Memory where TryGetArray fails.
|
|
/// This is the key difference from production Kestrel PinnedBlockMemoryPool.
|
|
/// </summary>
|
|
private sealed class SlabMemoryManager : MemoryManager<byte>
|
|
{
|
|
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<byte> 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) { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// ReadOnlySequenceSegment for building multi-segment sequences from committed data.
|
|
/// </summary>
|
|
private sealed class MemorySegment : ReadOnlySequenceSegment<byte>
|
|
{
|
|
public MemorySegment(ReadOnlyMemory<byte> memory)
|
|
{
|
|
Memory = memory;
|
|
}
|
|
|
|
public MemorySegment Append(ReadOnlyMemory<byte> memory)
|
|
{
|
|
var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length };
|
|
Next = next;
|
|
return next;
|
|
}
|
|
}
|
|
}
|