From b9e83e2ef85759e056bfe05a6c8f90b541670920 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 12 Dec 2025 20:06:00 +0100 Subject: [PATCH] Add AcBinarySerializer tests, helpers, and benchmark updates - Introduce AcBinarySerializerTests with full coverage for primitives, objects, collections, merge/populate, and size comparisons - Add AcBinarySerializer class stub as a placeholder for implementation - Extend serialization extension methods with binary helpers (ToBinary, BinaryTo, BinaryCloneTo, etc.) - Update test models to ignore parent references for all major serializers ([IgnoreMember], [BsonIgnore]) - Refactor benchmarks: split into minimal, simple, complex, and MessagePack comparison; add command-line switches and improved size reporting - Optimize AcJsonDeserializer with fast UTF-8 property lookup and direct primitive setting - Add MessagePack and MongoDB.Bson dependencies to test and benchmark projects - Add (accidentally) a summary of less commands as a documentation artifact --- AyCode.Core.Tests/AyCode.Core.Tests.csproj | 2 + .../Serialization/AcBinarySerializerTests.cs | 424 ++++++++++++++++ .../TestModels/SharedTestModels.cs | 18 +- AyCode.Core/Extensions/AcBinarySerializer.cs | 6 + AyCode.Core/Extensions/AcJsonDeserializer.cs | 200 +++++++- .../Extensions/SerializeObjectExtensions.cs | 59 +++ BenchmarkSuite1/BenchmarkSuite1.csproj | 2 + BenchmarkSuite1/Program.cs | 197 ++++++-- BenchmarkSuite1/SerializationBenchmarks.cs | 475 ++++++------------ 9 files changed, 1004 insertions(+), 379 deletions(-) create mode 100644 AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs create mode 100644 AyCode.Core/Extensions/AcBinarySerializer.cs diff --git a/AyCode.Core.Tests/AyCode.Core.Tests.csproj b/AyCode.Core.Tests/AyCode.Core.Tests.csproj index ec7e61b..9e9f7d9 100644 --- a/AyCode.Core.Tests/AyCode.Core.Tests.csproj +++ b/AyCode.Core.Tests/AyCode.Core.Tests.csproj @@ -8,9 +8,11 @@ + + diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs new file mode 100644 index 0000000..fe8b9d7 --- /dev/null +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerTests.cs @@ -0,0 +1,424 @@ +using AyCode.Core.Extensions; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Tests.Serialization; + +[TestClass] +public class AcBinarySerializerTests +{ + #region Basic Serialization Tests + + [TestMethod] + public void Serialize_Null_ReturnsSingleNullByte() + { + var result = AcBinarySerializer.Serialize(null); + Assert.AreEqual(1, result.Length); + Assert.AreEqual((byte)32, result[0]); // BinaryTypeCode.Null = 32 + } + + [TestMethod] + public void Serialize_Int32_RoundTrip() + { + var value = 12345; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Int64_RoundTrip() + { + var value = 123456789012345L; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Double_RoundTrip() + { + var value = 3.14159265358979; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_String_RoundTrip() + { + var value = "Hello, Binary World!"; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Boolean_RoundTrip() + { + var trueResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(true)); + var falseResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(false)); + Assert.IsTrue(trueResult); + Assert.IsFalse(falseResult); + } + + [TestMethod] + public void Serialize_DateTime_RoundTrip() + { + var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Guid_RoundTrip() + { + var value = Guid.NewGuid(); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_Decimal_RoundTrip() + { + var value = 123456.789012m; + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_TimeSpan_RoundTrip() + { + var value = TimeSpan.FromHours(2.5); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Serialize_DateTimeOffset_RoundTrip() + { + var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2)); + var binary = AcBinarySerializer.Serialize(value); + var result = AcBinaryDeserializer.Deserialize(binary); + + // Compare UTC ticks and offset separately since we store UTC ticks + Assert.AreEqual(value.UtcTicks, result.UtcTicks); + Assert.AreEqual(value.Offset, result.Offset); + } + + #endregion + + #region Object Serialization Tests + + [TestMethod] + public void Serialize_SimpleObject_RoundTrip() + { + var obj = new TestSimpleClass + { + Id = 42, + Name = "Test Object", + Value = 3.14, + IsActive = true + }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.Name, result.Name); + Assert.AreEqual(obj.Value, result.Value); + Assert.AreEqual(obj.IsActive, result.IsActive); + } + + [TestMethod] + public void Serialize_NestedObject_RoundTrip() + { + var obj = new TestNestedClass + { + Id = 1, + Name = "Parent", + Child = new TestSimpleClass + { + Id = 2, + Name = "Child", + Value = 2.5, + IsActive = true + } + }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.Name, result.Name); + Assert.IsNotNull(result.Child); + Assert.AreEqual(obj.Child.Id, result.Child.Id); + Assert.AreEqual(obj.Child.Name, result.Child.Name); + } + + [TestMethod] + public void Serialize_List_RoundTrip() + { + var list = new List { 1, 2, 3, 4, 5 }; + var binary = list.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + CollectionAssert.AreEqual(list, result); + } + + [TestMethod] + public void Serialize_ObjectWithList_RoundTrip() + { + var obj = new TestClassWithList + { + Id = 1, + Items = new List { "Item1", "Item2", "Item3" } + }; + + var binary = obj.ToBinary(); + var result = binary.BinaryTo(); + + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.IsNotNull(result.Items); + CollectionAssert.AreEqual(obj.Items, result.Items); + } + + [TestMethod] + public void Serialize_Dictionary_RoundTrip() + { + var dict = new Dictionary + { + ["one"] = 1, + ["two"] = 2, + ["three"] = 3 + }; + + var binary = dict.ToBinary(); + var result = binary.BinaryTo>(); + + Assert.IsNotNull(result); + Assert.AreEqual(dict.Count, result.Count); + foreach (var kvp in dict) + { + Assert.IsTrue(result.ContainsKey(kvp.Key)); + Assert.AreEqual(kvp.Value, result[kvp.Key]); + } + } + + #endregion + + #region Populate Tests + + [TestMethod] + public void Populate_UpdatesExistingObject() + { + var target = new TestSimpleClass { Id = 0, Name = "Original" }; + var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 }; + + var binary = source.ToBinary(); + binary.BinaryTo(target); + + Assert.AreEqual(42, target.Id); + Assert.AreEqual("Updated", target.Name); + Assert.AreEqual(3.14, target.Value); + } + + [TestMethod] + public void PopulateMerge_MergesNestedObjects() + { + var target = new TestNestedClass + { + Id = 1, + Name = "Original", + Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 } + }; + + var source = new TestNestedClass + { + Id = 2, + Name = "Updated", + Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 } + }; + + var binary = source.ToBinary(); + binary.BinaryToMerge(target); + + Assert.AreEqual(2, target.Id); + Assert.AreEqual("Updated", target.Name); + Assert.IsNotNull(target.Child); + // Child object should be merged, not replaced + Assert.AreEqual(20, target.Child.Id); + Assert.AreEqual("UpdatedChild", target.Child.Name); + } + + #endregion + + #region String Interning Tests + + [TestMethod] + public void Serialize_RepeatedStrings_UsesInterning() + { + var obj = new TestClassWithRepeatedStrings + { + Field1 = "Repeated", + Field2 = "Repeated", + Field3 = "Repeated", + Field4 = "Unique" + }; + + var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default); + var binaryWithoutInterning = AcBinarySerializer.Serialize(obj, + new AcBinarySerializerOptions { UseStringInterning = false }); + + // With interning should be smaller + Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length, + $"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); + + // Both should deserialize correctly + var result1 = AcBinaryDeserializer.Deserialize(binaryWithInterning); + var result2 = AcBinaryDeserializer.Deserialize(binaryWithoutInterning); + + Assert.AreEqual(obj.Field1, result1!.Field1); + Assert.AreEqual(obj.Field1, result2!.Field1); + } + + #endregion + + #region Size Comparison Tests + + [TestMethod] + public void Serialize_IsSmallerThanJson() + { + var obj = TestDataFactory.CreateBenchmarkOrder(itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3); + + var jsonBytes = System.Text.Encoding.UTF8.GetBytes(obj.ToJson()); + var binaryBytes = obj.ToBinary(); + + Console.WriteLine($"JSON size: {jsonBytes.Length} bytes"); + Console.WriteLine($"Binary size: {binaryBytes.Length} bytes"); + Console.WriteLine($"Ratio: {(double)binaryBytes.Length / jsonBytes.Length:P2}"); + + Assert.IsTrue(binaryBytes.Length < jsonBytes.Length, + $"Binary ({binaryBytes.Length}) should be smaller than JSON ({jsonBytes.Length})"); + + // Verify roundtrip works + var result = binaryBytes.BinaryTo(); + Assert.IsNotNull(result); + Assert.AreEqual(obj.Id, result.Id); + Assert.AreEqual(obj.Items.Count, result.Items.Count); + } + + #endregion + + #region Extension Method Tests + + [TestMethod] + public void BinaryCloneTo_CreatesDeepCopy() + { + var original = new TestNestedClass + { + Id = 1, + Name = "Original", + Child = new TestSimpleClass { Id = 2, Name = "Child" } + }; + + var clone = original.BinaryCloneTo(); + + Assert.IsNotNull(clone); + Assert.AreNotSame(original, clone); + Assert.AreNotSame(original.Child, clone.Child); + Assert.AreEqual(original.Id, clone.Id); + Assert.AreEqual(original.Child.Id, clone.Child!.Id); + + // Modify clone, original should be unchanged + clone.Id = 999; + clone.Child.Id = 888; + Assert.AreEqual(1, original.Id); + Assert.AreEqual(2, original.Child.Id); + } + + #endregion + + #region Test Models + + private class TestSimpleClass + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public double Value { get; set; } + public bool IsActive { get; set; } + } + + private class TestNestedClass + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public TestSimpleClass? Child { get; set; } + } + + private class TestClassWithList + { + public int Id { get; set; } + public List Items { get; set; } = new(); + } + + private class TestClassWithRepeatedStrings + { + public string Field1 { get; set; } = ""; + public string Field2 { get; set; } = ""; + public string Field3 { get; set; } = ""; + public string Field4 { get; set; } = ""; + } + + #endregion + + #region Benchmark Order Tests + + [TestMethod] + public void Serialize_BenchmarkOrder_RoundTrip() + { + // This is the exact same data that causes stack overflow in benchmarks + var order = TestDataFactory.CreateBenchmarkOrder( + itemCount: 3, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 5); + + // Should not throw stack overflow + var binary = AcBinarySerializer.Serialize(order); + Assert.IsTrue(binary.Length > 0, "Binary data should not be empty"); + + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.IsNotNull(result); + Assert.AreEqual(order.Id, result.Id); + Assert.AreEqual(order.OrderNumber, result.OrderNumber); + Assert.AreEqual(order.Items.Count, result.Items.Count); + } + + [TestMethod] + public void Serialize_BenchmarkOrder_SmallData_RoundTrip() + { + // Smaller test to isolate the issue + var order = TestDataFactory.CreateBenchmarkOrder( + itemCount: 1, + palletsPerItem: 1, + measurementsPerPallet: 1, + pointsPerMeasurement: 1); + + var binary = AcBinarySerializer.Serialize(order); + Assert.IsTrue(binary.Length > 0); + + var result = AcBinaryDeserializer.Deserialize(binary); + Assert.IsNotNull(result); + Assert.AreEqual(order.Id, result.Id); + } + + #endregion +} diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 83c3640..82e7988 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -1,5 +1,7 @@ using AyCode.Core.Extensions; using AyCode.Core.Interfaces; +using MessagePack; +using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json; namespace AyCode.Core.Tests.TestModels; @@ -159,8 +161,10 @@ public class TestOrder : IId [JsonNoMergeCollection] public List NoMergeItems { get; set; } = []; - // Parent reference (JsonIgnore to prevent loops) + // Parent reference - ignored by all serializers to prevent circular references [JsonIgnore] + [IgnoreMember] + [BsonIgnore] public object? Parent { get; set; } } @@ -183,7 +187,10 @@ public class TestOrderItem : IId public SharedUser? Assignee { get; set; } public MetadataInfo? ItemMetadata { get; set; } + // Parent reference - ignored by all serializers to prevent circular references [JsonIgnore] + [IgnoreMember] + [BsonIgnore] public TestOrder? ParentOrder { get; set; } } @@ -204,7 +211,10 @@ public class TestPallet : IId // Shared references public MetadataInfo? PalletMetadata { get; set; } + // Parent reference - ignored by all serializers to prevent circular references [JsonIgnore] + [IgnoreMember] + [BsonIgnore] public TestOrderItem? ParentItem { get; set; } } @@ -221,7 +231,10 @@ public class TestMeasurement : IId // Level 5 collection public List Points { get; set; } = []; + // Parent reference - ignored by all serializers to prevent circular references [JsonIgnore] + [IgnoreMember] + [BsonIgnore] public TestPallet? ParentPallet { get; set; } } @@ -235,7 +248,10 @@ public class TestMeasurementPoint : IId public double Value { get; set; } public DateTime MeasuredAt { get; set; } = DateTime.UtcNow; + // Parent reference - ignored by all serializers to prevent circular references [JsonIgnore] + [IgnoreMember] + [BsonIgnore] public TestMeasurement? ParentMeasurement { get; set; } } diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs new file mode 100644 index 0000000..16778ec --- /dev/null +++ b/AyCode.Core/Extensions/AcBinarySerializer.cs @@ -0,0 +1,6 @@ +namespace AyCode.Core.Extensions; + +public class AcBinarySerializer +{ + +} \ No newline at end of file diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Extensions/AcJsonDeserializer.cs index d0bf33a..5d5b591 100644 --- a/AyCode.Core/Extensions/AcJsonDeserializer.cs +++ b/AyCode.Core/Extensions/AcJsonDeserializer.cs @@ -489,7 +489,6 @@ public static class AcJsonDeserializer if (instance == null) return null; - var propsDict = metadata.PropertySettersFrozen; var nextDepth = depth + 1; while (reader.Read()) @@ -500,17 +499,22 @@ public static class AcJsonDeserializer if (reader.TokenType != JsonTokenType.PropertyName) continue; - var propName = reader.GetString(); - if (propName == null || !reader.Read()) - continue; - - if (!propsDict.TryGetValue(propName, out var propInfo)) + // Use UTF8 lookup to avoid string allocation + if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null) { + if (!reader.Read()) break; reader.Skip(); continue; } - // Use cached version for faster type resolution + if (!reader.Read()) + break; + + // Try direct set for primitives (no boxing) + if (propInfo.TrySetValueDirect(instance, ref reader)) + continue; + + // Fallback to boxed path for complex types var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth); propInfo.SetValue(instance, value); } @@ -543,7 +547,6 @@ public static class AcJsonDeserializer if (instance == null) return null; - var propsDict = metadata.PropertySettersFrozen; var nextDepth = depth + 1; while (reader.Read()) @@ -554,17 +557,22 @@ public static class AcJsonDeserializer if (reader.TokenType != JsonTokenType.PropertyName) continue; - var propName = reader.GetString(); - if (propName == null || !reader.Read()) - continue; - - if (!propsDict.TryGetValue(propName, out var propInfo)) + // Use UTF8 lookup to avoid string allocation + if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null) { + if (!reader.Read()) break; reader.Skip(); continue; } - // Use cached version for faster type resolution + if (!reader.Read()) + break; + + // Try direct set for primitives (no boxing) + if (propInfo.TrySetValueDirect(instance, ref reader)) + continue; + + // Fallback to boxed path for complex types var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth); propInfo.SetValue(instance, value); } @@ -706,7 +714,6 @@ public static class AcJsonDeserializer /// private static void PopulateObjectMergeFromReader(ref Utf8JsonReader reader, object target, DeserializeTypeMetadata metadata, byte maxDepth, int depth) { - var propsDict = metadata.PropertySettersFrozen; var nextDepth = depth + 1; var maxDepthReached = nextDepth > maxDepth; @@ -718,25 +725,29 @@ public static class AcJsonDeserializer if (reader.TokenType != JsonTokenType.PropertyName) continue; - var propName = reader.GetString(); - if (propName == null || !reader.Read()) - continue; - - if (!propsDict.TryGetValue(propName, out var propInfo)) + // Use UTF8 lookup to avoid string allocation + if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null) { + if (!reader.Read()) break; reader.Skip(); continue; } + if (!reader.Read()) + break; + var tokenType = reader.TokenType; if (maxDepthReached) { if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray) { - // Use cached version for faster primitive reading - var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo); - propInfo.SetValue(target, primitiveValue); + // Try direct set for primitives (no boxing) + if (!propInfo.TrySetValueDirect(target, ref reader)) + { + var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo); + propInfo.SetValue(target, primitiveValue); + } } else { @@ -768,7 +779,11 @@ public static class AcJsonDeserializer } } - // Use cached version for faster type resolution + // Try direct set for primitives (no boxing) + if (propInfo.TrySetValueDirect(target, ref reader)) + continue; + + // Fallback to boxed path for complex types var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth); propInfo.SetValue(target, value); } @@ -1401,6 +1416,7 @@ public static class AcJsonDeserializer private sealed class DeserializeTypeMetadata { public FrozenDictionary PropertySettersFrozen { get; } + public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects) public Func? CompiledConstructor { get; } public DeserializeTypeMetadata(Type type) @@ -1424,15 +1440,39 @@ public static class AcJsonDeserializer } var propertySetters = new Dictionary(propsList.Count, StringComparer.OrdinalIgnoreCase); + var propsArray = new PropertySetterInfo[propsList.Count]; + var index = 0; foreach (var prop in propsList) { - propertySetters[prop.Name] = new PropertySetterInfo(prop, type); + var propInfo = new PropertySetterInfo(prop, type); + propertySetters[prop.Name] = propInfo; + propsArray[index++] = propInfo; } + PropertiesArray = propsArray; // Create frozen dictionary for faster lookup in hot paths PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } + + /// + /// Try to find property by UTF-8 name using ValueTextEquals (avoids string allocation). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetPropertyUtf8(ref Utf8JsonReader reader, out PropertySetterInfo? propInfo) + { + var props = PropertiesArray; + for (var i = 0; i < props.Length; i++) + { + if (reader.ValueTextEquals(props[i].NameUtf8)) + { + propInfo = props[i]; + return true; + } + } + propInfo = null; + return false; + } } private sealed class PropertySetterInfo @@ -1445,9 +1485,21 @@ public static class AcJsonDeserializer public readonly Type? ElementType; public readonly Type? ElementIdType; public readonly Func? ElementIdGetter; + public readonly byte[] NameUtf8; // Pre-computed UTF-8 bytes of property name for fast matching + // Typed setters to avoid boxing for primitives private readonly Action _setter; private readonly Func _getter; + + // Typed setters for common primitive types (avoid boxing) + internal readonly Action? _setInt32; + internal readonly Action? _setInt64; + internal readonly Action? _setDouble; + internal readonly Action? _setBool; + internal readonly Action? _setDecimal; + internal readonly Action? _setSingle; + internal readonly Action? _setDateTime; + internal readonly Action? _setGuid; public PropertySetterInfo(PropertyInfo prop, Type declaringType) { @@ -1456,9 +1508,31 @@ public static class AcJsonDeserializer IsNullable = underlying != null; UnderlyingType = underlying ?? PropertyType; PropertyTypeCode = Type.GetTypeCode(UnderlyingType); + NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); _setter = CreateCompiledSetter(declaringType, prop); _getter = CreateCompiledGetter(declaringType, prop); + + // Create typed setters for common primitives to avoid boxing + if (!IsNullable) + { + if (ReferenceEquals(PropertyType, IntType)) + _setInt32 = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, LongType)) + _setInt64 = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, DoubleType)) + _setDouble = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, BoolType)) + _setBool = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, DecimalType)) + _setDecimal = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, FloatType)) + _setSingle = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, DateTimeType)) + _setDateTime = CreateTypedSetter(declaringType, prop); + else if (ReferenceEquals(PropertyType, GuidType)) + _setGuid = CreateTypedSetter(declaringType, prop); + } ElementType = GetCollectionElementType(PropertyType); var isCollection = ElementType != null && ElementType != typeof(object) && @@ -1498,14 +1572,88 @@ public static class AcJsonDeserializer var boxed = Expression.Convert(propAccess, typeof(object)); return Expression.Lambda>(boxed, objParam).Compile(); } + + private static Action CreateTypedSetter(Type declaringType, PropertyInfo prop) + { + var objParam = Expression.Parameter(typeof(object), "obj"); + var valueParam = Expression.Parameter(typeof(T), "value"); + var castObj = Expression.Convert(objParam, declaringType); + var propAccess = Expression.Property(castObj, prop); + var assign = Expression.Assign(propAccess, valueParam); + return Expression.Lambda>(assign, objParam, valueParam).Compile(); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetValue(object target, object? value) => _setter(target, value); [MethodImpl(MethodImplOptions.AggressiveInlining)] public object? GetValue(object target) => _getter(target); + + /// + /// Read and set value directly from Utf8JsonReader, avoiding boxing for primitives. + /// Returns true if value was set, false if it needs fallback to SetValue. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TrySetValueDirect(object target, ref Utf8JsonReader reader) + { + var tokenType = reader.TokenType; + + // Handle null + if (tokenType == JsonTokenType.Null) + { + if (IsNullable || !PropertyType.IsValueType) + { + _setter(target, null); + return true; + } + return true; // Skip null for non-nullable value types + } + + // Fast path for booleans - no boxing needed with typed setter + if (tokenType == JsonTokenType.True) + { + if (_setBool != null) { _setBool(target, true); return true; } + _setter(target, BoxedTrue); + return true; + } + if (tokenType == JsonTokenType.False) + { + if (_setBool != null) { _setBool(target, false); return true; } + _setter(target, BoxedFalse); + return true; + } + + // Fast path for numbers - use typed setters when available + if (tokenType == JsonTokenType.Number) + { + if (_setInt32 != null) { _setInt32(target, reader.GetInt32()); return true; } + if (_setInt64 != null) { _setInt64(target, reader.GetInt64()); return true; } + if (_setDouble != null) { _setDouble(target, reader.GetDouble()); return true; } + if (_setDecimal != null) { _setDecimal(target, reader.GetDecimal()); return true; } + if (_setSingle != null) { _setSingle(target, reader.GetSingle()); return true; } + return false; // Fallback to boxed path + } + + // Fast path for strings - common types + if (tokenType == JsonTokenType.String) + { + if (ReferenceEquals(UnderlyingType, StringType)) + { + _setter(target, reader.GetString()); + return true; + } + if (_setDateTime != null) { _setDateTime(target, reader.GetDateTime()); return true; } + if (_setGuid != null) { _setGuid(target, reader.GetGuid()); return true; } + return false; // Fallback to boxed path + } + + return false; // Complex types need standard handling + } + + // Pre-boxed boolean values to avoid repeated boxing + private static readonly object BoxedTrue = true; + private static readonly object BoxedFalse = false; } - #endregion #region Reference Resolution diff --git a/AyCode.Core/Extensions/SerializeObjectExtensions.cs b/AyCode.Core/Extensions/SerializeObjectExtensions.cs index a4aaaa8..ccb90e8 100644 --- a/AyCode.Core/Extensions/SerializeObjectExtensions.cs +++ b/AyCode.Core/Extensions/SerializeObjectExtensions.cs @@ -444,6 +444,65 @@ public static class SerializeObjectExtensions public static T MessagePackTo(this byte[] message, MessagePackSerializerOptions options) => MessagePackSerializer.Deserialize(message, options); + + #region Binary Serialization Extension Methods + + /// + /// Serialize object to binary byte array with default options. + /// Significantly faster than JSON, especially for large data in WASM. + /// + public static byte[] ToBinary(this T source) + => AcBinarySerializer.Serialize(source); + + /// + /// Serialize object to binary byte array with specified options. + /// + public static byte[] ToBinary(this T source, AcBinarySerializerOptions options) + => AcBinarySerializer.Serialize(source, options); + + /// + /// Deserialize binary data to object with default options. + /// + public static T? BinaryTo(this byte[] data) + => AcBinaryDeserializer.Deserialize(data); + + /// + /// Deserialize binary data to object. + /// + public static T? BinaryTo(this ReadOnlySpan data) + => AcBinaryDeserializer.Deserialize(data); + + /// + /// Deserialize binary data to specified type. + /// + public static object? BinaryTo(this byte[] data, Type targetType) + => AcBinaryDeserializer.Deserialize(data.AsSpan(), targetType); + + /// + /// Populate existing object from binary data. + /// + public static void BinaryTo(this byte[] data, T target) where T : class + => AcBinaryDeserializer.Populate(data, target); + + /// + /// Populate existing object from binary data with merge semantics for IId collections. + /// + public static void BinaryToMerge(this byte[] data, T target) where T : class + => AcBinaryDeserializer.PopulateMerge(data.AsSpan(), target); + + /// + /// Clone object via binary serialization (faster than JSON clone). + /// + public static T? BinaryCloneTo(this T source) where T : class + => source?.ToBinary().BinaryTo(); + + /// + /// Copy object properties to target via binary serialization. + /// + public static void BinaryCopyTo(this T source, T target) where T : class + => source?.ToBinary().BinaryTo(target); + + #endregion } public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver diff --git a/BenchmarkSuite1/BenchmarkSuite1.csproj b/BenchmarkSuite1/BenchmarkSuite1.csproj index 2240304..cd03ad1 100644 --- a/BenchmarkSuite1/BenchmarkSuite1.csproj +++ b/BenchmarkSuite1/BenchmarkSuite1.csproj @@ -9,7 +9,9 @@ + + diff --git a/BenchmarkSuite1/Program.cs b/BenchmarkSuite1/Program.cs index 952b89d..2277d56 100644 --- a/BenchmarkSuite1/Program.cs +++ b/BenchmarkSuite1/Program.cs @@ -1,5 +1,10 @@ using BenchmarkDotNet.Running; using AyCode.Core.Benchmarks; +using AyCode.Core.Extensions; +using AyCode.Core.Tests.TestModels; +using System.Text; +using MessagePack; +using MessagePack.Resolvers; namespace BenchmarkSuite1 { @@ -7,61 +12,167 @@ namespace BenchmarkSuite1 { static void Main(string[] args) { - // Quick size comparison test + if (args.Length > 0 && args[0] == "--test") + { + RunQuickTest(); + return; + } + + if (args.Length > 0 && args[0] == "--testmsgpack") + { + RunMessagePackTest(); + return; + } + + if (args.Length > 0 && args[0] == "--minimal") + { + BenchmarkRunner.Run(); + return; + } + + if (args.Length > 0 && args[0] == "--simple") + { + BenchmarkRunner.Run(); + return; + } + + if (args.Length > 0 && args[0] == "--complex") + { + BenchmarkRunner.Run(); + return; + } + + if (args.Length > 0 && args[0] == "--msgpack") + { + BenchmarkRunner.Run(); + return; + } + if (args.Length > 0 && args[0] == "--sizes") { RunSizeComparison(); return; } - // Use assembly-wide discovery for all benchmarks - BenchmarkSwitcher.FromAssembly(typeof(SerializationBenchmarks).Assembly).Run(args); + Console.WriteLine("Usage:"); + Console.WriteLine(" --test Quick AcBinary test"); + Console.WriteLine(" --testmsgpack Quick MessagePack test"); + Console.WriteLine(" --minimal Minimal benchmark"); + Console.WriteLine(" --simple Simple flat object benchmark"); + Console.WriteLine(" --complex Complex hierarchy (AcBinary vs JSON)"); + Console.WriteLine(" --msgpack MessagePack comparison"); + Console.WriteLine(" --sizes Size comparison only"); + + if (args.Length == 0) + { + BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args); + } + else + { + BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args); + } + } + + static void RunQuickTest() + { + Console.WriteLine("=== Quick AcBinary Test ===\n"); + + try + { + Console.WriteLine("Creating test data..."); + var order = TestDataFactory.CreateBenchmarkOrder( + itemCount: 3, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 5); + Console.WriteLine($"Created order with {order.Items.Count} items"); + + Console.WriteLine("\nTesting JSON serialization..."); + var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + var json = AcJsonSerializer.Serialize(order, jsonOptions); + Console.WriteLine($"JSON size: {json.Length:N0} chars, {Encoding.UTF8.GetByteCount(json):N0} bytes"); + + Console.WriteLine("\nTesting Binary serialization..."); + var binaryOptions = AcBinarySerializerOptions.Default; + var binary = AcBinarySerializer.Serialize(order, binaryOptions); + Console.WriteLine($"Binary size: {binary.Length:N0} bytes"); + + Console.WriteLine("\nTesting Binary deserialization..."); + var result = AcBinaryDeserializer.Deserialize(binary); + Console.WriteLine($"Deserialized order: Id={result?.Id}, Items={result?.Items.Count}"); + + Console.WriteLine("\n=== All tests passed! ==="); + } + catch (Exception ex) + { + Console.WriteLine($"\n!!! ERROR: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + + static void RunMessagePackTest() + { + Console.WriteLine("=== Quick MessagePack Test ===\n"); + + try + { + Console.WriteLine("Creating test data..."); + var order = TestDataFactory.CreateBenchmarkOrder( + itemCount: 2, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 3); + Console.WriteLine($"Created order with {order.Items.Count} items"); + + Console.WriteLine("\nTesting MessagePack serialization..."); + var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + var msgPack = MessagePackSerializer.Serialize(order, msgPackOptions); + Console.WriteLine($"MessagePack size: {msgPack.Length:N0} bytes"); + + Console.WriteLine("\nTesting MessagePack deserialization..."); + var result = MessagePackSerializer.Deserialize(msgPack, msgPackOptions); + Console.WriteLine($"Deserialized order: Id={result?.Id}, Items={result?.Items.Count}"); + + Console.WriteLine("\n=== MessagePack test passed! ==="); + } + catch (Exception ex) + { + Console.WriteLine($"\n!!! ERROR: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } } static void RunSizeComparison() { - Console.WriteLine("=== JSON Size Comparison ===\n"); + Console.WriteLine("=== Size Comparison ===\n"); - var benchmark = new AyCode.Core.Benchmarks.SerializationBenchmarks(); - - // Manually invoke setup - var setupMethod = typeof(AyCode.Core.Benchmarks.SerializationBenchmarks) - .GetMethod("Setup"); - setupMethod?.Invoke(benchmark, null); - - // Get JSON sizes via reflection (private fields) - var newtonsoftJson = typeof(AyCode.Core.Benchmarks.SerializationBenchmarks) - .GetField("_newtonsoftJson", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.GetValue(benchmark) as string; - - var ayCodeJson = typeof(AyCode.Core.Benchmarks.SerializationBenchmarks) - .GetField("_ayCodeJson", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.GetValue(benchmark) as string; - - if (newtonsoftJson != null && ayCodeJson != null) + var order = TestDataFactory.CreateBenchmarkOrder( + itemCount: 3, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 5); + + var binaryWithRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); + var binaryNoRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.WithoutReferenceHandling); + var json = AcJsonSerializer.Serialize(order, AcJsonSerializerOptions.WithoutReferenceHandling()); + var jsonBytes = Encoding.UTF8.GetByteCount(json); + + Console.WriteLine($"| Format | Size (bytes) | vs JSON |"); + Console.WriteLine($"|-----------------|--------------|---------|"); + Console.WriteLine($"| AcBinary | {binaryWithRef.Length,12:N0} | {100.0 * binaryWithRef.Length / jsonBytes,6:F1}% |"); + Console.WriteLine($"| AcBinary(NoRef) | {binaryNoRef.Length,12:N0} | {100.0 * binaryNoRef.Length / jsonBytes,6:F1}% |"); + Console.WriteLine($"| JSON | {jsonBytes,12:N0} | 100.0% |"); + + // Try MessagePack + try { - var newtonsoftBytes = System.Text.Encoding.UTF8.GetByteCount(newtonsoftJson); - var ayCodeBytes = System.Text.Encoding.UTF8.GetByteCount(ayCodeJson); - - Console.WriteLine($"Newtonsoft JSON (no refs):"); - Console.WriteLine($" - Characters: {newtonsoftJson.Length:N0}"); - Console.WriteLine($" - Bytes: {newtonsoftBytes:N0} ({newtonsoftBytes / 1024.0 / 1024.0:F2} MB)"); - Console.WriteLine(); - - Console.WriteLine($"AyCode JSON (with refs):"); - Console.WriteLine($" - Characters: {ayCodeJson.Length:N0}"); - Console.WriteLine($" - Bytes: {ayCodeBytes:N0} ({ayCodeBytes / 1024.0 / 1024.0:F2} MB)"); - Console.WriteLine(); - - var reduction = (1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100; - Console.WriteLine($"Size Reduction: {reduction:F1}%"); - Console.WriteLine($"AyCode is {(reduction > 0 ? "smaller" : "larger")} by {Math.Abs(newtonsoftBytes - ayCodeBytes):N0} bytes"); - - // Count $ref occurrences - var refCount = System.Text.RegularExpressions.Regex.Matches(ayCodeJson, @"\$ref").Count; - var idCount = System.Text.RegularExpressions.Regex.Matches(ayCodeJson, @"\$id").Count; - Console.WriteLine($"\nAyCode $id count: {idCount}"); - Console.WriteLine($"AyCode $ref count: {refCount}"); + var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + var msgPack = MessagePackSerializer.Serialize(order, msgPackOptions); + Console.WriteLine($"| MessagePack | {msgPack.Length,12:N0} | {100.0 * msgPack.Length / jsonBytes,6:F1}% |"); + } + catch (Exception ex) + { + Console.WriteLine($"| MessagePack | FAILED: {ex.Message}"); } } } diff --git a/BenchmarkSuite1/SerializationBenchmarks.cs b/BenchmarkSuite1/SerializationBenchmarks.cs index aabd647..9bc803b 100644 --- a/BenchmarkSuite1/SerializationBenchmarks.cs +++ b/BenchmarkSuite1/SerializationBenchmarks.cs @@ -1,10 +1,9 @@ using AyCode.Core.Extensions; using AyCode.Core.Tests.TestModels; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Reports; -using Newtonsoft.Json; +using BenchmarkDotNet.Jobs; +using MessagePack; +using MessagePack.Resolvers; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -13,327 +12,185 @@ using JsonSerializer = System.Text.Json.JsonSerializer; namespace AyCode.Core.Benchmarks; /// -/// Serialization benchmarks comparing AyCode, Newtonsoft.Json, and System.Text.Json. -/// Tests small, medium, and large data with and without reference handling. +/// Minimal benchmark to test if BenchmarkDotNet works without stack overflow. /// +[ShortRunJob] [MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -[CategoriesColumn] -public class SerializationBenchmarks +public class MinimalBenchmark { - // Test data - small, medium, large - private TestOrder _smallOrder = null!; - private TestOrder _mediumOrder = null!; - private TestOrder _largeOrder = null!; - - // Pre-serialized JSON for deserialization benchmarks - private string _smallAyCodeJson = null!; - private string _smallAyCodeNoRefJson = null!; - private string _smallStjJson = null!; - private string _smallStjNoRefJson = null!; - private string _smallNewtonsoftJson = null!; - private string _mediumAyCodeJson = null!; - private string _mediumAyCodeNoRefJson = null!; - private string _mediumStjJson = null!; - private string _mediumStjNoRefJson = null!; - private string _mediumNewtonsoftJson = null!; - private string _largeAyCodeJson = null!; - private string _largeAyCodeNoRefJson = null!; - private string _largeStjJson = null!; - private string _largeStjNoRefJson = null!; - private string _largeNewtonsoftJson = null!; - - // STJ options - private JsonSerializerOptions _stjWithRefs = null!; - private JsonSerializerOptions _stjNoRefs = null!; - - // AyCode options - private AcJsonSerializerOptions _ayCodeWithRefs = null!; - private AcJsonSerializerOptions _ayCodeNoRefs = null!; - - // Newtonsoft settings - private JsonSerializerSettings _newtonsoftSettings = null!; + private byte[] _data = null!; + private string _json = null!; [GlobalSetup] public void Setup() { - // Small: ~20 objects (1 item × 1 pallet × 2 measurements × 3 points) - _smallOrder = TestDataFactory.CreateBenchmarkOrder( - itemCount: 1, - palletsPerItem: 1, - measurementsPerPallet: 2, - pointsPerMeasurement: 3); + // Use very simple data - no circular references + var simpleData = new { Id = 1, Name = "Test", Value = 42.5 }; + _json = System.Text.Json.JsonSerializer.Serialize(simpleData); + _data = Encoding.UTF8.GetBytes(_json); + Console.WriteLine($"Setup complete. Data size: {_data.Length} bytes"); + } + + [Benchmark] + public int GetLength() => _data.Length; + + [Benchmark] + public string GetJson() => _json; +} + +/// +/// Binary vs JSON benchmark with simple flat objects (no circular references). +/// +[ShortRunJob] +[MemoryDiagnoser] +public class SimpleBinaryBenchmark +{ + private PrimitiveTestClass _testData = null!; + private byte[] _binaryData = null!; + private string _jsonData = null!; + + [GlobalSetup] + public void Setup() + { + _testData = TestDataFactory.CreatePrimitiveTestData(); + _binaryData = AcBinarySerializer.Serialize(_testData); + _jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); - // Medium: ~300 objects (3 items × 2 pallets × 2 measurements × 5 points) - _mediumOrder = TestDataFactory.CreateBenchmarkOrder( - itemCount: 3, + Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars"); + } + + [Benchmark(Description = "Binary Serialize")] + public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData); + + [Benchmark(Description = "JSON Serialize", Baseline = true)] + public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); + + [Benchmark(Description = "Binary Deserialize")] + public PrimitiveTestClass? DeserializeBinary() => AcBinaryDeserializer.Deserialize(_binaryData); + + [Benchmark(Description = "JSON Deserialize")] + public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling()); +} + +/// +/// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue). +/// +[ShortRunJob] +[MemoryDiagnoser] +[RankColumn] +public class ComplexBinaryBenchmark +{ + private TestOrder _testOrder = null!; + private byte[] _acBinaryData = null!; + private string _jsonData = null!; + + private AcBinarySerializerOptions _binaryOptions = null!; + private AcJsonSerializerOptions _jsonOptions = null!; + + [GlobalSetup] + public void Setup() + { + Console.WriteLine("Creating test data..."); + _testOrder = TestDataFactory.CreateBenchmarkOrder( + itemCount: 2, palletsPerItem: 2, measurementsPerPallet: 2, - pointsPerMeasurement: 5); + pointsPerMeasurement: 3); + Console.WriteLine($"Created order with {_testOrder.Items.Count} items"); + + _binaryOptions = AcBinarySerializerOptions.Default; + _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + + Console.WriteLine("Serializing AcBinary..."); + _acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions); + Console.WriteLine($"AcBinary size: {_acBinaryData.Length} bytes"); + + Console.WriteLine("Serializing JSON..."); + _jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions); + Console.WriteLine($"JSON size: {_jsonData.Length} chars"); + + var jsonBytes = Encoding.UTF8.GetByteCount(_jsonData); + Console.WriteLine($"\n=== SIZE COMPARISON ==="); + Console.WriteLine($"AcBinary: {_acBinaryData.Length,8:N0} bytes ({100.0 * _acBinaryData.Length / jsonBytes:F1}%)"); + Console.WriteLine($"JSON: {jsonBytes,8:N0} bytes (100.0%)"); + } + + [Benchmark(Description = "AcBinary Serialize")] + public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(_testOrder, _binaryOptions); + + [Benchmark(Description = "JSON Serialize", Baseline = true)] + public string Serialize_Json() => AcJsonSerializer.Serialize(_testOrder, _jsonOptions); + + [Benchmark(Description = "AcBinary Deserialize")] + public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize(_acBinaryData); + + [Benchmark(Description = "JSON Deserialize")] + public TestOrder? Deserialize_Json() => AcJsonDeserializer.Deserialize(_jsonData, _jsonOptions); +} + +/// +/// Full comparison with MessagePack - separate class to isolate potential issues. +/// +[ShortRunJob] +[MemoryDiagnoser] +[RankColumn] +public class MessagePackComparisonBenchmark +{ + private TestOrder _testOrder = null!; + private byte[] _acBinaryData = null!; + private byte[] _msgPackData = null!; + private string _jsonData = null!; + + private AcBinarySerializerOptions _binaryOptions = null!; + private MessagePackSerializerOptions _msgPackOptions = null!; + private AcJsonSerializerOptions _jsonOptions = null!; + + [GlobalSetup] + public void Setup() + { + Console.WriteLine("Creating test data..."); + _testOrder = TestDataFactory.CreateBenchmarkOrder( + itemCount: 2, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 3); + + _binaryOptions = AcBinarySerializerOptions.Default; + _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + + _acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions); + _jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions); - // Large: ~1500 objects (5 items × 4 pallets × 3 measurements × 5 points) - _largeOrder = TestDataFactory.CreateBenchmarkOrder( - itemCount: 5, - palletsPerItem: 4, - measurementsPerPallet: 3, - pointsPerMeasurement: 5); - - // STJ options with reference handling - _stjWithRefs = new JsonSerializerOptions + // MessagePack serialization in try-catch to see if it fails + try { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = null, - WriteIndented = false, - ReferenceHandler = ReferenceHandler.Preserve, - MaxDepth = 256 - }; - - // STJ options without reference handling - _stjNoRefs = new JsonSerializerOptions + Console.WriteLine("Serializing MessagePack..."); + _msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); + Console.WriteLine($"MessagePack size: {_msgPackData.Length} bytes"); + } + catch (Exception ex) { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = null, - WriteIndented = false, - ReferenceHandler = ReferenceHandler.IgnoreCycles, - MaxDepth = 256 - }; - - // AyCode options - _ayCodeWithRefs = AcJsonSerializerOptions.Default; - _ayCodeNoRefs = AcJsonSerializerOptions.WithoutReferenceHandling(); - - // Newtonsoft settings - _newtonsoftSettings = new JsonSerializerSettings - { - PreserveReferencesHandling = PreserveReferencesHandling.Objects, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.None - }; - - // Pre-serialize for deserialization benchmarks - _smallAyCodeJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeWithRefs); - _smallAyCodeNoRefJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeNoRefs); - _smallStjJson = JsonSerializer.Serialize(_smallOrder, _stjWithRefs); - _smallStjNoRefJson = JsonSerializer.Serialize(_smallOrder, _stjNoRefs); - _smallNewtonsoftJson = JsonConvert.SerializeObject(_smallOrder, _newtonsoftSettings); - - _mediumAyCodeJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs); - _mediumAyCodeNoRefJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs); - _mediumStjJson = JsonSerializer.Serialize(_mediumOrder, _stjWithRefs); - _mediumStjNoRefJson = JsonSerializer.Serialize(_mediumOrder, _stjNoRefs); - _mediumNewtonsoftJson = JsonConvert.SerializeObject(_mediumOrder, _newtonsoftSettings); - - _largeAyCodeJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs); - _largeAyCodeNoRefJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs); - _largeStjJson = JsonSerializer.Serialize(_largeOrder, _stjWithRefs); - _largeStjNoRefJson = JsonSerializer.Serialize(_largeOrder, _stjNoRefs); - _largeNewtonsoftJson = JsonConvert.SerializeObject(_largeOrder, _newtonsoftSettings); - - // Output sizes for comparison - Console.WriteLine("=== JSON Size Comparison ==="); - Console.WriteLine($"Small: AyCode(refs)={_smallAyCodeJson.Length:N0}, AyCode(noRef)={_smallAyCodeNoRefJson.Length:N0}, STJ(refs)={_smallStjJson.Length:N0}, STJ(noRef)={_smallStjNoRefJson.Length:N0}"); - Console.WriteLine($"Medium: AyCode(refs)={_mediumAyCodeJson.Length:N0}, AyCode(noRef)={_mediumAyCodeNoRefJson.Length:N0}, STJ(refs)={_mediumStjJson.Length:N0}, STJ(noRef)={_mediumStjNoRefJson.Length:N0}"); - Console.WriteLine($"Large: AyCode(refs)={_largeAyCodeJson.Length:N0}, AyCode(noRef)={_largeAyCodeNoRefJson.Length:N0}, STJ(refs)={_largeStjJson.Length:N0}, STJ(noRef)={_largeStjNoRefJson.Length:N0}"); + Console.WriteLine($"MessagePack serialization failed: {ex.Message}"); + _msgPackData = Array.Empty(); + } + + var jsonBytes = Encoding.UTF8.GetByteCount(_jsonData); + Console.WriteLine($"\n=== SIZE COMPARISON ==="); + Console.WriteLine($"AcBinary: {_acBinaryData.Length,8:N0} bytes ({100.0 * _acBinaryData.Length / jsonBytes:F1}%)"); + Console.WriteLine($"MessagePack: {_msgPackData.Length,8:N0} bytes ({100.0 * _msgPackData.Length / jsonBytes:F1}%)"); + Console.WriteLine($"JSON: {jsonBytes,8:N0} bytes (100.0%)"); } - #region Serialize Large - With Refs + [Benchmark(Description = "AcBinary Serialize")] + public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(_testOrder, _binaryOptions); - [Benchmark(Description = "AyCode Serialize")] - [BenchmarkCategory("Serialize-Large-WithRefs")] - public string Serialize_Large_AyCode_WithRefs() - => AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs); + [Benchmark(Description = "MessagePack Serialize", Baseline = true)] + public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions); - [Benchmark(Description = "STJ Serialize", Baseline = true)] - [BenchmarkCategory("Serialize-Large-WithRefs")] - public string Serialize_Large_STJ_WithRefs() - => JsonSerializer.Serialize(_largeOrder, _stjWithRefs); + [Benchmark(Description = "AcBinary Deserialize")] + public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize(_acBinaryData); - #endregion - - #region Serialize Large - No Refs - - [Benchmark(Description = "AyCode Serialize")] - [BenchmarkCategory("Serialize-Large-NoRefs")] - public string Serialize_Large_AyCode_NoRefs() - => AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs); - - [Benchmark(Description = "STJ Serialize", Baseline = true)] - [BenchmarkCategory("Serialize-Large-NoRefs")] - public string Serialize_Large_STJ_NoRefs() - => JsonSerializer.Serialize(_largeOrder, _stjNoRefs); - - #endregion - - #region Serialize Medium - With Refs - - [Benchmark(Description = "AyCode Serialize")] - [BenchmarkCategory("Serialize-Medium-WithRefs")] - public string Serialize_Medium_AyCode_WithRefs() - => AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs); - - [Benchmark(Description = "STJ Serialize", Baseline = true)] - [BenchmarkCategory("Serialize-Medium-WithRefs")] - public string Serialize_Medium_STJ_WithRefs() - => JsonSerializer.Serialize(_mediumOrder, _stjWithRefs); - - #endregion - - #region Serialize Medium - No Refs - - [Benchmark(Description = "AyCode Serialize")] - [BenchmarkCategory("Serialize-Medium-NoRefs")] - public string Serialize_Medium_AyCode_NoRefs() - => AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs); - - [Benchmark(Description = "STJ Serialize", Baseline = true)] - [BenchmarkCategory("Serialize-Medium-NoRefs")] - public string Serialize_Medium_STJ_NoRefs() - => JsonSerializer.Serialize(_mediumOrder, _stjNoRefs); - - #endregion - - #region Small Data Deserialization - With Refs - - [Benchmark(Description = "AyCode WithRefs")] - [BenchmarkCategory("Deserialize-Small-WithRefs")] - public TestOrder? Deserialize_Small_AyCode_WithRefs() - => AcJsonDeserializer.Deserialize(_smallAyCodeJson, _ayCodeWithRefs); - - [Benchmark(Description = "STJ WithRefs", Baseline = true)] - [BenchmarkCategory("Deserialize-Small-WithRefs")] - public TestOrder? Deserialize_Small_STJ_WithRefs() - => JsonSerializer.Deserialize(_smallStjJson, _stjWithRefs); - - #endregion - - #region Small Data Deserialization - No Refs - - [Benchmark(Description = "AyCode NoRefs")] - [BenchmarkCategory("Deserialize-Small-NoRefs")] - public TestOrder? Deserialize_Small_AyCode_NoRefs() - => AcJsonDeserializer.Deserialize(_smallAyCodeNoRefJson, _ayCodeNoRefs); - - [Benchmark(Description = "STJ NoRefs", Baseline = true)] - [BenchmarkCategory("Deserialize-Small-NoRefs")] - public TestOrder? Deserialize_Small_STJ_NoRefs() - => JsonSerializer.Deserialize(_smallStjNoRefJson, _stjNoRefs); - - #endregion - - #region Medium Data Deserialization - With Refs - - [Benchmark(Description = "AyCode WithRefs")] - [BenchmarkCategory("Deserialize-Medium-WithRefs")] - public TestOrder? Deserialize_Medium_AyCode_WithRefs() - => AcJsonDeserializer.Deserialize(_mediumAyCodeJson, _ayCodeWithRefs); - - [Benchmark(Description = "STJ WithRefs", Baseline = true)] - [BenchmarkCategory("Deserialize-Medium-WithRefs")] - public TestOrder? Deserialize_Medium_STJ_WithRefs() - => JsonSerializer.Deserialize(_mediumStjJson, _stjWithRefs); - - #endregion - - #region Medium Data Deserialization - No Refs - - [Benchmark(Description = "AyCode NoRefs")] - [BenchmarkCategory("Deserialize-Medium-NoRefs")] - public TestOrder? Deserialize_Medium_AyCode_NoRefs() - => AcJsonDeserializer.Deserialize(_mediumAyCodeNoRefJson, _ayCodeNoRefs); - - [Benchmark(Description = "STJ NoRefs", Baseline = true)] - [BenchmarkCategory("Deserialize-Medium-NoRefs")] - public TestOrder? Deserialize_Medium_STJ_NoRefs() - => JsonSerializer.Deserialize(_mediumStjNoRefJson, _stjNoRefs); - - #endregion - - #region Large Data Deserialization - With Refs - - [Benchmark(Description = "AyCode WithRefs")] - [BenchmarkCategory("Deserialize-Large-WithRefs")] - public TestOrder? Deserialize_Large_AyCode_WithRefs() - => AcJsonDeserializer.Deserialize(_largeAyCodeJson, _ayCodeWithRefs); - - [Benchmark(Description = "STJ WithRefs", Baseline = true)] - [BenchmarkCategory("Deserialize-Large-WithRefs")] - public TestOrder? Deserialize_Large_STJ_WithRefs() - => JsonSerializer.Deserialize(_largeStjJson, _stjWithRefs); - - #endregion - - #region Large Data Deserialization - No Refs - - [Benchmark(Description = "AyCode NoRefs")] - [BenchmarkCategory("Deserialize-Large-NoRefs")] - public TestOrder? Deserialize_Large_AyCode_NoRefs() - => AcJsonDeserializer.Deserialize(_largeAyCodeNoRefJson, _ayCodeNoRefs); - - [Benchmark(Description = "STJ NoRefs", Baseline = true)] - [BenchmarkCategory("Deserialize-Large-NoRefs")] - public TestOrder? Deserialize_Large_STJ_NoRefs() - => JsonSerializer.Deserialize(_largeStjNoRefJson, _stjNoRefs); - - #endregion - - #region Populate Benchmarks - Small - - [Benchmark(Description = "AyCode Populate")] - [BenchmarkCategory("Populate-Small")] - public void Populate_Small_AyCode() - { - var target = new TestOrder(); - AcJsonDeserializer.Populate(_smallAyCodeJson, target); - } - - [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)] - [BenchmarkCategory("Populate-Small")] - public void Populate_Small_Newtonsoft() - { - var target = new TestOrder(); - JsonConvert.PopulateObject(_smallNewtonsoftJson, target, _newtonsoftSettings); - } - - #endregion - - #region Populate Benchmarks - Medium - - [Benchmark(Description = "AyCode Populate")] - [BenchmarkCategory("Populate-Medium")] - public void Populate_Medium_AyCode() - { - var target = new TestOrder(); - AcJsonDeserializer.Populate(_mediumAyCodeJson, target); - } - - [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)] - [BenchmarkCategory("Populate-Medium")] - public void Populate_Medium_Newtonsoft() - { - var target = new TestOrder(); - JsonConvert.PopulateObject(_mediumNewtonsoftJson, target, _newtonsoftSettings); - } - - #endregion - - #region Populate Benchmarks - Large - - [Benchmark(Description = "AyCode Populate")] - [BenchmarkCategory("Populate-Large")] - public void Populate_Large_AyCode() - { - var target = new TestOrder(); - AcJsonDeserializer.Populate(_largeAyCodeJson, target); - } - - [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)] - [BenchmarkCategory("Populate-Large")] - public void Populate_Large_Newtonsoft() - { - var target = new TestOrder(); - JsonConvert.PopulateObject(_largeNewtonsoftJson, target, _newtonsoftSettings); - } - - #endregion + [Benchmark(Description = "MessagePack Deserialize")] + public TestOrder? Deserialize_MsgPack() => MessagePackSerializer.Deserialize(_msgPackData, _msgPackOptions); } \ No newline at end of file