diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f9527aa..b1cff9b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -75,7 +75,8 @@ "Bash(dotnet tool *)", "Bash(dotnet-trace convert *)", "Bash(find ~/.nuget/packages/memorypack* -name \"*.cs\" 2>/dev/null | head -5; find /mnt/c/Users/Fullepi/.nuget/packages/memorypack* -name \"MemoryPackSerializer*.cs\" 2>/dev/null | head -5)", - "PowerShell($path = \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core.Serializers.Console\\\\Program.cs\"; $c = [IO.File]::ReadAllText\\($path\\); $c = $c -replace 'MeasureAllocationTotal', 'BenchmarkLoop.MeasureAllocationTotal'; $c = $c -replace 'MeasureAllocation\\\\\\(', 'BenchmarkLoop.MeasureAllocation\\('; $c = $c -replace 'ForceGcCollect\\\\\\(', 'BenchmarkLoop.ForceGcCollect\\('; $c = $c -replace 'CalibrateIterations\\\\\\(', 'BenchmarkLoop.CalibrateIterations\\('; $c = $c -replace 'RunTimed\\\\\\(', 'BenchmarkLoop.RunTimed\\('; $c = $c -replace 'DeepEqualsViaJson\\\\\\(', 'BenchmarkLoop.DeepEqualsViaJson\\('; $c = $c -replace 'ValidateMemoryPackSetup\\\\\\(', 'BenchmarkLoop.ValidateMemoryPackSetup\\('; $c = $c -replace 'FilterByLayer\\\\\\(', 'BenchmarkLoop.FilterByLayer\\('; [IO.File]::WriteAllText\\($path, $c\\); Write-Output \"OK new length: $\\($c.Length\\)\")" + "PowerShell($path = \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core.Serializers.Console\\\\Program.cs\"; $c = [IO.File]::ReadAllText\\($path\\); $c = $c -replace 'MeasureAllocationTotal', 'BenchmarkLoop.MeasureAllocationTotal'; $c = $c -replace 'MeasureAllocation\\\\\\(', 'BenchmarkLoop.MeasureAllocation\\('; $c = $c -replace 'ForceGcCollect\\\\\\(', 'BenchmarkLoop.ForceGcCollect\\('; $c = $c -replace 'CalibrateIterations\\\\\\(', 'BenchmarkLoop.CalibrateIterations\\('; $c = $c -replace 'RunTimed\\\\\\(', 'BenchmarkLoop.RunTimed\\('; $c = $c -replace 'DeepEqualsViaJson\\\\\\(', 'BenchmarkLoop.DeepEqualsViaJson\\('; $c = $c -replace 'ValidateMemoryPackSetup\\\\\\(', 'BenchmarkLoop.ValidateMemoryPackSetup\\('; $c = $c -replace 'FilterByLayer\\\\\\(', 'BenchmarkLoop.FilterByLayer\\('; [IO.File]::WriteAllText\\($path, $c\\); Write-Output \"OK new length: $\\($c.Length\\)\")", + "PowerShell($progPath = \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core.Serializers.Console\\\\Program.cs\"; $c = [IO.File]::ReadAllText\\($progPath\\); $usings = \"using AyCode.Core.Serializers.Console.Benchmarks;`r`n\"; if \\(-not $c.Contains\\($usings.Trim\\(\\)\\)\\) { $idx = $c.IndexOf\\(\"namespace AyCode.Core.Serializers.Console;\"\\); $before = $c.Substring\\(0, $idx\\); $after = $c.Substring\\($idx\\); $c2 = $before + $usings + \"`r`n\" + $after; [IO.File]::WriteAllText\\($progPath, $c2\\); Write-Output \"Added 'using AyCode.Core.Serializers.Console.Benchmarks;' to Program.cs\" } else { Write-Output \"Using already present in Program.cs\" })" ] } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs b/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs new file mode 100644 index 0000000..ed39f22 --- /dev/null +++ b/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs @@ -0,0 +1,58 @@ +namespace AyCode.Core.Serializers.Console.Benchmarks; + +/// +/// Common contract for every per-engine, per-I/O-mode benchmark row. Each implementation captures +/// one (Engine × IoMode × OptionsPreset) combination — e.g. AcBinary Byte[] FastMode SGen — +/// and exposes a uniform Serialize / Deserialize hot-path that the benchmark loop +/// drives through warmup + adaptive-iter calibration + measurement. +/// +/// The default + methods iterate +/// the hot path N times — overrides are only needed when an implementor wants Ser/Des-specific +/// warmup state (rare). Round-trip-only benchmarks (NamedPipe etc.) set +/// to true so the bench loop skips the Des-phase and routes timing into the RT columns. +/// +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". + string OptionsPreset { get; } + /// Synthesized display name from Engine + IoMode + OptionsPreset. + string Name => $"{Engine} ({IoMode}, {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. + long SetupSerializeAllocBytes { get; } + /// One-time DESERIALIZER-side setup allocation cost (e.g., long-lived AsyncPipeReaderInput's ArrayPool rent + ManualResetEventSlim, drain-task scaffolding). Captured in constructor; 0 for byte[] API and any setup-free deserialize path. + long SetupDeserializeAllocBytes { get; } + /// True when Serialize() does a full round-trip (e.g. NamedPipe) and Deserialize() is a no-op. + /// Used by the SUMMARY: WINNERS section to skip such cells from "Fastest Serialize" and "Fastest Deserialize" + /// rankings (because both metrics are misleading there) — they still participate in "Fastest Round-trip". + /// Default false for in-memory IO modes which measure Ser and Des separately. + bool IsRoundTripOnly => false; + /// Warm only the Serialize path. Default body iterates N times. + /// Overrides are only needed when the implementor wants Ser-specific warmup-state (e.g. pre-allocate buffers). + /// On benchmarks (NamedPipe-style) performs the full RT, + /// so this warms the entire round-trip path. + void WarmupSerialize(int iterations) + { + for (var i = 0; i < iterations; i++) Serialize(); + } + + /// Warm only the Deserialize path. Default body iterates N times. + /// On benchmarks is a no-op, so the bench loop + /// skips the Des-phase entirely for those cells. + void WarmupDeserialize(int iterations) + { + for (var i = 0; i < iterations; i++) Deserialize(); + } + + void Serialize(); + void Deserialize(); + /// Round-trip correctness check — called once per cell before warmup. Returns true if Serialize+Deserialize preserves data. + bool VerifyRoundTrip(); +} diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 8588af2..49cec85 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -18,6 +18,8 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; +using AyCode.Core.Serializers.Console.Benchmarks; + namespace AyCode.Core.Serializers.Console; /// @@ -635,53 +637,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa #region Serializer Implementations - private 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". - string OptionsPreset { get; } - /// Synthesized display name from Engine + IoMode + OptionsPreset. - string Name => $"{Engine} ({IoMode}, {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. - long SetupSerializeAllocBytes { get; } - /// One-time DESERIALIZER-side setup allocation cost (e.g., long-lived AsyncPipeReaderInput's ArrayPool rent + ManualResetEventSlim, drain-task scaffolding). Captured in constructor; 0 for byte[] API and any setup-free deserialize path. - long SetupDeserializeAllocBytes { get; } - /// True when Serialize() does a full round-trip (e.g. NamedPipe) and Deserialize() is a no-op. - /// Used by the SUMMARY: WINNERS section to skip such cells from "Fastest Serialize" and "Fastest Deserialize" - /// rankings (because both metrics are misleading there) — they still participate in "Fastest Round-trip". - /// Default false for in-memory IO modes which measure Ser and Des separately. - bool IsRoundTripOnly => false; - /// Warm only the Serialize path. Default body iterates N times. - /// Overrides are only needed when the implementor wants Ser-specific warmup-state (e.g. pre-allocate buffers). - /// On benchmarks (NamedPipe-style) performs the full RT, - /// so this warms the entire round-trip path. - void WarmupSerialize(int iterations) - { - for (var i = 0; i < iterations; i++) Serialize(); - } - - /// Warm only the Deserialize path. Default body iterates N times. - /// On benchmarks is a no-op, so the bench loop - /// skips the Des-phase entirely for those cells. - void WarmupDeserialize(int iterations) - { - for (var i = 0; i < iterations; i++) Deserialize(); - } - - void Serialize(); - void Deserialize(); - /// Round-trip correctness check — called once per cell before warmup. Returns true if Serialize+Deserialize preserves data. - bool VerifyRoundTrip(); - } - - private sealed class AcBinaryBenchmark : ISerializerBenchmark +internal sealed class AcBinaryBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -729,7 +685,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa } } - private sealed class MemoryPackBenchmark : ISerializerBenchmark + internal sealed class MemoryPackBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly MemoryPackSerializerOptions _options; @@ -771,7 +727,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa // to DynamicGenericResolver for closed-generic types (List et al.), which uses // Activator.CreateInstance on formatter types the AOT trimmer drops → MissingMethodException at runtime. // Available for regular JIT runs (`dotnet run`) only. - private sealed class MessagePackBenchmark : ISerializerBenchmark + internal sealed class MessagePackBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly MessagePackSerializerOptions _options; @@ -827,7 +783,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// 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). /// - private sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark + internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -909,7 +865,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// 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. /// - private sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDisposable + internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDisposable { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -1124,7 +1080,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// custom message brokers). The numbers from this row reflect that scenario, NOT the kernel-pipe loopback /// of the NamedPipe benchmark. /// - private sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDisposable + internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDisposable { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -1309,7 +1265,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// 's API contract); the receive-side scratch buffer is also allocated per-iter /// on the consumer-task (counted via GC.GetTotalAllocatedBytes in BenchmarkLoop.MeasureAllocationTotal). /// - private sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchmark, IDisposable + internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchmark, IDisposable { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -1513,7 +1469,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// Side-by-side with this isolates the kernel-NamedPipe /// overhead on the raw-byte[] side. /// - private sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchmark, IDisposable + internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchmark, IDisposable { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -1663,7 +1619,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// Benchmarks MemoryPack via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call. /// Apples-to-apples counterpart to AcBinaryFreshBufferWriterBenchmark. /// - private sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmark + internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly MemoryPackSerializerOptions _options; @@ -1707,7 +1663,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa } } - private sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark + internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; @@ -1769,7 +1725,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa /// Benchmarks MemoryPack via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter. /// Apples-to-apples counterpart to AcBinaryBufferWriterBenchmark — MemoryPack's IBufferWriter is the path it's designed for. /// - private sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark + internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly MemoryPackSerializerOptions _options; @@ -1821,7 +1777,7 @@ private static List RunBenchmarksForTestData(TestDataSet testDa } } - private sealed class SystemTextJsonBenchmark : ISerializerBenchmark + internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly JsonSerializerOptions _options;