using AyCode.Core.Compression; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using MemoryPack; using MessagePack; using MessagePack.Resolvers; using Microsoft.Extensions.Options; using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; namespace AyCode.Core.Serializers.Console; /// /// Comprehensive benchmark application for all serializers. /// Compares: AcBinary (all options), MemoryPack, MessagePack, Newtonsoft.Json, System.Text.Json /// /// Usage: /// dotnet run # Run all benchmarks /// dotnet run -- quick # Quick mode (fewer iterations) /// dotnet run -- serialize # Serialize only /// dotnet run -- deserialize # Deserialize only /// public static class Program { private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark"; #if DEBUG private const string BuildConfiguration = "Debug"; #else private const string BuildConfiguration = "Release"; #endif // Serializer name constants private const string SerializerMessagePack = "MessagePack"; private const string SerializerAcBinaryDefault = "AcBinary (Default)"; private const string SerializerAcBinaryDefaultNoSgen = "AcBinary (Def, NoSgen)"; private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)"; private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)"; private const string SerializerAcBinaryFastNoSgen = "AcBinary (Fast, NoSgen)"; private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)"; private const string SerializerMemoryPack = "MemoryPack"; //private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)"; //private const string SerializerSystemTextJson = "System.Text.Json"; private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); #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; private static int TestIterations = 1000; private static int BenchmarkSamples = 5; // Release: 5-sample median for stability (~±5% variance vs. ~±15% single-sample) //private static int WarmupIterations = 5000; //private static int TestIterations = 2000; #endif public static void Main(string[] args) { // Set console encoding to UTF-8 for proper Unicode character display System.Console.OutputEncoding = Encoding.UTF8; // Setup validation — abort BEFORE any benchmark logic if MemoryPack baseline is invalid. // Done early so user is told immediately, not after warmup. ValidateMemoryPackSetup(); // Determine layer (which test data to run) and opMode (ser/des/all). // CLI args take precedence; if no args, show interactive menu. string layer; string opMode = "all"; if (args.Length == 0) { var selection = ShowInteractiveMenu(); if (selection == null) return; // user pressed Q layer = selection; } else { var arg = args[0].ToLower(); // Profiler mode: warmup only, then exit (for memory profiler analysis) if (arg == "profiler") { RunProfilerMode(); return; } // Quick mode: short warmup, few iterations, small sample count if (arg == "quick") { WarmupIterations = 5; TestIterations = 100; BenchmarkSamples = 3; layer = "all"; } else if (arg is "core" or "comprehensive" or "edge" or "all") { layer = arg; } 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; } } System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗"); System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║"); System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝"); var allResults = new List(); var allTestDataSets = BenchmarkTestDataProvider.CreateTestDataSets(); var testDataSets = FilterByLayer(allTestDataSets, layer); System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | Iterations: {TestIterations} | Warmup: {WarmupIterations} | Samples: {BenchmarkSamples} (median)"); System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"} | Test Cells: {testDataSets.Count}/{allTestDataSets.Count}"); System.Console.WriteLine(); foreach (var testData in testDataSets) { System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}"); System.Console.WriteLine($"TEST DATA: {testData.DisplayName}"); System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}"); var results = RunBenchmarksForTestData(testData, opMode); allResults.AddRange(results); } // Print grouped results PrintGroupedResults(allResults, testDataSets); // Save results to file SaveResults(allResults, testDataSets); System.Console.WriteLine("\n✓ Benchmark complete!"); } /// /// 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; byte[] 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 private static List RunBenchmarksForTestData(TestDataSet testData, string mode) { var results = new List(); var serializers = CreateSerializers(testData); // Round-trip correctness check — once per (cell × serializer), BEFORE warmup. Aborts the entire benchmark on failure. System.Console.WriteLine("Verifying round-trip correctness..."); foreach (var serializer in serializers) { if (!serializer.VerifyRoundTrip()) { System.Console.Error.WriteLine($"❌ FATAL: Round-trip verification FAILED for {serializer.Name} on {testData.DisplayName}"); System.Console.Error.WriteLine("Benchmark numbers from a serializer with broken round-trip would be meaningless. Aborting."); Environment.Exit(1); } } System.Console.WriteLine("✓ All serializers passed round-trip verification."); // Warmup all serializers System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)..."); foreach (var serializer in serializers) { serializer.Warmup(WarmupIterations); } // Wait for tiered JIT background compilation to complete Thread.Sleep(3000); // Run benchmarks System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations × {BenchmarkSamples} samples median)...\n"); foreach (var serializer in serializers) { var result = new BenchmarkResult { TestDataName = testData.DisplayName, // Use DisplayName for IId% info SerializerName = serializer.Name, OptionsDescription = serializer.OptionsDescription, SerializedSize = serializer.SerializedSize }; if (mode is "all" or "serialize" or "ser") { result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations); // Dedicated alloc-only sample (separate from timing samples; keeps timing pure) result.SerializeAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), TestIterations); } if (mode is "all" or "deserialize" or "des") { result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations); result.DeserializeAllocBytesPerOp = MeasureAllocation(() => serializer.Deserialize(), TestIterations); } results.Add(result); PrintResult(result); } return results; } private static List CreateSerializers(TestDataSet testData) { var binaryNoInternOption = AcBinarySerializerOptions.Default; binaryNoInternOption.UseStringInterning = StringInterningMode.None; var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default; binaryDefaultNoSgenOption.UseGeneratedCode = false; var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode; binaryFastModeNoSgenOption.UseGeneratedCode = false; return new List { // AcBinary variants //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), ////new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, SerializerAcBinaryFastNoSgen), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), ////new AcBinaryBenchmark(testData.Order, binaryDefaultNoSgenOption, SerializerAcBinaryDefaultNoSgen), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef), //new AcBinaryBenchmark(testData.Order, binaryNoInternOption, SerializerAcBinaryNoIntern), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastNoSgen), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefaultNoSgen), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern), // MemoryPack new MemoryPackBenchmark(testData.Order, SerializerMemoryPack), // MessagePack new MessagePackBenchmark(testData.Order, SerializerMessagePack), // AcBinary BufferWriter //new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter), // System.Text.Json //new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) }; } /// /// Runs the action times for independent samples, /// returning the median elapsed time. Multi-sample design reduces single-run variance from ~±15% to ~±5% /// by smoothing transient effects (background activity, thermal/turbo state, JIT tier-promotion timing). /// When <= 1, falls back to single-sample timing (Debug / quick mode). /// private static double RunTimed(Action action, int iterations) { var samples = BenchmarkSamples; if (samples <= 1) { // Single-sample fast path (Debug or trivial run) — no allocation, no sort. var sw = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) action(); sw.Stop(); return sw.Elapsed.TotalMilliseconds; } var times = new double[samples]; for (int s = 0; s < samples; s++) { var sw = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) action(); sw.Stop(); times[s] = sw.Elapsed.TotalMilliseconds; } Array.Sort(times); // Median: middle value for odd sample counts, average of two middles for even counts. return samples % 2 == 1 ? times[samples / 2] : (times[samples / 2 - 1] + times[samples / 2]) / 2.0; } /// /// Measures per-call allocation in bytes after a clean GC. Single dedicated sample (no median) — keeps timing samples pure. /// private static long MeasureAllocation(Action action, int iterations) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var before = GC.GetAllocatedBytesForCurrentThread(); for (var i = 0; i < iterations; i++) action(); var after = GC.GetAllocatedBytesForCurrentThread(); return (after - before) / iterations; } private static readonly JsonSerializerOptions VerifyJsonOpts = new() { WriteIndented = false, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles }; /// /// Round-trip equality check: serialize both via System.Text.Json (canonical form) and compare strings. /// Slower than property-by-property compare, but universal — works for any object graph without custom comparer. /// private static bool DeepEqualsViaJson(object? a, object? b) { if (a == null && b == null) return true; if (a == null || b == null) return false; var jsonA = JsonSerializer.Serialize(a, VerifyJsonOpts); var jsonB = JsonSerializer.Serialize(b, VerifyJsonOpts); return jsonA == jsonB; } /// /// Validates MemoryPack setup at startup. Aborts the benchmark if TestOrder is not [MemoryPackable]. /// Without this attribute, MemoryPack falls back to runtime resolver (slower) — comparison would be INVALID. /// private static void ValidateMemoryPackSetup() { var typesToCheck = new[] { typeof(TestOrder) }; foreach (var type in typesToCheck) { var hasAttr = type.GetCustomAttributes(typeof(MemoryPackableAttribute), inherit: true).Any(); if (!hasAttr) { System.Console.Error.WriteLine($"❌ FATAL: {type.FullName} is not [MemoryPackable] — MemoryPack would fall back to runtime resolver, comparison is INVALID for SGen-vs-SGen claim."); System.Console.Error.WriteLine("Add [MemoryPackable] to the type and any nested types referenced from it."); Environment.Exit(1); } } } /// /// Interactive menu shown when no CLI args. Returns the layer keyword (core/comprehensive/edge/all) or null on Quit. /// private static string? ShowInteractiveMenu() { System.Console.WriteLine(); System.Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); System.Console.WriteLine("║ AcBinary Benchmark Suite ║"); System.Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); System.Console.WriteLine(); System.Console.WriteLine("Select benchmark layer:"); System.Console.WriteLine(); System.Console.WriteLine(" [1] Core — daily iteration"); System.Console.WriteLine(" [2] Comprehensive — release validation"); System.Console.WriteLine(" [3] Edge cases — refactor verification"); System.Console.WriteLine(" [A] All layers"); System.Console.WriteLine(" [Q] Quit"); System.Console.Write("\nSelection: "); var key = System.Console.ReadKey(intercept: false).KeyChar; System.Console.WriteLine(); return char.ToLower(key) switch { '1' => "core", '2' => "comprehensive", '3' => "edge", 'a' => "all", 'q' => null, _ => "core" }; } /// /// 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. /// private static List FilterByLayer(List all, string layer) { if (layer == "all") return all.ToList(); var coreNames = new[] { "Small", "Medium", "Large", "Repeated", "Deep" }; // P2 will add: "Flat", "Polymorphic", "Collection", "Numeric", "NonAscii", etc. var comprehensiveExtras = new string[] { /* P2 */ }; // P3 will add: "ColdStart", "VeryLarge", "PathologicalString", etc. var edgeExtras = new string[] { /* P3 */ }; bool StartsWithAny(string name, string[] prefixes) => prefixes.Any(p => name.StartsWith(p)); 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(), _ => all.ToList() }; } #endregion #region Serializer Implementations private interface ISerializerBenchmark { string Name { get; } int SerializedSize { get; } string? OptionsDescription => null; void Warmup(int iterations); void Serialize(); void Deserialize(); /// Round-trip correctness check — called once per cell before warmup. Returns true if Serialize+Deserialize preserves data. bool VerifyRoundTrip(); } private sealed class AcBinaryBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; public string Name { get; } public int SerializedSize => _serialized.Length; public string OptionsDescription => $"WireMode={_options.WireMode}, RefHandling={_options.ReferenceHandling}, Interning={_options.UseStringInterning}, Metadata={_options.UseMetadata}, SGen={_options.UseGeneratedCode}, Compression={_options.UseCompression}"; public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name) { _order = order; _options = options; Name = name; _serialized = AcBinarySerializer.Serialize(order, options); //_options.UseCompression = Lz4CompressionMode.Block; } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() { AcBinarySerializer.Serialize(_order, _options); //if (_options.ReferenceHandling != ReferenceHandlingMode.None || _options.UseStringInterning != StringInterningMode.None) //{ // AcBinarySerializer.ScanOnly(_order, _options); //} //else AcBinarySerializer.Serialize(_order, _options); } [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var bytes = AcBinarySerializer.Serialize(_order, _options); var roundTripped = AcBinaryDeserializer.Deserialize(bytes, _options); return DeepEqualsViaJson(_order, roundTripped); } } private sealed class MemoryPackBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly byte[] _serialized; public string Name { get; } public int SerializedSize => _serialized.Length; public MemoryPackBenchmark(TestOrder order, string name) { _order = order; Name = name; _serialized = MemoryPackSerializer.Serialize(order); } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() => MemoryPackSerializer.Serialize(_order); [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => MemoryPackSerializer.Deserialize(_serialized); public bool VerifyRoundTrip() { var bytes = MemoryPackSerializer.Serialize(_order); var roundTripped = MemoryPackSerializer.Deserialize(bytes); return DeepEqualsViaJson(_order, roundTripped); } } private sealed class MessagePackBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly MessagePackSerializerOptions _options; private readonly byte[] _serialized; public string Name { get; } public int SerializedSize => _serialized.Length; public string OptionsDescription { get; } public MessagePackBenchmark(TestOrder order, string name) { _order = order; Name = name; //_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); //_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block); _options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.None); var isContractless = _options.Resolver is ContractlessStandardResolver; OptionsDescription = $"Mode={( isContractless ? "Contractless" : "ContractBased")}, Compression={_options.Compression}"; _serialized = MessagePackSerializer.Serialize(order, _options); } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() => MessagePackSerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => MessagePackSerializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var bytes = MessagePackSerializer.Serialize(_order, _options); var roundTripped = MessagePackSerializer.Deserialize(bytes, _options); return DeepEqualsViaJson(_order, roundTripped); } } private sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly AcBinarySerializerOptions _options; private readonly byte[] _serialized; private ArrayBufferWriter _bufferWriter; public string Name { get; } public int SerializedSize => _serialized.Length; public string OptionsDescription => $"WireMode={_options.WireMode}, RefHandling={_options.ReferenceHandling}, Interning={_options.UseStringInterning}, Metadata={_options.UseMetadata}, SGen={_options.UseGeneratedCode}, Compression={_options.UseCompression}"; public AcBinaryBufferWriterBenchmark(TestOrder order, AcBinarySerializerOptions options, string name) { _order = order; _options = options; Name = name; _serialized = AcBinarySerializer.Serialize(order, options); //_bufferWriter = new ArrayBufferWriter(); } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() { //_bufferWriter.ResetWrittenCount(); _bufferWriter = new ArrayBufferWriter(); AcBinarySerializer.Serialize(_order, _bufferWriter, _options); } [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var bw = new ArrayBufferWriter(); AcBinarySerializer.Serialize(_order, bw, _options); var roundTripped = AcBinaryDeserializer.Deserialize(bw.WrittenSpan.ToArray(), _options); return DeepEqualsViaJson(_order, roundTripped); } } private sealed class SystemTextJsonBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly JsonSerializerOptions _options; private readonly string _serialized; private readonly byte[] _serializedUtf8; public string Name { get; } public int SerializedSize => _serializedUtf8.Length; public SystemTextJsonBenchmark(TestOrder order, string name) { _order = order; Name = name; _options = new JsonSerializerOptions { WriteIndented = false, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles }; _serialized = System.Text.Json.JsonSerializer.Serialize(order, _options); _serializedUtf8 = Utf8NoBom.GetBytes(_serialized); } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() => System.Text.Json.JsonSerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => System.Text.Json.JsonSerializer.Deserialize(_serialized, _options); public bool VerifyRoundTrip() { var json = System.Text.Json.JsonSerializer.Serialize(_order, _options); var roundTripped = System.Text.Json.JsonSerializer.Deserialize(json, _options); return DeepEqualsViaJson(_order, roundTripped); } } #endregion #region Results private sealed class BenchmarkResult { public string TestDataName { get; set; } = ""; public string SerializerName { get; set; } = ""; public string? OptionsDescription { get; set; } public int SerializedSize { get; set; } public double SerializeTimeMs { get; set; } public double DeserializeTimeMs { get; set; } public long SerializeAllocBytesPerOp { get; set; } public long DeserializeAllocBytesPerOp { get; set; } public double RoundTripTimeMs => SerializeTimeMs + DeserializeTimeMs; } private static void PrintResult(BenchmarkResult result) { var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs,8:F2} ms" : " N/A"; var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs,8:F2} ms" : " N/A"; var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp,8:N0} B/op" : " N/A"; var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp,8:N0} B/op" : " N/A"; System.Console.WriteLine($" {result.SerializerName,-25} | Size: {result.SerializedSize,8:N0} | Ser: {ser} ({serAlloc}) | Des: {des} ({desAlloc})"); } private static void PrintGroupedResults(List results, List testDataSets) { System.Console.WriteLine("\n"); System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗"); System.Console.WriteLine("║ GROUPED RESULTS BY TEST DATA ║"); System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); // Print serializer options var optionsMap = results .Where(r => r.OptionsDescription != null) .Select(r => (r.SerializerName, r.OptionsDescription!)) .Distinct() .ToList(); if (optionsMap.Count > 0) { System.Console.WriteLine(); System.Console.WriteLine(" Serializer Options:"); foreach (var (name, opts) in optionsMap) System.Console.WriteLine($" {name}: {opts}"); } foreach (var testData in testDataSets) { var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList(); // Baseline switched MessagePack → MemoryPack: MemoryPack is the SOTA performance leader. var memPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMemoryPack); var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault); System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(98, '─') + "┐"); System.Console.WriteLine($"│ {"#",-4} │ {"Serializer",-25} │ {"Size",-10} │ {"Serialize",-12} │ {"Deserialize",-12} │ {"Round-trip",-12} │"); System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤"); var rank = 1; foreach (var result in testResults) { var size = $"{result.SerializedSize:N0}"; var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A"; var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A"; var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A"; // Highlight MemoryPack (baseline) and AcBinary (Default) with win/lose colors var isHighlighted = result.SerializerName is SerializerMemoryPack or SerializerAcBinaryDefault; var prefix = isHighlighted ? "│►" : "│ "; var suffix = isHighlighted ? "◄│" : " │"; // Color logic: Green = winner (faster), Red = loser (slower) if (isHighlighted && memPackResult != null && acBinaryResult != null) { var isMemPack = result.SerializerName == SerializerMemoryPack; var memPackFaster = memPackResult.RoundTripTimeMs < acBinaryResult.RoundTripTimeMs; if (isMemPack) { System.Console.ForegroundColor = memPackFaster ? ConsoleColor.Green : ConsoleColor.Red; } else { System.Console.ForegroundColor = memPackFaster ? ConsoleColor.Red : ConsoleColor.Green; } } System.Console.WriteLine($"{prefix}{rank++,4} │ {result.SerializerName,-25} │ {size,10} │ {ser,12} │ {des,12} │ {rt,12}{suffix}"); if (isHighlighted) { System.Console.ResetColor(); } } // Allocation summary row (per-op allocation in bytes; lower is better) System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤"); foreach (var result in testResults) { if (result.SerializerName is not (SerializerMemoryPack or SerializerAcBinaryDefault)) continue; var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp:N0} B/op" : "N/A"; var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp:N0} B/op" : "N/A"; System.Console.WriteLine($"│ alloc │ {result.SerializerName,-25} │ {"",10} │ {serAlloc,12} │ {desAlloc,12} │ {"",12} │"); } // Footer row: AcBinary (Default) vs MemoryPack comparison per column if (memPackResult != null && acBinaryResult != null) { var sizePct = (acBinaryResult.SerializedSize / (double)memPackResult.SerializedSize - 1) * 100; var serPct = memPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / memPackResult.SerializeTimeMs - 1) * 100 : 0; var desPct = memPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / memPackResult.DeserializeTimeMs - 1) * 100 : 0; var rtPct = memPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / memPackResult.RoundTripTimeMs - 1) * 100 : 0; System.Console.WriteLine($"├{"─".PadRight(6, '─')}┴{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(13, '─')}┤"); System.Console.Write($"│ ► Default vs {SerializerMemoryPack,-19} │ "); // Size System.Console.ForegroundColor = sizePct <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.Write($"{sizePct,+9:+0;-0}%"); System.Console.ResetColor(); System.Console.Write(" │ "); // Serialize System.Console.ForegroundColor = serPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.Write($"{serPct,+11:+0;-0}%"); System.Console.ResetColor(); System.Console.Write(" │ "); // Deserialize System.Console.ForegroundColor = desPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.Write($"{desPct,+11:+0;-0}%"); System.Console.ResetColor(); System.Console.Write(" │ "); // Round-trip System.Console.ForegroundColor = rtPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.Write($"{rtPct,+10:+0;-0}%"); System.Console.ResetColor(); System.Console.WriteLine(" │"); } System.Console.WriteLine($"└{"─".PadRight(6, '─')}─{"─".PadRight(27, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(13, '─')}┘"); //System.Console.WriteLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}"); //System.Console.WriteLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes"); } // Summary: Best serializer for each category System.Console.WriteLine("\n"); System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗"); System.Console.WriteLine("║ SUMMARY: WINNERS ║"); System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); System.Console.WriteLine($"\n{"Category",-20} │ {"Winner",-25} │ {"Avg Value",-18}"); System.Console.WriteLine($"{"─".PadRight(20, '─')}─┼─{"─".PadRight(25, '─')}─┼─{"─".PadRight(18, '─')}"); // Fastest Serialize var fastestSer = results.Where(r => r.SerializeTimeMs > 0) .GroupBy(r => r.SerializerName) .Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.SerializeTimeMs) }) .OrderBy(x => x.AvgTime) .FirstOrDefault(); if (fastestSer != null) System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-25} │ {fastestSer.AvgTime,15:F2} ms"); // Fastest Deserialize var fastestDes = results.Where(r => r.DeserializeTimeMs > 0) .GroupBy(r => r.SerializerName) .Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.DeserializeTimeMs) }) .OrderBy(x => x.AvgTime) .FirstOrDefault(); if (fastestDes != null) System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-25} │ {fastestDes.AvgTime,15:F2} ms"); // Smallest Size var smallestSize = results .GroupBy(r => r.SerializerName) .Select(g => new { Name = g.Key, AvgSize = g.Average(r => r.SerializedSize) }) .OrderBy(x => x.AvgSize) .FirstOrDefault(); if (smallestSize != null) System.Console.WriteLine($"{"Smallest Size",-20} │ {smallestSize.Name,-25} │ {smallestSize.AvgSize,15:F0} B"); // Fastest Round-trip var fastestRt = results.Where(r => r.RoundTripTimeMs > 0) .GroupBy(r => r.SerializerName) .Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.RoundTripTimeMs) }) .OrderBy(x => x.AvgTime) .FirstOrDefault(); if (fastestRt != null) System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-25} │ {fastestRt.AvgTime,15:F2} ms"); // Overall AcBinary Default vs MemoryPack comparison (baseline switched MessagePack → MemoryPack as SOTA reference) var memPackSerResults = results.Where(r => r.SerializerName == SerializerMemoryPack && r.SerializeTimeMs > 0).ToList(); var memPackDesResults = results.Where(r => r.SerializerName == SerializerMemoryPack && r.DeserializeTimeMs > 0).ToList(); var memPackRtResults = results.Where(r => r.SerializerName == SerializerMemoryPack && r.RoundTripTimeMs > 0).ToList(); var acBinarySerResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList(); var acBinaryDesResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList(); var acBinaryRtResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList(); // Skip comparison if no data available if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0) { System.Console.WriteLine(); System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMemoryPack} (Overall) ──"); System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)"); return; } var memPackAvgSer = memPackSerResults.Count > 0 ? memPackSerResults.Average(r => r.SerializeTimeMs) : 0; var memPackAvgDes = memPackDesResults.Average(r => r.DeserializeTimeMs); var memPackAvgRt = memPackRtResults.Average(r => r.RoundTripTimeMs); var memPackAvgSize = results.Where(r => r.SerializerName == SerializerMemoryPack).Average(r => r.SerializedSize); var memPackAvgSerAlloc = memPackSerResults.Count > 0 ? memPackSerResults.Average(r => r.SerializeAllocBytesPerOp) : 0; var memPackAvgDesAlloc = memPackDesResults.Count > 0 ? memPackDesResults.Average(r => r.DeserializeAllocBytesPerOp) : 0; var acBinaryAvgSer = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeTimeMs) : 0; var acBinaryAvgDes = acBinaryDesResults.Average(r => r.DeserializeTimeMs); var acBinaryAvgRt = acBinaryRtResults.Average(r => r.RoundTripTimeMs); var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize); var acBinaryAvgSerAlloc = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeAllocBytesPerOp) : 0; var acBinaryAvgDesAlloc = acBinaryDesResults.Count > 0 ? acBinaryDesResults.Average(r => r.DeserializeAllocBytesPerOp) : 0; System.Console.WriteLine(); System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMemoryPack} (Overall) ──"); // Only show serialize comparison if data available if (memPackAvgSer > 0 && acBinaryAvgSer > 0) { var serPctAll = (acBinaryAvgSer / memPackAvgSer - 1) * 100; System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {memPackAvgSer:F2} ms)"); System.Console.ResetColor(); } var desPctAll = (acBinaryAvgDes / memPackAvgDes - 1) * 100; var rtPctAll = (acBinaryAvgRt / memPackAvgRt - 1) * 100; var sizePctAll = (acBinaryAvgSize / memPackAvgSize - 1) * 100; System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {memPackAvgDes:F2} ms)"); System.Console.ResetColor(); System.Console.ForegroundColor = rtPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Round-trip: {rtPctAll:+0;-0}% ({acBinaryAvgRt:F2} ms vs {memPackAvgRt:F2} ms)"); System.Console.ResetColor(); System.Console.ForegroundColor = sizePctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Size: {sizePctAll:+0;-0}% ({acBinaryAvgSize:F0} B vs {memPackAvgSize:F0} B)"); System.Console.ResetColor(); // Allocation comparison: byte[] API allocates the output array on both sides — delta shows serializer-overhead diff. if (memPackAvgSerAlloc > 0 && acBinaryAvgSerAlloc > 0) { var serAllocPct = (acBinaryAvgSerAlloc / memPackAvgSerAlloc - 1) * 100; System.Console.ForegroundColor = serAllocPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Ser Alloc: {serAllocPct:+0;-0}% ({acBinaryAvgSerAlloc:F0} B/op vs {memPackAvgSerAlloc:F0} B/op)"); System.Console.ResetColor(); } if (memPackAvgDesAlloc > 0 && acBinaryAvgDesAlloc > 0) { var desAllocPct = (acBinaryAvgDesAlloc / memPackAvgDesAlloc - 1) * 100; System.Console.ForegroundColor = desAllocPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Des Alloc: {desAllocPct:+0;-0}% ({acBinaryAvgDesAlloc:F0} B/op vs {memPackAvgDesAlloc:F0} B/op)"); System.Console.ResetColor(); } } private static void SaveResults(List results, List testDataSets) { Directory.CreateDirectory(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"); // Save binary output to separate .output file var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large")); if (largeTestData != null) { var outputSb = new StringBuilder(); outputSb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗"); outputSb.AppendLine("║ SERIALIZED BINARY OUTPUT ║"); outputSb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║"); outputSb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); outputSb.AppendLine(); outputSb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ==="); var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default); outputSb.AppendLine($"Size: {serializedBytes.Length:N0} bytes"); outputSb.AppendLine(); outputSb.AppendLine("Hex dump:"); outputSb.AppendLine(FormatHexDump(serializedBytes)); File.WriteAllText(outputFilePath, outputSb.ToString(), Utf8NoBom); System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}"); } // Save benchmark results to .log file var sb = new StringBuilder(); 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($"║ Iterations: {TestIterations}".PadRight(100) + "║"); sb.AppendLine($"║ Samples: {BenchmarkSamples} (median)".PadRight(100) + "║"); sb.AppendLine($"║ Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}".PadRight(100) + "║"); sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); sb.AppendLine(); // Serializer options summary var optionsMap = results .Where(r => r.OptionsDescription != null) .Select(r => (r.SerializerName, r.OptionsDescription!)) .Distinct() .ToList(); if (optionsMap.Count > 0) { sb.AppendLine("=== SERIALIZER OPTIONS ==="); foreach (var (name, opts) in optionsMap) sb.AppendLine($" {name}: {opts}"); sb.AppendLine(); } // CSV-like data for easy import (now includes per-op allocation columns) sb.AppendLine("=== RAW DATA (CSV) ==="); sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp"); foreach (var testData in testDataSets) { var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList(); foreach (var result in testResults) { sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp}"); } } sb.AppendLine(); // Formatted results sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ==="); sb.AppendLine($"(►) = Highlighted: {SerializerMemoryPack} (baseline) and {SerializerAcBinaryDefault}"); sb.AppendLine(); foreach (var testData in testDataSets) { var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList(); var memPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMemoryPack); var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault); sb.AppendLine(); sb.AppendLine($"--- {testData.DisplayName} ---"); sb.AppendLine($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14} {"SerAlloc",-12} {"DesAlloc",-12}"); sb.AppendLine(new string('-', 110)); var rank = 1; foreach (var result in testResults) { var isHighlighted = result.SerializerName is SerializerMemoryPack or SerializerAcBinaryDefault; var prefix = isHighlighted ? "► " : " "; var size = $"{result.SerializedSize:N0}"; var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A"; var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A"; var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A"; var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp:N0} B" : "N/A"; var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp:N0} B" : "N/A"; sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-24} {size,-12} {ser,-14} {des,-14} {rt,-14} {serAlloc,-12} {desAlloc,-12}"); } // Summary row for this test data (vs MemoryPack — baseline switched MessagePack → MemoryPack) if (memPackResult != null && acBinaryResult != null) { var sizePct = (acBinaryResult.SerializedSize / (double)memPackResult.SerializedSize - 1) * 100; var serPct = memPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / memPackResult.SerializeTimeMs - 1) * 100 : 0; var desPct = memPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / memPackResult.DeserializeTimeMs - 1) * 100 : 0; var rtPct = memPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / memPackResult.RoundTripTimeMs - 1) * 100 : 0; sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMemoryPack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%"); } //sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}"); //sb.AppendLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes"); } // Summary comparison (vs MemoryPack) sb.AppendLine(); sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMemoryPack} (Overall) ==="); var memPackSerResults2 = results.Where(r => r.SerializerName == SerializerMemoryPack && r.SerializeTimeMs > 0).ToList(); var memPackDesResults2 = results.Where(r => r.SerializerName == SerializerMemoryPack && r.DeserializeTimeMs > 0).ToList(); var memPackRtResults2 = results.Where(r => r.SerializerName == SerializerMemoryPack && r.RoundTripTimeMs > 0).ToList(); var acBinarySerResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList(); var acBinaryDesResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList(); var acBinaryRtResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList(); if (memPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0) { var memPackAvgSer2 = memPackSerResults2.Average(r => r.SerializeTimeMs); var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs); var memPackAvgSerAlloc2 = memPackSerResults2.Average(r => r.SerializeAllocBytesPerOp); var acBinaryAvgSerAlloc2 = acBinarySerResults2.Average(r => r.SerializeAllocBytesPerOp); sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / memPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {memPackAvgSer2:F2} ms)"); if (memPackAvgSerAlloc2 > 0) sb.AppendLine($" Ser Alloc: {((acBinaryAvgSerAlloc2 / memPackAvgSerAlloc2 - 1) * 100):+0;-0}% ({acBinaryAvgSerAlloc2:F0} B/op vs {memPackAvgSerAlloc2:F0} B/op)"); } if (memPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0) { var memPackAvgDes2 = memPackDesResults2.Average(r => r.DeserializeTimeMs); var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs); var memPackAvgDesAlloc2 = memPackDesResults2.Average(r => r.DeserializeAllocBytesPerOp); var acBinaryAvgDesAlloc2 = acBinaryDesResults2.Average(r => r.DeserializeAllocBytesPerOp); sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / memPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {memPackAvgDes2:F2} ms)"); if (memPackAvgDesAlloc2 > 0) sb.AppendLine($" Des Alloc: {((acBinaryAvgDesAlloc2 / memPackAvgDesAlloc2 - 1) * 100):+0;-0}% ({acBinaryAvgDesAlloc2:F0} B/op vs {memPackAvgDesAlloc2:F0} B/op)"); } if (memPackRtResults2.Count > 0 && acBinaryRtResults2.Count > 0) { var memPackAvgRt2 = memPackRtResults2.Average(r => r.RoundTripTimeMs); var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs); sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / memPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {memPackAvgRt2:F2} ms)"); } var memPackAvgSize2 = results.Where(r => r.SerializerName == SerializerMemoryPack).Average(r => r.SerializedSize); var acBinaryAvgSize2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize); sb.AppendLine($" Size: {((acBinaryAvgSize2 / memPackAvgSize2 - 1) * 100):+0;-0}% ({acBinaryAvgSize2:F0} B vs {memPackAvgSize2:F0} B)"); File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom); System.Console.WriteLine($"✓ Results saved to: {logFilePath}"); // Save LLM-optimized results var llmFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.LLM"); SaveLlmResults(llmFilePath, results, testDataSets); } private static void SaveLlmResults(string filePath, List results, List testDataSets) { 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($"Iterations: {TestIterations} | Warmup: {WarmupIterations} | Samples: {BenchmarkSamples} (median) | .NET: {Environment.Version} | TestType: {testTypeName}"); sb.AppendLine($"Baseline: {SerializerMemoryPack} (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup"); // Options summary var optionsMap = results .Where(r => r.OptionsDescription != null) .Select(r => (r.SerializerName, r.OptionsDescription!)) .Distinct() .ToList(); if (optionsMap.Count > 0) { sb.AppendLine(); sb.AppendLine("## Options"); sb.AppendLine(); foreach (var (name, opts) in optionsMap) sb.AppendLine($"- **{name}**: {opts}"); } // Flat results table sorted by test data then round-trip (now includes Alloc columns) sb.AppendLine(); sb.AppendLine("## Results"); sb.AppendLine(); sb.AppendLine("TestData | Serializer | Size(B) | Ser(ms) | Deser(ms) | RT(ms) | SerAlloc(B/op) | DesAlloc(B/op)"); sb.AppendLine("---|---|---|---|---|---|---|---"); foreach (var testData in testDataSets) { var testResults = results .Where(r => r.TestDataName == testData.DisplayName) .OrderBy(r => r.RoundTripTimeMs) .ToList(); foreach (var r in testResults) { var inv = System.Globalization.CultureInfo.InvariantCulture; var ser = r.SerializeTimeMs > 0 ? r.SerializeTimeMs.ToString("F2", inv) : "-"; var des = r.DeserializeTimeMs > 0 ? r.DeserializeTimeMs.ToString("F2", inv) : "-"; var rt = r.RoundTripTimeMs > 0 ? r.RoundTripTimeMs.ToString("F2", inv) : "-"; var serAlloc = r.SerializeTimeMs > 0 ? r.SerializeAllocBytesPerOp.ToString(inv) : "-"; var desAlloc = r.DeserializeTimeMs > 0 ? r.DeserializeAllocBytesPerOp.ToString(inv) : "-"; sb.AppendLine($"{r.TestDataName} | {r.SerializerName} | {r.SerializedSize} | {ser} | {des} | {rt} | {serAlloc} | {desAlloc}"); } } File.WriteAllText(filePath, sb.ToString(), Utf8NoBom); System.Console.WriteLine($"✓ LLM results saved to: {filePath}"); } /// /// Formats byte array as hex dump with offset, hex values, and ASCII representation. /// private static string FormatHexDump(byte[] bytes, int bytesPerLine = 16) { var sb = new StringBuilder(); for (var i = 0; i < bytes.Length; i += bytesPerLine) { // Offset sb.Append($"{i:X8} "); // Hex bytes for (var j = 0; j < bytesPerLine; j++) { if (i + j < bytes.Length) sb.Append($"{bytes[i + j]:X2} "); else sb.Append(" "); if (j == 7) sb.Append(' '); // Extra space in middle } sb.Append(" |"); // ASCII representation for (var j = 0; j < bytesPerLine && i + j < bytes.Length; j++) { var b = bytes[i + j]; sb.Append(b is >= 32 and < 127 ? (char)b : '.'); } sb.AppendLine("|"); } return sb.ToString(); } #endregion }