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> +{ }