Refactor BenchmarkTestDataProvider for flexibility & clarity
Moved BenchmarkTestDataProvider and TestDataSet to AyCode.Core.Tests.TestModels with public accessibility. Refactored dataset creation methods to accept a resetId parameter, allowing control over TestDataFactory ID resets. Improved code structure, formatting, and documentation for maintainability. The provider is now more flexible and easier to use in tests.
This commit is contained in:
parent
accb38cf75
commit
26c8cd85ce
|
|
@ -1,23 +1,22 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console;
|
namespace AyCode.Core.Tests.TestModels;
|
||||||
|
|
||||||
internal static class BenchmarkTestDataProvider
|
public static class BenchmarkTestDataProvider
|
||||||
{
|
{
|
||||||
internal static List<TestDataSet> CreateTestDataSets()
|
public static List<TestDataSet> CreateTestDataSets(bool resetId = true)
|
||||||
{
|
{
|
||||||
return new List<TestDataSet>
|
return new List<TestDataSet>
|
||||||
{
|
{
|
||||||
CreateSmallTestData(),
|
CreateSmallTestData(resetId),
|
||||||
CreateMediumTestData(),
|
CreateMediumTestData(resetId),
|
||||||
CreateLargeTestData(),
|
CreateLargeTestData(resetId),
|
||||||
CreateRepeatedStringsTestData(),
|
CreateRepeatedStringsTestData(resetId),
|
||||||
CreateDeepNestedTestData()
|
CreateDeepNestedTestData(resetId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TestOrder CreateProfilerOrder()
|
public static TestOrder CreateProfilerOrder()
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
|
|
@ -31,9 +30,9 @@ internal static class BenchmarkTestDataProvider
|
||||||
sharedUser: sharedUser);
|
sharedUser: sharedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateSmallTestData()
|
private static TestDataSet CreateSmallTestData(bool resetId = true)
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
if (resetId) TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
|
|
@ -51,9 +50,9 @@ internal static class BenchmarkTestDataProvider
|
||||||
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
|
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateMediumTestData()
|
private static TestDataSet CreateMediumTestData(bool resetId = true)
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
if (resetId) TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
|
|
@ -83,9 +82,9 @@ internal static class BenchmarkTestDataProvider
|
||||||
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
|
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateLargeTestData()
|
private static TestDataSet CreateLargeTestData(bool resetId = true)
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
if (resetId) TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
|
|
@ -113,9 +112,9 @@ internal static class BenchmarkTestDataProvider
|
||||||
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
|
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateRepeatedStringsTestData()
|
private static TestDataSet CreateRepeatedStringsTestData(bool resetId = true)
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
if (resetId) TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
|
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("repeateduser");
|
var sharedUser = TestDataFactory.CreateUser("repeateduser");
|
||||||
|
|
@ -149,9 +148,9 @@ internal static class BenchmarkTestDataProvider
|
||||||
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
|
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateDeepNestedTestData()
|
private static TestDataSet CreateDeepNestedTestData(bool resetId = true)
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
if (resetId) TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
var sharedTag = TestDataFactory.CreateTag("DeepTag");
|
var sharedTag = TestDataFactory.CreateTag("DeepTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("deepuser");
|
var sharedUser = TestDataFactory.CreateUser("deepuser");
|
||||||
|
|
@ -207,7 +206,7 @@ internal static class BenchmarkTestDataProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class TestDataSet
|
public sealed class TestDataSet
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public TestOrder Order { get; }
|
public TestOrder Order { get; }
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -140,10 +141,21 @@ public static partial class AcBinaryDeserializer
|
||||||
var flags = ints[3];
|
var flags = ints[3];
|
||||||
var isNegative = (flags & unchecked((int)0x80000000)) != 0;
|
var isNegative = (flags & unchecked((int)0x80000000)) != 0;
|
||||||
var scale = (byte)((flags >> 16) & 0x7F);
|
var scale = (byte)((flags >> 16) & 0x7F);
|
||||||
|
LogDecimalDrift(scale);
|
||||||
_position += 16;
|
_position += 16;
|
||||||
return new decimal(lo, mid, hi, isNegative, scale);
|
return new decimal(lo, mid, hi, isNegative, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private void LogDecimalDrift(byte scale)
|
||||||
|
{
|
||||||
|
if (scale <= 28) return;
|
||||||
|
var hex = BitConverter.ToString(_buffer, _position, Math.Min(16, _bufferLength - _position));
|
||||||
|
throw new AcBinaryDeserializationException(
|
||||||
|
$"[DECIMAL_DRIFT] scale={scale}, pos={_position}, bufLen={_bufferLength}, " +
|
||||||
|
$"bufArray={_buffer.Length}, hex={hex}", _position);
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public DateTime ReadDateTimeUnsafe()
|
public DateTime ReadDateTimeUnsafe()
|
||||||
{
|
{
|
||||||
|
|
@ -444,7 +456,17 @@ public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, length))
|
if (!Input.TryAdvanceSegment(ref _buffer, ref _position, ref _bufferLength, length))
|
||||||
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
|
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
|
||||||
}
|
AssertGuarantee(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private void AssertGuarantee(int needed)
|
||||||
|
{
|
||||||
|
if (_bufferLength - _position < needed)
|
||||||
|
throw new AcBinaryDeserializationException(
|
||||||
|
$"[GUARANTEE_VIOLATED] TryAdvanceSegment returned true but available={_bufferLength - _position} < needed={needed}, " +
|
||||||
|
$"pos={_position}, bufLen={_bufferLength}, bufArray={_buffer.Length}", _position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,18 @@ public struct SequenceBinaryInput : IBinaryInputBase
|
||||||
var remaining = bufferLength - position;
|
var remaining = bufferLength - position;
|
||||||
|
|
||||||
if (remaining > 0 && remaining < needed)
|
if (remaining > 0 && remaining < needed)
|
||||||
{
|
|
||||||
// Cross-boundary: value spans segment boundary
|
|
||||||
return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining);
|
return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining);
|
||||||
}
|
|
||||||
|
|
||||||
// Current segment fully consumed — advance to next
|
// Current segment fully consumed — advance to next
|
||||||
return TryLoadNextSegment(ref buffer, ref position, ref bufferLength);
|
if (!TryLoadNextSegment(ref buffer, ref position, ref bufferLength))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Loaded segment smaller than needed — cross-boundary into subsequent segments
|
||||||
|
remaining = bufferLength - position;
|
||||||
|
if (remaining < needed)
|
||||||
|
return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1046,6 +1046,30 @@ public abstract class SignalRClientToHubTestBase
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Large Dataset Tests
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task RoundTrip_LargeOrderList_PreservesAllData()
|
||||||
|
{
|
||||||
|
TestDataFactory.ResetIdCounter();
|
||||||
|
var dataSets = BenchmarkTestDataProvider.CreateTestDataSets(resetId: false);
|
||||||
|
var orders = dataSets.Select(ds => ds.Order).ToList();
|
||||||
|
|
||||||
|
var result = await _client.PostDataAsync<List<TestOrder>, List<TestOrder>>(
|
||||||
|
TestSignalRTags.TestOrderListParam, orders);
|
||||||
|
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(orders.Count, result.Count);
|
||||||
|
for (int i = 0; i < orders.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(orders[i].Id, result[i].Id);
|
||||||
|
Assert.AreEqual(orders[i].OrderNumber, result[i].OrderNumber);
|
||||||
|
Assert.AreEqual(orders[i].Items.Count, result[i].Items.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
|
|
@ -7,32 +8,60 @@ using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test protocol that forces multi-segment ReadOnlySequence parsing.
|
/// Test protocol that simulates production Kestrel pipe behavior.
|
||||||
/// Splits serialized bytes into chunks before calling base.TryParseMessage,
|
///
|
||||||
/// exercising SequenceBinaryInput cross-boundary reads and SequenceToByteArray multi-segment paths.
|
/// 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.
|
||||||
|
///
|
||||||
|
/// Read side: splits the serialized bytes into 256-byte segments before parsing,
|
||||||
|
/// exercising SequenceBinaryInput cross-boundary reads at every boundary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
|
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
|
||||||
{
|
{
|
||||||
private const int SegmentSize = 4096;
|
private const int SegmentSize = 256;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize via Pipe (production-like stable memory blocks) instead of ArrayBufferWriter.
|
||||||
|
/// </summary>
|
||||||
|
public new ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
|
||||||
|
{
|
||||||
|
var pipe = new Pipe();
|
||||||
|
WriteMessage(message, pipe.Writer);
|
||||||
|
pipe.Writer.Complete();
|
||||||
|
pipe.Reader.TryRead(out var result);
|
||||||
|
var bytes = result.Buffer.ToArray();
|
||||||
|
pipe.Reader.Complete();
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split input into 256-byte segments before parsing — forces multi-segment ReadOnlySequence
|
||||||
|
/// through SequenceBinaryInput, exercising cross-boundary reads on every test.
|
||||||
|
/// </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)
|
||||||
{
|
{
|
||||||
// Temporarily bypass multi-segment to isolate the issue
|
var multiSegment = CreateMultiSegmentSequence(input, SegmentSize);
|
||||||
return base.TryParseMessage(ref input, binder, out message);
|
return base.TryParseMessage(ref multiSegment, binder, out message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReadOnlySequence<byte> CreateMultiSegmentSequence(ReadOnlySequence<byte> source, int chunkSize)
|
private static ReadOnlySequence<byte> CreateMultiSegmentSequence(ReadOnlySequence<byte> source, int chunkSize)
|
||||||
{
|
{
|
||||||
var bytes = source.ToArray();
|
var bytes = source.ToArray();
|
||||||
|
|
||||||
var first = new MemorySegment(bytes.AsMemory(0, Math.Min(chunkSize, bytes.Length)));
|
// 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;
|
var current = first;
|
||||||
|
|
||||||
for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
|
for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
|
||||||
{
|
{
|
||||||
var length = Math.Min(chunkSize, bytes.Length - offset);
|
var length = Math.Min(chunkSize, bytes.Length - offset);
|
||||||
current = current.Append(bytes.AsMemory(offset, length));
|
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);
|
return new ReadOnlySequence<byte>(first, 0, current, current.Memory.Length);
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,16 @@ public class AcBinaryHubProtocol : IHubProtocol
|
||||||
[Conditional("DEBUG")]
|
[Conditional("DEBUG")]
|
||||||
private static void LogDiagnostic(string message) => DiagnosticLogger?.Invoke(message);
|
private static void LogDiagnostic(string message) => DiagnosticLogger?.Invoke(message);
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void LogReadSingleArgument(ReadOnlySequence<byte> argSlice, int argLength, Type targetType)
|
||||||
|
{
|
||||||
|
if (DiagnosticLogger == null) return;
|
||||||
|
var segmentCount = 0;
|
||||||
|
foreach (var _ in argSlice)
|
||||||
|
segmentCount++;
|
||||||
|
DiagnosticLogger($"[AcBinaryHubProtocol] ReadSingleArgument: argLength={argLength}, isSingleSegment={argSlice.IsSingleSegment}, segments={segmentCount}, type={targetType.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
[Conditional("DEBUG")]
|
||||||
private static void LogParseInvocation(string target, IReadOnlyList<Type> paramTypes, long remaining)
|
private static void LogParseInvocation(string target, IReadOnlyList<Type> paramTypes, long remaining)
|
||||||
{
|
{
|
||||||
|
|
@ -455,6 +465,8 @@ public class AcBinaryHubProtocol : IHubProtocol
|
||||||
var argSlice = r.UnreadSequence.Slice(0, argLength);
|
var argSlice = r.UnreadSequence.Slice(0, argLength);
|
||||||
r.Advance(argLength);
|
r.Advance(argLength);
|
||||||
|
|
||||||
|
LogReadSingleArgument(argSlice, argLength, targetType);
|
||||||
|
|
||||||
// byte[] fast-path: first byte is BinaryTypeCode.ByteArray tag →
|
// byte[] fast-path: first byte is BinaryTypeCode.ByteArray tag →
|
||||||
// strip tag + VarUInt length prefix, return raw payload. No deserializer.
|
// strip tag + VarUInt length prefix, return raw payload. No deserializer.
|
||||||
var argReader = new SequenceReader<byte>(argSlice);
|
var argReader = new SequenceReader<byte>(argSlice);
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ namespace AyCode.Services.SignalRs
|
||||||
if (useAcBinaryProtocol)
|
if (useAcBinaryProtocol)
|
||||||
{
|
{
|
||||||
hubBuilder.Services.AddSingleton<IHubProtocol, AyCodeBinaryHubProtocol>();
|
hubBuilder.Services.AddSingleton<IHubProtocol, AyCodeBinaryHubProtocol>();
|
||||||
|
AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
HubConnection = hubBuilder.Build();
|
HubConnection = hubBuilder.Build();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue