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
This commit is contained in:
Loretta 2025-12-12 20:06:00 +01:00
parent a945db9b09
commit b9e83e2ef8
9 changed files with 1004 additions and 379 deletions

View File

@ -8,9 +8,11 @@
<Import Project="..//AyCode.Core.targets" />
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="MongoDB.Bson" Version="3.5.2" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">

View File

@ -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<object?>(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<int>(binary);
Assert.AreEqual(value, result);
}
[TestMethod]
public void Serialize_Int64_RoundTrip()
{
var value = 123456789012345L;
var binary = AcBinarySerializer.Serialize(value);
var result = AcBinaryDeserializer.Deserialize<long>(binary);
Assert.AreEqual(value, result);
}
[TestMethod]
public void Serialize_Double_RoundTrip()
{
var value = 3.14159265358979;
var binary = AcBinarySerializer.Serialize(value);
var result = AcBinaryDeserializer.Deserialize<double>(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<string>(binary);
Assert.AreEqual(value, result);
}
[TestMethod]
public void Serialize_Boolean_RoundTrip()
{
var trueResult = AcBinaryDeserializer.Deserialize<bool>(AcBinarySerializer.Serialize(true));
var falseResult = AcBinaryDeserializer.Deserialize<bool>(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<DateTime>(binary);
Assert.AreEqual(value, result);
}
[TestMethod]
public void Serialize_Guid_RoundTrip()
{
var value = Guid.NewGuid();
var binary = AcBinarySerializer.Serialize(value);
var result = AcBinaryDeserializer.Deserialize<Guid>(binary);
Assert.AreEqual(value, result);
}
[TestMethod]
public void Serialize_Decimal_RoundTrip()
{
var value = 123456.789012m;
var binary = AcBinarySerializer.Serialize(value);
var result = AcBinaryDeserializer.Deserialize<decimal>(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<TimeSpan>(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<DateTimeOffset>(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<TestSimpleClass>();
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<TestNestedClass>();
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<int> { 1, 2, 3, 4, 5 };
var binary = list.ToBinary();
var result = binary.BinaryTo<List<int>>();
Assert.IsNotNull(result);
CollectionAssert.AreEqual(list, result);
}
[TestMethod]
public void Serialize_ObjectWithList_RoundTrip()
{
var obj = new TestClassWithList
{
Id = 1,
Items = new List<string> { "Item1", "Item2", "Item3" }
};
var binary = obj.ToBinary();
var result = binary.BinaryTo<TestClassWithList>();
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<string, int>
{
["one"] = 1,
["two"] = 2,
["three"] = 3
};
var binary = dict.ToBinary();
var result = binary.BinaryTo<Dictionary<string, int>>();
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<TestClassWithRepeatedStrings>(binaryWithInterning);
var result2 = AcBinaryDeserializer.Deserialize<TestClassWithRepeatedStrings>(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<TestOrder>();
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<string> 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<TestOrder>(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<TestOrder>(binary);
Assert.IsNotNull(result);
Assert.AreEqual(order.Id, result.Id);
}
#endregion
}

View File

@ -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<int>
[JsonNoMergeCollection]
public List<TestOrderItem> 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<int>
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<int>
// 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<int>
// Level 5 collection
public List<TestMeasurementPoint> 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<int>
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; }
}

View File

@ -0,0 +1,6 @@
namespace AyCode.Core.Extensions;
public class AcBinarySerializer
{
}

View File

@ -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
/// </summary>
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<string, PropertySetterInfo> PropertySettersFrozen { get; }
public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects)
public Func<object>? CompiledConstructor { get; }
public DeserializeTypeMetadata(Type type)
@ -1424,15 +1440,39 @@ public static class AcJsonDeserializer
}
var propertySetters = new Dictionary<string, PropertySetterInfo>(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);
}
/// <summary>
/// Try to find property by UTF-8 name using ValueTextEquals (avoids string allocation).
/// </summary>
[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<object, object?>? 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<object, object?> _setter;
private readonly Func<object, object?> _getter;
// Typed setters for common primitive types (avoid boxing)
internal readonly Action<object, int>? _setInt32;
internal readonly Action<object, long>? _setInt64;
internal readonly Action<object, double>? _setDouble;
internal readonly Action<object, bool>? _setBool;
internal readonly Action<object, decimal>? _setDecimal;
internal readonly Action<object, float>? _setSingle;
internal readonly Action<object, DateTime>? _setDateTime;
internal readonly Action<object, Guid>? _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<int>(declaringType, prop);
else if (ReferenceEquals(PropertyType, LongType))
_setInt64 = CreateTypedSetter<long>(declaringType, prop);
else if (ReferenceEquals(PropertyType, DoubleType))
_setDouble = CreateTypedSetter<double>(declaringType, prop);
else if (ReferenceEquals(PropertyType, BoolType))
_setBool = CreateTypedSetter<bool>(declaringType, prop);
else if (ReferenceEquals(PropertyType, DecimalType))
_setDecimal = CreateTypedSetter<decimal>(declaringType, prop);
else if (ReferenceEquals(PropertyType, FloatType))
_setSingle = CreateTypedSetter<float>(declaringType, prop);
else if (ReferenceEquals(PropertyType, DateTimeType))
_setDateTime = CreateTypedSetter<DateTime>(declaringType, prop);
else if (ReferenceEquals(PropertyType, GuidType))
_setGuid = CreateTypedSetter<Guid>(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<Func<object, object?>>(boxed, objParam).Compile();
}
private static Action<object, T> CreateTypedSetter<T>(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<Action<object, T>>(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);
/// <summary>
/// Read and set value directly from Utf8JsonReader, avoiding boxing for primitives.
/// Returns true if value was set, false if it needs fallback to SetValue.
/// </summary>
[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

View File

@ -444,6 +444,65 @@ public static class SerializeObjectExtensions
public static T MessagePackTo<T>(this byte[] message, MessagePackSerializerOptions options)
=> MessagePackSerializer.Deserialize<T>(message, options);
#region Binary Serialization Extension Methods
/// <summary>
/// Serialize object to binary byte array with default options.
/// Significantly faster than JSON, especially for large data in WASM.
/// </summary>
public static byte[] ToBinary<T>(this T source)
=> AcBinarySerializer.Serialize(source);
/// <summary>
/// Serialize object to binary byte array with specified options.
/// </summary>
public static byte[] ToBinary<T>(this T source, AcBinarySerializerOptions options)
=> AcBinarySerializer.Serialize(source, options);
/// <summary>
/// Deserialize binary data to object with default options.
/// </summary>
public static T? BinaryTo<T>(this byte[] data)
=> AcBinaryDeserializer.Deserialize<T>(data);
/// <summary>
/// Deserialize binary data to object.
/// </summary>
public static T? BinaryTo<T>(this ReadOnlySpan<byte> data)
=> AcBinaryDeserializer.Deserialize<T>(data);
/// <summary>
/// Deserialize binary data to specified type.
/// </summary>
public static object? BinaryTo(this byte[] data, Type targetType)
=> AcBinaryDeserializer.Deserialize(data.AsSpan(), targetType);
/// <summary>
/// Populate existing object from binary data.
/// </summary>
public static void BinaryTo<T>(this byte[] data, T target) where T : class
=> AcBinaryDeserializer.Populate(data, target);
/// <summary>
/// Populate existing object from binary data with merge semantics for IId collections.
/// </summary>
public static void BinaryToMerge<T>(this byte[] data, T target) where T : class
=> AcBinaryDeserializer.PopulateMerge(data.AsSpan(), target);
/// <summary>
/// Clone object via binary serialization (faster than JSON clone).
/// </summary>
public static T? BinaryCloneTo<T>(this T source) where T : class
=> source?.ToBinary().BinaryTo<T>();
/// <summary>
/// Copy object properties to target via binary serialization.
/// </summary>
public static void BinaryCopyTo<T>(this T source, T target) where T : class
=> source?.ToBinary().BinaryTo(target);
#endregion
}
public class IgnoreAndRenamePropertySerializerContractResolver : DefaultContractResolver

View File

@ -9,7 +9,9 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.3.36726.2" />
<PackageReference Include="MongoDB.Bson" Version="3.5.2" />
</ItemGroup>
<ItemGroup>

View File

@ -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<MinimalBenchmark>();
return;
}
if (args.Length > 0 && args[0] == "--simple")
{
BenchmarkRunner.Run<SimpleBinaryBenchmark>();
return;
}
if (args.Length > 0 && args[0] == "--complex")
{
BenchmarkRunner.Run<ComplexBinaryBenchmark>();
return;
}
if (args.Length > 0 && args[0] == "--msgpack")
{
BenchmarkRunner.Run<MessagePackComparisonBenchmark>();
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<TestOrder>(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<TestOrder>(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}");
}
}
}

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
[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;
}
/// <summary>
/// Binary vs JSON benchmark with simple flat objects (no circular references).
/// </summary>
[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<PrimitiveTestClass>(_binaryData);
[Benchmark(Description = "JSON Deserialize")]
public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling());
}
/// <summary>
/// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue).
/// </summary>
[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<TestOrder>(_acBinaryData);
[Benchmark(Description = "JSON Deserialize")]
public TestOrder? Deserialize_Json() => AcJsonDeserializer.Deserialize<TestOrder>(_jsonData, _jsonOptions);
}
/// <summary>
/// Full comparison with MessagePack - separate class to isolate potential issues.
/// </summary>
[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<byte>();
}
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<TestOrder>(_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<TestOrder>(_smallAyCodeJson, _ayCodeWithRefs);
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
[BenchmarkCategory("Deserialize-Small-WithRefs")]
public TestOrder? Deserialize_Small_STJ_WithRefs()
=> JsonSerializer.Deserialize<TestOrder>(_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<TestOrder>(_smallAyCodeNoRefJson, _ayCodeNoRefs);
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
[BenchmarkCategory("Deserialize-Small-NoRefs")]
public TestOrder? Deserialize_Small_STJ_NoRefs()
=> JsonSerializer.Deserialize<TestOrder>(_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<TestOrder>(_mediumAyCodeJson, _ayCodeWithRefs);
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
[BenchmarkCategory("Deserialize-Medium-WithRefs")]
public TestOrder? Deserialize_Medium_STJ_WithRefs()
=> JsonSerializer.Deserialize<TestOrder>(_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<TestOrder>(_mediumAyCodeNoRefJson, _ayCodeNoRefs);
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
[BenchmarkCategory("Deserialize-Medium-NoRefs")]
public TestOrder? Deserialize_Medium_STJ_NoRefs()
=> JsonSerializer.Deserialize<TestOrder>(_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<TestOrder>(_largeAyCodeJson, _ayCodeWithRefs);
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
[BenchmarkCategory("Deserialize-Large-WithRefs")]
public TestOrder? Deserialize_Large_STJ_WithRefs()
=> JsonSerializer.Deserialize<TestOrder>(_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<TestOrder>(_largeAyCodeNoRefJson, _ayCodeNoRefs);
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
[BenchmarkCategory("Deserialize-Large-NoRefs")]
public TestOrder? Deserialize_Large_STJ_NoRefs()
=> JsonSerializer.Deserialize<TestOrder>(_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<TestOrder>(_msgPackData, _msgPackOptions);
}