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; }