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;