using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; using Newtonsoft.Json; namespace AyCode.Core.Serializers.Console; /// /// Comprehensive benchmark application for all serializers. /// Compares: AcBinary (all options), AcJson, 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 SerializerAcBinaryNoRef = "AcBinary (NoRef)"; private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)"; private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)"; private const string SerializerAcJsonDefault = "AcJson (Default)"; private const string SerializerNewtonsoftJson = "Newtonsoft.Json"; private const string SerializerSystemTextJson = "System.Text.Json"; private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); private static int WarmupIterations = 2000; private static int TestIterations = 1000; public static void Main(string[] args) { // Set console encoding to UTF-8 for proper Unicode character display System.Console.OutputEncoding = System.Text.Encoding.UTF8; var mode = args.Length > 0 ? args[0].ToLower() : "all"; if (mode == "quick") { WarmupIterations = 5; TestIterations = 100; mode = "all"; } // Profiler mode: warmup only, then exit (for memory profiler analysis) if (mode == "profiler") { RunProfilerMode(); return; } System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗"); System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║"); System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝"); System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations}"); System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}"); System.Console.WriteLine(); var allResults = new List(); var testDataSets = CreateTestDataSets(); 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, mode); 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(); // Create medium test data TestDataFactory.ResetIdCounter(); var sharedTag = TestDataFactory.CreateTag("SharedTag"); var sharedUser = TestDataFactory.CreateUser("shareduser"); var order = TestDataFactory.CreateOrder( itemCount: 3, palletsPerItem: 3, measurementsPerPallet: 3, pointsPerMeasurement: 4, sharedTag: sharedTag, sharedUser: sharedUser); var options = AcBinarySerializerOptions.WithoutReferenceHandling; options.UseStringInterning = StringInterningMode.None; // Warmup (fills caches) System.Console.WriteLine("Warming up (10 iterations)..."); for (var i = 0; i < 10; i++) { _ = AcBinarySerializer.Serialize(order, options); } 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 (1000 iterations for profiling)..."); for (var i = 0; i < 1000; i++) { _ = AcBinarySerializer.Serialize(order, options); } 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 Test Data Creation private static List CreateTestDataSets() { return new List { CreateSmallTestData(), CreateMediumTestData(), CreateLargeTestData(), CreateRepeatedStringsTestData(), CreateDeepNestedTestData() }; } private static TestDataSet CreateSmallTestData() { TestDataFactory.ResetIdCounter(); // Create shared references - IId types (only at Order/Item level) var sharedTag = TestDataFactory.CreateTag("SharedTag"); var sharedUser = TestDataFactory.CreateUser("shareduser"); var order = TestDataFactory.CreateOrder( itemCount: 2, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 2, sharedTag: sharedTag, sharedUser: sharedUser); // Clear deeper level refs for realistic ~10% ratio ClearDeepLevelRefs(order); return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10); } private static TestDataSet CreateMediumTestData() { TestDataFactory.ResetIdCounter(); // IId shared references var sharedTag = TestDataFactory.CreateTag("SharedTag"); var sharedUser = TestDataFactory.CreateUser("shareduser"); var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); // Non-IId shared reference - create separate preferences for 2 users var sharedPreferences = new UserPreferences { Theme = "dark", Language = "en-US", NotificationsEnabled = true, EmailDigestFrequency = "weekly" }; sharedUser.Preferences = sharedPreferences; var order = TestDataFactory.CreateOrder( itemCount: 3, palletsPerItem: 3, measurementsPerPallet: 3, pointsPerMeasurement: 4, sharedTag: sharedTag, sharedUser: sharedUser, sharedMetadata: sharedMeta, sharedPreferences: sharedPreferences); // Clear deeper level refs for realistic ~10% ratio ClearDeepLevelRefs(order); return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10); } private static TestDataSet CreateLargeTestData() { TestDataFactory.ResetIdCounter(); // IId shared references var sharedTag = TestDataFactory.CreateTag("SharedTag"); var sharedUser = TestDataFactory.CreateUser("shareduser"); // Non-IId shared reference var sharedPreferences = new UserPreferences { Theme = "light", Language = "de-DE", NotificationsEnabled = false, EmailDigestFrequency = "daily" }; sharedUser.Preferences = sharedPreferences; var order = TestDataFactory.CreateOrder( itemCount: 5, palletsPerItem: 5, measurementsPerPallet: 5, pointsPerMeasurement: 10, sharedTag: sharedTag, sharedUser: sharedUser, sharedPreferences: sharedPreferences); // Clear deeper level refs for realistic ~10% ratio ClearDeepLevelRefs(order); return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10); } private static TestDataSet CreateRepeatedStringsTestData() { TestDataFactory.ResetIdCounter(); // IId shared references var sharedTag = TestDataFactory.CreateTag("RepeatedTag"); var sharedUser = TestDataFactory.CreateUser("repeateduser"); // Non-IId shared reference var sharedPreferences = new UserPreferences { Theme = "dark", Language = "en-US", NotificationsEnabled = true, EmailDigestFrequency = "weekly" }; sharedUser.Preferences = sharedPreferences; // Create order with many items to test string interning on repeated property names var order = TestDataFactory.CreateOrder( itemCount: 10, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 2, sharedTag: sharedTag, sharedUser: sharedUser, sharedPreferences: sharedPreferences); // Set same status and ProductName on all items to test enum and string handling foreach (var item in order.Items) { item.Status = TestStatus.Processing; item.ProductName = "CommonProductName_RepeatedForTesting"; } // Clear deeper level refs for realistic ~10% ratio ClearDeepLevelRefs(order); return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10); } /// /// Clears IId shared references from Pallet, Measurement, and Point levels. /// This creates a realistic ~10% IId ref ratio (only Order and Item levels have refs). /// private static void ClearDeepLevelRefs(TestOrder order) { foreach (var item in order.Items) { foreach (var pallet in item.Pallets) { pallet.Tag = null; pallet.Inspector = null; pallet.Category = null; foreach (var measurement in pallet.Measurements) { measurement.Tag = null; measurement.Operator = null; foreach (var point in measurement.Points) { point.Tag = null; point.Verifier = null; } } } } } private static TestDataSet CreateDeepNestedTestData() { TestDataFactory.ResetIdCounter(); // IId shared references - only at Order and Item levels for ~10% ratio var sharedTag = TestDataFactory.CreateTag("DeepTag"); var sharedUser = TestDataFactory.CreateUser("deepuser"); var sharedCategory = TestDataFactory.CreateCategory("DeepCategory"); // Non-IId shared reference var sharedPreferences = new UserPreferences { Theme = "light", Language = "fr-FR", NotificationsEnabled = false, EmailDigestFrequency = "monthly" }; sharedUser.Preferences = sharedPreferences; var order = TestDataFactory.CreateOrder( itemCount: 2, palletsPerItem: 4, measurementsPerPallet: 4, pointsPerMeasurement: 8, sharedTag: sharedTag, sharedUser: sharedUser, sharedPreferences: sharedPreferences, sharedCategory: sharedCategory); // Clear deeper level refs for realistic ~10% ratio ClearDeepLevelRefs(order); return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10); } #endregion #region Benchmark Execution private static List RunBenchmarksForTestData(TestDataSet testData, string mode) { var results = new List(); var serializers = CreateSerializers(testData); // 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(2000); // Run benchmarks System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n"); foreach (var serializer in serializers) { var result = new BenchmarkResult { TestDataName = testData.DisplayName, // Use DisplayName for IId% info SerializerName = serializer.Name, SerializedSize = serializer.SerializedSize }; if (mode is "all" or "serialize" or "ser") { result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations); } if (mode is "all" or "deserialize" or "des") { result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations); } results.Add(result); PrintResult(result); } return results; } private static List CreateSerializers(TestDataSet testData) { return new List { // AcBinary variants new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern), // AcJson new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault), // MessagePack new MessagePackBenchmark(testData.Order, SerializerMessagePack), // Newtonsoft.Json new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson), // System.Text.Json new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) }; } private static double RunTimed(Action action, int iterations) { var sw = Stopwatch.StartNew(); for (var i = 0; i < iterations; i++) { action(); } sw.Stop(); return sw.Elapsed.TotalMilliseconds; } #endregion #region Serializer Implementations private interface ISerializerBenchmark { string Name { get; } int SerializedSize { get; } void Warmup(int iterations); void Serialize(); void Deserialize(); } 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 AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name) { _order = order; _options = options; Name = name; _serialized = AcBinarySerializer.Serialize(order, options); } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() => AcBinarySerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); } private sealed class AcJsonBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly AcJsonSerializerOptions _options; private readonly string _serialized; private readonly byte[] _serializedUtf8; public string Name { get; } public int SerializedSize => _serializedUtf8.Length; public AcJsonBenchmark(TestOrder order, AcJsonSerializerOptions options, string name) { _order = order; _options = options; Name = name; _serialized = AcJsonSerializer.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() => AcJsonSerializer.Serialize(_order, _options); [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => AcJsonDeserializer.Deserialize(_serialized); } 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 MessagePackBenchmark(TestOrder order, string name) { _order = order; Name = name; _options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _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); } private sealed class NewtonsoftBenchmark : ISerializerBenchmark { private readonly TestOrder _order; private readonly JsonSerializerSettings _settings; private readonly string _serialized; private readonly byte[] _serializedUtf8; public string Name { get; } public int SerializedSize => _serializedUtf8.Length; public NewtonsoftBenchmark(TestOrder order, string name) { _order = order; Name = name; _settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }; _serialized = JsonConvert.SerializeObject(order, _settings); _serializedUtf8 = Utf8NoBom.GetBytes(_serialized); } public void Warmup(int iterations) { for (var i = 0; i < iterations; i++) { Serialize(); Deserialize(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void Serialize() => JsonConvert.SerializeObject(_order, _settings); [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => JsonConvert.DeserializeObject(_serialized, _settings); } 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); } #endregion #region Results private sealed class TestDataSet { public string Name { get; } public TestOrder Order { get; } /// /// Percentage of IId shared references in the data (0-100). /// Higher values mean more deduplication benefit for Default mode. /// public int IIdRefPercent { get; } public TestDataSet(string name, TestOrder order, int iidRefPercent = 0) { Name = name; Order = order; IIdRefPercent = iidRefPercent; } /// /// Gets display name including IId ref percentage if set. /// public string DisplayName => IIdRefPercent > 0 ? $"{Name} [{IIdRefPercent}% IId refs]" : Name; } private sealed class BenchmarkResult { public string TestDataName { get; set; } = ""; public string SerializerName { get; set; } = ""; public int SerializedSize { get; set; } public double SerializeTimeMs { get; set; } public double DeserializeTimeMs { 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"; System.Console.WriteLine($" {result.SerializerName,-25} | Size: {result.SerializedSize,8:N0} | Ser: {ser} | Des: {des}"); } 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("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); foreach (var testData in testDataSets) { var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList(); var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack); 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 MessagePack and AcBinary (Default) with win/lose colors var isHighlighted = result.SerializerName is SerializerMessagePack or SerializerAcBinaryDefault; var prefix = isHighlighted ? "│►" : "│ "; var suffix = isHighlighted ? "◄│" : " │"; // Color logic: Green = winner (faster), Red = loser (slower) if (isHighlighted && msgPackResult != null && acBinaryResult != null) { var isMsgPack = result.SerializerName == SerializerMessagePack; var msgPackFaster = msgPackResult.RoundTripTimeMs < acBinaryResult.RoundTripTimeMs; if (isMsgPack) { System.Console.ForegroundColor = msgPackFaster ? ConsoleColor.Green : ConsoleColor.Red; } else { System.Console.ForegroundColor = msgPackFaster ? 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(); } } // Footer row: AcBinary (Default) vs MessagePack comparison per column if (msgPackResult != null && acBinaryResult != null) { var sizePct = (acBinaryResult.SerializedSize / (double)msgPackResult.SerializedSize - 1) * 100; var serPct = msgPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / msgPackResult.SerializeTimeMs - 1) * 100 : 0; var desPct = msgPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / msgPackResult.DeserializeTimeMs - 1) * 100 : 0; var rtPct = msgPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / msgPackResult.RoundTripTimeMs - 1) * 100 : 0; System.Console.WriteLine($"├{"─".PadRight(6, '─')}┴{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(13, '─')}┤"); System.Console.Write($"│ ► Default vs {SerializerMessagePack,-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, '─')}┘"); } // 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 MessagePack comparison var msgPackSerResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList(); var msgPackDesResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList(); var msgPackRtResults = results.Where(r => r.SerializerName == SerializerMessagePack && 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 (msgPackRtResults.Count == 0 || acBinaryRtResults.Count == 0) { System.Console.WriteLine(); System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──"); System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)"); return; } var msgPackAvgSer = msgPackSerResults.Count > 0 ? msgPackSerResults.Average(r => r.SerializeTimeMs) : 0; var msgPackAvgDes = msgPackDesResults.Average(r => r.DeserializeTimeMs); var msgPackAvgRt = msgPackRtResults.Average(r => r.RoundTripTimeMs); var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize); 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); System.Console.WriteLine(); System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──"); // Only show serialize comparison if data available if (msgPackAvgSer > 0 && acBinaryAvgSer > 0) { var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100; System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)"); System.Console.ResetColor(); } var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100; var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100; var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100; System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red; System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes: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 {msgPackAvgRt: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 {msgPackAvgSize:F0} B)"); 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("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝"); sb.AppendLine(); // CSV-like data for easy import sb.AppendLine("=== RAW DATA (CSV) ==="); sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs"); 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}"); } } sb.AppendLine(); // Formatted results sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ==="); sb.AppendLine($"(►) = Highlighted: {SerializerMessagePack} (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 msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack); 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}"); sb.AppendLine(new string('-', 86)); var rank = 1; foreach (var result in testResults) { var isHighlighted = result.SerializerName is SerializerMessagePack 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"; sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-24} {size,-12} {ser,-14} {des,-14} {rt,-14}"); } // Summary row for this test data if (msgPackResult != null && acBinaryResult != null) { var sizePct = (acBinaryResult.SerializedSize / (double)msgPackResult.SerializedSize - 1) * 100; var serPct = msgPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / msgPackResult.SerializeTimeMs - 1) * 100 : 0; var desPct = msgPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / msgPackResult.DeserializeTimeMs - 1) * 100 : 0; var rtPct = msgPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / msgPackResult.RoundTripTimeMs - 1) * 100 : 0; sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMessagePack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%"); } } // Summary comparison sb.AppendLine(); sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ==="); var msgPackSerResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList(); var msgPackDesResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList(); var msgPackRtResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && 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 (msgPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0) { var msgPackAvgSer2 = msgPackSerResults2.Average(r => r.SerializeTimeMs); var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs); sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / msgPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {msgPackAvgSer2:F2} ms)"); } if (msgPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0) { var msgPackAvgDes2 = msgPackDesResults2.Average(r => r.DeserializeTimeMs); var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs); sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / msgPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {msgPackAvgDes2:F2} ms)"); } if (msgPackRtResults2.Count > 0 && acBinaryRtResults2.Count > 0) { var msgPackAvgRt2 = msgPackRtResults2.Average(r => r.RoundTripTimeMs); var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs); sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / msgPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {msgPackAvgRt2:F2} ms)"); } var msgPackAvgSize2 = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize); var acBinaryAvgSize2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize); sb.AppendLine($" Size: {((acBinaryAvgSize2 / msgPackAvgSize2 - 1) * 100):+0;-0}% ({acBinaryAvgSize2:F0} B vs {msgPackAvgSize2:F0} B)"); File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom); System.Console.WriteLine($"✓ Results saved to: {logFilePath}"); } /// /// 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 }