From 027ff6bd498e59ed7b4d28b2fb6c64dd8e739bc2 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 13 May 2026 13:54:53 +0200 Subject: [PATCH] Refactor benchmark infra: generic, multi-variant test data Refactored the benchmark and test data infrastructure to use generic, type-safe, and multi-variant models. Introduced generic base classes for the test data hierarchy and factories, with closing-generic aliases for _All_True and _All_False families. Benchmarks now select the correct test data variant per serializer options, and all serializers are generic over the order type. Output and result reporting now include the CLR type name for clarity. Centralized string property handling and improved documentation throughout. --- .../BenchmarkLoop.cs | 143 ++++- .../BenchmarkResult.cs | 9 + .../Benchmarks/AcBinaryBenchmark.cs | 13 +- .../AcBinaryBufferWriterBenchmark.cs | 13 +- .../AcBinaryFreshBufferWriterBenchmark.cs | 13 +- .../AcBinaryInMemoryPipeBenchmark.cs | 13 +- .../AcBinaryInMemoryRawByteArrayBenchmark.cs | 13 +- .../Benchmarks/AcBinaryNamedPipeBenchmark.cs | 13 +- .../AcBinaryNamedPipeRawByteArrayBenchmark.cs | 13 +- .../Benchmarks/ISerializerBenchmark.cs | 14 + .../Benchmarks/MemoryPackBenchmark.cs | 13 +- .../MemoryPackBufferWriterBenchmark.cs | 13 +- .../MemoryPackFreshBufferWriterBenchmark.cs | 13 +- .../Benchmarks/MessagePackBenchmark.cs | 13 +- .../Benchmarks/SystemTextJsonBenchmark.cs | 13 +- AyCode.Core.Serializers.Console/Output.cs | 35 +- ...SerializerSGenRuntimeCompatibilityTests.cs | 7 +- .../TestModels/BenchmarkTestDataProvider.cs | 607 +++++++++++------- .../TestModels/SharedTestBaseModels.cs | 104 +-- .../TestModels/SharedTestOrderModels.cs | 182 +++--- .../TestModels/TestDataFactory.cs | 177 +++-- 21 files changed, 905 insertions(+), 529 deletions(-) diff --git a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs index dffc596..11f2a17 100644 --- a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs +++ b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Console.Benchmarks; using AyCode.Core.Tests.TestModels; using MemoryPack; @@ -73,7 +73,7 @@ internal static class BenchmarkLoop try { var allResults = new List(); - var allTestDataSets = BenchmarkTestDataProvider.CreateTestDataSets(); + var allTestDataSets = BuildMultiVariantTestDataSets(); var testDataSets = FilterByLayer(allTestDataSets, layer); System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | SerializerMode: {serializerMode} | Charset: {Configuration.GetCurrentCharsetName()} | Iterations: per-cell adaptive (~{Configuration.TargetSampleMs} ms target) | Warmup: {Configuration.WarmupIterations} per phase (Ser/Des isolated) | Samples: {Configuration.BenchmarkSamples} (median) + pilot discard"); @@ -196,6 +196,7 @@ internal static class BenchmarkLoop IoMode = serializer.IoMode, DispatchMode = serializer.DispatchMode, OptionsPreset = serializer.OptionsPreset, + OrderTypeName = serializer.OrderTypeName, OptionsDescription = serializer.OptionsDescription, SerializedSize = serializer.SerializedSize, SetupSerializeAllocBytes = serializer.SetupSerializeAllocBytes, @@ -293,8 +294,96 @@ internal static class BenchmarkLoop return results; } + /// + /// Phase 2 multi-variant test-data builder. Constructs each cell in both the _All_False and + /// _All_True families, then cross-registers _All_True on the _All_False primaries so the + /// CreateSerializers downstream can pick the matching variant per AcBinary options preset. + /// + /// + /// Memory cost: ~600 KB across 5 cells (Large dominates at ~340 KB for both variants). The two + /// families are built independently — same data values + same numeric sequence (per-family + /// _idCounter reset). MemPack/MsgPack benchmarks consume the _All_True variant canonically; + /// AcBinary's variant is preset-dependent (see CreateSerializers). + /// + private static List BuildMultiVariantTestDataSets() + { + var allFalse = BenchmarkTestDataProvider_All_False.CreateTestDataSets(); + var allTrue = BenchmarkTestDataProvider.CreateTestDataSets(); + + // Zip by ordinal — both providers emit the same 5 cells in the same order + // (Small / Medium / Large / Repeated / Deep), confirmed by their identical + // CreateTestDataSets call sequence on the generic base. + for (var i = 0; i < allFalse.Count; i++) + { + var falseDs = (TestDataSet)allFalse[i]; + var trueDs = (TestDataSet)allTrue[i]; + falseDs.RegisterVariant(trueDs.Order); + } + return allFalse; + } + + /// + /// Phase 2 variant dispatch rule for AcBinary: a preset uses TestOrder_All_False iff every + /// AcBinary "feature flag" is off (no string interning, no reference handling, no metadata, no + /// property filter). Any "true"-flagged feature promotes the benchmark to TestOrder_All_True + /// — the richer graph + opt-out attribute model exercises the feature's deduplication / dispatch + /// path on real shared-reference content. WireMode, SGen mode, and Compression are encoding-axis + /// options and intentionally NOT part of this decision (they don't change which graph shape is + /// meaningful to feed). + /// + private static bool UsesAllFalseVariant(AcBinarySerializerOptions options) => + options.UseStringInterning == StringInterningMode.None && + options.ReferenceHandling == ReferenceHandlingMode.None && + !options.UseMetadata && + options.PropertyFilter == null; + + // Per-class factory helpers — each returns ISerializerBenchmark closed over the variant T + // selected by UsesAllFalseVariant(options). Compile-time T at the new T() call site preserves + // SGen apples-to-apples (no runtime reflection, no type erasure across the JIT boundary). + private static ISerializerBenchmark MakeAcBinary(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryBenchmark(td.GetOrder(), opt, preset); + + private static ISerializerBenchmark MakeAcBinaryBufferWriter(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryBufferWriterBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryBufferWriterBenchmark(td.GetOrder(), opt, preset); + + private static ISerializerBenchmark MakeAcBinaryFreshBufferWriter(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryFreshBufferWriterBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryFreshBufferWriterBenchmark(td.GetOrder(), opt, preset); + + private static ISerializerBenchmark MakeAcBinaryNamedPipe(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryNamedPipeBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryNamedPipeBenchmark(td.GetOrder(), opt, preset); + + private static ISerializerBenchmark MakeAcBinaryNamedPipeRaw(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryNamedPipeRawByteArrayBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryNamedPipeRawByteArrayBenchmark(td.GetOrder(), opt, preset); + + private static ISerializerBenchmark MakeAcBinaryInMemoryPipe(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryInMemoryPipeBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryInMemoryPipeBenchmark(td.GetOrder(), opt, preset); + + private static ISerializerBenchmark MakeAcBinaryInMemoryRaw(TestDataSet td, AcBinarySerializerOptions opt, string preset) => + UsesAllFalseVariant(opt) + ? new AcBinaryInMemoryRawByteArrayBenchmark(td.GetOrder(), opt, preset) + : new AcBinaryInMemoryRawByteArrayBenchmark(td.GetOrder(), opt, preset); + private static List CreateSerializers(TestDataSet testData, SerializerSelectionMode serializerMode) { + // Phase 2 variant dispatch (refined): AcBinary picks variant per UsesAllFalseVariant(options). + // MemPack / MsgPack canonically use _All_False (no AcBinary opt-in/opt-out axis — both + // produce identical MemPack/MsgPack wire on either variant since their contract is family- + // agnostic). `orderFalse` is the cell primary; `orderTrue` is fetched on-demand by the AcBinary + // factory helpers when an options preset has a "true" flag. + var orderFalse = testData.GetOrder(); + // FastestByte mode — focused 1:1 comparison on the "fastest Byte[]" path. // TWO benchmarks: AcBinary FastMode Byte[] (Compact UTF-8) + MemoryPack Byte[]. // - Compact: smallest wire, UTF-8 encode/decode CPU cost vs MemPack head-to-head. @@ -310,9 +399,11 @@ internal static class BenchmarkLoop return new List { - new AcBinaryBenchmark(testData.Order, fastestByteOptions, "FastMode"), - //new AcBinaryBenchmark(testData.Order, fastWireOptions, "FastMode (FastWire)"), - new MemoryPackBenchmark(testData.Order, "Default"), + MakeAcBinary(testData, fastestByteOptions, "FastMode"), + //MakeAcBinary(testData, fastWireOptions, "FastMode (FastWire)"), + // MemPack canonically on _All_True (no AcBinary opt-in/opt-out axis applies; the MemoryPackable + // contract serialises identical bytes either way, but _All_True is the established baseline). + new MemoryPackBenchmark(orderFalse, "Default"), }; } @@ -337,13 +428,13 @@ internal static class BenchmarkLoop // Chunked-framed AsyncPipe: SerializeChunkedFramed + AsyncPipeReaderInput.DrainFromAsync. // Measures the FULL streaming-I/O stack — wire framing + drain task + sliding-window buffer + // MRES wait-on-byte-shortage — over a kernel NamedPipe. - new AcBinaryNamedPipeBenchmark(testData.Order, binaryFastModePipeChunkOnly, "FastMode (PipeChunk)"), + MakeAcBinaryNamedPipe(testData, binaryFastModePipeChunkOnly, "FastMode (PipeChunk)"), // Raw byte[] over NamedPipe (sync receive, no chunk-framing). Same kernel-pipe transport, // same inBufferSize, but: serialize → byte[] → Stream.Write → Stream.Read → Deserialize(byte[]). // No drain task, no AsyncPipeReaderInput, no [201][UINT16][data]…[202] framing. Side-by-side with // the chunked-row above this isolates AsyncPipe-framework-overhead (Δ vs raw) from // kernel-transport-overhead (raw vs in-process Byte[]). - new AcBinaryNamedPipeRawByteArrayBenchmark(testData.Order, binaryFastModePipeChunkOnly, "FastMode (PipeRaw)"), + MakeAcBinaryNamedPipeRaw(testData, binaryFastModePipeChunkOnly, "FastMode (PipeRaw)"), // Chunked-framed AsyncPipe over an IN-MEMORY System.IO.Pipelines.Pipe (NO NamedPipe, NO kernel). // Same chunked-streaming code path (SerializeChunkedFramed → AsyncPipeReaderInput) but with the // kernel-pipe replaced by a managed-only Pipe. Eliminates per-chunk syscall overhead (~30 µs/chunk @@ -352,11 +443,11 @@ internal static class BenchmarkLoop // in-memory Pipe row should be much closer to the raw-byte[] row, validating that NamedPipe loopback // is the worst-case benchmark scenario for chunked-streaming and not representative of real network // / file / cross-thread Pipe scenarios. - new AcBinaryInMemoryPipeBenchmark(testData.Order, binaryFastModePipeChunkOnly, "FastMode (PipeChunk)"), + MakeAcBinaryInMemoryPipe(testData, binaryFastModePipeChunkOnly, "FastMode (PipeChunk)"), // Raw byte[] over IN-MEMORY direct cross-thread handoff (no transport at all). Apples-to-apples // baseline for the in-memory chunked row above: same in-memory transport (zero kernel), but raw // byte[] vs chunked-streaming wire format. Completes the 2x2 matrix [chunked,raw] × [kernel,memory]. - new AcBinaryInMemoryRawByteArrayBenchmark(testData.Order, binaryFastModePipeChunkOnly, "FastMode (PipeRaw)"), + MakeAcBinaryInMemoryRaw(testData, binaryFastModePipeChunkOnly, "FastMode (PipeRaw)"), }; } @@ -405,39 +496,42 @@ internal static class BenchmarkLoop // AcBinary — Byte[] API (uncomment to compare option presets side-by-side) // ============================================================ // Fastest Byte[] — SGen path (UseGeneratedCode=true, default). - new AcBinaryBenchmark(testData.Order, binaryFastModeOption, "FastMode"), + MakeAcBinary(testData, binaryFastModeOption, "FastMode"), // Fastest Byte[] — Runtime path (UseGeneratedCode=false). Same wire/options, no source-generated dispatch. // Always paired with the SGen variant so every layer can compare the SGen speed-up apples-to-apples. // NativeAOT-safe: AcSerializerCommon.Create*Getter/Setter falls back to reflection-based delegates // when RuntimeFeature.IsDynamicCodeSupported is false (slower but works under AOT publish). - new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, "FastMode"), + MakeAcBinary(testData, binaryFastModeNoSgenOption, "FastMode"), // Default preset Byte[] — RefHandling=OnlyId (deduplicates IId-shared references on the wire) + // UseStringInterning=All (deduplicates repeated strings). Showcases the Default preset's wire-size // and CPU trade-off vs FastMode on the ~20% IId-ref / repeated-string test data. - new AcBinaryBenchmark(testData.Order, defaultOptions, "Default"), - //new AcBinaryBenchmark(testData.Order, binaryDefaultNoSgenOption, "Default"), - //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, "NoRef"), - //new AcBinaryBenchmark(testData.Order, binaryNoInternOption, "NoIntern"), + // Default preset (ReferenceHandling=OnlyId + StringInterning) → _All_True graph. + // Phase 2 variant-dispatch rule: any options preset with a "true"-flagged feature uses + // the _All_True family (rich graph, opt-out AcBinarySerializable attribute matches). + MakeAcBinary(testData, defaultOptions, "Default"), + //MakeAcBinary(testData, binaryDefaultNoSgenOption, "Default"), + //MakeAcBinary(testData, AcBinarySerializerOptions.WithoutReferenceHandling, "NoRef"), + //MakeAcBinary(testData, binaryNoInternOption, "NoIntern"), // AcBinary via IBufferWriter (reused ArrayBufferWriter — long-running service / batch scenario) - new AcBinaryBufferWriterBenchmark(testData.Order, binaryFastModeOption, "FastMode"), + MakeAcBinaryBufferWriter(testData, binaryFastModeOption, "FastMode"), // AcBinary via IBufferWriter (FRESH ArrayBufferWriter per call — one-shot scenario). // 4 KB chunk size from binaryFastModeBufWrChunk — minimises the per-call ArrayBufferWriter // allocation. Optimum for this scenario. - new AcBinaryFreshBufferWriterBenchmark(testData.Order, binaryFastModeBufWrChunk, "FastMode (4KB)"), + MakeAcBinaryFreshBufferWriter(testData, binaryFastModeBufWrChunk, "FastMode (4KB)"), // AcBinary chunked-streaming over an IN-MEMORY Pipe (no kernel transport). Side-by-side with the // Byte[] / IBufferWriter rows above this shows the chunked-streaming framework's pure CPU cost // (no NamedPipe loopback noise) vs the simpler in-process serialize-then-deserialize patterns. // The IO column shows "Pipe(in-mem)" — distinct from the NamedPipe AsyncPipe rows in [P] mode. - new AcBinaryInMemoryPipeBenchmark(testData.Order, binaryFastModePipeChunkInMem, "FastMode (PipeChunk)"), + MakeAcBinaryInMemoryPipe(testData, binaryFastModePipeChunkInMem, "FastMode (PipeChunk)"), // Raw byte[] over IN-MEMORY direct cross-thread handoff (no transport, no kernel, no Pipe). Apples-to- // apples baseline for the in-memory chunked row above: same in-memory pattern, but raw byte[] vs // chunked-streaming wire format. The IO column shows "Bytes(in-mem)". - new AcBinaryInMemoryRawByteArrayBenchmark(testData.Order, binaryFastModePipeChunkInMem, "FastMode (PipeRaw)"), + MakeAcBinaryInMemoryRaw(testData, binaryFastModePipeChunkInMem, "FastMode (PipeRaw)"), // AsyncPipe streaming over kernel NamedPipe (AcBinaryNamedPipeBenchmark) is intentionally OMITTED // here — run it via the dedicated AsyncPipe menu [P] / CLI mode for isolated kernel-transport @@ -446,9 +540,10 @@ internal static class BenchmarkLoop // ============================================================ // MemoryPack — three I/O modes for apples-to-apples comparison // ============================================================ - new MemoryPackBenchmark(testData.Order, "Default"), - new MemoryPackBufferWriterBenchmark(testData.Order, "Default"), - new MemoryPackFreshBufferWriterBenchmark(testData.Order, "Default"), + // MemPack canonically on _All_True (see FastestByte-mode comment above for rationale). + new MemoryPackBenchmark(orderFalse, "Default"), + new MemoryPackBufferWriterBenchmark(orderFalse, "Default"), + new MemoryPackFreshBufferWriterBenchmark(orderFalse, "Default"), // ============================================================ // MessagePack — for legacy comparison @@ -457,11 +552,11 @@ internal static class BenchmarkLoop // MessagePack v3's DynamicGenericResolver uses Activator.CreateInstance on trimmed // ListFormatter et al. — fails under NativeAOT publish with "No parameterless constructor". // Excluded from the AOT build; available for regular JIT runs only. - new MessagePackBenchmark(testData.Order, "ContractBased"), + new MessagePackBenchmark(orderFalse, "ContractBased"), #endif // System.Text.Json (commented — JSON serializer for reference; not in active suite) - //new SystemTextJsonBenchmark(testData.Order, "Default") + //new SystemTextJsonBenchmark(orderFalse, "Default") }; } diff --git a/AyCode.Core.Serializers.Console/BenchmarkResult.cs b/AyCode.Core.Serializers.Console/BenchmarkResult.cs index 6b60dbd..0ea10f8 100644 --- a/AyCode.Core.Serializers.Console/BenchmarkResult.cs +++ b/AyCode.Core.Serializers.Console/BenchmarkResult.cs @@ -15,6 +15,15 @@ internal sealed class BenchmarkResult public BenchmarkDispatchMode DispatchMode { get; set; } public string OptionsPreset { get; set; } = ""; + /// + /// CLR type name of the order graph serialised in this row (e.g. "TestOrder_All_False", + /// "TestOrder_All_True"). Captured from in + /// RunBenchmarksForTestData; surfaced in the SERIALIZER OPTIONS section of every output + /// (.log, .LLM, console) so the reader can correlate each preset with its TestOrder variant + /// without inflating the per-row tables with an extra column. + /// + public string OrderTypeName { get; set; } = ""; + /// True if Serialize() captures a full round-trip and Deserialize() is a no-op /// (single-use streaming transports like NamedPipe). Excluded from "Fastest Serialize" / "Fastest Deserialize" /// winners rankings; still ranked in "Fastest Round-trip". Display-side: Ser µs/op / SerAlloc / Des µs/op / DesAlloc diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs index 000280b..6f6db10 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Runtime.CompilerServices; @@ -8,22 +8,23 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// AcBinary benchmark, Byte[] I/O mode. The headline AcBinary row in every cell — compared /// against as the SOTA baseline. /// -internal sealed class AcBinaryBenchmark : ISerializerBenchmark +internal sealed class AcBinaryBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.ByteArray; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; public long SetupDeserializeAllocBytes => 0; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options); - public AcBinaryBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; _options = options; @@ -35,12 +36,12 @@ internal sealed class AcBinaryBenchmark : ISerializerBenchmark public void Serialize() => AcBinarySerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); + public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var bytes = AcBinarySerializer.Serialize(_order, _options); - var roundTripped = AcBinaryDeserializer.Deserialize(bytes, _options); + var roundTripped = AcBinaryDeserializer.Deserialize(bytes, _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs index 40db3e4..c8b6163 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Buffers; using System.Runtime.CompilerServices; @@ -9,9 +9,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Benchmarks AcBinary via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter. /// Realistic IBufferWriter usage pattern: caller owns + reuses the writer (zero alloc per call after warmup). /// -internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark +internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; private readonly ArrayBufferWriter _bufferWriter; @@ -19,13 +19,14 @@ internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrReuse; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } public long SetupDeserializeAllocBytes => 0; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options); - public AcBinaryBufferWriterBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryBufferWriterBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; _options = options; @@ -55,14 +56,14 @@ internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark // (the production-realistic surface for SignalR / Pipe consumers) rather than secretly testing // byte[] Deser under the BufWr label. [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_serialized), _options); + public void Deserialize() => AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_serialized), _options); public bool VerifyRoundTrip() { _bufferWriter.ResetWrittenCount(); AcBinarySerializer.Serialize(_order, _bufferWriter, _options); - var roundTripped = AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_bufferWriter.WrittenMemory), _options); + var roundTripped = AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_bufferWriter.WrittenMemory), _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs index e098f63..3aa20bd 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Buffers; using System.Runtime.CompilerServices; @@ -12,22 +12,23 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// otherwise AcBinary would request 64KB upfront via GetSpan(), forcing the fresh ABW to allocate 64KB /// regardless of payload size (heavy over-allocation for small payloads). /// -internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark +internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrNew; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; public long SetupDeserializeAllocBytes => 0; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options, $", BufferSize={_options.BufferWriterChunkSize}B"); - public AcBinaryFreshBufferWriterBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryFreshBufferWriterBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; // BufferWriterChunkSize comes from the caller (central source of truth in CreateSerializers @@ -51,13 +52,13 @@ internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark // (the production-realistic surface for SignalR / Pipe consumers) rather than secretly testing // byte[] Deser under the BufWr label. [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_serialized), _options); + public void Deserialize() => AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_serialized), _options); public bool VerifyRoundTrip() { var abw = new ArrayBufferWriter(); AcBinarySerializer.Serialize(_order, abw, _options); - var roundTripped = AcBinaryDeserializer.Deserialize(new ReadOnlySequence(abw.WrittenMemory), _options); + var roundTripped = AcBinaryDeserializer.Deserialize(new ReadOnlySequence(abw.WrittenMemory), _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs index c69bb92..20a0d28 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.Serialization; // DrainFromAsync extension (test-only, used by benchmark) using AyCode.Core.Tests.TestModels; using System.IO.Pipelines; @@ -24,9 +24,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// custom message brokers). The numbers from this row reflect that scenario, NOT the kernel-pipe loopback /// of the NamedPipe benchmark. /// -internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDisposable +internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDisposable where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; // for SerializedSize reporting only @@ -50,6 +50,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDis public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.InMemoryPipe; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -57,7 +58,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDis public bool IsRoundTripOnly => true; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options, $", BufferSize={_options.BufferWriterChunkSize}B, Transport=Pipe(in-memory,multiMessage,2-task)"); - public AcBinaryInMemoryPipeBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryInMemoryPipeBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; _options = options; @@ -106,7 +107,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDis try { - var result = AcBinaryDeserializer.Deserialize(_input, _options); + var result = AcBinaryDeserializer.Deserialize(_input, _options); if (_captureResult) _lastResult = result; } catch @@ -158,7 +159,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDis try { Serialize(); - var result = _lastResult as TestOrder_All_True; + var result = _lastResult as T; return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); } finally diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs index 9e13e9b..fd44e30 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Runtime.CompilerServices; @@ -21,9 +21,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Side-by-side with this isolates the kernel-NamedPipe /// overhead on the raw-byte[] side. /// -internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchmark, IDisposable +internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchmark, IDisposable where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; // for SerializedSize reporting only @@ -41,6 +41,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchma public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.InMemoryRaw; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -48,7 +49,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchma public bool IsRoundTripOnly => true; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options, $", BufferSize={_options.BufferWriterChunkSize}B, Transport=in-memory(raw,2-task)"); - public AcBinaryInMemoryRawByteArrayBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryInMemoryRawByteArrayBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; _options = options; @@ -89,7 +90,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchma var bytes = _pendingBytes; if (bytes != null) { - var result = AcBinaryDeserializer.Deserialize(bytes, _options); + var result = AcBinaryDeserializer.Deserialize(bytes, _options); if (_captureResult) _lastResult = result; } } @@ -142,7 +143,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchma try { Serialize(); - var result = _lastResult as TestOrder_All_True; + var result = _lastResult as T; return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); } finally diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs index 1442c7c..6127173 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.Serialization; // DrainFromAsync extension (test-only, used by benchmark) using AyCode.Core.Tests.TestModels; using System.IO.Pipelines; @@ -39,9 +39,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Approximation note: single-process loopback NamedPipe. Real cross-process / cross-machine SignalR /// adds further transport latency (TCP, WebSocket framing) on top. The benchmark gives a lower bound. /// -internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDisposable +internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDisposable where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; // for SerializedSize reporting only @@ -65,6 +65,7 @@ internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDispos public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.NamedPipe; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -72,7 +73,7 @@ internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDispos public bool IsRoundTripOnly => true; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options, $", BufferSize={_options.BufferWriterChunkSize}B, Transport=NamedPipe(long-lived,multiMessage,2-task)"); - public AcBinaryNamedPipeBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryNamedPipeBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; // BufferWriterChunkSize comes from the caller (central source of truth in CreateSerializers @@ -152,7 +153,7 @@ internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDispos try { - var result = AcBinaryDeserializer.Deserialize(_input, _options); + var result = AcBinaryDeserializer.Deserialize(_input, _options); if (_captureResult) _lastResult = result; } catch @@ -203,7 +204,7 @@ internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDispos try { Serialize(); - var result = _lastResult as TestOrder_All_True; + var result = _lastResult as T; return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); } finally diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs index cd4f6a1..a45c8d0 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.IO.Pipes; using System.Runtime.CompilerServices; @@ -25,9 +25,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// 's API contract); the receive-side scratch buffer is also allocated per-iter /// on the consumer-task (counted via GC.GetTotalAllocatedBytes in BenchmarkLoop.MeasureAllocationTotal). /// -internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchmark, IDisposable +internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchmark, IDisposable where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; // for SerializedSize reporting + receive-side size known upfront @@ -52,6 +52,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchm public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; public BenchmarkIoMode IoMode => BenchmarkIoMode.NamedPipeRaw; public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -59,7 +60,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchm public bool IsRoundTripOnly => true; public string OptionsDescription => BenchmarkOptions.BuildAcBinary(_options, $", BufferSize={_options.BufferWriterChunkSize}B, Transport=NamedPipe(raw,2-task)"); - public AcBinaryNamedPipeRawByteArrayBenchmark(TestOrder_All_True order, AcBinarySerializerOptions options, string optionsPreset) + public AcBinaryNamedPipeRawByteArrayBenchmark(T order, AcBinarySerializerOptions options, string optionsPreset) { _order = order; // BufferWriterChunkSize comes from the caller — same source-of-truth contract as @@ -125,7 +126,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchm if (n == 0) break; // pipe closed / EOF — partial read swallowed totalRead += n; } - var result = AcBinaryDeserializer.Deserialize(bytes, _options); + var result = AcBinaryDeserializer.Deserialize(bytes, _options); if (_captureResult) _lastResult = result; } catch @@ -183,7 +184,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchm try { Serialize(); - var result = _lastResult as TestOrder_All_True; + var result = _lastResult as T; return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); } finally diff --git a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs index 2215e24..48aedcd 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs @@ -21,6 +21,20 @@ internal interface ISerializerBenchmark BenchmarkDispatchMode DispatchMode { get; } /// Options preset name — e.g. "FastMode", "Default", "NoIntern", "WithCompression". Stays string because preset names are open-ended (per-instance constructor argument). string OptionsPreset { get; } + /// + /// CLR type of the order graph this benchmark serializes (e.g. typeof(TestOrder_All_False), + /// typeof(TestOrder_All_True)). Per-instance: AcBinary picks variant by options preset + /// (), MemPack / MsgPack always use _All_False. + /// Concrete benchmarks return typeof(T) for their generic parameter. + /// + Type OrderType { get; } + /// + /// Derived display name for the . Default-interface impl reads + /// OrderType.Name; concrete classes don't need to override. Surfaced in the SERIALIZER + /// OPTIONS section of every output (.log, .LLM, console) — not in the per-row tables — so the + /// reader correlates each preset with its TestOrder variant without inflating the result columns. + /// + string OrderTypeName => OrderType.Name; /// Synthesized display name from Engine + IoMode + OptionsPreset. string Name => $"{Engine.ToDisplay()} ({IoMode.ToDisplay()}, {OptionsPreset})"; int SerializedSize { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs index 700dfe1..f270c28 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Tests.TestModels; using MemoryPack; using System.Runtime.CompilerServices; @@ -9,22 +9,23 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// cell. WireMode-aligned options via so Compact ↔ UTF-8 /// and FastWire ↔ UTF-16 are apples-to-apples on the string-encoding axis. /// -internal sealed class MemoryPackBenchmark : ISerializerBenchmark +internal sealed class MemoryPackBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; public BenchmarkEngine Engine => BenchmarkEngine.MemoryPack; public BenchmarkIoMode IoMode => BenchmarkIoMode.ByteArray; public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; public long SetupDeserializeAllocBytes => 0; public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}"; - public MemoryPackBenchmark(TestOrder_All_True order, string optionsPreset) + public MemoryPackBenchmark(T order, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; @@ -36,12 +37,12 @@ internal sealed class MemoryPackBenchmark : ISerializerBenchmark public void Serialize() => MemoryPackSerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => MemoryPackSerializer.Deserialize(_serialized, _options); + public void Deserialize() => MemoryPackSerializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var bytes = MemoryPackSerializer.Serialize(_order, _options); - var roundTripped = MemoryPackSerializer.Deserialize(bytes, _options); + var roundTripped = MemoryPackSerializer.Deserialize(bytes, _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs index 91f956e..bbcae0b 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Tests.TestModels; using MemoryPack; using System.Buffers; using System.Runtime.CompilerServices; @@ -10,9 +10,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Apples-to-apples counterpart to — MemoryPack's IBufferWriter /// is the path it's designed for. /// -internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark +internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; private readonly ArrayBufferWriter _bufferWriter; @@ -20,13 +20,14 @@ internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark public BenchmarkEngine Engine => BenchmarkEngine.MemoryPack; public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrReuse; public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } public long SetupDeserializeAllocBytes => 0; public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}"; - public MemoryPackBufferWriterBenchmark(TestOrder_All_True order, string optionsPreset) + public MemoryPackBufferWriterBenchmark(T order, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; @@ -51,13 +52,13 @@ internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark // BufWr semantic: read from a ReadOnlySequence overload (apples-to-apples with AcBinary's // BufWr Deser path). MemoryPack's ROS overload also single-segment-fast-paths internally. [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => MemoryPackSerializer.Deserialize(new ReadOnlySequence(_serialized), _options); + public void Deserialize() => MemoryPackSerializer.Deserialize(new ReadOnlySequence(_serialized), _options); public bool VerifyRoundTrip() { _bufferWriter.ResetWrittenCount(); MemoryPackSerializer.Serialize(_bufferWriter, _order, _options); - var roundTripped = MemoryPackSerializer.Deserialize(new ReadOnlySequence(_bufferWriter.WrittenMemory), _options); + var roundTripped = MemoryPackSerializer.Deserialize(new ReadOnlySequence(_bufferWriter.WrittenMemory), _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs index 144aada..804ff63 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Tests.TestModels; using MemoryPack; using System.Buffers; using System.Runtime.CompilerServices; @@ -9,22 +9,23 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Benchmarks MemoryPack via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call. /// Apples-to-apples counterpart to . /// -internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmark +internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; public BenchmarkEngine Engine => BenchmarkEngine.MemoryPack; public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrNew; public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; public long SetupDeserializeAllocBytes => 0; public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}"; - public MemoryPackFreshBufferWriterBenchmark(TestOrder_All_True order, string optionsPreset) + public MemoryPackFreshBufferWriterBenchmark(T order, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; @@ -42,13 +43,13 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmar // BufWr semantic: read from a ReadOnlySequence overload (apples-to-apples with AcBinary's // BufWr Deser path). MemoryPack's ROS overload also single-segment-fast-paths internally. [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => MemoryPackSerializer.Deserialize(new ReadOnlySequence(_serialized), _options); + public void Deserialize() => MemoryPackSerializer.Deserialize(new ReadOnlySequence(_serialized), _options); public bool VerifyRoundTrip() { var abw = new ArrayBufferWriter(); MemoryPackSerializer.Serialize(abw, _order, _options); - var roundTripped = MemoryPackSerializer.Deserialize(new ReadOnlySequence(abw.WrittenMemory), _options); + var roundTripped = MemoryPackSerializer.Deserialize(new ReadOnlySequence(abw.WrittenMemory), _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs index 204473a..15583cc 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs @@ -1,4 +1,4 @@ -#if !AYCODE_NATIVEAOT +#if !AYCODE_NATIVEAOT using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; @@ -12,22 +12,23 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// which uses Activator.CreateInstance on formatter types the AOT trimmer drops → /// MissingMethodException at runtime. Available for regular JIT runs (dotnet run) only. /// -internal sealed class MessagePackBenchmark : ISerializerBenchmark +internal sealed class MessagePackBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly MessagePackSerializerOptions _options; private readonly byte[] _serialized; public BenchmarkEngine Engine => BenchmarkEngine.MessagePack; public BenchmarkIoMode IoMode => BenchmarkIoMode.ByteArray; public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver) + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; public long SetupDeserializeAllocBytes => 0; public string OptionsDescription { get; } - public MessagePackBenchmark(TestOrder_All_True order, string optionsPreset) + public MessagePackBenchmark(T order, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; @@ -46,12 +47,12 @@ internal sealed class MessagePackBenchmark : ISerializerBenchmark public void Serialize() => MessagePackSerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => MessagePackSerializer.Deserialize(_serialized, _options); + public void Deserialize() => MessagePackSerializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var bytes = MessagePackSerializer.Serialize(_order, _options); - var roundTripped = MessagePackSerializer.Deserialize(bytes, _options); + var roundTripped = MessagePackSerializer.Deserialize(bytes, _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs index ac8bce0..02debb9 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Tests.TestModels; using System.Runtime.CompilerServices; using System.Text.Json; @@ -10,9 +10,9 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// BenchmarkLoop.CreateSerializers); ranks far behind binary serializers on µs/op but provides /// a familiar JSON baseline when needed. /// -internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark +internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark where T : class { - private readonly TestOrder_All_True _order; + private readonly T _order; private readonly JsonSerializerOptions _options; private readonly string _serialized; private readonly byte[] _serializedUtf8; @@ -20,12 +20,13 @@ internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark public BenchmarkEngine Engine => BenchmarkEngine.SystemTextJson; public BenchmarkIoMode IoMode => BenchmarkIoMode.String; public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.Runtime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here) + public Type OrderType => typeof(T); public string OptionsPreset { get; } public int SerializedSize => _serializedUtf8.Length; public long SetupSerializeAllocBytes => 0; public long SetupDeserializeAllocBytes => 0; - public SystemTextJsonBenchmark(TestOrder_All_True order, string optionsPreset) + public SystemTextJsonBenchmark(T order, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; @@ -43,12 +44,12 @@ internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark public void Serialize() => JsonSerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] - public void Deserialize() => JsonSerializer.Deserialize(_serialized, _options); + public void Deserialize() => JsonSerializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var json = JsonSerializer.Serialize(_order, _options); - var roundTripped = JsonSerializer.Deserialize(json, _options); + var roundTripped = JsonSerializer.Deserialize(json, _options); return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Output.cs b/AyCode.Core.Serializers.Console/Output.cs index 1cccfcc..803d379 100644 --- a/AyCode.Core.Serializers.Console/Output.cs +++ b/AyCode.Core.Serializers.Console/Output.cs @@ -172,10 +172,10 @@ internal static class Output System.Console.WriteLine("║ GROUPED RESULTS BY TEST DATA ║"); System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); - // Print serializer options + // Print serializer options. [OrderType] suffix shows which TestOrder variant each preset serialised. var optionsMap = results .Where(r => r.OptionsDescription != null) - .Select(r => (r.SerializerName, r.OptionsDescription!)) + .Select(r => (r.SerializerName, r.OrderTypeName, r.OptionsDescription!)) .Distinct() .ToList(); @@ -183,8 +183,8 @@ internal static class Output { System.Console.WriteLine(); System.Console.WriteLine(" Serializer Options:"); - foreach (var (name, opts) in optionsMap) - System.Console.WriteLine($" {name}: {opts}"); + foreach (var (name, orderType, opts) in optionsMap) + System.Console.WriteLine($" {name} [{orderType}]: {opts}"); } foreach (var testData in testDataSets) @@ -415,8 +415,10 @@ internal static class Output var logFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.log"); var outputFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.output"); - // Save binary output to separate .output file - var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large")); + // Save binary output to separate .output file. + // Cast to TestDataSet because Phase 1 hardcodes the benchmark variant. + // Phase 2 will replace the cast with an options-driven dispatch (matching CreateSerializers). + var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large")) as TestDataSet; if (largeTestData != null) { var outputSb = new StringBuilder(); @@ -450,17 +452,21 @@ internal static class Output sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); sb.AppendLine(); - // Serializer options summary + // Serializer options summary. The bracketed [OrderType] suffix shows which TestOrder variant + // graph each benchmark serialised — AcBinary picks variant per options preset + // (FastMode → _All_False, Default → _All_True; see BenchmarkLoop.UsesAllFalseVariant), + // MemPack / MsgPack always use _All_False. Distinct() de-dupes across cells (each preset + // appears once even though it runs on every test data set). var optionsMap = results .Where(r => r.OptionsDescription != null) - .Select(r => (r.SerializerName, r.OptionsDescription!)) + .Select(r => (r.SerializerName, r.OrderTypeName, r.OptionsDescription!)) .Distinct() .ToList(); if (optionsMap.Count > 0) { sb.AppendLine("=== SERIALIZER OPTIONS ==="); - foreach (var (name, opts) in optionsMap) - sb.AppendLine($" {name}: {opts}"); + foreach (var (name, orderType, opts) in optionsMap) + sb.AppendLine($" {name} [{orderType}]: {opts}"); sb.AppendLine(); } @@ -574,10 +580,11 @@ internal static class Output sb.AppendLine($"Charset: {Configuration.GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{Configuration.TargetSampleMs} ms/sample) | Warmup: {Configuration.WarmupIterations} per phase (Ser/Des isolated) | Samples: {Configuration.BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {Configuration.UnstableCVThreshold * 100:F0}%"); sb.AppendLine("Baseline: MemoryPack (Byte[]) (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup"); - // Options summary + // Options summary. Bracketed [OrderType] surfaces the TestOrder variant each preset serialised — + // see SaveResults for the variant-dispatch rationale. var optionsMap = results .Where(r => r.OptionsDescription != null) - .Select(r => (r.SerializerName, r.OptionsDescription!)) + .Select(r => (r.SerializerName, r.OrderTypeName, r.OptionsDescription!)) .Distinct() .ToList(); if (optionsMap.Count > 0) @@ -585,8 +592,8 @@ internal static class Output sb.AppendLine(); sb.AppendLine("## Options"); sb.AppendLine(); - foreach (var (name, opts) in optionsMap) - sb.AppendLine($"- **{name}**: {opts}"); + foreach (var (name, orderType, opts) in optionsMap) + sb.AppendLine($"- **{name} [{orderType}]**: {opts}"); } sb.AppendLine(); diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerSGenRuntimeCompatibilityTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerSGenRuntimeCompatibilityTests.cs index 11ea151..9bf128a 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerSGenRuntimeCompatibilityTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerSGenRuntimeCompatibilityTests.cs @@ -66,10 +66,15 @@ public class AcBinarySerializerSGenRuntimeCompatibilityTests } } - private static IEnumerable GetTargetDataSets() + private static IEnumerable> GetTargetDataSets() { + // SGen↔Runtime compatibility test depends on TestOrder_All_True graphs (the AssertOrderEquivalent + // signature + JSON canonicalisation are typed for _All_True). The bare-name BenchmarkTestDataProvider + // alias closes the generic provider on _All_True — Phase 1 benchmark uses the sibling + // BenchmarkTestDataProvider_All_False alias instead. return BenchmarkTestDataProvider .CreateTestDataSets() + .Cast>() .Where(x => x.Name.StartsWith("Large") || x.Name.StartsWith("Deep")); } diff --git a/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs b/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs index 6db1963..5837619 100644 --- a/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs +++ b/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs @@ -7,10 +7,10 @@ namespace AyCode.Core.Tests.TestModels; /// /// Charset suffix presets for the per-property string augmentation in -/// BenchmarkTestDataProvider.ToLongString. The benchmark applies the configured suffix -/// to every short (≤ FixStrMaxLength) string property across the test data graph (via reflection -/// in BenchmarkTestDataProvider.EnsureAllStringsBypassFixStr), producing long-string -/// benchmark payloads with a controlled UTF-8 content profile. +/// BenchmarkStringSupport.ToLongString. The benchmark applies the configured suffix to every +/// short (≤ FixStrMaxLength) string property across the test data graph (via reflection in +/// BenchmarkStringSupport.EnsureAllStringsBypassFixStr), producing long-string benchmark payloads +/// with a controlled UTF-8 content profile. /// /// Switch by assigning to from the interactive /// Settings → Charset submenu (or programmatically). The active charset is recorded in the .LLM @@ -18,7 +18,7 @@ namespace AyCode.Core.Tests.TestModels; /// public static class CharsetSuffixes { - /// Empty suffix — short Hungarian baseline strings (e.g. "SharedTag_All_True") stay short, hitting + /// Empty suffix — short Hungarian baseline strings (e.g. "SharedTag") stay short, hitting /// the FixStr fast-path. Stress-test for FixStr / short-string code paths. Note: the baseline /// property values remain Hungarian; only the suffix is empty. Despite the "FixAscii" name, this /// option does NOT change baseline values to ASCII — it suppresses the suffix that would otherwise @@ -47,17 +47,19 @@ public static class CharsetSuffixes public const string Mixed = " árvíz 你好 Привет 😀"; } -public static class BenchmarkTestDataProvider -{ - private const int FixStrMaxLength = 31; +// ============================================================================================ +// Cross-family shared state. The charset suffix is a global benchmark configuration — settable +// once via the interactive Menu, applied uniformly to every family's data construction. Lives in +// a non-generic helper so it ISN'T per-closed-generic (which would cause the Menu setter to affect +// only one family). The forwarding +// property preserves the existing Menu.cs API surface. +// ============================================================================================ - /// - /// Active long-string suffix appended to short string properties during benchmark data construction. - /// Defaults to (~47-char Latin1 mixed) — backward-compatible - /// in spirit with the prior fixed default (Latin1 mixed family, ~32 char). Switch from - /// to measure other UTF-8 content profiles. - /// - public static string LongStringSuffix = CharsetSuffixes.Latin1Long; +internal static class BenchmarkStringSupport +{ + internal const int FixStrMaxLength = 31; + + internal static string LongStringSuffix = CharsetSuffixes.Latin1Long; private sealed class ReferenceComparer : IEqualityComparer { @@ -66,219 +68,7 @@ public static class BenchmarkTestDataProvider public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); } - public static List CreateTestDataSets(bool resetId = true) - { - return new List - { - CreateSmallTestData(resetId), - CreateMediumTestData(resetId), - CreateLargeTestData(resetId), - CreateRepeatedStringsTestData(resetId), - CreateDeepNestedTestData(resetId) - }; - } - - private static TestDataSet CreateSmallTestData(bool resetId = true) - { - if (resetId) TestDataFactory.ResetIdCounter(); - - var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - - var order = TestDataFactory.CreateOrder( - itemCount: 2, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 2, - sharedTag: sharedTag, - sharedUser: sharedUser); - - EnsureAllStringsBypassFixStr(order); - - ClearDeepLevelRefs(order); - - return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 20); - } - - private static TestDataSet CreateMediumTestData(bool resetId = true) - { - if (resetId) TestDataFactory.ResetIdCounter(); - - var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); - - var sharedPreferences = new UserPreferences_All_True - { - Theme = "dark", - Language = "hungarian", - NotificationsEnabled = true, - EmailDigestFrequency = "weekly" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 3, - palletsPerItem: 3, - measurementsPerPallet: 3, - pointsPerMeasurement: 4, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedMetadata: sharedMeta, - sharedPreferences: sharedPreferences); - - EnsureAllStringsBypassFixStr(order); - - ClearDeepLevelRefs(order); - - return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 20); - } - - private static TestDataSet CreateLargeTestData(bool resetId = true) - { - if (resetId) TestDataFactory.ResetIdCounter(); - - var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - - var sharedPreferences = new UserPreferences_All_True - { - Theme = "light", - Language = "german", - NotificationsEnabled = false, - EmailDigestFrequency = "daily" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 5, - palletsPerItem: 5, - measurementsPerPallet: 5, - pointsPerMeasurement: 10, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedPreferences: sharedPreferences); - - EnsureAllStringsBypassFixStr(order); - - ClearDeepLevelRefs(order); - - return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 20); - } - - private static TestDataSet CreateRepeatedStringsTestData(bool resetId = true) - { - if (resetId) TestDataFactory.ResetIdCounter(); - - var sharedTag = TestDataFactory.CreateTag("RepeatedTag"); - var sharedUser = TestDataFactory.CreateUser("repeateduser"); - - var sharedPreferences = new UserPreferences_All_True - { - Theme = "dark", - Language = "hungarian", - NotificationsEnabled = true, - EmailDigestFrequency = "weekly" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 10, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 2, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedPreferences: sharedPreferences); - - // Repeated string fields — ProductName on items + PalletCode on pallets. Both are common - // across the hierarchy, exercising string-interning deduplication on the Default preset - // (which has UseStringInterning = All). Targeting ~20% repeated-string share overall. - // Baselines are short ASCII (≤ FixStrMaxLength) so EnsureAllStringsBypassFixStr appends the - // active CharsetSuffix — the resulting payload's UTF-8 content profile is governed entirely - // by the selected charset (not contaminated by hard-coded Hungarian baseline values). - foreach (var item in order.Items) - { - item.Status = TestStatus.Processing; - item.ProductName = "ProductName"; - - foreach (var pallet in item.Pallets) - { - pallet.PalletCode = "PalletCode"; - } - } - - EnsureAllStringsBypassFixStr(order); - - ClearDeepLevelRefs(order); - - return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 20); - } - - private static TestDataSet CreateDeepNestedTestData(bool resetId = true) - { - if (resetId) TestDataFactory.ResetIdCounter(); - - var sharedTag = TestDataFactory.CreateTag("DeepTag"); - var sharedUser = TestDataFactory.CreateUser("deepuser"); - var sharedCategory = TestDataFactory.CreateCategory("DeepCategory"); - - var sharedPreferences = new UserPreferences_All_True - { - Theme = "light", - Language = "french", - NotificationsEnabled = false, - EmailDigestFrequency = "monthly" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 2, - palletsPerItem: 4, - measurementsPerPallet: 4, - pointsPerMeasurement: 8, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedPreferences: sharedPreferences, - sharedCategory: sharedCategory); - - EnsureAllStringsBypassFixStr(order); - - ClearDeepLevelRefs(order); - - return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 20); - } - - private static void ClearDeepLevelRefs(TestOrder_All_True order) - { - // Keep shared IId refs at the pallet level (Tag + Inspector) — these contribute the bulk of - // the ~20% IId-ref share that the test data targets. Only Category is cleared at this level - // (one-of-three clears keep the share moderate). The deeper measurement / point levels are - // cleared entirely so deep-tree ref noise does not skew the share upward beyond ~20%. - foreach (var item in order.Items) - { - foreach (var pallet in item.Pallets) - { - // pallet.Tag = null; // KEEP for ~20% IId-ref share (was cleared) - // pallet.Inspector = null; // KEEP for ~20% IId-ref share (was cleared) - pallet.Category = null; - - foreach (var measurement in pallet.Measurements) - { - measurement.Tag = null; - measurement.Operator = null; - - foreach (var point in measurement.Points) - { - point.Tag = null; - point.Verifier = null; - } - } - } - } - } - - private static void EnsureAllStringsBypassFixStr(object? root) + internal static void EnsureAllStringsBypassFixStr(object? root) { if (root == null) return; @@ -338,10 +128,318 @@ public static class BenchmarkTestDataProvider } } -public class TestDataSet +// ============================================================================================ +// Generic test-data provider. One closing-generic alias per family — see +// (the _All_True family, MSTEST-compatible name) and +// (the _All_False family, Phase 1 benchmark +// target). The five cell-creator methods + ClearDeepLevelRefs are written once on the generic base, +// using the constrained TestDataFactory<TOrder, ...> for per-family element creation. +// ============================================================================================ + +public abstract class BenchmarkTestDataProvider + where TOrder : TestOrderBase, new() + where TItem : TestOrderItemBase, new() + where TPallet : TestPalletBase, new() + where TMeasurement : TestMeasurementBase, new() + where TPoint : TestMeasurementPointBase, new() + where TTag : SharedTagBase, new() + where TUser : SharedUserBase, new() + where TCategory : SharedCategoryBase, new() + where TMetadata : MetadataInfoBase, new() + where TPreferences : UserPreferencesBase, new() +{ + /// + /// Active long-string suffix appended to short string properties during benchmark data construction. + /// Forwards to (a non-generic shared field) so + /// the setter is family-agnostic — both BenchmarkTestDataProvider.LongStringSuffix = … and + /// BenchmarkTestDataProvider_All_False.LongStringSuffix = … route to the same backing value. + /// Without this forwarding, a per-closed-generic static field on the base would store the suffix + /// independently per family — the Menu setter would only affect whichever alias it addressed. + /// + public static string LongStringSuffix + { + get => BenchmarkStringSupport.LongStringSuffix; + set => BenchmarkStringSupport.LongStringSuffix = value; + } + + // Shortcut alias for the matching factory closing-generic. Saves typing the 10-param cluster + // on every Create* call inside this class. + private static class Factory + { + public static void ResetIdCounter() => + TestDataFactory.ResetIdCounter(); + public static TTag CreateTag(string? name = null) => + TestDataFactory.CreateTag(name); + public static TUser CreateUser(string? username = null) => + TestDataFactory.CreateUser(username); + public static TCategory CreateCategory(string? name = null) => + TestDataFactory.CreateCategory(name); + public static TMetadata CreateMetadata(string? key = null, bool withChild = false) => + TestDataFactory.CreateMetadata(key, withChild); + public static TOrder CreateOrder( + int itemCount, int palletsPerItem, int measurementsPerPallet, int pointsPerMeasurement, + TTag? sharedTag = null, TUser? sharedUser = null, TMetadata? sharedMetadata = null, + TPreferences? sharedPreferences = null, TCategory? sharedCategory = null) => + TestDataFactory.CreateOrder( + itemCount, palletsPerItem, measurementsPerPallet, pointsPerMeasurement, + sharedTag, sharedUser, sharedMetadata, sharedPreferences, sharedCategory); + } + + public static List CreateTestDataSets(bool resetId = true) + { + return new List + { + CreateSmallTestData(resetId), + CreateMediumTestData(resetId), + CreateLargeTestData(resetId), + CreateRepeatedStringsTestData(resetId), + CreateDeepNestedTestData(resetId) + }; + } + + private static TestDataSet CreateSmallTestData(bool resetId = true) + { + if (resetId) Factory.ResetIdCounter(); + + var sharedTag = Factory.CreateTag("SharedTag"); + var sharedUser = Factory.CreateUser("shareduser"); + + var order = Factory.CreateOrder( + itemCount: 2, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 2, + sharedTag: sharedTag, + sharedUser: sharedUser); + + BenchmarkStringSupport.EnsureAllStringsBypassFixStr(order); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 20); + } + + private static TestDataSet CreateMediumTestData(bool resetId = true) + { + if (resetId) Factory.ResetIdCounter(); + + var sharedTag = Factory.CreateTag("SharedTag"); + var sharedUser = Factory.CreateUser("shareduser"); + var sharedMeta = Factory.CreateMetadata("shared", withChild: true); + + var sharedPreferences = new TPreferences + { + Theme = "dark", + Language = "hungarian", + NotificationsEnabled = true, + EmailDigestFrequency = "weekly" + }; + sharedUser.Preferences = sharedPreferences; + + var order = Factory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedMetadata: sharedMeta, + sharedPreferences: sharedPreferences); + + BenchmarkStringSupport.EnsureAllStringsBypassFixStr(order); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 20); + } + + private static TestDataSet CreateLargeTestData(bool resetId = true) + { + if (resetId) Factory.ResetIdCounter(); + + var sharedTag = Factory.CreateTag("SharedTag"); + var sharedUser = Factory.CreateUser("shareduser"); + + var sharedPreferences = new TPreferences + { + Theme = "light", + Language = "german", + NotificationsEnabled = false, + EmailDigestFrequency = "daily" + }; + sharedUser.Preferences = sharedPreferences; + + var order = Factory.CreateOrder( + itemCount: 5, + palletsPerItem: 5, + measurementsPerPallet: 5, + pointsPerMeasurement: 10, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedPreferences: sharedPreferences); + + BenchmarkStringSupport.EnsureAllStringsBypassFixStr(order); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 20); + } + + private static TestDataSet CreateRepeatedStringsTestData(bool resetId = true) + { + if (resetId) Factory.ResetIdCounter(); + + var sharedTag = Factory.CreateTag("RepeatedTag"); + var sharedUser = Factory.CreateUser("repeateduser"); + + var sharedPreferences = new TPreferences + { + Theme = "dark", + Language = "hungarian", + NotificationsEnabled = true, + EmailDigestFrequency = "weekly" + }; + sharedUser.Preferences = sharedPreferences; + + var order = Factory.CreateOrder( + itemCount: 10, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 2, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedPreferences: sharedPreferences); + + // Repeated string fields — ProductName on items + PalletCode on pallets. Both are common + // across the hierarchy, exercising string-interning deduplication on the Default preset + // (which has UseStringInterning = All). Targeting ~20% repeated-string share overall. + // Baselines are short ASCII (≤ FixStrMaxLength) so EnsureAllStringsBypassFixStr appends the + // active CharsetSuffix — the resulting payload's UTF-8 content profile is governed entirely + // by the selected charset (not contaminated by hard-coded Hungarian baseline values). + foreach (var item in order.Items) + { + item.Status = TestStatus.Processing; + item.ProductName = "ProductName"; + + foreach (var pallet in item.Pallets) + { + pallet.PalletCode = "PalletCode"; + } + } + + BenchmarkStringSupport.EnsureAllStringsBypassFixStr(order); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 20); + } + + private static TestDataSet CreateDeepNestedTestData(bool resetId = true) + { + if (resetId) Factory.ResetIdCounter(); + + var sharedTag = Factory.CreateTag("DeepTag"); + var sharedUser = Factory.CreateUser("deepuser"); + var sharedCategory = Factory.CreateCategory("DeepCategory"); + + var sharedPreferences = new TPreferences + { + Theme = "light", + Language = "french", + NotificationsEnabled = false, + EmailDigestFrequency = "monthly" + }; + sharedUser.Preferences = sharedPreferences; + + var order = Factory.CreateOrder( + itemCount: 2, + palletsPerItem: 4, + measurementsPerPallet: 4, + pointsPerMeasurement: 8, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedPreferences: sharedPreferences, + sharedCategory: sharedCategory); + + BenchmarkStringSupport.EnsureAllStringsBypassFixStr(order); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 20); + } + + private static void ClearDeepLevelRefs(TOrder order) + { + // Keep shared IId refs at the pallet level (Tag + Inspector) — these contribute the bulk of + // the ~20% IId-ref share that the test data targets. Only Category is cleared at this level + // (one-of-three clears keep the share moderate). The deeper measurement / point levels are + // cleared entirely so deep-tree ref noise does not skew the share upward beyond ~20%. + foreach (var item in order.Items) + { + foreach (var pallet in item.Pallets) + { + // pallet.Tag = null; // KEEP for ~20% IId-ref share (was cleared) + // pallet.Inspector = null; // KEEP for ~20% IId-ref share (was cleared) + pallet.Category = null; + + foreach (var measurement in pallet.Measurements) + { + measurement.Tag = null; + measurement.Operator = null; + + foreach (var point in measurement.Points) + { + point.Tag = null; + point.Verifier = null; + } + } + } + } + } +} + +// ============================================================================================ +// Closing-generic aliases for the provider. Same pattern as the factory: a bare-name class for +// MSTEST backward compatibility (kept on _All_True), and a _All_False suffix variant for the +// Phase 1 benchmark target. The static LongStringSuffix forwarding property lives on the +// generic base above — accessible identically through either alias (BenchmarkTestDataProvider.LongStringSuffix +// or BenchmarkTestDataProvider_All_False.LongStringSuffix), both routing to the same +// shared field. Symmetric API surface across +// families — no per-alias asymmetry. +// ============================================================================================ + +/// +/// _All_True family provider — preserves the bare-name API surface +/// (BenchmarkTestDataProvider.CreateTestDataSets()) that the SGen-vs-runtime compatibility +/// test depends on. LongStringSuffix is inherited from the generic base. +/// +public sealed class BenchmarkTestDataProvider : BenchmarkTestDataProvider< + TestOrder_All_True, TestOrderItem_All_True, TestPallet_All_True, TestMeasurement_All_True, TestMeasurementPoint_All_True, + SharedTag_All_True, SharedUser_All_True, SharedCategory_All_True, MetadataInfo_All_True, UserPreferences_All_True> +{ +} + +/// +/// _All_False family provider — Phase 1 benchmark target. Inherits the generic cell-creator +/// methods unchanged; the closed-generic new TOrder() calls inside the cell methods construct +/// TestOrder_All_False graphs. +/// +public sealed class BenchmarkTestDataProvider_All_False : BenchmarkTestDataProvider< + TestOrder_All_False, TestOrderItem_All_False, TestPallet_All_False, TestMeasurement_All_False, TestMeasurementPoint_All_False, + SharedTag_All_False, SharedUser_All_False, SharedCategory_All_False, MetadataInfo_All_False, UserPreferences_All_False> +{ +} + +// ============================================================================================ +// TestDataSet — abstract metadata base + generic-ordered concrete. Orchestration code iterates +// over the base type (Name/DisplayName/TypeName/IIdRefPercent only); concrete consumers +// (CreateSerializers, Output binary-output dump) downcast to TestDataSet to access the +// typed Order. +// ============================================================================================ + +public abstract class TestDataSet { public string Name { get; } - public TOrder Order { get; } /// /// Percentage of IId shared references in the data (0-100). @@ -349,14 +447,20 @@ public class TestDataSet /// public int IIdRefPercent { get; } - public TestDataSet(string name, TOrder order, int iidRefPercent = 0) + // Type-keyed variant registry. Phase 2 multi-variant dispatch: AcBinary's options preset + // decides which variant graph it serializes (FastMode → _All_False, Default → _All_True), + // while MemPack/MsgPack canonically use one (typically _All_True). The cells build all + // known variants upfront and register them here so CreateSerializers can hand each benchmark + // its matching graph instance. + private readonly Dictionary _variants = new(); + + protected TestDataSet(string name, int iidRefPercent) { Name = name; - Order = order; IIdRefPercent = iidRefPercent; } - public string TypeName => Order.GetType().Name; + public abstract string TypeName { get; } /// /// Gets display name including IId ref percentage if set. @@ -364,12 +468,43 @@ public class TestDataSet public string DisplayName => IIdRefPercent > 0 ? $"{Name} [{IIdRefPercent}% IId refs]" : Name; + + /// + /// Register a variant graph for this cell. Called by builders. Idempotent on the same type + /// (last-write-wins, no error) so an alias's primary registration is harmless even if + /// cross-registration adds the same variant later. + /// + public void RegisterVariant(T variant) where T : class => _variants[typeof(T)] = variant; + + /// + /// Get a registered variant by type. Throws if not + /// registered — fail-fast surfaces a mismatch between the variant a benchmark expects and + /// what the cell-builder populated. + /// + public T GetOrder() where T : class + { + if (_variants.TryGetValue(typeof(T), out var v)) return (T)v; + throw new InvalidOperationException($"Variant '{typeof(T).Name}' not registered for cell '{Name}' (registered: {string.Join(", ", _variants.Keys.Select(k => k.Name))})"); + } + + /// + /// Check whether a variant is registered. Use to gate optional benchmarks that may not have + /// their variant prepared in every cell. + /// + public bool HasOrder() where T : class => _variants.ContainsKey(typeof(T)); } -public sealed class TestDataSet : TestDataSet +public sealed class TestDataSet : TestDataSet + where TOrder : class { - public TestDataSet(string name, TestOrder_All_True order, int iidRefPercent = 0) - : base(name, order, iidRefPercent) + public TOrder Order { get; } + + public TestDataSet(string name, TOrder order, int iidRefPercent = 0) + : base(name, iidRefPercent) { + Order = order; + RegisterVariant(order); // primary registers itself } + + public override string TypeName => Order.GetType().Name; } diff --git a/AyCode.Core.Tests/TestModels/SharedTestBaseModels.cs b/AyCode.Core.Tests/TestModels/SharedTestBaseModels.cs index 091b2ec..03064dd 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestBaseModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestBaseModels.cs @@ -51,7 +51,8 @@ public abstract class SharedCategoryBase : IId public DateTime? UpdatedAt { get; set; } } -public abstract class SharedUserBase : IId +public abstract class SharedUserBase : IId + where TPreferences : UserPreferencesBase { [Key(0)] public int Id { get; set; } @@ -72,7 +73,7 @@ public abstract class SharedUserBase : IId [Key(8)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [Key(9)] - public UserPreferences_All_True? Preferences { get; set; } + public TPreferences? Preferences { get; set; } } public abstract class UserPreferencesBase @@ -90,7 +91,8 @@ public abstract class UserPreferencesBase public string? EmailDigestFrequency { get; set; } } -public abstract class MetadataInfoBase +public abstract class MetadataInfoBase + where TSelf : MetadataInfoBase { [AcStringIntern(true)] [Key(0)] @@ -102,14 +104,20 @@ public abstract class MetadataInfoBase public DateTime Timestamp { get; set; } = DateTime.UtcNow; [Key(3)] - public MetadataInfo_All_True? ChildMetadata { get; set; } + public TSelf? ChildMetadata { get; set; } } #endregion #region Order Hierarchy Base Types -public abstract class TestOrderBase : IId +public abstract class TestOrderBase : IId + where TItem : class + where TTag : SharedTagBase + where TUser : SharedUserBase + where TCategory : SharedCategoryBase + where TMetadata : MetadataInfoBase + where TPreferences : UserPreferencesBase { [Key(0)] public int Id { get; set; } @@ -130,35 +138,35 @@ public abstract class TestOrderBase : IId public decimal TotalAmount { get; set; } [Key(6)] - public List Items { get; set; } = []; + public List Items { get; set; } = []; [Key(7)] - public SharedTag_All_True? PrimaryTag { get; set; } + public TTag? PrimaryTag { get; set; } [Key(8)] - public SharedTag_All_True? SecondaryTag { get; set; } + public TTag? SecondaryTag { get; set; } [Key(9)] - public SharedUser_All_True? Owner { get; set; } + public TUser? Owner { get; set; } [Key(10)] - public SharedCategory_All_True? Category { get; set; } + public TCategory? Category { get; set; } [Key(11)] - public List Tags { get; set; } = []; + public List Tags { get; set; } = []; [Key(12)] - public MetadataInfo_All_True? OrderMetadata { get; set; } + public TMetadata? OrderMetadata { get; set; } [Key(13)] - public MetadataInfo_All_True? AuditMetadata { get; set; } + public TMetadata? AuditMetadata { get; set; } [Key(14)] - public List MetadataList { get; set; } = []; + public List MetadataList { get; set; } = []; [JsonNoMergeCollection] [Key(15)] - public List NoMergeItems { get; set; } = []; + public List NoMergeItems { get; set; } = []; [MemoryPackIgnore] [JsonIgnore] @@ -167,7 +175,13 @@ public abstract class TestOrderBase : IId public object? Parent { get; set; } } -public abstract class TestOrderItemBase : IId +public abstract class TestOrderItemBase : IId + where TPallet : class + where TTag : SharedTagBase + where TUser : SharedUserBase + where TMetadata : MetadataInfoBase + where TParentOrder : class + where TPreferences : UserPreferencesBase { [Key(0)] public int Id { get; set; } @@ -186,25 +200,32 @@ public abstract class TestOrderItemBase : IId public TestStatus Status { get; set; } = TestStatus.Pending; [Key(5)] - public List Pallets { get; set; } = []; + public List Pallets { get; set; } = []; [Key(6)] - public SharedTag_All_True? Tag { get; set; } + public TTag? Tag { get; set; } [Key(7)] - public SharedUser_All_True? Assignee { get; set; } + public TUser? Assignee { get; set; } [Key(8)] - public MetadataInfo_All_True? ItemMetadata { get; set; } + public TMetadata? ItemMetadata { get; set; } [MemoryPackIgnore] [JsonIgnore] [IgnoreMember] [BsonIgnore] - public TestOrder_All_True? ParentOrder { get; set; } + public TParentOrder? ParentOrder { get; set; } } -public abstract class TestPalletBase : IId +public abstract class TestPalletBase : IId + where TMeasurement : class + where TTag : SharedTagBase + where TUser : SharedUserBase + where TCategory : SharedCategoryBase + where TMetadata : MetadataInfoBase + where TParentItem : class + where TPreferences : UserPreferencesBase { [Key(0)] public int Id { get; set; } @@ -222,28 +243,33 @@ public abstract class TestPalletBase : IId public double Weight { get; set; } [Key(5)] - public List Measurements { get; set; } = []; + public List Measurements { get; set; } = []; [Key(6)] - public SharedTag_All_True? Tag { get; set; } + public TTag? Tag { get; set; } [Key(7)] - public SharedUser_All_True? Inspector { get; set; } + public TUser? Inspector { get; set; } [Key(8)] - public SharedCategory_All_True? Category { get; set; } + public TCategory? Category { get; set; } [Key(9)] - public MetadataInfo_All_True? PalletMetadata { get; set; } + public TMetadata? PalletMetadata { get; set; } [MemoryPackIgnore] [JsonIgnore] [IgnoreMember] [BsonIgnore] - public TestOrderItem_All_True? ParentItem { get; set; } + public TParentItem? ParentItem { get; set; } } -public abstract class TestMeasurementBase : IId +public abstract class TestMeasurementBase : IId + where TPoint : class + where TTag : SharedTagBase + where TUser : SharedUserBase + where TParentPallet : class + where TPreferences : UserPreferencesBase { [Key(0)] public int Id { get; set; } @@ -258,22 +284,26 @@ public abstract class TestMeasurementBase : IId public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [Key(4)] - public List Points { get; set; } = []; + public List Points { get; set; } = []; [Key(5)] - public SharedTag_All_True? Tag { get; set; } + public TTag? Tag { get; set; } [Key(6)] - public SharedUser_All_True? Operator { get; set; } + public TUser? Operator { get; set; } [MemoryPackIgnore] [JsonIgnore] [IgnoreMember] [BsonIgnore] - public TestPallet_All_True? ParentPallet { get; set; } + public TParentPallet? ParentPallet { get; set; } } -public abstract class TestMeasurementPointBase : IId +public abstract class TestMeasurementPointBase : IId + where TTag : SharedTagBase + where TUser : SharedUserBase + where TParentMeasurement : class + where TPreferences : UserPreferencesBase { [Key(0)] public int Id { get; set; } @@ -288,16 +318,16 @@ public abstract class TestMeasurementPointBase : IId public DateTime MeasuredAt { get; set; } = DateTime.UtcNow; [Key(4)] - public SharedTag_All_True? Tag { get; set; } + public TTag? Tag { get; set; } [Key(5)] - public SharedUser_All_True? Verifier { get; set; } + public TUser? Verifier { get; set; } [MemoryPackIgnore] [JsonIgnore] [IgnoreMember] [BsonIgnore] - public TestMeasurement_All_True? ParentMeasurement { get; set; } + public TParentMeasurement? ParentMeasurement { get; set; } } #endregion diff --git a/AyCode.Core.Tests/TestModels/SharedTestOrderModels.cs b/AyCode.Core.Tests/TestModels/SharedTestOrderModels.cs index af0b947..e6441b1 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestOrderModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestOrderModels.cs @@ -6,73 +6,44 @@ using MessagePack; namespace AyCode.Core.Tests.TestModels; +// ============================================================================ +// _All_True family — every leaf marked [AcBinarySerializable(true)] (opt-out). +// All sub-references are _All_True-typed via the generic closing. +// `sealed` to enable AcBinary's non-polymorphic fast-path (no type-discriminator). +// ============================================================================ + [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class SharedTag_All_True : SharedTagBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class SharedTag_All_False : SharedTagBase +public sealed partial class SharedTag_All_True : SharedTagBase { } [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class SharedCategory_All_True : SharedCategoryBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class SharedCategory_All_False : SharedCategoryBase +public sealed partial class SharedCategory_All_True : SharedCategoryBase { } [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class SharedUser_All_True : SharedUserBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class SharedUser_All_False : SharedUserBase +public sealed partial class SharedUser_All_True : SharedUserBase { } [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class UserPreferences_All_True : UserPreferencesBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class UserPreferences_All_False : UserPreferencesBase +public sealed partial class UserPreferences_All_True : UserPreferencesBase { } [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class MetadataInfo_All_True : MetadataInfoBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class MetadataInfo_All_False : MetadataInfoBase +public sealed partial class MetadataInfo_All_True : MetadataInfoBase { } @@ -82,14 +53,9 @@ public partial class MetadataInfo_All_False : MetadataInfoBase [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class TestOrder_All_True : TestOrderBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class TestOrder_All_False : TestOrderBase +public sealed partial class TestOrder_All_True + : TestOrderBase { } @@ -99,14 +65,9 @@ public partial class TestOrder_All_False : TestOrderBase [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class TestOrderItem_All_True : TestOrderItemBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class TestOrderItem_All_False : TestOrderItemBase +public sealed partial class TestOrderItem_All_True + : TestOrderItemBase { } @@ -116,14 +77,10 @@ public partial class TestOrderItem_All_False : TestOrderItemBase [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class TestPallet_All_True : TestPalletBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class TestPallet_All_False : TestPalletBase +public sealed partial class TestPallet_All_True + : TestPalletBase { } @@ -133,14 +90,9 @@ public partial class TestPallet_All_False : TestPalletBase [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class TestMeasurement_All_True : TestMeasurementBase -{ -} - -[MemoryPackable] -[AcBinarySerializable(false)] -[MessagePackObject] -public partial class TestMeasurement_All_False : TestMeasurementBase +public sealed partial class TestMeasurement_All_True + : TestMeasurementBase { } @@ -150,13 +102,95 @@ public partial class TestMeasurement_All_False : TestMeasurementBase [MemoryPackable] [AcBinarySerializable(true)] [MessagePackObject] -public partial class TestMeasurementPoint_All_True : TestMeasurementPointBase +public sealed partial class TestMeasurementPoint_All_True + : TestMeasurementPointBase +{ +} + +// ============================================================================ +// _All_False family — every leaf marked [AcBinarySerializable(false)] (opt-in). +// All sub-references are _All_False-typed via the generic closing. +// `sealed` to enable AcBinary's non-polymorphic fast-path. +// ============================================================================ + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class SharedTag_All_False : SharedTagBase { } [MemoryPackable] [AcBinarySerializable(false)] [MessagePackObject] -public partial class TestMeasurementPoint_All_False : TestMeasurementPointBase +public sealed partial class SharedCategory_All_False : SharedCategoryBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class SharedUser_All_False : SharedUserBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class UserPreferences_All_False : UserPreferencesBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class MetadataInfo_All_False : MetadataInfoBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class TestOrder_All_False + : TestOrderBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class TestOrderItem_All_False + : TestOrderItemBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class TestPallet_All_False + : TestPalletBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class TestMeasurement_All_False + : TestMeasurementBase +{ +} + +[MemoryPackable] +[AcBinarySerializable(false)] +[MessagePackObject] +public sealed partial class TestMeasurementPoint_All_False + : TestMeasurementPointBase { } diff --git a/AyCode.Core.Tests/TestModels/TestDataFactory.cs b/AyCode.Core.Tests/TestModels/TestDataFactory.cs index 47889c0..d521026 100644 --- a/AyCode.Core.Tests/TestModels/TestDataFactory.cs +++ b/AyCode.Core.Tests/TestModels/TestDataFactory.cs @@ -1,24 +1,43 @@ namespace AyCode.Core.Tests.TestModels; /// -/// Factory for creating test data hierarchies. -/// Used by both unit tests and benchmarks. +/// Generic factory for the 5-level test-data hierarchy (Order → OrderItem → Pallet → Measurement → +/// MeasurementPoint) + cross-cutting shared types (Tag, Category, User, Metadata, UserPreferences). +/// One closing-generic alias per family — see (the _All_True +/// family, kept on the bare class name for MSTEST backward compatibility) and +/// (the _All_False family). /// -/// All placeholder strings use Hungarian (UTF-8 multi-byte) content to exercise the UTF-8 -/// encoder/decoder path rather than the ASCII fast-path. This makes the benchmark reflect -/// realistic i18n payloads, not just the FixStrAscii / StringAscii marker fast-paths. +/// The static _idCounter below is per-closed-generic (verified via C# smoke): each family +/// has an independent ID sequence, so calls like TestDataFactory.ResetIdCounter() reset only the +/// _All_True counter, leaving any TestDataFactory_All_False.NextId() sequence intact. +/// Each family's "Reset → Next" pattern stays internally consistent. +/// +/// All placeholder strings use Hungarian (UTF-8 multi-byte) content to exercise the UTF-8 +/// encoder/decoder path rather than the ASCII fast-path. This makes the benchmark reflect realistic +/// i18n payloads, not just the FixStrAscii / StringAscii marker fast-paths. /// -public static class TestDataFactory +public abstract class TestDataFactory + where TOrder : TestOrderBase, new() + where TItem : TestOrderItemBase, new() + where TPallet : TestPalletBase, new() + where TMeasurement : TestMeasurementBase, new() + where TPoint : TestMeasurementPointBase, new() + where TTag : SharedTagBase, new() + where TUser : SharedUserBase, new() + where TCategory : SharedCategoryBase, new() + where TMetadata : MetadataInfoBase, new() + where TPreferences : UserPreferencesBase, new() { private static int _idCounter = 1; /// - /// Reset the ID counter (call in test setup) + /// Reset the ID counter (call in test setup). Resets ONLY this family's counter — sibling families + /// keep their own independent counter state. /// public static void ResetIdCounter() => _idCounter = 1; /// - /// Get the next unique ID + /// Get the next unique ID. Per-family counter — see class docs for the isolation rationale. /// public static int NextId() => _idCounter++; @@ -27,10 +46,10 @@ public static class TestDataFactory /// /// Create a shared tag for cross-reference testing /// - public static SharedTag_All_True CreateTag(string? name = null, string? color = null) + public static TTag CreateTag(string? name = null, string? color = null) { var id = _idCounter++; - return new SharedTag_All_True + return new TTag { Id = id, Name = name ?? $"Címke-{id}", @@ -45,10 +64,10 @@ public static class TestDataFactory /// /// Create a shared category /// - public static SharedCategory_All_True CreateCategory(string? name = null, int? parentId = null) + public static TCategory CreateCategory(string? name = null, int? parentId = null) { var id = _idCounter++; - return new SharedCategory_All_True + return new TCategory { Id = id, Name = name ?? $"Kategória-{id}", @@ -64,10 +83,10 @@ public static class TestDataFactory /// /// Create a shared user for cross-reference testing /// - public static SharedUser_All_True CreateUser(string? username = null, TestUserRole role = TestUserRole.User) + public static TUser CreateUser(string? username = null, TestUserRole role = TestUserRole.User) { var id = _idCounter++; - return new SharedUser_All_True + return new TUser { Id = id, Username = username ?? $"felhasználó{id}", @@ -78,7 +97,7 @@ public static class TestDataFactory Role = role, LastLoginAt = DateTime.UtcNow.AddHours(-id), CreatedAt = DateTime.UtcNow.AddYears(-1), - Preferences = new UserPreferences_All_True + Preferences = new TPreferences { Theme = id % 2 == 0 ? "sötét" : "világos", Language = "magyar", @@ -91,10 +110,10 @@ public static class TestDataFactory /// /// Create metadata info (non-IId) /// - public static MetadataInfo_All_True CreateMetadata(string? key = null, bool withChild = false) + public static TMetadata CreateMetadata(string? key = null, bool withChild = false) { var id = _idCounter++; - return new MetadataInfo_All_True + return new TMetadata { Key = key ?? $"Metaadat-{id}", Value = $"MetaÉrték-{id}", @@ -109,23 +128,23 @@ public static class TestDataFactory /// /// Create a deep order hierarchy with configurable depth. - /// Supports both IId-based (SharedTag_All_True, SharedUser, SharedCategory_All_True) and Non-IId (UserPreferences_All_True) shared references. + /// Supports both IId-based (Tag, User, Category) and Non-IId (Preferences) shared references. /// - public static TestOrder_All_True CreateOrder( + public static TOrder CreateOrder( int itemCount = 2, int palletsPerItem = 2, int measurementsPerPallet = 2, int pointsPerMeasurement = 3, - SharedTag_All_True? sharedTag = null, - SharedUser_All_True? sharedUser = null, - MetadataInfo_All_True? sharedMetadata = null, - UserPreferences_All_True? sharedPreferences = null, - SharedCategory_All_True? sharedCategory = null) + TTag? sharedTag = null, + TUser? sharedUser = null, + TMetadata? sharedMetadata = null, + TPreferences? sharedPreferences = null, + TCategory? sharedCategory = null) { // If sharedUser is provided but no sharedPreferences, use the user's preferences as shared sharedPreferences ??= sharedUser?.Preferences; - var order = new TestOrder_All_True + var order = new TOrder { Id = _idCounter++, OrderNumber = $"Megrendelés-{_idCounter:D4}", @@ -165,20 +184,19 @@ public static class TestDataFactory /// /// Create an order item with pallets. - /// Supports both IId-based and Non-IId shared references. /// - public static TestOrderItem_All_True CreateOrderItem( + public static TItem CreateOrderItem( int palletCount = 2, int measurementsPerPallet = 2, int pointsPerMeasurement = 3, - SharedTag_All_True? sharedTag = null, - SharedUser_All_True? sharedUser = null, - MetadataInfo_All_True? sharedMetadata = null, - UserPreferences_All_True? sharedPreferences = null, - SharedCategory_All_True? sharedCategory = null) + TTag? sharedTag = null, + TUser? sharedUser = null, + TMetadata? sharedMetadata = null, + TPreferences? sharedPreferences = null, + TCategory? sharedCategory = null) { // Create assignee - if sharedUser provided, use it. Otherwise create new user with sharedPreferences - SharedUser_All_True? assignee = sharedUser; + TUser? assignee = sharedUser; if (assignee == null && sharedPreferences != null) { // Create a new user but with shared preferences (Non-IId ref testing) @@ -186,7 +204,7 @@ public static class TestDataFactory assignee.Preferences = sharedPreferences; } - var item = new TestOrderItem_All_True + var item = new TItem { Id = _idCounter++, ProductName = $"Termék-{_idCounter}", @@ -215,21 +233,18 @@ public static class TestDataFactory return item; } - - - /// /// Create a pallet with measurements /// - public static TestPallet_All_True CreatePallet( + public static TPallet CreatePallet( int measurementCount = 2, int pointsPerMeasurement = 3, - MetadataInfo_All_True? sharedMetadata = null, - SharedTag_All_True? sharedTag = null, - SharedUser_All_True? sharedInspector = null, - SharedCategory_All_True? sharedCategory = null) + TMetadata? sharedMetadata = null, + TTag? sharedTag = null, + TUser? sharedInspector = null, + TCategory? sharedCategory = null) { - var pallet = new TestPallet_All_True + var pallet = new TPallet { Id = _idCounter++, PalletCode = $"Raklapkód-{_idCounter:D4}", @@ -255,12 +270,12 @@ public static class TestDataFactory /// /// Create a measurement with points /// - public static TestMeasurement_All_True CreateMeasurement( + public static TMeasurement CreateMeasurement( int pointCount = 3, - SharedTag_All_True? sharedTag = null, - SharedUser_All_True? sharedOperator = null) + TTag? sharedTag = null, + TUser? sharedOperator = null) { - var measurement = new TestMeasurement_All_True + var measurement = new TMeasurement { Id = _idCounter++, Name = $"Mérés-{_idCounter}", @@ -283,12 +298,12 @@ public static class TestDataFactory /// /// Create a measurement point /// - public static TestMeasurementPoint_All_True CreateMeasurementPoint( - SharedTag_All_True? sharedTag = null, - SharedUser_All_True? sharedVerifier = null) + public static TPoint CreateMeasurementPoint( + TTag? sharedTag = null, + TUser? sharedVerifier = null) { var id = _idCounter++; - return new TestMeasurementPoint_All_True + return new TPoint { Id = id, Label = $"MérőPont-{id}", @@ -300,9 +315,24 @@ public static class TestDataFactory } #endregion +} - #region Benchmark Data Generation +// ============================================================================================ +// Closing-generic aliases. Each family carries its own static _idCounter (per-closed-generic +// isolation — see C# runtime semantics). The base-class generic methods are accessible through both +// aliases unchanged. +// ============================================================================================ +/// +/// _All_True family factory — preserves the bare-name API surface +/// (TestDataFactory.CreateTag(...), etc.) that the MSTEST tests and benchmark consumers depend +/// on. Adds family-specific extras (, , +/// ) that the generic base intentionally doesn't carry. +/// +public sealed class TestDataFactory : TestDataFactory< + TestOrder_All_True, TestOrderItem_All_True, TestPallet_All_True, TestMeasurement_All_True, TestMeasurementPoint_All_True, + SharedTag_All_True, SharedUser_All_True, SharedCategory_All_True, MetadataInfo_All_True, UserPreferences_All_True> +{ /// /// Create a large graph for benchmarking with many cross-references. /// Creates approximately (itemCount * palletsPerItem * measurementsPerPallet * pointsPerMeasurement) objects. @@ -322,8 +352,8 @@ public static class TestDataFactory var order = new TestOrder_All_True { - Id = _idCounter++, - OrderNumber = $"MÉRŐTESZT-{_idCounter:D6}", + Id = NextId(), + OrderNumber = $"MÉRŐTESZT-{NextId():D6}", Status = TestStatus.Processing, CreatedAt = DateTime.UtcNow, TotalAmount = 999999.99m, @@ -340,7 +370,7 @@ public static class TestDataFactory { var item = new TestOrderItem_All_True { - Id = _idCounter++, + Id = NextId(), ProductName = $"MérőTermék-{i}", Quantity = 100 + i * 10, UnitPrice = 25.99m + i, @@ -355,7 +385,7 @@ public static class TestDataFactory { var pallet = new TestPallet_All_True { - Id = _idCounter++, + Id = NextId(), PalletCode = $"Raklapkód-{i}-{p}", TrayCount = 10 + p, Status = (TestStatus)(p % 4), @@ -368,7 +398,7 @@ public static class TestDataFactory { var measurement = new TestMeasurement_All_True { - Id = _idCounter++, + Id = NextId(), Name = $"Mérés-{i}-{p}-{m}", TotalWeight = 50.0 + m * 10, CreatedAt = DateTime.UtcNow.AddMinutes(-m) @@ -379,7 +409,7 @@ public static class TestDataFactory { var point = new TestMeasurementPoint_All_True { - Id = _idCounter++, + Id = NextId(), Label = $"MérőPnt-{i}-{p}-{m}-{pt}", Value = 1.0 + pt * 0.5, MeasuredAt = DateTime.UtcNow.AddSeconds(-pt) @@ -401,11 +431,6 @@ public static class TestDataFactory /// Create a large-scale benchmark order similar to production workloads. /// Targets ~50,000-100,000+ IId objects with deep hierarchy and shared references. /// - /// Number of root items (default 500 for ~50K objects, use 2200 for production-like) - /// Pallets per item - /// Measurements per pallet - /// Points per measurement - /// Large TestOrder_All_True with many IId references public static TestOrder_All_True CreateLargeScaleBenchmarkOrder( int rootItemCount = 500, int palletsPerItem = 3, @@ -422,8 +447,8 @@ public static class TestDataFactory var order = new TestOrder_All_True { - Id = _idCounter++, - OrderNumber = $"NAGYMÉRET-{_idCounter:D8}", + Id = NextId(), + OrderNumber = $"NAGYMÉRET-{NextId():D8}", Status = TestStatus.Processing, CreatedAt = DateTime.UtcNow, TotalAmount = 9999999.99m, @@ -440,7 +465,7 @@ public static class TestDataFactory { var item = new TestOrderItem_All_True { - Id = _idCounter++, + Id = NextId(), ProductName = $"Termék-{i}", Quantity = 100 + i, UnitPrice = 10.99m + (i % 100), @@ -455,7 +480,7 @@ public static class TestDataFactory { var pallet = new TestPallet_All_True { - Id = _idCounter++, + Id = NextId(), PalletCode = $"Raklapkód-{i}-{p}", TrayCount = 5 + (p % 10), Status = (TestStatus)(p % 4), @@ -468,7 +493,7 @@ public static class TestDataFactory { var measurement = new TestMeasurement_All_True { - Id = _idCounter++, + Id = NextId(), Name = $"Mérés-{i}-{p}-{m}", TotalWeight = 10.0 + m, CreatedAt = DateTime.UtcNow @@ -479,7 +504,7 @@ public static class TestDataFactory { var point = new TestMeasurementPoint_All_True { - Id = _idCounter++, + Id = NextId(), Label = $"MérőPnt-{i}-{p}-{m}-{pt}", Value = pt * 0.1, MeasuredAt = DateTime.UtcNow @@ -532,6 +557,16 @@ public static class TestDataFactory NullableIntNull = null }; } - - #endregion +} + +/// +/// _All_False family factory — benchmark Phase 1 target. The generic-base factory methods +/// produce _All_False-typed graphs via the closed-generic new T() calls. No +/// family-specific extras here (the legacy etc. +/// stay on the _All_True alias because their existing consumers are _All_True-tied). +/// +public sealed class TestDataFactory_All_False : TestDataFactory< + TestOrder_All_False, TestOrderItem_All_False, TestPallet_All_False, TestMeasurement_All_False, TestMeasurementPoint_All_False, + SharedTag_All_False, SharedUser_All_False, SharedCategory_All_False, MetadataInfo_All_False, UserPreferences_All_False> +{ }