diff --git a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs index 13fc83b..7ce5ee8 100644 --- a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs +++ b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs @@ -227,7 +227,7 @@ internal static class BenchmarkLoop result.RoundTripIterations = rtIter; // Process-wide allocation measurement: server-drain-thread allocations (server-side new byte[len]) // also show up — otherwise current-thread alloc would only count the client side and look ~halved. - result.RoundTripAllocBytesPerOp = MeasureAllocationTotal(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT alloc]"); + result.RoundTripAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT alloc]", processWide: true); } // mode == "deserialize" alone is meaningless for a round-trip-only benchmark; skip silently. } @@ -611,43 +611,24 @@ internal static class BenchmarkLoop } /// - /// Measures per-call allocation in bytes after a clean GC. Single dedicated sample (no median) — keeps timing samples pure. + /// Measures per-call allocation in bytes after a clean GC. Single dedicated sample (no median) — keeps + /// timing samples pure. When is true, uses + /// instead of + /// — needed for round-trip-only benchmarks (NamedPipe etc.) where the work happens across multiple + /// threads (server-side new byte[len] buffers, drain-pump-thread allocations). Per-thread mode + /// is slightly cleaner for in-memory benchmarks; process-wide mode is slightly noisier (background + /// threads / GC bookkeeping leak in) but over 1000 iterations the signal dominates. /// - internal static long MeasureAllocation(Action action, int iterations, string? progressLabel = null) + internal static long MeasureAllocation(Action action, int iterations, string? progressLabel = null, bool processWide = false) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var sw = Stopwatch.StartNew(); - var before = GC.GetAllocatedBytesForCurrentThread(); + var before = processWide ? GC.GetTotalAllocatedBytes(precise: true) : GC.GetAllocatedBytesForCurrentThread(); RunWithProgress(action, iterations, progressLabel, samples: 1, sampleIndex: 0); - - var after = GC.GetAllocatedBytesForCurrentThread(); - sw.Stop(); - EndProgress(progressLabel, sw.Elapsed.TotalMilliseconds); - return (after - before) / iterations; - } - - /// - /// Process-wide allocation measurement — needed for round-trip-only benchmarks (NamedPipe etc.) where - /// the work happens across multiple threads. would - /// only count the caller-thread allocations, missing the server-side new byte[len] buffers and - /// any drain-pump-thread allocations. covers the entire process. - /// Slightly noisier than the per-thread variant (background threads / GC bookkeeping leak in), but - /// over 1000 iterations the signal dominates. - /// - internal static long MeasureAllocationTotal(Action action, int iterations, string? progressLabel = null) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - var sw = Stopwatch.StartNew(); - var before = GC.GetTotalAllocatedBytes(precise: true); - RunWithProgress(action, iterations, progressLabel, samples: 1, sampleIndex: 0); - - var after = GC.GetTotalAllocatedBytes(precise: true); + var after = processWide ? GC.GetTotalAllocatedBytes(precise: true) : GC.GetAllocatedBytesForCurrentThread(); sw.Stop(); EndProgress(progressLabel, sw.Elapsed.TotalMilliseconds); return (after - before) / iterations; diff --git a/AyCode.Core.Serializers.Console/BenchmarkResult.cs b/AyCode.Core.Serializers.Console/BenchmarkResult.cs index 7f80c20..6b60dbd 100644 --- a/AyCode.Core.Serializers.Console/BenchmarkResult.cs +++ b/AyCode.Core.Serializers.Console/BenchmarkResult.cs @@ -1,3 +1,5 @@ +using AyCode.Core.Serializers.Console.Benchmarks; + namespace AyCode.Core.Serializers.Console; /// @@ -8,9 +10,9 @@ namespace AyCode.Core.Serializers.Console; internal sealed class BenchmarkResult { public string TestDataName { get; set; } = ""; - public string Engine { get; set; } = ""; - public string IoMode { get; set; } = ""; - public string DispatchMode { get; set; } = ""; + public BenchmarkEngine Engine { get; set; } + public BenchmarkIoMode IoMode { get; set; } + public BenchmarkDispatchMode DispatchMode { get; set; } public string OptionsPreset { get; set; } = ""; /// True if Serialize() captures a full round-trip and Deserialize() is a no-op @@ -20,7 +22,7 @@ internal sealed class BenchmarkResult public bool IsRoundTripOnly { get; set; } /// Synthesized display name for backwards compatibility / single-string-row scenarios. Includes DispatchMode so SGen and Runtime variants of the same preset don't collide in grouping (e.g. SUMMARY: WINNERS). - public string SerializerName => $"{Engine} ({IoMode}, {OptionsPreset}, {DispatchMode})"; + public string SerializerName => $"{Engine.ToDisplay()} ({IoMode.ToDisplay()}, {OptionsPreset}, {DispatchMode.ToDisplay()})"; public string? OptionsDescription { get; set; } public int SerializedSize { get; set; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs index e77a187..1bd4883 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs @@ -14,9 +14,9 @@ internal sealed class AcBinaryBenchmark : ISerializerBenchmark private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoByteArray; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.ByteArray; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs index aacafe1..58415b2 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs @@ -16,9 +16,9 @@ internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark private readonly byte[] _serialized; private readonly ArrayBufferWriter _bufferWriter; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoBufWrReuse; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrReuse; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs index 2e43f42..420ceb9 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs @@ -18,9 +18,9 @@ internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoBufWrNew; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrNew; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs index 1b09377..363e7cb 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs @@ -47,9 +47,9 @@ internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDis private bool _captureResult; private bool _disposed; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoInMemoryPipe; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.InMemoryPipe; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs index ccde326..d2e305a 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs @@ -38,9 +38,9 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchma private bool _captureResult; private bool _disposed; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoInMemoryRaw; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.InMemoryRaw; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs index 9065cfc..99bd6c7 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs @@ -62,9 +62,9 @@ internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDispos private bool _captureResult; // toggle: when true, ConsumeLoop stores result; otherwise discards private bool _disposed; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoNamedPipe; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.NamedPipe; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs index d6b49ec..6f85d79 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs @@ -49,9 +49,9 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchm private bool _captureResult; // toggle: when true, ConsumerLoop stores result; otherwise discards private bool _disposed; - public string Engine => Configuration.EngineAcBinary; - public string IoMode => Configuration.IoNamedPipeRaw; - public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; + public BenchmarkEngine Engine => BenchmarkEngine.AcBinary; + public BenchmarkIoMode IoMode => BenchmarkIoMode.NamedPipeRaw; + public BenchmarkDispatchMode DispatchMode => _options.UseGeneratedCode ? BenchmarkDispatchMode.SGen : BenchmarkDispatchMode.Runtime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs b/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs new file mode 100644 index 0000000..3e4fc04 --- /dev/null +++ b/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs @@ -0,0 +1,90 @@ +namespace AyCode.Core.Serializers.Console.Benchmarks; + +/// +/// Serializer engine identifier — replaces the prior Configuration.EngineXxx string constants +/// with a type-safe enum. The benchmark-result Engine column uses for +/// the human-readable form. +/// +internal enum BenchmarkEngine +{ + AcBinary, + MemoryPack, +#if !AYCODE_NATIVEAOT + MessagePack, +#endif + SystemTextJson, +} + +/// +/// I/O mode identifier — replaces the prior Configuration.IoXxx string constants. Note that +/// and share the display string "NamedPipe" +/// (they distinguish chunked-framed vs raw-byte[] semantics, but render identically in the IO column); +/// the same applies to + ("Pipe(in-mem)"). +/// +internal enum BenchmarkIoMode +{ + ByteArray, + BufWrReuse, + BufWrNew, + String, + NamedPipe, + NamedPipeRaw, + InMemoryPipe, + InMemoryRaw, +} + +/// +/// Dispatch mode identifier — replaces the prior Configuration.ModeXxx string constants. +/// Describes how property access / type dispatch happens for a given benchmark row: +/// +/// — compile-time source generator path (Unsafe.As<T> direct fields, slot-array wrapper lookup). +/// — reflection / compiled-delegate path. +/// — SGen root with non-SGen child types reached via bridge methods (see docs/BINARY/BINARY_SGEN.md). +/// +/// +internal enum BenchmarkDispatchMode +{ + SGen, + Runtime, + Hybrid, +} + +/// +/// Display-string converters for the benchmark enums. Renders enum values into the column-friendly +/// human-readable form used by the per-row console table, the .log file CSV/formatted output, +/// and the .LLM markdown table. Centralised here so every output formatter renders identically. +/// +internal static class BenchmarkEnumExtensions +{ + internal static string ToDisplay(this BenchmarkEngine engine) => engine switch + { + BenchmarkEngine.AcBinary => "AcBinary", + BenchmarkEngine.MemoryPack => "MemoryPack", +#if !AYCODE_NATIVEAOT + BenchmarkEngine.MessagePack => "MessagePack", +#endif + BenchmarkEngine.SystemTextJson => "System.Text.Json", + _ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null), + }; + + internal static string ToDisplay(this BenchmarkIoMode mode) => mode switch + { + BenchmarkIoMode.ByteArray => "Byte[]", + BenchmarkIoMode.BufWrReuse => "BufWr reuse", + BenchmarkIoMode.BufWrNew => "BufWr new", + BenchmarkIoMode.String => "String", + BenchmarkIoMode.NamedPipe => "NamedPipe", + BenchmarkIoMode.NamedPipeRaw => "NamedPipe", + BenchmarkIoMode.InMemoryPipe => "Pipe(in-mem)", + BenchmarkIoMode.InMemoryRaw => "Pipe(in-mem)", + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), + }; + + internal static string ToDisplay(this BenchmarkDispatchMode mode) => mode switch + { + BenchmarkDispatchMode.SGen => "SGen", + BenchmarkDispatchMode.Runtime => "Runtime", + BenchmarkDispatchMode.Hybrid => "Hybrid", + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), + }; +} diff --git a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs index ed39f22..2215e24 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs @@ -13,16 +13,16 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// internal interface ISerializerBenchmark { - /// Serializer engine — e.g. "AcBinary", "MemoryPack", "MessagePack". - string Engine { get; } - /// I/O mode — e.g. "Byte[]", "BufWr reuse", "BufWr new", "NamedPipe", "FileStream". - string IoMode { get; } - /// Dispatch mode — "SGen", "Runtime", or "Hybrid". For AcBinary derived from UseGeneratedCode + child-type SGen coverage; non-AcBinary engines report their own native dispatch model. - string DispatchMode { get; } - /// Options preset name — e.g. "FastMode", "Default", "NoIntern", "WithCompression". + /// Serializer engine — typed enum, see for the human-readable form. + BenchmarkEngine Engine { get; } + /// I/O mode — typed enum, see for the human-readable form. + BenchmarkIoMode IoMode { get; } + /// Dispatch mode — typed enum ( / / ). For AcBinary derived from UseGeneratedCode + child-type SGen coverage; non-AcBinary engines report their own native dispatch model. + 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; } /// Synthesized display name from Engine + IoMode + OptionsPreset. - string Name => $"{Engine} ({IoMode}, {OptionsPreset})"; + string Name => $"{Engine.ToDisplay()} ({IoMode.ToDisplay()}, {OptionsPreset})"; int SerializedSize { get; } string? OptionsDescription => null; /// One-time SERIALIZER-side setup allocation cost (e.g., pre-allocated ArrayBufferWriter with internal buffer). Captured in constructor; 0 for byte[] API and Fresh-BufWriter variants. diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs index 52444e3..1606ed5 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs @@ -15,9 +15,9 @@ internal sealed class MemoryPackBenchmark : ISerializerBenchmark private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; - public string Engine => Configuration.EngineMemoryPack; - public string IoMode => Configuration.IoByteArray; - public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public BenchmarkEngine Engine => BenchmarkEngine.MemoryPack; + public BenchmarkIoMode IoMode => BenchmarkIoMode.ByteArray; + public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MemoryPack always uses [MemoryPackable] source-generated formatters public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs index 0320e6f..e61aa7f 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs @@ -17,9 +17,9 @@ internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark private readonly byte[] _serialized; private readonly ArrayBufferWriter _bufferWriter; - public string Engine => Configuration.EngineMemoryPack; - public string IoMode => Configuration.IoBufWrReuse; - public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public BenchmarkEngine Engine => BenchmarkEngine.MemoryPack; + public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrReuse; + public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MemoryPack always uses [MemoryPackable] source-generated formatters public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs index 032f21a..e69cde4 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs @@ -15,9 +15,9 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmar private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; - public string Engine => Configuration.EngineMemoryPack; - public string IoMode => Configuration.IoBufWrNew; - public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public BenchmarkEngine Engine => BenchmarkEngine.MemoryPack; + public BenchmarkIoMode IoMode => BenchmarkIoMode.BufWrNew; + public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MemoryPack always uses [MemoryPackable] source-generated formatters public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs index 7f4e2b0..af2d67b 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs @@ -18,9 +18,9 @@ internal sealed class MessagePackBenchmark : ISerializerBenchmark private readonly MessagePackSerializerOptions _options; private readonly byte[] _serialized; - public string Engine => Configuration.EngineMessagePack; - public string IoMode => Configuration.IoByteArray; - public string DispatchMode => Configuration.ModeSGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver) + public BenchmarkEngine Engine => BenchmarkEngine.MessagePack; + public BenchmarkIoMode IoMode => BenchmarkIoMode.ByteArray; + public BenchmarkDispatchMode DispatchMode => BenchmarkDispatchMode.SGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver) public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; diff --git a/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs index 2acca54..2d45ccc 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs @@ -17,9 +17,9 @@ internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark private readonly string _serialized; private readonly byte[] _serializedUtf8; - public string Engine => Configuration.EngineSystemTextJson; - public string IoMode => Configuration.IoString; - public string DispatchMode => Configuration.ModeRuntime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here) + 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 string OptionsPreset { get; } public int SerializedSize => _serializedUtf8.Length; public long SetupSerializeAllocBytes => 0; diff --git a/AyCode.Core.Serializers.Console/Configuration.cs b/AyCode.Core.Serializers.Console/Configuration.cs index d7290de..1e52e64 100644 --- a/AyCode.Core.Serializers.Console/Configuration.cs +++ b/AyCode.Core.Serializers.Console/Configuration.cs @@ -41,24 +41,7 @@ internal static class Configuration // 1 = Compact, 2 = Fast internal static WireMode SelectedWireMode = WireMode.Compact; - // Serializer name constants - // Engine identifiers (used in Engine column + comparison logic) - internal const string EngineAcBinary = "AcBinary"; - internal const string EngineMemoryPack = "MemoryPack"; -#if !AYCODE_NATIVEAOT - internal const string EngineMessagePack = "MessagePack"; -#endif - internal const string EngineSystemTextJson = "System.Text.Json"; - - // IO mode identifiers (used in IO column + comparison logic) - internal const string IoByteArray = "Byte[]"; - internal const string IoBufWrReuse = "BufWr reuse"; - internal const string IoBufWrNew = "BufWr new"; - internal const string IoString = "String"; - internal const string IoNamedPipe = "NamedPipe"; - internal const string IoNamedPipeRaw = "NamedPipe"; - internal const string IoInMemoryPipe = "Pipe(in-mem)"; - internal const string IoInMemoryRaw = "Pipe(in-mem)"; + // Engine / IO mode / Dispatch mode identifiers → Benchmarks/BenchmarkEnums.cs (typed enums with ToDisplay) // Single source of truth for the chunk size used by ALL pipe-related benchmarks (NamedPipe PipeChunk, // NamedPipe PipeRaw, in-memory Pipe, in-memory RawMem) AND the NamedPipe server's inBufferSize/outBufferSize. @@ -67,14 +50,6 @@ internal static class Configuration // overrides across individual benchmark rows. internal const int PipeChunkSize = 4096; - // Dispatch mode identifiers — describes how property access / type dispatch happens for a given run. - // SGen = compile-time source generator path (Unsafe.As direct fields, slot-array wrapper lookup). - // Runtime= reflection / compiled-delegate path. - // Hybrid = SGen root with non-SGen child types reached via bridge methods. See docs/BINARY/BINARY_SGEN.md. - internal const string ModeSGen = "SGen"; - internal const string ModeRuntime = "Runtime"; - internal const string ModeHybrid = "Hybrid"; - // Per-cell adaptive iteration target wall-clock duration. Each Ser/Des function calibrates its // own iteration count post-warmup so the sample batch lands in this range — equalizes the // per-sample window across cells of vastly different per-op cost (Small ~6 ns/op vs Large diff --git a/AyCode.Core.Serializers.Console/Output.cs b/AyCode.Core.Serializers.Console/Output.cs index e6247d5..1cccfcc 100644 --- a/AyCode.Core.Serializers.Console/Output.cs +++ b/AyCode.Core.Serializers.Console/Output.cs @@ -1,4 +1,5 @@ using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Console.Benchmarks; using AyCode.Core.Tests.TestModels; using System.Globalization; using System.Runtime.CompilerServices; @@ -191,10 +192,10 @@ internal static class Output // Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration. var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList(); // Baseline switched MessagePack → MemoryPack: MemoryPack is the SOTA performance leader. - var memPackResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)); + var memPackResult = testResults.FirstOrDefault(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray)); // Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated). // The Runtime variant is shown alongside in the table for context, not used as the headline number. - var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)); + var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen)); System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(172, '─') + "┐"); // Header-only units; per-row entries are numbers (µs/op for time, KB/op for alloc, KB pair "ser / des" for Setup, B for Size). @@ -215,8 +216,8 @@ internal static class Output // Highlight MemoryPack baseline (any Byte[]) and AcBinary headline contender (Byte[] + SGen) with win/lose colors. // The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline. - var isHighlighted = (result.Engine == Configuration.EngineMemoryPack && result.IoMode == Configuration.IoByteArray) - || (result.Engine == Configuration.EngineAcBinary && result.IoMode == Configuration.IoByteArray && result.DispatchMode == Configuration.ModeSGen); + var isHighlighted = (result.Engine == BenchmarkEngine.MemoryPack && result.IoMode == BenchmarkIoMode.ByteArray) + || (result.Engine == BenchmarkEngine.AcBinary && result.IoMode == BenchmarkIoMode.ByteArray && result.DispatchMode == BenchmarkDispatchMode.SGen); var prefix = isHighlighted ? "│►" : "│ "; var suffix = isHighlighted ? "◄│" : " │"; @@ -224,7 +225,7 @@ internal static class Output // Color logic: Green = winner (faster), Red = loser (slower) if (isHighlighted && memPackResult != null && acBinaryResult != null) { - var isMemPack = (result.Engine == Configuration.EngineMemoryPack && result.IoMode == Configuration.IoByteArray); + var isMemPack = (result.Engine == BenchmarkEngine.MemoryPack && result.IoMode == BenchmarkIoMode.ByteArray); var memPackFaster = RtPerOp(memPackResult) < RtPerOp(acBinaryResult); if (isMemPack) @@ -237,7 +238,7 @@ internal static class Output } } - System.Console.WriteLine($"{prefix}{rank++,4} │ {result.Engine,-11} │ {result.OptionsPreset,-22} │ {result.IoMode,-12} │ {result.DispatchMode,-8} │ {setup,14} │ {size,8} │ {ser,10} │ {serAlloc,10} │ {des,10} │ {desAlloc,10} │ {rt,10} │ {rtAlloc,10}{suffix}"); + System.Console.WriteLine($"{prefix}{rank++,4} │ {result.Engine.ToDisplay(),-11} │ {result.OptionsPreset,-22} │ {result.IoMode.ToDisplay(),-12} │ {result.DispatchMode.ToDisplay(),-8} │ {setup,14} │ {size,8} │ {ser,10} │ {serAlloc,10} │ {des,10} │ {desAlloc,10} │ {rt,10} │ {rtAlloc,10}{suffix}"); if (isHighlighted) { @@ -366,13 +367,13 @@ internal static class Output System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-40} │ {fastestRt.AvgPerOp,12:F2} µs/op"); // Overall AcBinary (SGen) vs MemoryPack comparison. - var memPackSerResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.SerializeTimeMs > 0).ToList(); - var memPackDesResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.DeserializeTimeMs > 0).ToList(); - var memPackRtResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.RoundTripTimeMs > 0).ToList(); + var memPackSerResults = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray) && r.SerializeTimeMs > 0).ToList(); + var memPackDesResults = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray) && r.DeserializeTimeMs > 0).ToList(); + var memPackRtResults = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray) && r.RoundTripTimeMs > 0).ToList(); - var acBinarySerResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.SerializeTimeMs > 0).ToList(); - var acBinaryDesResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.DeserializeTimeMs > 0).ToList(); - var acBinaryRtResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.RoundTripTimeMs > 0).ToList(); + var acBinarySerResults = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen) && r.SerializeTimeMs > 0).ToList(); + var acBinaryDesResults = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen) && r.DeserializeTimeMs > 0).ToList(); + var acBinaryRtResults = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen) && r.RoundTripTimeMs > 0).ToList(); // Skip comparison if no data available if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0) @@ -384,8 +385,8 @@ internal static class Output } // All averages are over per-op µs (iter-independent). Three aggregations per metric. - var sizeAcResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)).ToList(); - var sizeMpResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)).ToList(); + var sizeAcResults = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen)).ToList(); + var sizeMpResults = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray)).ToList(); var serStats = ComputeOverallStats(acBinarySerResults, memPackSerResults, SerPerOp); var desStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, DesPerOp); @@ -472,7 +473,7 @@ internal static class Output var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList(); foreach (var result in testResults) { - sb.AppendLine($"{result.TestDataName},{result.Engine},{result.IoMode},{result.DispatchMode},{result.OptionsPreset},{result.SerializedSize},{SerPerOp(result):F2},{DesPerOp(result):F2},{RtPerOp(result):F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp},{result.RoundTripAllocBytesPerOp},{result.SetupSerializeAllocBytes},{result.SetupDeserializeAllocBytes}"); + sb.AppendLine($"{result.TestDataName},{result.Engine.ToDisplay()},{result.IoMode.ToDisplay()},{result.DispatchMode.ToDisplay()},{result.OptionsPreset},{result.SerializedSize},{SerPerOp(result):F2},{DesPerOp(result):F2},{RtPerOp(result):F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp},{result.RoundTripAllocBytesPerOp},{result.SetupSerializeAllocBytes},{result.SetupDeserializeAllocBytes}"); } } sb.AppendLine(); @@ -486,8 +487,8 @@ internal static class Output { // Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration. var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList(); - var memPackResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)); - var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)); + var memPackResult = testResults.FirstOrDefault(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray)); + var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen)); sb.AppendLine(); sb.AppendLine($"--- {testData.DisplayName} ---"); @@ -497,7 +498,7 @@ internal static class Output var rank = 1; foreach (var result in testResults) { - var isHighlighted = ((result.Engine == Configuration.EngineMemoryPack || result.Engine == Configuration.EngineAcBinary) && result.IoMode == Configuration.IoByteArray); + var isHighlighted = ((result.Engine == BenchmarkEngine.MemoryPack || result.Engine == BenchmarkEngine.AcBinary) && result.IoMode == BenchmarkIoMode.ByteArray); var prefix = isHighlighted ? "► " : " "; var size = $"{result.SerializedSize:N0}"; @@ -527,13 +528,13 @@ internal static class Output sb.AppendLine(); sb.AppendLine("=== AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ==="); - var memPackSerResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.SerializeTimeMs > 0).ToList(); - var memPackDesResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.DeserializeTimeMs > 0).ToList(); - var memPackRtResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.RoundTripTimeMs > 0).ToList(); + var memPackSerResults2 = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray) && r.SerializeTimeMs > 0).ToList(); + var memPackDesResults2 = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray) && r.DeserializeTimeMs > 0).ToList(); + var memPackRtResults2 = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray) && r.RoundTripTimeMs > 0).ToList(); - var acBinarySerResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.SerializeTimeMs > 0).ToList(); - var acBinaryDesResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.DeserializeTimeMs > 0).ToList(); - var acBinaryRtResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.RoundTripTimeMs > 0).ToList(); + var acBinarySerResults2 = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen) && r.SerializeTimeMs > 0).ToList(); + var acBinaryDesResults2 = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen) && r.DeserializeTimeMs > 0).ToList(); + var acBinaryRtResults2 = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen) && r.RoundTripTimeMs > 0).ToList(); // Skip comparison block if either side has no Byte[] data if (memPackRtResults2.Count == 0 || acBinaryRtResults2.Count == 0) @@ -547,8 +548,8 @@ internal static class Output return; } - var sizeAcResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)).ToList(); - var sizeMpResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)).ToList(); + var sizeAcResults2 = results.Where(r => (r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen)).ToList(); + var sizeMpResults2 = results.Where(r => (r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray)).ToList(); AppendOverallLine(sb, "Serialize", "µs/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, SerPerOp)); AppendOverallLine(sb, "Ser Alloc", "B/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, r => r.SerializeAllocBytesPerOp), "F0"); @@ -620,13 +621,13 @@ internal static class Output var iterCol = r.IsRoundTripOnly ? r.RoundTripIterations.ToString(inv) : $"{(r.SerializeIterations > 0 ? r.SerializeIterations.ToString(inv) : "-")} / {(r.DeserializeIterations > 0 ? r.DeserializeIterations.ToString(inv) : "-")}"; - sb.AppendLine($"{r.TestDataName} | {r.Engine} | {r.IoMode} | {r.DispatchMode} | {r.OptionsPreset} | {r.SerializedSize} | {ser} | {des} | {rt} | {serAlloc} | {desAlloc} | {rtAlloc} | {setupAlloc} | {iterCol}"); + sb.AppendLine($"{r.TestDataName} | {r.Engine.ToDisplay()} | {r.IoMode.ToDisplay()} | {r.DispatchMode.ToDisplay()} | {r.OptionsPreset} | {r.SerializedSize} | {ser} | {des} | {rt} | {serAlloc} | {desAlloc} | {rtAlloc} | {setupAlloc} | {iterCol}"); } } // Overall AcBinary (SGen, Byte[]) vs MemoryPack (Byte[]) comparison - var memPackByteArrayResults = results.Where(r => r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray).ToList(); - var acBinarySGenByteArrayResults = results.Where(r => r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen).ToList(); + var memPackByteArrayResults = results.Where(r => r.Engine == BenchmarkEngine.MemoryPack && r.IoMode == BenchmarkIoMode.ByteArray).ToList(); + var acBinarySGenByteArrayResults = results.Where(r => r.Engine == BenchmarkEngine.AcBinary && r.IoMode == BenchmarkIoMode.ByteArray && r.DispatchMode == BenchmarkDispatchMode.SGen).ToList(); var memPackSerResultsLlm = memPackByteArrayResults.Where(r => r.SerializeTimeMs > 0).ToList(); var memPackDesResultsLlm = memPackByteArrayResults.Where(r => r.DeserializeTimeMs > 0).ToList(); var memPackRtResultsLlm = memPackByteArrayResults.Where(r => r.RoundTripTimeMs > 0).ToList();