diff --git a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
index c9d3aec..446e507 100644
--- a/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs
@@ -8,24 +8,30 @@ using Microsoft.AspNetCore.SignalR.Protocol;
namespace AyCode.Services.Server.Tests.SignalRs;
///
-/// 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
-/// — matching Kestrel's memory pool behavior. This ensures Span back-patching for length prefixes works.
+/// Production: SignalR → WriteMessage(PipeWriter) → Kestrel slab 4096-byte segments → PipeReader → TryParseMessage
+/// Test: SignalR → WriteMessage(PipeWriter) → FixedSizePool 256-byte segments → PipeReader → TryParseMessage
///
-/// Read side: splits the serialized bytes into 256-byte segments before parsing,
-/// exercising SequenceBinaryInput cross-boundary reads at every boundary.
+/// Both sides go through a real Pipe with fixed-size memory segments,
+/// exercising BWO chunk writes and SequenceBinaryInput cross-boundary reads.
///
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
{
private const int SegmentSize = 256;
+ public TestMultiSegmentProtocol()
+ {
+ Options.BufferWriterChunkSize = SegmentSize;
+ }
+
///
- /// 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.
///
public new ReadOnlyMemory GetMessageBytes(HubMessage message)
{
- var pipe = new Pipe();
+ var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
WriteMessage(message, pipe.Writer);
pipe.Writer.Complete();
pipe.Reader.TryRead(out var result);
@@ -35,47 +41,62 @@ internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
}
///
- /// Split input into 256-byte segments before parsing — forces multi-segment ReadOnlySequence
- /// through SequenceBinaryInput, exercising cross-boundary reads on every test.
+ /// Read side: fill PipeWriter 256 bytes at a time → PipeReader gives multi-segment sequence.
+ /// Same as production Kestrel PipeReader delivering 4096-byte segments.
///
public override bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder,
[NotNullWhen(true)] out HubMessage? message)
{
- var multiSegment = CreateMultiSegmentSequence(input, SegmentSize);
- return base.TryParseMessage(ref multiSegment, binder, out message);
- }
+ var bytes = input.ToArray();
+ var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
+ var writer = pipe.Writer;
- private static ReadOnlySequence CreateMultiSegmentSequence(ReadOnlySequence source, int chunkSize)
- {
- var bytes = source.ToArray();
-
- // Each segment gets its own byte[] — matching Kestrel pool slab behavior
- // where each pipe segment is a separate memory block.
- var firstChunk = new byte[Math.Min(chunkSize, bytes.Length)];
- Buffer.BlockCopy(bytes, 0, firstChunk, 0, firstChunk.Length);
- var first = new MemorySegment(firstChunk);
- var current = first;
-
- for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
+ // Write in chunks — GetMemory may return less than requested (like Kestrel slab)
+ var remaining = bytes.AsSpan();
+ while (remaining.Length > 0)
{
- var length = Math.Min(chunkSize, bytes.Length - offset);
- var chunk = new byte[length];
- Buffer.BlockCopy(bytes, offset, chunk, 0, length);
- current = current.Append(chunk);
+ 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..];
}
- return new ReadOnlySequence(first, 0, current, current.Memory.Length);
+ writer.Complete();
+ pipe.Reader.TryRead(out var result);
+ var seq = result.Buffer;
+ var success = base.TryParseMessage(ref seq, binder, out message);
+ pipe.Reader.Complete();
+ return success;
}
- private sealed class MemorySegment : ReadOnlySequenceSegment
+ ///
+ /// MemoryPool that returns -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.
+ ///
+ private sealed class SlabSimulatingPool(int segmentSize) : MemoryPool
{
- public MemorySegment(ReadOnlyMemory memory) => Memory = memory;
+ private readonly Random _rng = new(42); // deterministic seed for reproducibility
- public MemorySegment Append(ReadOnlyMemory memory)
+ public override int MaxBufferSize => segmentSize;
+
+ public override IMemoryOwner Rent(int minBufferSize = -1)
{
- var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length };
- Next = next;
- return next;
+ 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);
+ }
+
+ protected override void Dispose(bool disposing) { }
+
+ private sealed class Owner(byte[] array, int offset, int length) : IMemoryOwner
+ {
+ public Memory Memory { get; } = array.AsMemory(offset, length);
+ public void Dispose() { }
}
}
}