[LOADED_DOCS: NONE]

Refactor benchmark suite to use enums for config

Replaced string parameters for layer, opMode, and serializerMode with strongly-typed enums (BenchmarkLayer, BenchmarkOpMode, SerializerSelectionMode) across BenchmarkLoop, Menu, and Program. Updated CLI parsing and menu logic to use Enum.TryParse and return enums. Added XML docs for new enums. Improves type safety, code clarity, and maintainability.
This commit is contained in:
Loretta 2026-05-13 06:19:58 +02:00
parent eaafb00739
commit 23f2f57fa7
4 changed files with 125 additions and 65 deletions

View File

@ -22,7 +22,7 @@ internal static class BenchmarkLoop
/// + measurement → grouped results print → save to disk. Used by both the CLI and interactive /// + measurement → grouped results print → save to disk. Used by both the CLI and interactive
/// menu paths; the interactive loop calls this repeatedly without restarting the process. /// menu paths; the interactive loop calls this repeatedly without restarting the process.
/// </summary> /// </summary>
internal static void RunBenchmark(string layer, string opMode, string serializerMode) internal static void RunBenchmark(BenchmarkLayer layer, BenchmarkOpMode opMode, SerializerSelectionMode serializerMode)
{ {
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗"); System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║"); System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
@ -149,7 +149,7 @@ internal static class BenchmarkLoop
} }
} }
private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode, string serializerMode) private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, BenchmarkOpMode mode, SerializerSelectionMode serializerMode)
{ {
var results = new List<BenchmarkResult>(); var results = new List<BenchmarkResult>();
var serializers = CreateSerializers(testData, serializerMode); var serializers = CreateSerializers(testData, serializerMode);
@ -212,7 +212,7 @@ internal static class BenchmarkLoop
// Round-trip-only benchmarks (NamedPipe etc.): single phase — Serialize() performs the full RT, // Round-trip-only benchmarks (NamedPipe etc.): single phase — Serialize() performs the full RT,
// Deserialize() is a no-op. We use the Ser-phase entry-points (WarmupSerialize) to warm the // Deserialize() is a no-op. We use the Ser-phase entry-points (WarmupSerialize) to warm the
// entire round-trip path, then record into the RT result columns. // entire round-trip path, then record into the RT result columns.
if (mode is "all" or "serialize" or "ser") if (mode is BenchmarkOpMode.All or BenchmarkOpMode.Serialize)
{ {
ForceGcCollect(); ForceGcCollect();
serializer.WarmupSerialize(Configuration.WarmupIterations); serializer.WarmupSerialize(Configuration.WarmupIterations);
@ -229,12 +229,12 @@ internal static class BenchmarkLoop
// also show up — otherwise current-thread alloc would only count the client side and look ~halved. // also show up — otherwise current-thread alloc would only count the client side and look ~halved.
result.RoundTripAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT alloc]", processWide: true); result.RoundTripAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT alloc]", processWide: true);
} }
// mode == "deserialize" alone is meaningless for a round-trip-only benchmark; skip silently. // mode == BenchmarkOpMode.Deserialize alone is meaningless for a round-trip-only benchmark; skip silently.
} }
else else
{ {
// ── Ser phase ── isolated warmup → Configuration.JitSleep → calibrate → time → alloc; preceded by GC.Collect. // ── Ser phase ── isolated warmup → Configuration.JitSleep → calibrate → time → alloc; preceded by GC.Collect.
if (mode is "all" or "serialize" or "ser") if (mode is BenchmarkOpMode.All or BenchmarkOpMode.Serialize)
{ {
ForceGcCollect(); ForceGcCollect();
serializer.WarmupSerialize(Configuration.WarmupIterations); serializer.WarmupSerialize(Configuration.WarmupIterations);
@ -254,7 +254,7 @@ internal static class BenchmarkLoop
// ── Des phase ── isolated warmup → Configuration.JitSleep → calibrate → time → alloc; preceded by GC.Collect. // ── Des phase ── isolated warmup → Configuration.JitSleep → calibrate → time → alloc; preceded by GC.Collect.
// The GC.Collect here is critical: it discards the Ser-phase's write-buffer pool churn so the // The GC.Collect here is critical: it discards the Ser-phase's write-buffer pool churn so the
// Des-phase's allocation measurement reflects ONLY Des-side allocations (deserialized object graph). // Des-phase's allocation measurement reflects ONLY Des-side allocations (deserialized object graph).
if (mode is "all" or "deserialize" or "des") if (mode is BenchmarkOpMode.All or BenchmarkOpMode.Deserialize)
{ {
ForceGcCollect(); ForceGcCollect();
serializer.WarmupDeserialize(Configuration.WarmupIterations); serializer.WarmupDeserialize(Configuration.WarmupIterations);
@ -293,7 +293,7 @@ internal static class BenchmarkLoop
return results; return results;
} }
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData, string serializerMode) private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData, SerializerSelectionMode serializerMode)
{ {
// FastestByte mode — focused 1:1 comparison on the "fastest Byte[]" path. // FastestByte mode — focused 1:1 comparison on the "fastest Byte[]" path.
// TWO benchmarks: AcBinary FastMode Byte[] (Compact UTF-8) + MemoryPack Byte[]. // TWO benchmarks: AcBinary FastMode Byte[] (Compact UTF-8) + MemoryPack Byte[].
@ -303,7 +303,7 @@ internal static class BenchmarkLoop
// FastWire row (UTF-16 raw memcpy) commented out for the current optimization sprint — // FastWire row (UTF-16 raw memcpy) commented out for the current optimization sprint —
// we are tuning Compact mode against MemPack directly; FastWire was used as a noise-floor // we are tuning Compact mode against MemPack directly; FastWire was used as a noise-floor
// reference earlier. Re-enable when revisiting Fast wire-mode performance. // reference earlier. Re-enable when revisiting Fast wire-mode performance.
if (serializerMode == "fastestbyte") if (serializerMode == SerializerSelectionMode.FastestByte)
{ {
var fastestByteOptions = AcBinarySerializerOptions.FastMode; var fastestByteOptions = AcBinarySerializerOptions.FastMode;
fastestByteOptions.WireMode = Configuration.SelectedWireMode; fastestByteOptions.WireMode = Configuration.SelectedWireMode;
@ -320,7 +320,7 @@ internal static class BenchmarkLoop
// Streaming I/O has long-lived pipe setup + kernel-buffer overhead that, when interleaved with // Streaming I/O has long-lived pipe setup + kernel-buffer overhead that, when interleaved with
// the standard byte-array / IBufferWriter measurements, masks the steady-state numbers. Run it // the standard byte-array / IBufferWriter measurements, masks the steady-state numbers. Run it
// in isolation so the timing numbers reflect ONLY the streaming path. // in isolation so the timing numbers reflect ONLY the streaming path.
if (serializerMode == "asyncpipe") if (serializerMode == SerializerSelectionMode.AsyncPipe)
{ {
// NamedPipe — pipe-aligned chunk size for the long-lived IPC scenario. The chunkSize here // NamedPipe — pipe-aligned chunk size for the long-lived IPC scenario. The chunkSize here
// drives the AsyncPipeWriterOutput's chunk-on-wire size (header + data, page-aligned thanks to // drives the AsyncPipeWriterOutput's chunk-on-wire size (header + data, page-aligned thanks to
@ -763,9 +763,9 @@ internal static class BenchmarkLoop
/// Filters test data sets by layer keyword. Layered approach lets you run only what's needed for the iteration cadence. /// Filters test data sets by layer keyword. Layered approach lets you run only what's needed for the iteration cadence.
/// P1: only "Core" data exists (Small/Medium/Large/Repeated/Deep). Comprehensive and Edge layers will be expanded in P2. /// P1: only "Core" data exists (Small/Medium/Large/Repeated/Deep). Comprehensive and Edge layers will be expanded in P2.
/// </summary> /// </summary>
internal static List<TestDataSet> FilterByLayer(List<TestDataSet> all, string layer) internal static List<TestDataSet> FilterByLayer(List<TestDataSet> all, BenchmarkLayer layer)
{ {
if (layer == "all") return all.ToList(); if (layer == BenchmarkLayer.All) return all.ToList();
var coreNames = new[] { "Small", "Medium", "Large", "Repeated", "Deep" }; var coreNames = new[] { "Small", "Medium", "Large", "Repeated", "Deep" };
// P2 will add: "Flat", "Polymorphic", "Collection", "Numeric", "NonAscii", etc. // P2 will add: "Flat", "Polymorphic", "Collection", "Numeric", "NonAscii", etc.
@ -775,17 +775,17 @@ internal static class BenchmarkLoop
return layer switch return layer switch
{ {
"core" => all.Where(t => StartsWithAny(t.Name, coreNames)).ToList(), BenchmarkLayer.Core => all.Where(t => StartsWithAny(t.Name, coreNames)).ToList(),
"comprehensive" => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras)).ToList(), BenchmarkLayer.Comprehensive => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras)).ToList(),
"edge" => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras) || StartsWithAny(t.Name, edgeExtras)).ToList(), BenchmarkLayer.Edge => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras) || StartsWithAny(t.Name, edgeExtras)).ToList(),
// Single-cell A/B mini-suite filters — match by case-insensitive prefix on Name. // Single-cell A/B mini-suite filters — match by case-insensitive prefix on Name.
// Use case: tight optimization-iteration loop on one specific cell (e.g. `dotnet run -- repeated` // Use case: tight optimization-iteration loop on one specific cell (e.g. `dotnet run -- repeated`
// or interactive menu shortcut), avoiding the full ~110 sec suite when only one cell is in scope. // or interactive menu shortcut), avoiding the full ~110 sec suite when only one cell is in scope.
"small" => all.Where(t => t.Name.StartsWith("Small", StringComparison.OrdinalIgnoreCase)).ToList(), BenchmarkLayer.Small => all.Where(t => t.Name.StartsWith("Small", StringComparison.OrdinalIgnoreCase)).ToList(),
"medium" => all.Where(t => t.Name.StartsWith("Medium", StringComparison.OrdinalIgnoreCase)).ToList(), BenchmarkLayer.Medium => all.Where(t => t.Name.StartsWith("Medium", StringComparison.OrdinalIgnoreCase)).ToList(),
"large" => all.Where(t => t.Name.StartsWith("Large", StringComparison.OrdinalIgnoreCase)).ToList(), BenchmarkLayer.Large => all.Where(t => t.Name.StartsWith("Large", StringComparison.OrdinalIgnoreCase)).ToList(),
"repeated" => all.Where(t => t.Name.StartsWith("Repeated", StringComparison.OrdinalIgnoreCase)).ToList(), BenchmarkLayer.Repeated => all.Where(t => t.Name.StartsWith("Repeated", StringComparison.OrdinalIgnoreCase)).ToList(),
"deep" => all.Where(t => t.Name.StartsWith("Deep", StringComparison.OrdinalIgnoreCase)).ToList(), BenchmarkLayer.Deep => all.Where(t => t.Name.StartsWith("Deep", StringComparison.OrdinalIgnoreCase)).ToList(),
_ => all.ToList() _ => all.ToList()
}; };

View File

@ -49,6 +49,59 @@ internal enum BenchmarkDispatchMode
Hybrid, Hybrid,
} }
/// <summary>
/// Test-data layer filter — selects which <see cref="TestDataSet"/> cells participate in the run.
/// Replaces the prior string-typed <c>layer</c> parameter; CLI/menu callers parse user input via
/// <see cref="Enum.TryParse{T}(string, bool, out T)"/> with <c>ignoreCase: true</c>.
/// <list type="bullet">
/// <item><see cref="All"/> — no filter; every test data set runs.</item>
/// <item><see cref="Core"/>/<see cref="Comprehensive"/>/<see cref="Edge"/> — preset bundles (Comprehensive ⊇ Core, Edge ⊇ Comprehensive).</item>
/// <item><see cref="Small"/>/<see cref="Medium"/>/<see cref="Large"/>/<see cref="Repeated"/>/<see cref="Deep"/> — single-cell mini-suites for tight A/B iteration loops.</item>
/// </list>
/// </summary>
internal enum BenchmarkLayer
{
All,
Core,
Comprehensive,
Edge,
Small,
Medium,
Large,
Repeated,
Deep,
}
/// <summary>
/// Per-phase operation filter — selects which sides of the benchmark (Ser, Des, both) run for each
/// serializer. Round-trip-only benchmarks (NamedPipe etc.) treat <see cref="Deserialize"/> alone as
/// a no-op and only run on <see cref="Serialize"/> or <see cref="All"/>. Replaces the prior string-typed
/// <c>mode</c>/<c>opMode</c> parameter.
/// </summary>
internal enum BenchmarkOpMode
{
All,
Serialize,
Deserialize,
}
/// <summary>
/// Serializer-set selection — drives <c>BenchmarkLoop.CreateSerializers</c> to return one of three
/// preset bundles instead of a magic string. Replaces the prior string-typed <c>serializerMode</c>
/// parameter.
/// <list type="bullet">
/// <item><see cref="Standard"/> — full suite minus AsyncPipe (the streaming benchmark is opt-in).</item>
/// <item><see cref="FastestByte"/> — focused AcBinary FastMode Byte[] vs MemoryPack Byte[] 1:1 comparison.</item>
/// <item><see cref="AsyncPipe"/> — streaming I/O isolation (NamedPipe + in-memory Pipe variants only).</item>
/// </list>
/// </summary>
internal enum SerializerSelectionMode
{
Standard,
FastestByte,
AsyncPipe,
}
/// <summary> /// <summary>
/// Display-string converters for the benchmark enums. Renders enum values into the column-friendly /// 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 <c>.log</c> file CSV/formatted output, /// human-readable form used by the per-row console table, the <c>.log</c> file CSV/formatted output,

View File

@ -1,4 +1,5 @@
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Console.Benchmarks;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
namespace AyCode.Core.Serializers.Console; namespace AyCode.Core.Serializers.Console;
@ -12,10 +13,10 @@ namespace AyCode.Core.Serializers.Console;
internal static class Menu internal static class Menu
{ {
/// <summary> /// <summary>
/// Interactive menu shown when no CLI args. Returns the layer keyword (core/comprehensive/edge/all) or null on Quit. /// Interactive menu shown when no CLI args. Returns the (layer, serializerMode) tuple, or null on Quit.
/// Loops on settings-changes ([S]) — user is returned to this menu after modifying iteration counts. /// Loops on settings-changes ([S]) — user is returned to this menu after modifying iteration counts.
/// </summary> /// </summary>
internal static (string layer, string serializerMode)? ShowInteractiveMenu() internal static (BenchmarkLayer layer, SerializerSelectionMode serializerMode)? ShowInteractiveMenu()
{ {
while (true) while (true)
{ {
@ -41,17 +42,17 @@ internal static class Menu
switch (char.ToLower(key)) switch (char.ToLower(key))
{ {
case '1': return ("core", "standard"); case '1': return (BenchmarkLayer.Core, SerializerSelectionMode.Standard);
case '2': return ("comprehensive", "standard"); case '2': return (BenchmarkLayer.Comprehensive, SerializerSelectionMode.Standard);
case '3': return ("edge", "standard"); case '3': return (BenchmarkLayer.Edge, SerializerSelectionMode.Standard);
case 'a': return ("all", "standard"); case 'a': return (BenchmarkLayer.All, SerializerSelectionMode.Standard);
case 'f': return ("all", "fastestbyte"); case 'f': return (BenchmarkLayer.All, SerializerSelectionMode.FastestByte);
case 'p': return ("all", "asyncpipe"); case 'p': return (BenchmarkLayer.All, SerializerSelectionMode.AsyncPipe);
case 's': case 's':
ShowSettingsMenu(); ShowSettingsMenu();
continue; // re-display the main menu after settings update continue; // re-display the main menu after settings update
case 'q': return null; case 'q': return null;
default: return ("all", "standard"); default: return (BenchmarkLayer.All, SerializerSelectionMode.Standard);
} }
} }
} }

View File

@ -63,7 +63,7 @@ public static class Program
var selection = Menu.ShowInteractiveMenu(); var selection = Menu.ShowInteractiveMenu();
if (selection == null) return; // user pressed Q if (selection == null) return; // user pressed Q
BenchmarkLoop.RunBenchmark(selection.Value.layer, "all", selection.Value.serializerMode); BenchmarkLoop.RunBenchmark(selection.Value.layer, BenchmarkOpMode.All, selection.Value.serializerMode);
System.Console.WriteLine(); System.Console.WriteLine();
System.Console.WriteLine("─────────────────────────────────────────────────────────────────────"); System.Console.WriteLine("─────────────────────────────────────────────────────────────────────");
@ -75,52 +75,58 @@ public static class Program
} }
/// <summary> /// <summary>
/// Parses CLI arguments into (layer, opMode, serializerMode). Returns <c>false</c> if the args /// Parses CLI arguments into (layer, opMode, serializerMode). Uses <see cref="Enum.TryParse{T}(string, bool, out T)"/>
/// are invalid; the caller should then exit without running the standard benchmark. /// case-insensitive against the three enums in this order: <see cref="SerializerSelectionMode"/> (matches
/// "FastestByte"/"AsyncPipe"), <see cref="BenchmarkOpMode"/> (matches "Serialize"/"Deserialize"), then
/// <see cref="BenchmarkLayer"/> (matches "Core"/"Comprehensive"/"Edge"/"Small"/...). Special-cased:
/// <c>"quick"</c> mutates <see cref="Configuration"/> warmup/iter counts but selects no layer/op/mode.
/// Unknown args silently default to (All, All, Standard) — matches the prior behavior where unrecognized
/// args fell through <see cref="BenchmarkLoop.FilterByLayer"/>'s default branch (full unfiltered suite).
/// Returns <c>false</c> only if the caller-side path would need to abort; currently always returns <c>true</c>.
/// </summary> /// </summary>
private static bool TryParseCliArgs(string[] args, out string layer, out string opMode, out string serializerMode) private static bool TryParseCliArgs(string[] args, out BenchmarkLayer layer, out BenchmarkOpMode opMode, out SerializerSelectionMode serializerMode)
{ {
layer = "all"; layer = BenchmarkLayer.All;
opMode = "all"; opMode = BenchmarkOpMode.All;
serializerMode = "standard"; serializerMode = SerializerSelectionMode.Standard;
var arg = args[0].ToLower(); var arg = args[0];
// Quick mode: short warmup, few iterations, small sample count // Quick mode: short warmup, few iterations, small sample count. Not an enum value — it's a
if (arg == "quick") // Configuration meta-flag, so handle it before the enum-parse cascade.
if (string.Equals(arg, "quick", StringComparison.OrdinalIgnoreCase))
{ {
Configuration.WarmupIterations = 5; Configuration.WarmupIterations = 5;
Configuration.TestIterations = 100; Configuration.TestIterations = 100;
Configuration.BenchmarkSamples = 3; Configuration.BenchmarkSamples = 3;
layer = "all"; return true;
}
else if (arg is "core" or "comprehensive" or "edge" or "all"
or "small" or "medium" or "large" or "repeated" or "deep")
{
layer = arg;
}
else if (arg is "asyncpipe" or "pipe")
{
// AsyncPipe-only mode: streaming I/O isolation across all test data.
layer = "all";
serializerMode = "asyncpipe";
}
else if (arg is "ser" or "serialize")
{
opMode = "serialize";
layer = "all";
}
else if (arg is "des" or "deserialize")
{
opMode = "deserialize";
layer = "all";
}
else
{
// Backwards compat: unknown arg → treat as layer keyword
layer = arg;
} }
// Serializer-selection first (AsyncPipe/FastestByte/Standard) — narrower set than layers,
// and "AsyncPipe" forced layer=All in the old code anyway.
if (Enum.TryParse<SerializerSelectionMode>(arg, ignoreCase: true, out var sm))
{
serializerMode = sm;
return true;
}
// Op-mode (Serialize/Deserialize/All).
if (Enum.TryParse<BenchmarkOpMode>(arg, ignoreCase: true, out var om))
{
opMode = om;
return true;
}
// Layer (Core/Comprehensive/Edge/Small/Medium/Large/Repeated/Deep/All).
if (Enum.TryParse<BenchmarkLayer>(arg, ignoreCase: true, out var ly))
{
layer = ly;
return true;
}
// Unknown arg — defaults remain (All, All, Standard). Matches prior behaviour where the
// unrecognized string fell through FilterByLayer's `_ => all.ToList()` default branch.
System.Console.Error.WriteLine($"Warning: unrecognized argument '{arg}'. Running full suite (Layer=All, OpMode=All, SerializerMode=Standard).");
return true; return true;
} }