diff --git a/AyCode.Core.Serializers.Console/Configuration.cs b/AyCode.Core.Serializers.Console/Configuration.cs new file mode 100644 index 0000000..e1b72d0 --- /dev/null +++ b/AyCode.Core.Serializers.Console/Configuration.cs @@ -0,0 +1,133 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Binaries; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; + +namespace AyCode.Core.Serializers.Console; + +/// +/// Configuration state for the benchmark application. Holds compile-time and runtime constants, +/// per-run mutable settings (WireMode, charset, iteration counts), and the attribute-flag +/// aggregation that drives the per-row Options column. Split out from Program.cs so the +/// entry-point file can focus on UX-flow and benchmark orchestration; everything in this class +/// is config / state — no benchmark logic. Single instance (static class) — the application is +/// a one-shot process, no multi-tenant state isolation needed. +/// +internal static class Configuration +{ + internal const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark"; + +#if AYCODE_NATIVEAOT + internal const string BuildConfiguration = "NativeAOT"; +#elif DEBUG + internal const string BuildConfiguration = "Debug"; +#else + internal const string BuildConfiguration = "Release"; +#endif + +#if DEBUG + internal static int WarmupIterations = 0; + internal static int TestIterations = 1; + internal static int BenchmarkSamples = 1; // Debug: single sample, fast iteration +#else + internal static int WarmupIterations = 5000; //10000 — per-phase (Ser AND Des get their own warmup separately) + internal static int TestIterations = 1000; //1000 + internal static int BenchmarkSamples = 10; +#endif + + // Interactive settings: selected AcBinary wire mode for benchmark runs. + // 1 = Compact, 2 = Fast + internal static WireMode SelectedWireMode = WireMode.Compact; + + // Serializer name constants + // Engine identifiers (used in Engine column + comparison logic) + internal const string EngineAcBinary = "AcBinary"; + internal const string EngineMemoryPack = "MemoryPack"; +#if !AYCODE_NATIVEAOT + internal const string EngineMessagePack = "MessagePack"; +#endif + internal const string EngineSystemTextJson = "System.Text.Json"; + + // IO mode identifiers (used in IO column + comparison logic) + internal const string IoByteArray = "Byte[]"; + internal const string IoBufWrReuse = "BufWr reuse"; + internal const string IoBufWrNew = "BufWr new"; + internal const string IoString = "String"; + internal const string IoNamedPipe = "NamedPipe"; + internal const string IoNamedPipeRaw = "NamedPipe"; + internal const string IoInMemoryPipe = "Pipe(in-mem)"; + internal const string IoInMemoryRaw = "Pipe(in-mem)"; + + // Single source of truth for the chunk size used by ALL pipe-related benchmarks (NamedPipe PipeChunk, + // NamedPipe PipeRaw, in-memory Pipe, in-memory RawMem) AND the NamedPipe server's inBufferSize/outBufferSize. + // Same value across both layers ensures apples-to-apples comparison: chunked-streaming chunk-on-wire size + // matches the kernel pipe-buffer slot exactly. Tweak HERE when experimenting; do NOT scatter chunkSize + // overrides across individual benchmark rows. + internal const int PipeChunkSize = 4096; + + // Dispatch mode identifiers — describes how property access / type dispatch happens for a given run. + // SGen = compile-time source generator path (Unsafe.As direct fields, slot-array wrapper lookup). + // Runtime= reflection / compiled-delegate path. + // Hybrid = SGen root with non-SGen child types reached via bridge methods. See docs/BINARY/BINARY_SGEN.md. + internal const string ModeSGen = "SGen"; + internal const string ModeRuntime = "Runtime"; + internal const string ModeHybrid = "Hybrid"; + + // Per-cell adaptive iteration target wall-clock duration. Each Ser/Des function calibrates its + // own iteration count post-warmup so the sample batch lands in this range — equalizes the + // per-sample window across cells of vastly different per-op cost (Small ~6 ns/op vs Large + // ~140 µs/op). Below ~100 ms Stopwatch precision and OS preempt spikes start to dominate. + internal const int TargetSampleMs = 250; + + // CV (coefficient of variation = stddev / mean) threshold above which a row's range is flagged + // as "unstable" in the markdown output (⚠️ marker). 3% is a reasonable noise-floor expectation + // for stabilized in-memory benchmarks; rows above it should be discounted when reading + // sub-3% inter-engine deltas. + internal const double UnstableCVThreshold = 0.03; + + // JIT-tier-promotion drain delay between warmup and measurement. + // - JIT mode (RuntimeFeature.IsDynamicCodeCompiled == true): tiered JIT promotes hot methods + // in a background thread; we wait briefly for the queue to drain so the first measurement + // sample doesn't catch a Tier-0 → Tier-1 transition mid-flight. + // - AOT mode (NativeAOT publish): no dynamic compilation happens; the sleep is pure noise. + // 250ms (vs the historical 3000ms) is sufficient for a few-method working set under .NET 9's + // tiered JIT — empirically the queue drains in <100ms for the bench's hot path. + internal static int JitSleep => RuntimeFeature.IsDynamicCodeCompiled ? 250 : 0; + + // OptionsPreset values are passed per-instance (constructor argument), not constants — + // each CreateSerializers call line specifies its own preset name (e.g. "FastMode", "NoIntern"). + + internal static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); + + /// + /// Aggregated feature flags across every type tagged with + /// the attribute in the loaded assemblies. Cached on first access (single reflection scan at startup). + /// Used by the benchmark's per-row Options-column formatter so the column shows BOTH the configured + /// options-level value AND the effective attribute-level enable flag — a feature flagged off at the + /// type level overrides the options regardless of preset, and that asymmetry must surface in the log + /// to avoid misreading a "RefHandling=OnlyId" / "Interning=All" line as actually active. + /// Aggregation rule: if ALL tagged types have the feature enabled → true; if any tagged type + /// disables it → false (a single disabling type suppresses the feature on the type-graph). + /// + internal static readonly (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) AttrFlags + = ScanAttributeFlags(); + + private static (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) ScanAttributeFlags() + { + var attrs = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty(); } }) + .Select(t => t.GetCustomAttribute()) + .Where(a => a != null) + .ToList(); + + if (attrs.Count == 0) return (false, false, false, false, false); + + return ( + refHandling: attrs.All(a => a!.EnableRefHandlingFeature), + internString: attrs.All(a => a!.EnableInternStringFeature), + metadata: attrs.All(a => a!.EnableMetadataFeature), + idTracking: attrs.All(a => a!.EnableIdTrackingFeature), + propertyFilter: attrs.All(a => a!.EnablePropertyFilterFeature)); + } +} diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 2b74bcc..61f31a1 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -32,120 +32,7 @@ namespace AyCode.Core.Serializers.Console; /// public static class Program { - private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark"; - -#if AYCODE_NATIVEAOT - private const string BuildConfiguration = "NativeAOT"; -#elif DEBUG - private const string BuildConfiguration = "Debug"; -#else - private const string BuildConfiguration = "Release"; -#endif - -#if DEBUG - private static int WarmupIterations = 0; - private static int TestIterations = 1; - private static int BenchmarkSamples = 1; // Debug: single sample, fast iteration -#else - private static int WarmupIterations = 5000; //10000 — per-phase (Ser AND Des get their own warmup separately) - private static int TestIterations = 1000; //1000 - private static int BenchmarkSamples = 10; -#endif - - // Interactive settings: selected AcBinary wire mode for benchmark runs. - // 1 = Compact, 2 = Fast - private static WireMode SelectedWireMode = WireMode.Compact; - - // Serializer name constants - // Engine identifiers (used in Engine column + comparison logic) - private const string EngineAcBinary = "AcBinary"; - private const string EngineMemoryPack = "MemoryPack"; -#if !AYCODE_NATIVEAOT - private const string EngineMessagePack = "MessagePack"; -#endif - private const string EngineSystemTextJson = "System.Text.Json"; - - // IO mode identifiers (used in IO column + comparison logic) - private const string IoByteArray = "Byte[]"; - private const string IoBufWrReuse = "BufWr reuse"; - private const string IoBufWrNew = "BufWr new"; - private const string IoString = "String"; - private const string IoNamedPipe = "NamedPipe"; - private const string IoNamedPipeRaw = "NamedPipe"; - private const string IoInMemoryPipe = "Pipe(in-mem)"; - private const string IoInMemoryRaw = "Pipe(in-mem)"; - - // Single source of truth for the chunk size used by ALL pipe-related benchmarks (NamedPipe PipeChunk, - // NamedPipe PipeRaw, in-memory Pipe, in-memory RawMem) AND the NamedPipe server's inBufferSize/outBufferSize. - // Same value across both layers ensures apples-to-apples comparison: chunked-streaming chunk-on-wire size - // matches the kernel pipe-buffer slot exactly. Tweak HERE when experimenting; do NOT scatter chunkSize - // overrides across individual benchmark rows. - private const int PipeChunkSize = 4096; - - // Dispatch mode identifiers — describes how property access / type dispatch happens for a given run. - // SGen = compile-time source generator path (Unsafe.As direct fields, slot-array wrapper lookup). - // Runtime= reflection / compiled-delegate path. - // Hybrid = SGen root with non-SGen child types reached via bridge methods. See docs/BINARY/BINARY_SGEN.md. - private const string ModeSGen = "SGen"; - private const string ModeRuntime = "Runtime"; - private const string ModeHybrid = "Hybrid"; - - // Per-cell adaptive iteration target wall-clock duration. Each Ser/Des function calibrates its - // own iteration count post-warmup so the sample batch lands in this range — equalizes the - // per-sample window across cells of vastly different per-op cost (Small ~6 ns/op vs Large - // ~140 µs/op). Below ~100 ms Stopwatch precision and OS preempt spikes start to dominate. - private const int TargetSampleMs = 250; - - // CV (coefficient of variation = stddev / mean) threshold above which a row's range is flagged - // as "unstable" in the markdown output (⚠️ marker). 3% is a reasonable noise-floor expectation - // for stabilized in-memory benchmarks; rows above it should be discounted when reading - // sub-3% inter-engine deltas. - private const double UnstableCVThreshold = 0.03; - - // JIT-tier-promotion drain delay between warmup and measurement. - // - JIT mode (RuntimeFeature.IsDynamicCodeCompiled == true): tiered JIT promotes hot methods - // in a background thread; we wait briefly for the queue to drain so the first measurement - // sample doesn't catch a Tier-0 → Tier-1 transition mid-flight. - // - AOT mode (NativeAOT publish): no dynamic compilation happens; the sleep is pure noise. - // 250ms (vs the historical 3000ms) is sufficient for a few-method working set under .NET 9's - // tiered JIT — empirically the queue drains in <100ms for the bench's hot path. - private static int JitSleep => RuntimeFeature.IsDynamicCodeCompiled ? 250 : 0; - - // OptionsPreset values are passed per-instance (constructor argument), not constants — - // each CreateSerializers call line specifies its own preset name (e.g. "FastMode", "NoIntern"). - - private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); - - /// - /// Aggregated feature flags across every type tagged with - /// the attribute in the loaded assemblies. Cached on first access (single reflection scan at startup). - /// Used by so the per-row Options column shows BOTH the - /// configured options-level value AND the effective attribute-level enable flag — a feature flagged - /// off at the type level overrides the options regardless of preset, and that asymmetry must surface - /// in the log to avoid misreading a "RefHandling=OnlyId" / "Interning=All" line as actually active. - /// Aggregation rule: if ALL tagged types have the feature enabled → true; if any tagged type - /// disables it → false (a single disabling type suppresses the feature on the type-graph). - /// - private static readonly (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) _attrFlags - = ScanAcBinaryAttributeFlags(); - - private static (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) ScanAcBinaryAttributeFlags() - { - var attrs = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty(); } }) - .Select(t => t.GetCustomAttribute()) - .Where(a => a != null) - .ToList(); - - if (attrs.Count == 0) return (false, false, false, false, false); - - return ( - refHandling: attrs.All(a => a!.EnableRefHandlingFeature), - internString: attrs.All(a => a!.EnableInternStringFeature), - metadata: attrs.All(a => a!.EnableMetadataFeature), - idTracking: attrs.All(a => a!.EnableIdTrackingFeature), - propertyFilter: attrs.All(a => a!.EnablePropertyFilterFeature)); - } + // Configuration (constants, mutable state, attribute-flag aggregation) → Configuration.cs /// /// Common Options-column formatter for every AcBinary serializer benchmark row. Renders the @@ -163,16 +50,16 @@ public static class Program var propFilterOpt = options.PropertyFilter == null ? "None" : "Set"; return $"WireMode={options.WireMode}, " + - $"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " + - $"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " + - $"Metadata={options.UseMetadata}(opt) | {_attrFlags.metadata} (attr), " + - $"PropertyFilter={propFilterOpt}(opt) | {_attrFlags.propertyFilter} (attr), " + + $"RefHandling={options.ReferenceHandling}(opt) | {Configuration.AttrFlags.refHandling} (attr), " + + $"Interning={options.UseStringInterning}(opt) | {Configuration.AttrFlags.internString} (attr), " + + $"Metadata={options.UseMetadata}(opt) | {Configuration.AttrFlags.metadata} (attr), " + + $"PropertyFilter={propFilterOpt}(opt) | {Configuration.AttrFlags.propertyFilter} (attr), " + $"SGen={options.UseGeneratedCode}, " + $"Compression={options.UseCompression}{extra}"; } /// - /// Returns MemoryPack serializer options aligned with for a fair + /// Returns MemoryPack serializer options aligned with for a fair /// apples-to-apples wire-format comparison: /// /// (UTF-8) — both @@ -185,15 +72,15 @@ public static class Program /// the encoding-family difference, NOT an AcBinary-specific overhead. /// private static MemoryPackSerializerOptions GetMemPackOptions() => - SelectedWireMode == WireMode.Fast + Configuration.SelectedWireMode == WireMode.Fast ? MemoryPackSerializerOptions.Utf16 : MemoryPackSerializerOptions.Default; /// - /// Converts a total-time (in ms across ) into per-operation microseconds. + /// Converts a total-time (in ms across ) into per-operation microseconds. /// Formula: totalMs / iterations × 1000. The benchmark stores *TimeMs as the cumulative /// median over the timing run; the display layer renders per-op µs to make numbers iteration-count - /// independent (e.g. switching TestIterations 1000 → 100 leaves the displayed µs/op unchanged + /// independent (e.g. switching Configuration.TestIterations 1000 → 100 leaves the displayed µs/op unchanged /// — only its sample noise grows). Symmetric with the already-per-op *AllocBytesPerOp fields. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -202,7 +89,7 @@ public static class Program /// Converts a total-time (in ms across ) into per-operation microseconds. /// Per-op µs is the iter-independent unit: 1000 iter and 50000 iter of the same operation should /// produce the same per-op µs (within noise). Necessary because per-cell adaptive iteration makes - /// iterations a per-row property — there is no longer a single global TestIterations to divide by. + /// iterations a per-row property — there is no longer a single global Configuration.TestIterations to divide by. /// private static double ToPerOpMicros(double totalMs, int iterations) => iterations > 0 ? totalMs / iterations * 1000.0 : 0; @@ -271,7 +158,7 @@ public static class Program /// /// Formats a per-op micros value with its inter-sample range and CV-threshold marker as /// "26.86 (24.5..29.1)" or "26.86 (24.5..29.1) ⚠️5.2%". Median first, range in parentheses, - /// CV warning suffix only when CV > . When min == max == median + /// CV warning suffix only when CV > . When min == max == median /// (single-sample / Debug / quick mode), collapses to bare median to avoid visual clutter. /// All time inputs are total-batch milliseconds; is the per-row iter /// count (post-adaptive-calibration). @@ -292,7 +179,7 @@ public static class Program if (medianMs > 0 && stdDevMs > 0) { var cv = stdDevMs / medianMs; - if (cv > UnstableCVThreshold) + if (cv > Configuration.UnstableCVThreshold) { var cvPct = (cv * 100).ToString("F1", inv); return $"{range} ⚠️{cvPct}%"; @@ -324,7 +211,7 @@ public static class Program if (args.Length > 0) { if (!TryParseCliArgs(args, out var layer, out var opMode, out var serializerMode)) - return; // profiler mode (already ran) or invalid args + return; // invalid args RunBenchmark(layer, opMode, serializerMode); return; @@ -350,8 +237,7 @@ public static class Program /// /// Parses CLI arguments into (layer, opMode, serializerMode). Returns false if the args - /// indicate a special mode that has already been handled (e.g. profiler) or are invalid; - /// the caller should then exit without running the standard benchmark. + /// are invalid; the caller should then exit without running the standard benchmark. /// private static bool TryParseCliArgs(string[] args, out string layer, out string opMode, out string serializerMode) { @@ -361,19 +247,12 @@ public static class Program var arg = args[0].ToLower(); - // Profiler mode: warmup only, then exit (for memory profiler analysis) - if (arg == "profiler") - { - RunProfilerMode(); - return false; - } - // Quick mode: short warmup, few iterations, small sample count if (arg == "quick") { - WarmupIterations = 5; - TestIterations = 100; - BenchmarkSamples = 3; + Configuration.WarmupIterations = 5; + Configuration.TestIterations = 100; + Configuration.BenchmarkSamples = 3; layer = "all"; } else if (arg is "core" or "comprehensive" or "edge" or "all" @@ -424,7 +303,7 @@ public static class Program // randomly inflate samples by 5-15%. // Try/finally guarantees the original state is restored even if a benchmark throws — leaving // a developer machine pinned to one core after a crashed run is a real foot-gun. - // Skipped on Debug single-sample mode (BenchmarkSamples <= 1) where stabilization is moot. + // Skipped on Debug single-sample mode (Configuration.BenchmarkSamples <= 1) where stabilization is moot. var process = Process.GetCurrentProcess(); var origAffinity = (IntPtr)0; var origPriority = ProcessPriorityClass.Normal; @@ -433,7 +312,7 @@ public static class Program // ProcessorAffinity is only supported on Windows + Linux (CA1416). macOS would throw at // runtime; skip the affinity step there but still raise priority class (which IS supported // on macOS, just less effective for stabilization than affinity pinning). - if (BenchmarkSamples > 1 && (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())) + if (Configuration.BenchmarkSamples > 1 && (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())) { try { @@ -465,8 +344,8 @@ public static class Program var allTestDataSets = BenchmarkTestDataProvider.CreateTestDataSets(); var testDataSets = FilterByLayer(allTestDataSets, layer); - System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | SerializerMode: {serializerMode} | Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (~{TargetSampleMs} ms target) | Warmup: {WarmupIterations} per phase (Ser/Des isolated) | Samples: {BenchmarkSamples} (median) + pilot discard"); - System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"} | Test Cells: {testDataSets.Count}/{allTestDataSets.Count}"); + System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | SerializerMode: {serializerMode} | Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (~{Configuration.TargetSampleMs} ms target) | Warmup: {Configuration.WarmupIterations} per phase (Ser/Des isolated) | Samples: {Configuration.BenchmarkSamples} (median) + pilot discard"); + System.Console.WriteLine($"Build: {Configuration.BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"} | Test Cells: {testDataSets.Count}/{allTestDataSets.Count}"); System.Console.WriteLine(); // Global JIT pre-warmup — touches every (testdata × serializer) code path BEFORE any timing happens. @@ -476,7 +355,7 @@ public static class Program // on later cells (e.g. Small BufWr reuse 9ms vs Medium BufWr reuse 4ms — even though Medium is bigger). // Pre-warmup runs every overload at least once with each data shape so .NET 9's tiered JIT promotes // them all in the background; the per-cell warmup that follows then locks in cache + branch state. - if (BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration) + if (Configuration.BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration) { System.Console.WriteLine($"Global JIT pre-warmup ({testDataSets.Count} cells × all serializers, light pass)..."); @@ -500,7 +379,7 @@ public static class Program } // Let background tiered-JIT compilation drain before we begin measuring. - if (JitSleep > 0) Thread.Sleep(JitSleep); + if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep); System.Console.WriteLine("✓ Global pre-warmup complete.\n"); } @@ -536,62 +415,6 @@ public static class Program } } - /// - /// Profiler mode: warmup only, then EXIT immediately. - /// Usage: dotnet run -- profiler - /// - private static void RunProfilerMode() - { - System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗"); - System.Console.WriteLine("║ PROFILER MODE (AcBinary only) ║"); - System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝"); - System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}"); - System.Console.WriteLine(); - - var order = BenchmarkTestDataProvider.CreateProfilerOrder(); - - var options = AcBinarySerializerOptions.WithoutReferenceHandling; - options.UseStringInterning = StringInterningMode.None; - - var bytes = AcBinarySerializer.Serialize(order, options); - - // Warmup (fills caches) - System.Console.WriteLine("Warming up (1000 iterations)..."); - for (var i = 0; i < 1000; i++) - { - _ = AcBinarySerializer.Serialize(order, options); - _ = AcBinaryDeserializer.Deserialize(bytes); - } - - Thread.Sleep(2000); - System.Console.WriteLine("Warmup complete. Caches are now populated."); - System.Console.WriteLine(); - - // HOT PATH - this is what the profiler should capture! - System.Console.WriteLine("Running hot path serialization (1000 iterations for profiling)..."); - for (var i = 0; i < 1000; i++) - { - _ = AcBinarySerializer.Serialize(order, options); - //_ = AcBinaryDeserializer.Deserialize(bytes); - } - - System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)..."); - for (var i = 0; i < 1000; i++) - { - _ = AcBinaryDeserializer.Deserialize(bytes); - } - - System.Console.WriteLine("Hot path complete."); - System.Console.WriteLine(); - - System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<"); - System.Console.WriteLine("Press any key to exit..."); - System.Console.ReadKey(intercept: true); - - System.Console.WriteLine(); - System.Console.WriteLine("✓ Profiler mode complete. Exiting now."); - } - #region Benchmark Execution /// @@ -644,9 +467,9 @@ public static class Program // pool churn from Ser, deserialized object graph from Des) so the next phase starts with a quiescent // heap — GC tier-promotion timing during measurement is then driven only by THAT phase's allocations. // - // JitSleep per-phase: tiered JIT background promotion drain after each warmup (mode-aware: 0 ms in AOT). + // Configuration.JitSleep per-phase: tiered JIT background promotion drain after each warmup (mode-aware: 0 ms in AOT). // Each phase's freshly-promoted methods settle before its timing starts. - System.Console.WriteLine($"Running benchmarks (target ~{TargetSampleMs} ms/sample × {BenchmarkSamples} samples median, phase-isolated warmup/measure per Ser/Des)...\n"); + System.Console.WriteLine($"Running benchmarks (target ~{Configuration.TargetSampleMs} ms/sample × {Configuration.BenchmarkSamples} samples median, phase-isolated warmup/measure per Ser/Des)...\n"); foreach (var serializer in serializers) { @@ -676,10 +499,10 @@ public static class Program if (mode is "all" or "serialize" or "ser") { ForceGcCollect(); - serializer.WarmupSerialize(WarmupIterations); - if (JitSleep > 0) Thread.Sleep(JitSleep); + serializer.WarmupSerialize(Configuration.WarmupIterations); + if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep); - var rtIter = CalibrateIterations(() => serializer.Serialize(), TargetSampleMs); + var rtIter = CalibrateIterations(() => serializer.Serialize(), Configuration.TargetSampleMs); var (rtMed, rtMin, rtMax, rtStd) = RunTimed(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT timing]"); result.RoundTripTimeMs = rtMed; result.RoundTripTimeMinMs = rtMin; @@ -694,14 +517,14 @@ public static class Program } else { - // ── Ser phase ── isolated warmup → 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") { ForceGcCollect(); - serializer.WarmupSerialize(WarmupIterations); - if (JitSleep > 0) Thread.Sleep(JitSleep); + serializer.WarmupSerialize(Configuration.WarmupIterations); + if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep); - var serIter = CalibrateIterations(() => serializer.Serialize(), TargetSampleMs); + var serIter = CalibrateIterations(() => serializer.Serialize(), Configuration.TargetSampleMs); var (serMed, serMin, serMax, serStd) = RunTimed(() => serializer.Serialize(), serIter, $"{groupLabel} [Ser timing]"); result.SerializeTimeMs = serMed; result.SerializeTimeMinMs = serMin; @@ -712,16 +535,16 @@ public static class Program result.SerializeAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), serIter, $"{groupLabel} [Ser alloc]"); } - // ── Des phase ── isolated warmup → 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 // Des-phase's allocation measurement reflects ONLY Des-side allocations (deserialized object graph). if (mode is "all" or "deserialize" or "des") { ForceGcCollect(); - serializer.WarmupDeserialize(WarmupIterations); - if (JitSleep > 0) Thread.Sleep(JitSleep); + serializer.WarmupDeserialize(Configuration.WarmupIterations); + if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep); - var desIter = CalibrateIterations(() => serializer.Deserialize(), TargetSampleMs); + var desIter = CalibrateIterations(() => serializer.Deserialize(), Configuration.TargetSampleMs); var (desMed, desMin, desMax, desStd) = RunTimed(() => serializer.Deserialize(), desIter, $"{groupLabel} [Des timing]"); result.DeserializeTimeMs = desMed; result.DeserializeTimeMinMs = desMin; @@ -767,7 +590,7 @@ public static class Program if (serializerMode == "fastestbyte") { var fastestByteOptions = AcBinarySerializerOptions.FastMode; - fastestByteOptions.WireMode = SelectedWireMode; + fastestByteOptions.WireMode = Configuration.SelectedWireMode; return new List { @@ -790,8 +613,8 @@ public static class Program // fits blocking-free in one kernel pipe-buffer slot. Single source of truth for both app-level // wire chunk AND kernel transfer unit; change ONLY this line when tuning. var binaryFastModePipeChunkOnly = AcBinarySerializerOptions.FastMode; - binaryFastModePipeChunkOnly.BufferWriterChunkSize = PipeChunkSize; - binaryFastModePipeChunkOnly.WireMode = SelectedWireMode; + binaryFastModePipeChunkOnly.BufferWriterChunkSize = Configuration.PipeChunkSize; + binaryFastModePipeChunkOnly.WireMode = Configuration.SelectedWireMode; return new List { @@ -826,18 +649,18 @@ public static class Program var binaryNoInternOption = AcBinarySerializerOptions.Default; binaryNoInternOption.UseStringInterning = StringInterningMode.None; - binaryNoInternOption.WireMode = SelectedWireMode; + binaryNoInternOption.WireMode = Configuration.SelectedWireMode; var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default; binaryDefaultNoSgenOption.UseGeneratedCode = false; - binaryDefaultNoSgenOption.WireMode = SelectedWireMode; + binaryDefaultNoSgenOption.WireMode = Configuration.SelectedWireMode; var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode; binaryFastModeNoSgenOption.UseGeneratedCode = false; - binaryFastModeNoSgenOption.WireMode = SelectedWireMode; + binaryFastModeNoSgenOption.WireMode = Configuration.SelectedWireMode; var binaryFastModeOption = AcBinarySerializerOptions.FastMode; - binaryFastModeOption.WireMode = SelectedWireMode; + binaryFastModeOption.WireMode = Configuration.SelectedWireMode; // BufWr new — 4 KB chunk size for the FRESH ArrayBufferWriter scenario. The chunkSize here drives // the serializer's GetSpan(N) request → the ArrayBufferWriter's internal allocation per call. @@ -845,20 +668,20 @@ public static class Program // allocates a fresh ABW. Independent of the AsyncPipe profile (different mechanism: alloc overhead // vs syscall count). var binaryFastModeBufWrChunk = AcBinarySerializerOptions.FastMode; - binaryFastModeBufWrChunk.BufferWriterChunkSize = PipeChunkSize; - binaryFastModeBufWrChunk.WireMode = SelectedWireMode; + binaryFastModeBufWrChunk.BufferWriterChunkSize = Configuration.PipeChunkSize; + binaryFastModeBufWrChunk.WireMode = Configuration.SelectedWireMode; // In-memory Pipe variant — same 4 KB chunkSize as the AsyncPipe mode, no kernel-pipe alignment // concern (managed slabs are not page-aligned anyway). Drives SerializeChunkedFramed via the in-memory // System.IO.Pipelines.Pipe (zero-copy slab handoff between producer and drain task). var binaryFastModePipeChunkInMem = AcBinarySerializerOptions.FastMode; - binaryFastModePipeChunkInMem.BufferWriterChunkSize = PipeChunkSize; - binaryFastModePipeChunkInMem.WireMode = SelectedWireMode; + binaryFastModePipeChunkInMem.BufferWriterChunkSize = Configuration.PipeChunkSize; + binaryFastModePipeChunkInMem.WireMode = Configuration.SelectedWireMode; var defaultOptions = AcBinarySerializerOptions.Default; defaultOptions.UseStringInterning = StringInterningMode.None; defaultOptions.ReferenceHandling = ReferenceHandlingMode.OnlyId; - defaultOptions.WireMode = SelectedWireMode; + defaultOptions.WireMode = Configuration.SelectedWireMode; return new List { @@ -927,10 +750,10 @@ public static class Program } /// - /// Runs the action times for independent samples, + /// Runs the action times for independent samples, /// returning the median, min, and max elapsed time. Multi-sample design reduces single-run variance /// from ~±15% to ~±5% by smoothing transient effects (background activity, thermal/turbo state). - /// When <= 1, falls back to single-sample timing (Debug / quick mode). + /// When <= 1, falls back to single-sample timing (Debug / quick mode). /// When is non-null, emits in-place \r progress updates so a /// stuck benchmark (e.g. deadlocked NamedPipe row) is visibly stuck at a specific %% rather than /// silently hanging. @@ -948,7 +771,7 @@ public static class Program /// private static (double medianMs, double minMs, double maxMs, double stdDevMs) RunTimed(Action action, int iterations, string? progressLabel = null) { - var samples = BenchmarkSamples; + var samples = Configuration.BenchmarkSamples; if (samples <= 1) { // Single-sample fast path (Debug or trivial run) — no allocation, no sort, no stddev. @@ -1018,16 +841,16 @@ public static class Program /// /// Per-cell adaptive iteration calibration. Runs a 100-iter measurement after warmup and computes - /// how many iterations are needed to reach wall-clock per sample. + /// how many iterations are needed to reach wall-clock per sample. /// Returns iter rounded UP to the nearest 1000, floored at 1000 (the prior fixed minimum) and /// ceiling-capped at 200_000 (sanity bound for pathologically fast ops). In Debug single-sample mode - /// (BenchmarkSamples <= 1) returns the global unchanged — + /// (Configuration.BenchmarkSamples <= 1) returns the global unchanged — /// calibration overhead is unjustified there. Calibration runs OUTSIDE the timed sample loop and /// does NOT count toward warmup; its sole purpose is to measure per-op cost. /// private static int CalibrateIterations(Action action, int targetMs) { - if (BenchmarkSamples <= 1) return TestIterations; // Debug fast path + if (Configuration.BenchmarkSamples <= 1) return Configuration.TestIterations; // Debug fast path GC.Collect(); GC.WaitForPendingFinalizers(); @@ -1244,7 +1067,7 @@ public static class Program System.Console.WriteLine(" [A] All layers"); System.Console.WriteLine(" [F] FastestByte — AcBinary FastMode Byte[] vs MemoryPack Byte[] only (tight optimization loop)"); System.Console.WriteLine(" [P] AsyncPipe — streaming I/O isolation (only AsyncPipe, all test data)"); - System.Console.WriteLine($" [S] Settings — Iteration / WireMode (current: {SelectedWireMode})"); + System.Console.WriteLine($" [S] Settings — Iteration / WireMode (current: {Configuration.SelectedWireMode})"); System.Console.WriteLine(" [Q] Quit"); System.Console.Write("\nSelection: "); @@ -1270,7 +1093,7 @@ public static class Program /// /// Settings sub-menu — prompts for Warmup / Iterations / Samples values. Empty input keeps the - /// current value. Validation: WarmupIterations ≥ 0; TestIterations ≥ 1; BenchmarkSamples ≥ 1. + /// current value. Validation: Configuration.WarmupIterations ≥ 0; Configuration.TestIterations ≥ 1; Configuration.BenchmarkSamples ≥ 1. /// Returns to the caller (which re-displays the main menu). /// private static void ShowSettingsMenu() @@ -1282,7 +1105,7 @@ public static class Program System.Console.WriteLine("Settings"); System.Console.WriteLine("─────────────────────────────────────────────"); System.Console.WriteLine(" [1] Iteration — Warmup / Iterations / Samples"); - System.Console.WriteLine($" [2] WireMode — current: {SelectedWireMode}"); + System.Console.WriteLine($" [2] WireMode — current: {Configuration.SelectedWireMode}"); System.Console.WriteLine($" [3] Charset — current: {GetCurrentCharsetName()}"); System.Console.WriteLine(" [B] Back"); System.Console.Write("\nSelection: "); @@ -1395,12 +1218,12 @@ public static class Program System.Console.WriteLine("─────────────────────────────────────────────"); System.Console.WriteLine(); - WarmupIterations = PromptInt("WarmupIterations", WarmupIterations, min: 0); - TestIterations = PromptInt("TestIterations ", TestIterations, min: 1); - BenchmarkSamples = PromptInt("BenchmarkSamples", BenchmarkSamples, min: 1); + Configuration.WarmupIterations = PromptInt("Configuration.WarmupIterations", Configuration.WarmupIterations, min: 0); + Configuration.TestIterations = PromptInt("Configuration.TestIterations ", Configuration.TestIterations, min: 1); + Configuration.BenchmarkSamples = PromptInt("Configuration.BenchmarkSamples", Configuration.BenchmarkSamples, min: 1); System.Console.WriteLine(); - System.Console.WriteLine($"✓ Iteration settings updated: Warmup={WarmupIterations} | Iterations={TestIterations} | Samples={BenchmarkSamples}"); + System.Console.WriteLine($"✓ Iteration settings updated: Warmup={Configuration.WarmupIterations} | Iterations={Configuration.TestIterations} | Samples={Configuration.BenchmarkSamples}"); } private static void ShowWireModeSettingsMenu() @@ -1411,7 +1234,7 @@ public static class Program System.Console.WriteLine("─────────────────────────────────────────────"); System.Console.WriteLine("WireMode settings"); System.Console.WriteLine("─────────────────────────────────────────────"); - System.Console.WriteLine($"Current: {SelectedWireMode}"); + System.Console.WriteLine($"Current: {Configuration.SelectedWireMode}"); System.Console.WriteLine(" [1] Compact"); System.Console.WriteLine(" [2] Fast"); System.Console.WriteLine(" [B] Back"); @@ -1423,11 +1246,11 @@ public static class Program switch (char.ToLower(key)) { case '1': - SelectedWireMode = WireMode.Compact; + Configuration.SelectedWireMode = WireMode.Compact; System.Console.WriteLine("✓ WireMode set to Compact"); return; case '2': - SelectedWireMode = WireMode.Fast; + Configuration.SelectedWireMode = WireMode.Fast; System.Console.WriteLine("✓ WireMode set to Fast"); return; case 'b': @@ -1515,8 +1338,8 @@ public static class Program /// 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; - /// Combined warmup (Ser + Deser interleaved). Kept for backward-compat with ProfilerMode - /// and other callers that don't need phase-separated warmup. The benchmark loop prefers the split + /// Combined warmup (Ser + Deser interleaved). Currently unused — kept as a legacy entry point + /// for any external caller that still wants single-call warmup. The benchmark loop uses the split /// + pair for cache-isolated measurements. void Warmup(int iterations); @@ -1549,9 +1372,9 @@ public static class Program private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; - public string Engine => EngineAcBinary; - public string IoMode => IoByteArray; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoByteArray; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; @@ -1606,9 +1429,9 @@ public static class Program private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; - public string Engine => EngineMemoryPack; - public string IoMode => IoByteArray; - public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public string Engine => Configuration.EngineMemoryPack; + public string IoMode => Configuration.IoByteArray; + public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; @@ -1657,9 +1480,9 @@ public static class Program private readonly MessagePackSerializerOptions _options; private readonly byte[] _serialized; - public string Engine => EngineMessagePack; - public string IoMode => IoByteArray; - public string DispatchMode => ModeSGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver) + public string Engine => Configuration.EngineMessagePack; + public string IoMode => Configuration.IoByteArray; + public string DispatchMode => Configuration.ModeSGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver) public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; @@ -1722,9 +1545,9 @@ public static class Program private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; - public string Engine => EngineAcBinary; - public string IoMode => IoBufWrNew; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoBufWrNew; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; @@ -1830,9 +1653,9 @@ public static class Program private bool _captureResult; // toggle: when true, ConsumeLoop stores result; otherwise discards private bool _disposed; - public string Engine => EngineAcBinary; - public string IoMode => IoNamedPipe; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoNamedPipe; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -2053,9 +1876,9 @@ public static class Program private bool _captureResult; private bool _disposed; - public string Engine => EngineAcBinary; - public string IoMode => IoInMemoryPipe; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoInMemoryPipe; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -2244,9 +2067,9 @@ public static class Program private bool _captureResult; // toggle: when true, ConsumerLoop stores result; otherwise discards private bool _disposed; - public string Engine => EngineAcBinary; - public string IoMode => IoNamedPipeRaw; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoNamedPipeRaw; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -2449,9 +2272,9 @@ public static class Program private bool _captureResult; private bool _disposed; - public string Engine => EngineAcBinary; - public string IoMode => IoInMemoryRaw; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoInMemoryRaw; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -2593,9 +2416,9 @@ public static class Program private readonly MemoryPackSerializerOptions _options; private readonly byte[] _serialized; - public string Engine => EngineMemoryPack; - public string IoMode => IoBufWrNew; - public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public string Engine => Configuration.EngineMemoryPack; + public string IoMode => Configuration.IoBufWrNew; + public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes => 0; @@ -2647,9 +2470,9 @@ public static class Program private readonly byte[] _serialized; private readonly ArrayBufferWriter _bufferWriter; - public string Engine => EngineAcBinary; - public string IoMode => IoBufWrReuse; - public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime; + public string Engine => Configuration.EngineAcBinary; + public string IoMode => Configuration.IoBufWrReuse; + public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime; public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -2718,9 +2541,9 @@ public static class Program private readonly byte[] _serialized; private readonly ArrayBufferWriter _bufferWriter; - public string Engine => EngineMemoryPack; - public string IoMode => IoBufWrReuse; - public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters + public string Engine => Configuration.EngineMemoryPack; + public string IoMode => Configuration.IoBufWrReuse; + public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters public string OptionsPreset { get; } public int SerializedSize => _serialized.Length; public long SetupSerializeAllocBytes { get; } @@ -2779,9 +2602,9 @@ public static class Program private readonly string _serialized; private readonly byte[] _serializedUtf8; - public string Engine => EngineSystemTextJson; - public string IoMode => IoString; - public string DispatchMode => ModeRuntime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here) + public string Engine => Configuration.EngineSystemTextJson; + public string IoMode => Configuration.IoString; + public string DispatchMode => Configuration.ModeRuntime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here) public string OptionsPreset { get; } public int SerializedSize => _serializedUtf8.Length; public long SetupSerializeAllocBytes => 0; @@ -2798,7 +2621,7 @@ public static class Program ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles }; _serialized = JsonSerializer.Serialize(order, _options); - _serializedUtf8 = Utf8NoBom.GetBytes(_serialized); + _serializedUtf8 = Configuration.Utf8NoBom.GetBytes(_serialized); } public void Warmup(int iterations) @@ -2854,11 +2677,11 @@ public static class Program public double DeserializeTimeMinMs { get; set; } public double DeserializeTimeMaxMs { get; set; } // Sample-population stddev (ms). Used by FormatMicrosWithRange to compute CV (stddev/mean) - // and emit the ⚠️ marker on rows above UnstableCVThreshold. 0 in single-sample mode. + // and emit the ⚠️ marker on rows above Configuration.UnstableCVThreshold. 0 in single-sample mode. public double SerializeTimeStdDevMs { get; set; } public double DeserializeTimeStdDevMs { get; set; } // Per-row adaptive iteration count (post-CalibrateIterations). Each Ser and Des function calibrates - // independently to land its sample window at ~TargetSampleMs; per-op µs is then iter-independent + // independently to land its sample window at ~Configuration.TargetSampleMs; per-op µs is then iter-independent // (`SerializeTimeMs / SerializeIterations * 1000`). For round-trip-only rows (NamedPipe etc.), // RoundTripIterations carries the calibrated iter count; SerializeIterations and DeserializeIterations // stay 0 (Ser and Des are not separately measurable on those rows). @@ -2923,10 +2746,10 @@ public static class Program // Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration. var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList(); // Baseline switched MessagePack → MemoryPack: MemoryPack is the SOTA performance leader. - var memPackResult = testResults.FirstOrDefault(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)); + var memPackResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)); // Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated). // The Runtime variant is shown alongside in the table for context, not used as the headline number. - var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen)); + var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)); System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(172, '─') + "┐"); // Header-only units; per-row entries are numbers (µs/op for time, KB/op for alloc, KB pair "ser / des" for Setup, B for Size). @@ -2947,8 +2770,8 @@ public static class Program // Highlight MemoryPack baseline (any Byte[]) and AcBinary headline contender (Byte[] + SGen) with win/lose colors. // The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline. - var isHighlighted = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray) - || (result.Engine == EngineAcBinary && result.IoMode == IoByteArray && result.DispatchMode == ModeSGen); + var isHighlighted = (result.Engine == Configuration.EngineMemoryPack && result.IoMode == Configuration.IoByteArray) + || (result.Engine == Configuration.EngineAcBinary && result.IoMode == Configuration.IoByteArray && result.DispatchMode == Configuration.ModeSGen); var prefix = isHighlighted ? "│►" : "│ "; var suffix = isHighlighted ? "◄│" : " │"; @@ -2956,7 +2779,7 @@ public static class Program // Color logic: Green = winner (faster), Red = loser (slower) if (isHighlighted && memPackResult != null && acBinaryResult != null) { - var isMemPack = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray); + var isMemPack = (result.Engine == Configuration.EngineMemoryPack && result.IoMode == Configuration.IoByteArray); var memPackFaster = RtPerOp(memPackResult) < RtPerOp(acBinaryResult); if (isMemPack) @@ -3103,13 +2926,13 @@ public static class Program // Overall AcBinary (SGen) vs MemoryPack comparison (baseline switched MessagePack → MemoryPack as SOTA reference). // AcBinary side is restricted to DispatchMode == SGen — apples-to-apples vs MemoryPack which is also source-generated. // The Runtime variant is shown side-by-side in each per-test fancy table for SGen-speedup context, but excluded from this headline. - var memPackSerResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.SerializeTimeMs > 0).ToList(); - var memPackDesResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.DeserializeTimeMs > 0).ToList(); - var memPackRtResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.RoundTripTimeMs > 0).ToList(); + var memPackSerResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.SerializeTimeMs > 0).ToList(); + var memPackDesResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.DeserializeTimeMs > 0).ToList(); + var memPackRtResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.RoundTripTimeMs > 0).ToList(); - var acBinarySerResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.SerializeTimeMs > 0).ToList(); - var acBinaryDesResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.DeserializeTimeMs > 0).ToList(); - var acBinaryRtResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.RoundTripTimeMs > 0).ToList(); + var acBinarySerResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.SerializeTimeMs > 0).ToList(); + var acBinaryDesResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.DeserializeTimeMs > 0).ToList(); + var acBinaryRtResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.RoundTripTimeMs > 0).ToList(); // Skip comparison if no data available if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0) @@ -3128,8 +2951,8 @@ public static class Program // - Median of per-cell ratios — outlier-resistant. // The geo/median variants surface when a single cell dominates the arithmetic average // (typical when one cell's µs-per-op is an order of magnitude larger than the others). - var sizeAcResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen)).ToList(); - var sizeMpResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)).ToList(); + var sizeAcResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)).ToList(); + var sizeMpResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)).ToList(); var serStats = ComputeOverallStats(acBinarySerResults, memPackSerResults, SerPerOp); var desStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, DesPerOp); @@ -3183,12 +3006,12 @@ public static class Program private static void SaveResults(List results, List testDataSets) { - Directory.CreateDirectory(ResultsDirectory); + Directory.CreateDirectory(Configuration.ResultsDirectory); var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - var baseFileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}"; - var logFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.log"); - var outputFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.output"); + var baseFileName = $"Console.FullBenchmark_{Configuration.BuildConfiguration}_{timestamp}"; + var logFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.log"); + var outputFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.output"); // Save binary output to separate .output file var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large")); @@ -3208,7 +3031,7 @@ public static class Program outputSb.AppendLine("Hex dump:"); outputSb.AppendLine(FormatHexDump(serializedBytes)); - File.WriteAllText(outputFilePath, outputSb.ToString(), Utf8NoBom); + File.WriteAllText(outputFilePath, outputSb.ToString(), Configuration.Utf8NoBom); System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}"); } @@ -3217,10 +3040,10 @@ public static class Program sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗"); sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║"); sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║"); - sb.AppendLine($"║ Build: {BuildConfiguration}".PadRight(100) + "║"); + sb.AppendLine($"║ Build: {Configuration.BuildConfiguration}".PadRight(100) + "║"); sb.AppendLine($"║ Charset: {GetCurrentCharsetName()}".PadRight(100) + "║"); - sb.AppendLine($"║ Iterations: per-cell adaptive (~{TargetSampleMs} ms target)".PadRight(100) + "║"); - sb.AppendLine($"║ Samples: {BenchmarkSamples} (median) + 1 pilot discarded".PadRight(100) + "║"); + sb.AppendLine($"║ Iterations: per-cell adaptive (~{Configuration.TargetSampleMs} ms target)".PadRight(100) + "║"); + sb.AppendLine($"║ Samples: {Configuration.BenchmarkSamples} (median) + 1 pilot discarded".PadRight(100) + "║"); sb.AppendLine($"║ Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}".PadRight(100) + "║"); sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); sb.AppendLine(); @@ -3262,10 +3085,10 @@ public static class Program { // Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration. var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList(); - var memPackResult = testResults.FirstOrDefault(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)); + var memPackResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)); // Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated). // The Runtime variant is shown alongside in the table for context, not used as the headline number. - var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen)); + var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)); sb.AppendLine(); sb.AppendLine($"--- {testData.DisplayName} ---"); @@ -3275,7 +3098,7 @@ public static class Program var rank = 1; foreach (var result in testResults) { - var isHighlighted = ((result.Engine == EngineMemoryPack || result.Engine == EngineAcBinary) && result.IoMode == IoByteArray); + var isHighlighted = ((result.Engine == Configuration.EngineMemoryPack || result.Engine == Configuration.EngineAcBinary) && result.IoMode == Configuration.IoByteArray); var prefix = isHighlighted ? "► " : " "; var size = $"{result.SerializedSize:N0}"; @@ -3312,13 +3135,13 @@ public static class Program sb.AppendLine(); sb.AppendLine("=== AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ==="); - var memPackSerResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.SerializeTimeMs > 0).ToList(); - var memPackDesResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.DeserializeTimeMs > 0).ToList(); - var memPackRtResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.RoundTripTimeMs > 0).ToList(); + var memPackSerResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.SerializeTimeMs > 0).ToList(); + var memPackDesResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.DeserializeTimeMs > 0).ToList(); + var memPackRtResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.RoundTripTimeMs > 0).ToList(); - var acBinarySerResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.SerializeTimeMs > 0).ToList(); - var acBinaryDesResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.DeserializeTimeMs > 0).ToList(); - var acBinaryRtResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.RoundTripTimeMs > 0).ToList(); + var acBinarySerResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.SerializeTimeMs > 0).ToList(); + var acBinaryDesResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.DeserializeTimeMs > 0).ToList(); + var acBinaryRtResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.RoundTripTimeMs > 0).ToList(); // Skip comparison block if either side has no Byte[] data — happens in AsyncPipe-only mode // where only NamedPipe rows exist (no MemoryPack baseline, no AcBinary Byte[] reference). @@ -3326,18 +3149,18 @@ public static class Program if (memPackRtResults2.Count == 0 || acBinaryRtResults2.Count == 0) { sb.AppendLine(" (Comparison requires both serialize and deserialize data)"); - File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom); + File.WriteAllText(logFilePath, sb.ToString(), Configuration.Utf8NoBom); System.Console.WriteLine($"✓ Results saved to: {logFilePath}"); - var llmFilePathEarly = Path.Combine(ResultsDirectory, $"{baseFileName}.LLM"); + var llmFilePathEarly = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.LLM"); SaveLlmResults(llmFilePathEarly, results, testDataSets); return; } // Per-cell-paired aggregation: arithmetic / geometric / median. See PrintSummary's parallel // block + the OverallStats record for the rationale (per-cell ratio vs magnitude-weighted mean). - var sizeAcResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen)).ToList(); - var sizeMpResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)).ToList(); + var sizeAcResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)).ToList(); + var sizeMpResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)).ToList(); AppendOverallLine(sb, "Serialize", "µs/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, SerPerOp)); AppendOverallLine(sb, "Ser Alloc", "B/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, r => r.SerializeAllocBytesPerOp), "F0"); @@ -3346,11 +3169,11 @@ public static class Program AppendOverallLine(sb, "Round-trip", "µs/op", ComputeOverallStats(acBinaryRtResults2, memPackRtResults2, RtPerOp)); AppendOverallLine(sb, "Size", "B", ComputeOverallStats(sizeAcResults2, sizeMpResults2, r => r.SerializedSize), "F0"); - File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom); + File.WriteAllText(logFilePath, sb.ToString(), Configuration.Utf8NoBom); System.Console.WriteLine($"✓ Results saved to: {logFilePath}"); // Save LLM-optimized results - var llmFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.LLM"); + var llmFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.LLM"); SaveLlmResults(llmFilePath, results, testDataSets); } @@ -3358,8 +3181,8 @@ public static class Program { var sb = new StringBuilder(); var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown"; - sb.AppendLine($"# AcBinary Benchmark {BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); - sb.AppendLine($"Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{TargetSampleMs} ms/sample) | Warmup: {WarmupIterations} per phase (Ser/Des isolated) | Samples: {BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {UnstableCVThreshold * 100:F0}%"); + sb.AppendLine($"# AcBinary Benchmark {Configuration.BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + sb.AppendLine($"Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{Configuration.TargetSampleMs} ms/sample) | Warmup: {Configuration.WarmupIterations} per phase (Ser/Des isolated) | Samples: {Configuration.BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {Configuration.UnstableCVThreshold * 100:F0}%"); sb.AppendLine("Baseline: MemoryPack (Byte[]) (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup"); // Options summary @@ -3379,7 +3202,7 @@ public static class Program // Flat results table sorted by test data then round-trip (now includes Alloc + Iter columns). // Iter column shows per-row Ser/Des iteration counts (post-adaptive-calibration), so the reader - // can verify that each cell's batch sample landed near the TargetSampleMs window. + // can verify that each cell's batch sample landed near the Configuration.TargetSampleMs window. sb.AppendLine(); sb.AppendLine("## Results"); sb.AppendLine(); @@ -3429,8 +3252,8 @@ public static class Program // arith mean is magnitude-weighted (Large cell dominates); geo/median are per-cell-equal // signals. Adding this lets an LLM diagnose whether a headline delta is a real overall // win/loss or a single-cell artifact. - var memPackByteArrayResults = results.Where(r => r.Engine == EngineMemoryPack && r.IoMode == IoByteArray).ToList(); - var acBinarySGenByteArrayResults = results.Where(r => r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen).ToList(); + var memPackByteArrayResults = results.Where(r => r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray).ToList(); + var acBinarySGenByteArrayResults = results.Where(r => r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen).ToList(); var memPackSerResultsLlm = memPackByteArrayResults.Where(r => r.SerializeTimeMs > 0).ToList(); var memPackDesResultsLlm = memPackByteArrayResults.Where(r => r.DeserializeTimeMs > 0).ToList(); var memPackRtResultsLlm = memPackByteArrayResults.Where(r => r.RoundTripTimeMs > 0).ToList(); @@ -3455,7 +3278,7 @@ public static class Program sb.AppendLine("```"); } - File.WriteAllText(filePath, sb.ToString(), Utf8NoBom); + File.WriteAllText(filePath, sb.ToString(), Configuration.Utf8NoBom); System.Console.WriteLine($"✓ LLM results saved to: {filePath}"); } diff --git a/AyCode.Core.Serializers.Console/README.md b/AyCode.Core.Serializers.Console/README.md index 1827386..4d58e7d 100644 --- a/AyCode.Core.Serializers.Console/README.md +++ b/AyCode.Core.Serializers.Console/README.md @@ -12,7 +12,7 @@ Standalone benchmark console application for comparing serializer performance. T ## Key Files -- **`Program.cs`** — Benchmark runner. Modes: `all` (default), `quick` (fewer iterations), `serialize`, `deserialize`, `profiler` (memory profiler warmup). Outputs results to `Test_Benchmark_Results/Benchmark/`. Iterations: 5000 warmup + 1000 test (Release), 0+1 (Debug). +- **`Program.cs`** — Benchmark runner. Modes: `all` (default), `quick` (fewer iterations), `serialize`, `deserialize`. Outputs results to `Test_Benchmark_Results/Benchmark/`. Iterations: 5000 warmup + 1000 test (Release), 0+1 (Debug). - **`BenchmarkTestDataProvider.cs`** — Test data factory producing 5 data shapes: - Small (2x2x2x2), Medium (3x3x3x4), Large (5x5x5x10) - Repeated Strings (10 items, string deduplication testing) diff --git a/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs b/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs index ce1b20c..1a829d5 100644 --- a/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs +++ b/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs @@ -78,20 +78,6 @@ public static class BenchmarkTestDataProvider }; } - public static TestOrder CreateProfilerOrder() - { - TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - return TestDataFactory.CreateOrder( - itemCount: 3, - palletsPerItem: 3, - measurementsPerPallet: 3, - pointsPerMeasurement: 4, - sharedTag: sharedTag, - sharedUser: sharedUser); - } - private static TestDataSet CreateSmallTestData(bool resetId = true) { if (resetId) TestDataFactory.ResetIdCounter();