From 7fe21480e15015296976f297a65be543c9ed948e Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 12 May 2026 13:02:39 +0200 Subject: [PATCH] [LOADED_DOCS: 2 files, no new loads] Extract ISerializerBenchmark to its own file Moved ISerializerBenchmark from Program.cs to a new ISerializerBenchmark.cs file under the AyCode.Core.Serializers.Console.Benchmarks namespace. Updated all benchmark classes in Program.cs to implement the interface from the new namespace and made them internal. Added the necessary using directive to Program.cs. Adjusted a PowerShell script in settings.local.json to ensure the new using is present. Removed the old interface definition from Program.cs. --- .claude/settings.local.json | 3 +- .../Benchmarks/ISerializerBenchmark.cs | 58 +++++++++++++++ AyCode.Core.Serializers.Console/Program.cs | 72 ++++--------------- 3 files changed, 74 insertions(+), 59 deletions(-) create mode 100644 AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs 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;