From 23f2f57fa7d16edbd4b308e63608a5982ed89b1a Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 13 May 2026 06:19:58 +0200 Subject: [PATCH] [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. --- .../BenchmarkLoop.cs | 38 ++++----- .../Benchmarks/BenchmarkEnums.cs | 53 ++++++++++++ AyCode.Core.Serializers.Console/Menu.cs | 19 ++--- AyCode.Core.Serializers.Console/Program.cs | 80 ++++++++++--------- 4 files changed, 125 insertions(+), 65 deletions(-) diff --git a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs index 7ce5ee8..1fb50c1 100644 --- a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs +++ b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs @@ -22,7 +22,7 @@ internal static class BenchmarkLoop /// + 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. /// - 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("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║"); @@ -149,7 +149,7 @@ internal static class BenchmarkLoop } } - private static List RunBenchmarksForTestData(TestDataSet testData, string mode, string serializerMode) + private static List RunBenchmarksForTestData(TestDataSet testData, BenchmarkOpMode mode, SerializerSelectionMode serializerMode) { var results = new List(); 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, // 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. - if (mode is "all" or "serialize" or "ser") + if (mode is BenchmarkOpMode.All or BenchmarkOpMode.Serialize) { ForceGcCollect(); 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. 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 { // ── 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(); 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. // 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). - if (mode is "all" or "deserialize" or "des") + if (mode is BenchmarkOpMode.All or BenchmarkOpMode.Deserialize) { ForceGcCollect(); serializer.WarmupDeserialize(Configuration.WarmupIterations); @@ -293,7 +293,7 @@ internal static class BenchmarkLoop return results; } - private static List CreateSerializers(TestDataSet testData, string serializerMode) + private static List CreateSerializers(TestDataSet testData, SerializerSelectionMode serializerMode) { // FastestByte mode — focused 1:1 comparison on the "fastest Byte[]" path. // 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 — // 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. - if (serializerMode == "fastestbyte") + if (serializerMode == SerializerSelectionMode.FastestByte) { var fastestByteOptions = AcBinarySerializerOptions.FastMode; 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 // the standard byte-array / IBufferWriter measurements, masks the steady-state numbers. Run it // 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 // 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. /// P1: only "Core" data exists (Small/Medium/Large/Repeated/Deep). Comprehensive and Edge layers will be expanded in P2. /// - internal static List FilterByLayer(List all, string layer) + internal static List FilterByLayer(List all, BenchmarkLayer layer) { - if (layer == "all") return all.ToList(); + if (layer == BenchmarkLayer.All) return all.ToList(); var coreNames = new[] { "Small", "Medium", "Large", "Repeated", "Deep" }; // P2 will add: "Flat", "Polymorphic", "Collection", "Numeric", "NonAscii", etc. @@ -775,17 +775,17 @@ internal static class BenchmarkLoop return layer switch { - "core" => all.Where(t => StartsWithAny(t.Name, coreNames)).ToList(), - "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.Core => all.Where(t => StartsWithAny(t.Name, coreNames)).ToList(), + BenchmarkLayer.Comprehensive => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras)).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. // 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. - "small" => all.Where(t => t.Name.StartsWith("Small", StringComparison.OrdinalIgnoreCase)).ToList(), - "medium" => all.Where(t => t.Name.StartsWith("Medium", StringComparison.OrdinalIgnoreCase)).ToList(), - "large" => all.Where(t => t.Name.StartsWith("Large", StringComparison.OrdinalIgnoreCase)).ToList(), - "repeated" => all.Where(t => t.Name.StartsWith("Repeated", StringComparison.OrdinalIgnoreCase)).ToList(), - "deep" => all.Where(t => t.Name.StartsWith("Deep", StringComparison.OrdinalIgnoreCase)).ToList(), + BenchmarkLayer.Small => all.Where(t => t.Name.StartsWith("Small", StringComparison.OrdinalIgnoreCase)).ToList(), + BenchmarkLayer.Medium => all.Where(t => t.Name.StartsWith("Medium", StringComparison.OrdinalIgnoreCase)).ToList(), + BenchmarkLayer.Large => all.Where(t => t.Name.StartsWith("Large", StringComparison.OrdinalIgnoreCase)).ToList(), + BenchmarkLayer.Repeated => all.Where(t => t.Name.StartsWith("Repeated", StringComparison.OrdinalIgnoreCase)).ToList(), + BenchmarkLayer.Deep => all.Where(t => t.Name.StartsWith("Deep", StringComparison.OrdinalIgnoreCase)).ToList(), _ => all.ToList() }; diff --git a/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs b/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs index 3e4fc04..4a107f9 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs +++ b/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs @@ -49,6 +49,59 @@ internal enum BenchmarkDispatchMode Hybrid, } +/// +/// Test-data layer filter — selects which cells participate in the run. +/// Replaces the prior string-typed layer parameter; CLI/menu callers parse user input via +/// with ignoreCase: true. +/// +/// — no filter; every test data set runs. +/// // — preset bundles (Comprehensive ⊇ Core, Edge ⊇ Comprehensive). +/// //// — single-cell mini-suites for tight A/B iteration loops. +/// +/// +internal enum BenchmarkLayer +{ + All, + Core, + Comprehensive, + Edge, + Small, + Medium, + Large, + Repeated, + Deep, +} + +/// +/// Per-phase operation filter — selects which sides of the benchmark (Ser, Des, both) run for each +/// serializer. Round-trip-only benchmarks (NamedPipe etc.) treat alone as +/// a no-op and only run on or . Replaces the prior string-typed +/// mode/opMode parameter. +/// +internal enum BenchmarkOpMode +{ + All, + Serialize, + Deserialize, +} + +/// +/// Serializer-set selection — drives BenchmarkLoop.CreateSerializers to return one of three +/// preset bundles instead of a magic string. Replaces the prior string-typed serializerMode +/// parameter. +/// +/// — full suite minus AsyncPipe (the streaming benchmark is opt-in). +/// — focused AcBinary FastMode Byte[] vs MemoryPack Byte[] 1:1 comparison. +/// — streaming I/O isolation (NamedPipe + in-memory Pipe variants only). +/// +/// +internal enum SerializerSelectionMode +{ + Standard, + FastestByte, + AsyncPipe, +} + /// /// 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, diff --git a/AyCode.Core.Serializers.Console/Menu.cs b/AyCode.Core.Serializers.Console/Menu.cs index 48b6ae9..e23ff18 100644 --- a/AyCode.Core.Serializers.Console/Menu.cs +++ b/AyCode.Core.Serializers.Console/Menu.cs @@ -1,4 +1,5 @@ using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Console.Benchmarks; using AyCode.Core.Tests.TestModels; namespace AyCode.Core.Serializers.Console; @@ -12,10 +13,10 @@ namespace AyCode.Core.Serializers.Console; internal static class Menu { /// - /// 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. /// - internal static (string layer, string serializerMode)? ShowInteractiveMenu() + internal static (BenchmarkLayer layer, SerializerSelectionMode serializerMode)? ShowInteractiveMenu() { while (true) { @@ -41,17 +42,17 @@ internal static class Menu switch (char.ToLower(key)) { - case '1': return ("core", "standard"); - case '2': return ("comprehensive", "standard"); - case '3': return ("edge", "standard"); - case 'a': return ("all", "standard"); - case 'f': return ("all", "fastestbyte"); - case 'p': return ("all", "asyncpipe"); + case '1': return (BenchmarkLayer.Core, SerializerSelectionMode.Standard); + case '2': return (BenchmarkLayer.Comprehensive, SerializerSelectionMode.Standard); + case '3': return (BenchmarkLayer.Edge, SerializerSelectionMode.Standard); + case 'a': return (BenchmarkLayer.All, SerializerSelectionMode.Standard); + case 'f': return (BenchmarkLayer.All, SerializerSelectionMode.FastestByte); + case 'p': return (BenchmarkLayer.All, SerializerSelectionMode.AsyncPipe); case 's': ShowSettingsMenu(); continue; // re-display the main menu after settings update case 'q': return null; - default: return ("all", "standard"); + default: return (BenchmarkLayer.All, SerializerSelectionMode.Standard); } } } diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 1287dd1..4606ae1 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -63,7 +63,7 @@ public static class Program var selection = Menu.ShowInteractiveMenu(); 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("─────────────────────────────────────────────────────────────────────"); @@ -75,52 +75,58 @@ public static class Program } /// - /// Parses CLI arguments into (layer, opMode, serializerMode). Returns false if the args - /// are invalid; the caller should then exit without running the standard benchmark. + /// Parses CLI arguments into (layer, opMode, serializerMode). Uses + /// case-insensitive against the three enums in this order: (matches + /// "FastestByte"/"AsyncPipe"), (matches "Serialize"/"Deserialize"), then + /// (matches "Core"/"Comprehensive"/"Edge"/"Small"/...). Special-cased: + /// "quick" mutates 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 's default branch (full unfiltered suite). + /// Returns false only if the caller-side path would need to abort; currently always returns true. /// - 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"; - opMode = "all"; - serializerMode = "standard"; + layer = BenchmarkLayer.All; + opMode = BenchmarkOpMode.All; + serializerMode = SerializerSelectionMode.Standard; - var arg = args[0].ToLower(); + var arg = args[0]; - // Quick mode: short warmup, few iterations, small sample count - if (arg == "quick") + // Quick mode: short warmup, few iterations, small sample count. Not an enum value — it's a + // Configuration meta-flag, so handle it before the enum-parse cascade. + if (string.Equals(arg, "quick", StringComparison.OrdinalIgnoreCase)) { Configuration.WarmupIterations = 5; Configuration.TestIterations = 100; Configuration.BenchmarkSamples = 3; - layer = "all"; - } - 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; + return true; } + // Serializer-selection first (AsyncPipe/FastestByte/Standard) — narrower set than layers, + // and "AsyncPipe" forced layer=All in the old code anyway. + if (Enum.TryParse(arg, ignoreCase: true, out var sm)) + { + serializerMode = sm; + return true; + } + + // Op-mode (Serialize/Deserialize/All). + if (Enum.TryParse(arg, ignoreCase: true, out var om)) + { + opMode = om; + return true; + } + + // Layer (Core/Comprehensive/Edge/Small/Medium/Large/Repeated/Deep/All). + if (Enum.TryParse(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; }