Refactor test protocol to use slab-like 256B pipe segments
TestMultiSegmentProtocol now uses a custom MemoryPool to simulate Kestrel's slab allocator with 256-byte segments for both writing and reading. This replaces manual multi-segment sequence creation with a real Pipe backed by SlabSimulatingPool, ensuring more realistic segment boundaries and offsets. Old helpers were removed, and comments updated to clarify the improved simulation of production SignalR/Kestrel pipe behavior.
This commit is contained in:
parent
d060508bd8
commit
7b1bce711e
|
|
@ -8,24 +8,30 @@ using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test protocol that simulates production Kestrel pipe behavior.
|
/// Test protocol that simulates production Kestrel pipe behavior with 256-byte segments.
|
||||||
///
|
///
|
||||||
/// Write side: uses Pipe (not ArrayBufferWriter) so GetSpan/GetMemory return stable slab segments
|
/// Production: SignalR → WriteMessage(PipeWriter) → Kestrel slab 4096-byte segments → PipeReader → TryParseMessage
|
||||||
/// — matching Kestrel's memory pool behavior. This ensures Span back-patching for length prefixes works.
|
/// Test: SignalR → WriteMessage(PipeWriter) → FixedSizePool 256-byte segments → PipeReader → TryParseMessage
|
||||||
///
|
///
|
||||||
/// Read side: splits the serialized bytes into 256-byte segments before parsing,
|
/// Both sides go through a real Pipe with fixed-size memory segments,
|
||||||
/// exercising SequenceBinaryInput cross-boundary reads at every boundary.
|
/// exercising BWO chunk writes and SequenceBinaryInput cross-boundary reads.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
|
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
|
||||||
{
|
{
|
||||||
private const int SegmentSize = 256;
|
private const int SegmentSize = 256;
|
||||||
|
|
||||||
|
public TestMultiSegmentProtocol()
|
||||||
|
{
|
||||||
|
Options.BufferWriterChunkSize = SegmentSize;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serialize via Pipe (production-like stable memory blocks) instead of ArrayBufferWriter.
|
/// Write side: WriteMessage → PipeWriter backed by 256-byte pool segments.
|
||||||
|
/// Same code path as production, just smaller segments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public new ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
|
public new ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
|
||||||
{
|
{
|
||||||
var pipe = new Pipe();
|
var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
|
||||||
WriteMessage(message, pipe.Writer);
|
WriteMessage(message, pipe.Writer);
|
||||||
pipe.Writer.Complete();
|
pipe.Writer.Complete();
|
||||||
pipe.Reader.TryRead(out var result);
|
pipe.Reader.TryRead(out var result);
|
||||||
|
|
@ -35,47 +41,62 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Split input into 256-byte segments before parsing — forces multi-segment ReadOnlySequence
|
/// Read side: fill PipeWriter 256 bytes at a time → PipeReader gives multi-segment sequence.
|
||||||
/// through SequenceBinaryInput, exercising cross-boundary reads on every test.
|
/// Same as production Kestrel PipeReader delivering 4096-byte segments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder,
|
public override bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder,
|
||||||
[NotNullWhen(true)] out HubMessage? message)
|
[NotNullWhen(true)] out HubMessage? message)
|
||||||
{
|
{
|
||||||
var multiSegment = CreateMultiSegmentSequence(input, SegmentSize);
|
var bytes = input.ToArray();
|
||||||
return base.TryParseMessage(ref multiSegment, binder, out message);
|
var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
|
||||||
}
|
var writer = pipe.Writer;
|
||||||
|
|
||||||
private static ReadOnlySequence<byte> CreateMultiSegmentSequence(ReadOnlySequence<byte> source, int chunkSize)
|
// Write in chunks — GetMemory may return less than requested (like Kestrel slab)
|
||||||
|
var remaining = bytes.AsSpan();
|
||||||
|
while (remaining.Length > 0)
|
||||||
{
|
{
|
||||||
var bytes = source.ToArray();
|
var mem = writer.GetMemory(Math.Min(SegmentSize, remaining.Length));
|
||||||
|
var chunk = Math.Min(mem.Length, remaining.Length);
|
||||||
|
remaining[..chunk].CopyTo(mem.Span);
|
||||||
|
writer.Advance(chunk);
|
||||||
|
remaining = remaining[chunk..];
|
||||||
|
}
|
||||||
|
|
||||||
// Each segment gets its own byte[] — matching Kestrel pool slab behavior
|
writer.Complete();
|
||||||
// where each pipe segment is a separate memory block.
|
pipe.Reader.TryRead(out var result);
|
||||||
var firstChunk = new byte[Math.Min(chunkSize, bytes.Length)];
|
var seq = result.Buffer;
|
||||||
Buffer.BlockCopy(bytes, 0, firstChunk, 0, firstChunk.Length);
|
var success = base.TryParseMessage(ref seq, binder, out message);
|
||||||
var first = new MemorySegment(firstChunk);
|
pipe.Reader.Complete();
|
||||||
var current = first;
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
|
/// <summary>
|
||||||
|
/// MemoryPool that returns <paramref name="segmentSize"/>-byte blocks at random offsets
|
||||||
|
/// within a larger backing array — simulating Kestrel's slab allocator where segments
|
||||||
|
/// share a large slab and have non-zero offsets.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class SlabSimulatingPool(int segmentSize) : MemoryPool<byte>
|
||||||
{
|
{
|
||||||
var length = Math.Min(chunkSize, bytes.Length - offset);
|
private readonly Random _rng = new(42); // deterministic seed for reproducibility
|
||||||
var chunk = new byte[length];
|
|
||||||
Buffer.BlockCopy(bytes, offset, chunk, 0, length);
|
|
||||||
current = current.Append(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReadOnlySequence<byte>(first, 0, current, current.Memory.Length);
|
public override int MaxBufferSize => segmentSize;
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MemorySegment : ReadOnlySequenceSegment<byte>
|
public override IMemoryOwner<byte> Rent(int minBufferSize = -1)
|
||||||
{
|
{
|
||||||
public MemorySegment(ReadOnlyMemory<byte> memory) => Memory = memory;
|
var size = Math.Max(minBufferSize, segmentSize);
|
||||||
|
var offset = _rng.Next(0, segmentSize); // random slab offset
|
||||||
|
var jitter = _rng.Next(-1, 2); // -1, 0, or +1
|
||||||
|
var actualSize = Math.Max(1, size + jitter); // random segment size variance
|
||||||
|
var array = new byte[actualSize + offset];
|
||||||
|
return new Owner(array, offset, actualSize);
|
||||||
|
}
|
||||||
|
|
||||||
public MemorySegment Append(ReadOnlyMemory<byte> memory)
|
protected override void Dispose(bool disposing) { }
|
||||||
|
|
||||||
|
private sealed class Owner(byte[] array, int offset, int length) : IMemoryOwner<byte>
|
||||||
{
|
{
|
||||||
var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length };
|
public Memory<byte> Memory { get; } = array.AsMemory(offset, length);
|
||||||
Next = next;
|
public void Dispose() { }
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue