using System.Diagnostics; using AyCode.Core.Extensions; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; namespace AyCode.Core.Tests.serialization; [TestClass] public class QuickBenchmark { private static readonly MessagePackSerializerOptions MsgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); [TestMethod] public void RunQuickBenchmark() { // Warmup var order = TestDataFactory.CreateBenchmarkOrder(2, 2, 2, 3); for (int i = 0; i < 10; i++) { var bytes = order.ToBinary(); var result = bytes.BinaryTo(); } // Measure serialize const int iterations = 1000; var sw = Stopwatch.StartNew(); byte[] serialized = null!; for (int i = 0; i < iterations; i++) { serialized = order.ToBinary(); } sw.Stop(); var serializeMs = sw.Elapsed.TotalMilliseconds; // Measure deserialize sw.Restart(); TestOrder? deserialized = null; for (int i = 0; i < iterations; i++) { deserialized = serialized.BinaryTo(); } sw.Stop(); var deserializeMs = sw.Elapsed.TotalMilliseconds; // JSON comparison var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); sw.Restart(); string json = null!; for (int i = 0; i < iterations; i++) { json = order.ToJson(jsonOptions); } sw.Stop(); var jsonSerializeMs = sw.Elapsed.TotalMilliseconds; sw.Restart(); for (int i = 0; i < iterations; i++) { var _ = json.JsonTo(jsonOptions); } sw.Stop(); var jsonDeserializeMs = sw.Elapsed.TotalMilliseconds; Console.WriteLine($"=== Quick Benchmark ({iterations} iterations) ==="); Console.WriteLine($"Binary size: {serialized.Length} bytes"); Console.WriteLine($"JSON size: {json.Length} chars ({System.Text.Encoding.UTF8.GetByteCount(json)} bytes)"); Console.WriteLine(); Console.WriteLine($"Binary Serialize: {serializeMs:F2}ms ({serializeMs / iterations:F4}ms/op)"); Console.WriteLine($"Binary Deserialize: {deserializeMs:F2}ms ({deserializeMs / iterations:F4}ms/op)"); Console.WriteLine($"JSON Serialize: {jsonSerializeMs:F2}ms ({jsonSerializeMs / iterations:F4}ms/op)"); Console.WriteLine($"JSON Deserialize: {jsonDeserializeMs:F2}ms ({jsonDeserializeMs / iterations:F4}ms/op)"); Console.WriteLine(); Console.WriteLine($"Binary vs JSON Serialize: {serializeMs / jsonSerializeMs:F2}x"); Console.WriteLine($"Binary vs JSON Deserialize: {deserializeMs / jsonDeserializeMs:F2}x"); Console.WriteLine($"Size ratio: {100.0 * serialized.Length / System.Text.Encoding.UTF8.GetByteCount(json):F1}%"); Assert.IsNotNull(deserialized); Assert.AreEqual(order.Id, deserialized.Id); } [TestMethod] public void RunStringInterningBenchmark() { // Create data with repeated strings var items = Enumerable.Range(0, 100).Select(i => new TestClassWithRepeatedValues { Id = i, Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed", Category = $"Category_{i % 5}", Priority = i % 2 == 0 ? "High_Priority" : "Low_Priority" }).ToList(); // Warmup for (int i = 0; i < 10; i++) { var bytes = items.ToBinary(); var result = bytes.BinaryTo>(); } const int iterations = 1000; // With interning (default) var sw = Stopwatch.StartNew(); byte[] withInterning = null!; for (int i = 0; i < iterations; i++) { withInterning = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default); } sw.Stop(); var withInterningMs = sw.Elapsed.TotalMilliseconds; // Without interning var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = false }; sw.Restart(); byte[] withoutInterning = null!; for (int i = 0; i < iterations; i++) { withoutInterning = AcBinarySerializer.Serialize(items, noInternOptions); } sw.Stop(); var withoutInterningMs = sw.Elapsed.TotalMilliseconds; Console.WriteLine($"=== String Interning Benchmark ({iterations} iterations) ==="); Console.WriteLine($"With interning: {withInterning.Length} bytes, {withInterningMs:F2}ms"); Console.WriteLine($"Without interning: {withoutInterning.Length} bytes, {withoutInterningMs:F2}ms"); Console.WriteLine($"Size savings: {withoutInterning.Length - withInterning.Length} bytes ({100.0 * (withoutInterning.Length - withInterning.Length) / withoutInterning.Length:F1}%)"); Console.WriteLine($"Speed ratio: {withInterningMs / withoutInterningMs:F2}x"); // Verify both deserialize correctly var result1 = withInterning.BinaryTo>(); var result2 = withoutInterning.BinaryTo>(); Assert.AreEqual(100, result1!.Count); Assert.AreEqual(100, result2!.Count); } [TestMethod] public void RunMessagePackComparison() { // Create test data var order = TestDataFactory.CreateBenchmarkOrder(3, 3, 3, 4); // Warmup for (int i = 0; i < 20; i++) { var binBytes = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); var binResult = AcBinaryDeserializer.Deserialize(binBytes); var msgBytes = MessagePackSerializer.Serialize(order, MsgPackOptions); var msgResult = MessagePackSerializer.Deserialize(msgBytes, MsgPackOptions); } const int iterations = 1000; // === AcBinary Serialize === var sw = Stopwatch.StartNew(); byte[] acBinaryData = null!; for (int i = 0; i < iterations; i++) { acBinaryData = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); } sw.Stop(); var acBinarySerMs = sw.Elapsed.TotalMilliseconds; // === MessagePack Serialize === sw.Restart(); byte[] msgPackData = null!; for (int i = 0; i < iterations; i++) { msgPackData = MessagePackSerializer.Serialize(order, MsgPackOptions); } sw.Stop(); var msgPackSerMs = sw.Elapsed.TotalMilliseconds; // === AcBinary Deserialize === sw.Restart(); TestOrder? acBinaryResult = null; for (int i = 0; i < iterations; i++) { acBinaryResult = AcBinaryDeserializer.Deserialize(acBinaryData); } sw.Stop(); var acBinaryDeserMs = sw.Elapsed.TotalMilliseconds; // === MessagePack Deserialize === sw.Restart(); TestOrder? msgPackResult = null; for (int i = 0; i < iterations; i++) { msgPackResult = MessagePackSerializer.Deserialize(msgPackData, MsgPackOptions); } sw.Stop(); var msgPackDeserMs = sw.Elapsed.TotalMilliseconds; // Print results Console.WriteLine($"=== AcBinary vs MessagePack Benchmark ({iterations} iterations) ==="); Console.WriteLine(); Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}"); Console.WriteLine(new string('-', 60)); Console.WriteLine($"{"Size (bytes)",-25} {acBinaryData.Length,12:N0} {msgPackData.Length,12:N0} {100.0 * acBinaryData.Length / msgPackData.Length,9:F1}%"); Console.WriteLine($"{"Serialize (ms)",-25} {acBinarySerMs,12:F2} {msgPackSerMs,12:F2} {acBinarySerMs / msgPackSerMs,9:F2}x"); Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x"); Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x"); Console.WriteLine(); var sizeDiff = msgPackData.Length - acBinaryData.Length; if (sizeDiff > 0) Console.WriteLine($"✅ AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)"); else Console.WriteLine($"⚠️ AcBinary {-sizeDiff:N0} bytes larger"); Assert.IsNotNull(acBinaryResult); Assert.IsNotNull(msgPackResult); Assert.AreEqual(order.Id, acBinaryResult.Id); } [TestMethod] public void RunStringInterningVsMessagePack() { // Create data with many repeated strings (worst case for MessagePack, best for interning) var items = Enumerable.Range(0, 200).Select(i => new TestClassWithRepeatedValues { Id = i, Status = i % 3 == 0 ? "PendingStatus" : i % 3 == 1 ? "ProcessingStatus" : "CompletedStatus", Category = $"Category_{i % 5}", Priority = i % 2 == 0 ? "HighPriority" : "LowPriority" }).ToList(); // Warmup for (int i = 0; i < 20; i++) { var b1 = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default); var r1 = AcBinaryDeserializer.Deserialize>(b1); var b2 = MessagePackSerializer.Serialize(items, MsgPackOptions); var r2 = MessagePackSerializer.Deserialize>(b2, MsgPackOptions); } const int iterations = 1000; // AcBinary with interning var sw = Stopwatch.StartNew(); byte[] acWithIntern = null!; for (int i = 0; i < iterations; i++) { acWithIntern = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default); } var acSerMs = sw.Elapsed.TotalMilliseconds; sw.Restart(); for (int i = 0; i < iterations; i++) { var _ = AcBinaryDeserializer.Deserialize>(acWithIntern); } var acDeserMs = sw.Elapsed.TotalMilliseconds; // MessagePack sw.Restart(); byte[] msgPack = null!; for (int i = 0; i < iterations; i++) { msgPack = MessagePackSerializer.Serialize(items, MsgPackOptions); } var msgSerMs = sw.Elapsed.TotalMilliseconds; sw.Restart(); for (int i = 0; i < iterations; i++) { var _ = MessagePackSerializer.Deserialize>(msgPack, MsgPackOptions); } var msgDeserMs = sw.Elapsed.TotalMilliseconds; Console.WriteLine($"=== String Interning Advantage ({iterations} iterations, 200 items with repeated strings) ==="); Console.WriteLine(); Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}"); Console.WriteLine(new string('-', 60)); Console.WriteLine($"{"Size (bytes)",-25} {acWithIntern.Length,12:N0} {msgPack.Length,12:N0} {100.0 * acWithIntern.Length / msgPack.Length,9:F1}%"); Console.WriteLine($"{"Serialize (ms)",-25} {acSerMs,12:F2} {msgSerMs,12:F2} {acSerMs / msgSerMs,9:F2}x"); Console.WriteLine($"{"Deserialize (ms)",-25} {acDeserMs,12:F2} {msgDeserMs,12:F2} {acDeserMs / msgDeserMs,9:F2}x"); Console.WriteLine(); var sizeSaving = msgPack.Length - acWithIntern.Length; Console.WriteLine($"✅ String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)"); Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller"); } // Public for MessagePack dynamic serializer compatibility public class TestClassWithRepeatedValues { public int Id { get; set; } public string Status { get; set; } = ""; public string Category { get; set; } = ""; public string Priority { get; set; } = ""; } }