diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs index 356f8fb..62317d5 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs @@ -230,10 +230,7 @@ public class AcBinarySerializerDiagnosticTests /// /// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false) - /// This test verifies what happens when: - /// 1. Metadata header registers ALL properties including StockTakingItems - /// 2. Body SKIPS StockTakingItems because it's null - /// 3. Deserializer reads the body and must correctly map indices + /// This test verifies that property-index-based serialization correctly handles null properties. /// [TestMethod] public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices() @@ -262,20 +259,13 @@ public class AcBinarySerializerDiagnosticTests var marker = binary[pos++]; Console.WriteLine($"Marker: 0x{marker:X2}"); - // Read property count from metadata header - if ((marker & 0x10) != 0) // HasMetadata flag + // Skip any header data (strings interning, etc.) + // New format uses PropertyIndex directly - no metadata header with property names + + // Find Object marker (0x19) + while (pos < binary.Length && binary[pos] != 0x19) { - var propCount = binary[pos++]; - Console.WriteLine($"\n=== METADATA HEADER ==="); - Console.WriteLine($"Property count in header: {propCount}"); - - for (int i = 0; i < propCount; i++) - { - var strLen = binary[pos++]; - var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen); - pos += strLen; - Console.WriteLine($" Header property [{i}]: '{propName}'"); - } + pos++; } Console.WriteLine($"\n=== BODY (starts at position {pos}) ==="); @@ -284,6 +274,7 @@ public class AcBinarySerializerDiagnosticTests var bodyStart = pos; var objectMarker = binary[pos++]; Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)"); + Assert.AreEqual(0x19, objectMarker, "Object marker should be 0x19"); // Read ref ID (if reference handling is enabled) // VarInt: if top bit is set, continue reading @@ -306,11 +297,11 @@ public class AcBinarySerializerDiagnosticTests var bodyPropCount = binary[pos++]; Console.WriteLine($"Property count in body: {bodyPropCount}"); - Console.WriteLine($"\n=== BODY PROPERTIES ==="); + Console.WriteLine($"\n=== BODY PROPERTIES (PropertyIndex format) ==="); for (int i = 0; i < bodyPropCount && pos < binary.Length; i++) { - var propIndex = binary[pos++]; - Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}"); + var propIndex = binary[pos++]; // This is now PropertyIndex (alphabetical order) + Console.WriteLine($" Body property [{i}]: PropertyIndex={propIndex}"); // Skip the value (simplified - just log) var valueType = binary[pos]; @@ -342,16 +333,6 @@ public class AcBinarySerializerDiagnosticTests } } - // Find where 0xD6 (Creator = 6) appears in the body - Console.WriteLine($"\n=== 0xD6 OCCURRENCES ==="); - for (int i = bodyStart; i < binary.Length; i++) - { - if (binary[i] == 0xD6) - { - Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}"); - } - } - // Deserialize and verify var result = binary.BinaryTo(); diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index a2b30fd..92550e2 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -2,51 +2,44 @@ using System.Collections; using System.Collections.Frozen; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; -using static AyCode.Core.Helpers.JsonUtilities; +using AyCode.Core.Serializers; namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { - internal sealed class BinaryDeserializeTypeMetadata : TypeMetadataBase + internal sealed class BinaryDeserializeTypeMetadata : BinaryTypeMetadataBase { private readonly FrozenDictionary _properties; + /// + /// Properties array ordered alphabetically by name for index-based lookup. + /// This matches the serializer's ordering, enabling O(1) array access. + /// public BinaryPropertySetterInfo[] PropertiesArray { get; } - - /// - /// Whether this type implements IId interface. - /// - public bool IsIId { get; } - - /// - /// Compiled getter for the Id property (if IsIId is true). - /// - public Func? IdGetter { get; } public BinaryDeserializeTypeMetadata(Type type) : base(type) { - PropertiesArray = GetSerializableProperties(type, requiresRead: true, requiresWrite: true) - .Where(static p => p.GetMethod is { IsPublic: true } && p.SetMethod is { IsPublic: true }) - .Select(static p => new BinaryPropertySetterInfo(p, p.DeclaringType!)) - .ToArray(); + var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true); + + PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length]; + for (var i = 0; i < orderedProperties.Length; i++) + { + PropertiesArray[i] = new BinaryPropertySetterInfo(orderedProperties[i], type); + } _properties = PropertiesArray.Length == 0 ? FrozenDictionary.Empty : PropertiesArray.ToFrozenDictionary(static p => p.Name, static p => p, StringComparer.Ordinal); - - // Check if type implements IId - var (isIId, _) = GetIdInfo(type); - IsIId = isIId; - if (isIId) - { - var idProp = type.GetProperty("Id"); - if (idProp != null) - IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp); - } } + /// + /// Gets property by index (O(1) array access). Used for index-based deserialization. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BinaryPropertySetterInfo? GetPropertyByIndex(int index) + => (uint)index < (uint)PropertiesArray.Length ? PropertiesArray[index] : null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propertyInfo) => _properties.TryGetValue(name, out propertyInfo); @@ -87,7 +80,7 @@ public static partial class AcBinaryDeserializer Type? elementType, Type? elementIdType, Func? elementIdGetter) - : base(CreateDummyProperty(), typeof(object)) + : base(CreateDummyProperty(), typeof(DummyClass)) { _isManualConstruction = true; _manualName = name; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs new file mode 100644 index 0000000..dabe3df --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -0,0 +1,497 @@ +using System.Collections; +using System.Runtime.CompilerServices; +using AyCode.Core.Helpers; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers.Binaries; + +public static partial class AcBinaryDeserializer +{ + #region Populate Object Methods + + private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + // Skip ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, target); + } + } + + PopulateObject(ref context, target, metadata, depth); + } + + private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth) + { + var propertyCount = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + var targetType = target.GetType(); + var targetTypeName = targetType.Name; + + for (int i = 0; i < propertyCount; i++) + { + var propertyIndexStartPosition = context.Position; + + // Read property index directly - no string lookup needed! + // PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms + var propIndex = (int)context.ReadVarUInt(); + + // O(1) array lookup instead of dictionary lookup + var propInfo = metadata.GetPropertyByIndex(propIndex); + + if (propInfo == null) + { + // Skip unknown property (schema evolution - new property on sender side) + SkipValue(ref context); + continue; + } + + // OPTIMIZATION: Reuse existing nested objects instead of creating new ones + var peekCode = context.PeekByte(); + + // Handle nested complex objects - reuse existing if available + if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + context.ReadByte(); // consume Object marker + PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth); + continue; + } + } + + // Handle collections - reuse existing collection and populate items + if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection is IList existingList) + { + context.ReadByte(); // consume Array marker + PopulateListOptimized(ref context, existingList, propInfo, nextDepth); + continue; + } + } + + // Default: read value and set (for primitives, strings, null cases) + var positionBeforeRead = context.Position; + try + { + // OPTIMIZATION: Use typed setters for primitives to avoid boxing + if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) + continue; + + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + catch (InvalidCastException ex) + { + // Add context about which property and what byte code was at the read position + throw new AcBinaryDeserializationException( + $"Type mismatch for property '{propInfo.Name}' (index {i}/{propertyCount}, propIndex={propIndex}) on '{targetTypeName}'. " + + $"Expected type: '{propInfo.PropertyType.FullName}'. " + + $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + + $"Position before read: {positionBeforeRead}, current: {context.Position}. " + + $"Depth: {depth}. " + + $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + + $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " + + $"Error: {ex.Message}", + positionBeforeRead, + propInfo.PropertyType, + ex); + } + } + } + + /// + /// Populate nested object, reusing existing object and recursively updating properties. + /// + private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + // Handle ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, target); + } + } + + PopulateObject(ref context, target, metadata, depth); + } + + private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + // Skip ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, target); + } + } + + var propertyCount = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + for (int i = 0; i < propertyCount; i++) + { + // Read property index directly - O(1) array lookup + var propIndex = (int)context.ReadVarUInt(); + var propInfo = metadata.GetPropertyByIndex(propIndex); + + if (propInfo == null) + { + SkipValue(ref context); + continue; + } + + var peekCode = context.PeekByte(); + + // Handle IId collection merge + if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection is IList existingList) + { + context.ReadByte(); // consume Array marker + MergeIIdCollection(ref context, existingList, propInfo, depth); + continue; + } + } + + // Handle nested object merge + if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + context.ReadByte(); // consume Object marker + PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth); + continue; + } + } + + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + } + + #endregion + + #region Populate List Methods + + private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) + { + var elementType = GetCollectionElementType(listType) ?? typeof(object); + + var acObservable = targetList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + targetList.Clear(); + + var count = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + // ChainMode: Get IId info for element type + var isIId = false; + Type? idType = null; + Func? idGetter = null; + + if (context.IsChainMode) + { + var idInfo = GetIdInfo(elementType); + isIId = idInfo.IsId; + idType = idInfo.IdType; + if (isIId && idType != null) + { + var idProp = elementType.GetProperty("Id"); + if (idProp != null) + idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp); + } + } + + for (int i = 0; i < count; i++) + { + var value = ReadValue(ref context, elementType, nextDepth); + + // ChainMode: Check if we already have this IId object + if (context.IsChainMode && value != null && idGetter != null && idType != null) + { + var id = idGetter(value); + if (id != null && !IsDefaultValue(id, idType)) + { + if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj)) + { + // Use existing object instead of new one + targetList.Add(existingObj); + continue; + } + } + } + + targetList.Add(value); + } + } + finally + { + acObservable?.EndUpdate(); + } + } + + private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) + { + var elementType = GetCollectionElementType(listType) ?? typeof(object); + var (isId, idType) = GetIdInfo(elementType); + + if (!isId || idType == null) + { + // No IId, just replace + PopulateList(ref context, targetList, listType, depth); + return; + } + + // IId merge logic + var idProp = elementType.GetProperty("Id"); + if (idProp == null) + { + PopulateList(ref context, targetList, listType, depth); + return; + } + + var idGetter = CreateCompiledGetter(elementType, idProp); + var propInfo = new BinaryPropertySetterInfo( + "Items", elementType, true, elementType, idType, idGetter); + + MergeIIdCollection(ref context, targetList, propInfo, depth); + } + + /// + /// Optimized list populate that reuses existing items when possible. + /// + private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) + { + var elementType = propInfo.ElementType ?? typeof(object); + var count = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + var acObservable = existingList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + var existingCount = existingList.Count; + var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null; + + for (int i = 0; i < count; i++) + { + var peekCode = context.PeekByte(); + + // If we have an existing item at this index and the incoming is an object, reuse it + if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null) + { + var existingItem = existingList[i]; + if (existingItem != null) + { + context.ReadByte(); // consume Object marker + + // Handle ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, existingItem); + } + } + + PopulateObject(ref context, existingItem, elementMetadata, nextDepth); + continue; + } + } + + // Read new value + var value = ReadValue(ref context, elementType, nextDepth); + + if (i < existingCount) + { + // Replace existing item + existingList[i] = value; + } + else + { + // Add new item + existingList.Add(value); + } + } + + // Remove extra items if new list is shorter + while (existingList.Count > count) + { + existingList.RemoveAt(existingList.Count - 1); + } + } + finally + { + acObservable?.EndUpdate(); + } + } + + #endregion + + #region Merge Methods + + /// + /// Optimized IId collection merge with capacity hints and reduced boxing. + /// + private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) + { + var elementType = propInfo.ElementType!; + var idGetter = propInfo.ElementIdGetter!; + var idType = propInfo.ElementIdType!; + + var count = existingList.Count; + var acObservable = existingList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + // Build lookup dictionary with capacity hint + Dictionary? existingById = null; + if (count > 0) + { + existingById = new Dictionary(count); + for (var idx = 0; idx < count; idx++) + { + var item = existingList[idx]; + if (item != null) + { + var id = idGetter(item); + if (id != null && !IsDefaultValue(id, idType)) + existingById[id] = item; + } + } + } + + var arrayCount = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + var elementMetadata = GetTypeMetadata(elementType); + + // Track which IDs we see in source (for orphan removal) + HashSet? sourceIds = context.RemoveOrphanedItems && existingById != null + ? new HashSet(arrayCount) + : null; + + for (int i = 0; i < arrayCount; i++) + { + var itemCode = context.PeekByte(); + if (itemCode != BinaryTypeCode.Object) + { + var value = ReadValue(ref context, elementType, nextDepth); + if (value != null) + existingList.Add(value); + continue; + } + + context.ReadByte(); // consume Object marker + var newItem = CreateInstance(elementType, elementMetadata); + if (newItem == null) continue; + + // Read ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + context.RegisterObject(refId, newItem); + } + + PopulateObject(ref context, newItem, elementMetadata, nextDepth); + + var itemId = idGetter(newItem); + if (itemId != null && !IsDefaultValue(itemId, idType)) + { + // Track this ID as seen in source + sourceIds?.Add(itemId); + + if (existingById != null && existingById.TryGetValue(itemId, out var existingItem)) + { + // Copy properties to existing item + CopyProperties(newItem, existingItem, elementMetadata); + continue; + } + } + + existingList.Add(newItem); + } + + // Remove orphaned items (items in destination but not in source) + if (context.RemoveOrphanedItems && existingById != null && sourceIds != null) + { + // Find items to remove (those not in sourceIds) + var itemsToRemove = new List(); + foreach (var kvp in existingById) + { + if (!sourceIds.Contains(kvp.Key)) + { + itemsToRemove.Add(kvp.Value); + } + } + + // Remove orphaned items + foreach (var item in itemsToRemove) + { + existingList.Remove(item); + } + } + } + finally + { + acObservable?.EndUpdate(); + } + } + + private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata) + { + var props = metadata.PropertiesArray; + for (var i = 0; i < props.Length; i++) + { + var prop = props[i]; + var value = prop.GetValue(source); + if (value != null) + prop.SetValue(target, value); + } + } + + /// + /// Determines if a type is a complex type (not primitive, string, or simple value type). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsComplexType(Type type) + { + if (type.IsPrimitive) return false; + if (ReferenceEquals(type, StringType)) return false; + if (type.IsEnum) return false; + if (ReferenceEquals(type, GuidType)) return false; + if (ReferenceEquals(type, DateTimeType)) return false; + if (ReferenceEquals(type, DecimalType)) return false; + if (ReferenceEquals(type, TimeSpanType)) return false; + if (ReferenceEquals(type, DateTimeOffsetType)) return false; + if (Nullable.GetUnderlyingType(type) != null) return false; + return true; + } + + #endregion +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 80db678..f23c933 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -43,6 +43,28 @@ public static partial class AcBinaryDeserializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); private static readonly ConcurrentDictionary TypeConversionCache = new(); + + /// + /// ThreadLocal cache for hot path type metadata lookup. + /// Avoids ConcurrentDictionary overhead for frequently accessed types. + /// + [ThreadStatic] + private static Dictionary? t_deserializeTypeMetadataLocalCache; + + /// + /// ThreadLocal cache for type conversion info. + /// + [ThreadStatic] + private static Dictionary? t_typeConversionLocalCache; + + /// + /// Maximum local cache size before clearing. + /// Clear() is preferred over new Dictionary() because: + /// 1. Reuses already allocated internal array (no new allocation) + /// 2. Reduces GC pressure + /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity + /// + private const int MaxLocalCacheSize = 64; // Type dispatch table for fast ReadValue private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth); @@ -819,13 +841,44 @@ public static partial class AcBinaryDeserializer }; } + /// + /// Gets type conversion info with ThreadLocal caching. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TypeConversionInfo GetConversionInfo(Type targetType) - => TypeConversionCache.GetOrAdd(targetType, static type => + { + // Fast path: check ThreadLocal cache first + var localCache = t_typeConversionLocalCache; + if (localCache != null && localCache.TryGetValue(targetType, out var cached)) + { + return cached; + } + + // Slow path + return GetConversionInfoSlow(targetType); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static TypeConversionInfo GetConversionInfoSlow(Type targetType) + { + var info = TypeConversionCache.GetOrAdd(targetType, static type => { var underlying = Nullable.GetUnderlyingType(type) ?? type; return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum); }); + + // Get or create local cache + var localCache = t_typeConversionLocalCache ??= new Dictionary(); + + // Clear when full - reuses internal array, better than new Dictionary() + if (localCache.Count >= MaxLocalCacheSize) + { + localCache.Clear(); + } + + localCache[targetType] = info; + return info; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType) @@ -919,463 +972,6 @@ public static partial class AcBinaryDeserializer return instance; } - private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) - { - var metadata = GetTypeMetadata(targetType); - - // Skip ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(refId, target); - } - } - - PopulateObject(ref context, target, metadata, depth); - } - - private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth) - { - var propertyCount = (int)context.ReadVarUInt(); - var nextDepth = depth + 1; - var targetType = target.GetType(); - var targetTypeName = targetType.Name; - - for (int i = 0; i < propertyCount; i++) - { - var propertyNameStartPosition = context.Position; - string propertyName; - int propIndex = -1; - if (context.HasMetadata) - { - propIndex = (int)context.ReadVarUInt(); - propertyName = context.GetPropertyName(propIndex); - } - else - { - var typeCode = context.ReadByte(); - if (typeCode == BinaryTypeCode.String) - { - propertyName = ReadAndInternString(ref context); - } - else if (typeCode == BinaryTypeCode.StringInterned) - { - propertyName = context.GetInternedString((int)context.ReadVarUInt()); - } - else if (typeCode == BinaryTypeCode.StringEmpty) - { - propertyName = string.Empty; - } - else if (typeCode == BinaryTypeCode.StringInternNew) - { - propertyName = ReadAndRegisterInternedString(ref context); - } - else if (BinaryTypeCode.IsFixStr(typeCode)) - { - // FixStr: short string with length encoded in type code - var length = BinaryTypeCode.DecodeFixStrLength(typeCode); - propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length); - } - else - { - throw new AcBinaryDeserializationException( - $"Expected string for property name, got: {typeCode} (0x{typeCode:X2}) at position {propertyNameStartPosition}. " + - $"Target: {targetTypeName}, PropertyIndex: {i}/{propertyCount}, Depth: {depth}", - context.Position, targetType); - } - } - - if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null) - { - // Skip unknown property - SkipValue(ref context); - continue; - } - - // OPTIMIZATION: Reuse existing nested objects instead of creating new ones - var peekCode = context.PeekByte(); - - // Handle nested complex objects - reuse existing if available - if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) - { - var existingObj = propInfo.GetValue(target); - if (existingObj != null) - { - context.ReadByte(); // consume Object marker - PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth); - continue; - } - } - - // Handle collections - reuse existing collection and populate items - if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) - { - var existingCollection = propInfo.GetValue(target); - if (existingCollection is IList existingList) - { - context.ReadByte(); // consume Array marker - PopulateListOptimized(ref context, existingList, propInfo, nextDepth); - continue; - } - } - - // Default: read value and set (for primitives, strings, null cases) - var positionBeforeRead = context.Position; - try - { - // OPTIMIZATION: Use typed setters for primitives to avoid boxing - if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) - continue; - - var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); - propInfo.SetValue(target, value); - } - catch (InvalidCastException ex) - { - // Add context about which property and what byte code was at the read position - throw new AcBinaryDeserializationException( - $"Type mismatch for property '{propertyName}' (index {i}/{propertyCount}, headerIndex={propIndex}) on '{targetTypeName}'. " + - $"Expected type: '{propInfo.PropertyType.FullName}'. " + - $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + - $"Position before read: {positionBeforeRead}, current: {context.Position}. " + - $"Depth: {depth}. " + - $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + - $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " + - $"Error: {ex.Message}", - positionBeforeRead, - propInfo.PropertyType, - ex); - } - } - } - - /// - /// Populate nested object, reusing existing object and recursively updating properties. - /// - private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth) - { - var metadata = GetTypeMetadata(targetType); - - // Handle ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(refId, target); - } - } - - PopulateObject(ref context, target, metadata, depth); - } - - private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth) - { - var metadata = GetTypeMetadata(targetType); - - // Skip ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(refId, target); - } - } - - var propertyCount = (int)context.ReadVarUInt(); - var nextDepth = depth + 1; - - for (int i = 0; i < propertyCount; i++) - { - string propertyName; - if (context.HasMetadata) - { - var propIndex = (int)context.ReadVarUInt(); - propertyName = context.GetPropertyName(propIndex); - } - else - { - var typeCode = context.ReadByte(); - if (typeCode == BinaryTypeCode.String) - { - propertyName = ReadAndInternString(ref context); - } - else if (typeCode == BinaryTypeCode.StringInterned) - { - propertyName = context.GetInternedString((int)context.ReadVarUInt()); - } - else if (typeCode == BinaryTypeCode.StringEmpty) - { - propertyName = string.Empty; - } - else if (typeCode == BinaryTypeCode.StringInternNew) - { - propertyName = ReadAndRegisterInternedString(ref context); - } - else if (BinaryTypeCode.IsFixStr(typeCode)) - { - // FixStr: short string with length encoded in type code - var length = BinaryTypeCode.DecodeFixStrLength(typeCode); - propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length); - } - else - { - throw new AcBinaryDeserializationException( - $"Expected string for property name, got: {typeCode} (0x{typeCode:X2})", - context.Position, targetType); - } - } - - if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null) - { - SkipValue(ref context); - continue; - } - - var peekCode = context.PeekByte(); - - // Handle IId collection merge - if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array) - { - var existingCollection = propInfo.GetValue(target); - if (existingCollection is IList existingList) - { - context.ReadByte(); // consume Array marker - MergeIIdCollection(ref context, existingList, propInfo, depth); - continue; - } - } - - // Handle nested object merge - if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) - { - var existingObj = propInfo.GetValue(target); - if (existingObj != null) - { - context.ReadByte(); // consume Object marker - PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth); - continue; - } - } - - var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); - propInfo.SetValue(target, value); - } - } - - /// - /// Optimized IId collection merge with capacity hints and reduced boxing. - /// - private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) - { - var elementType = propInfo.ElementType!; - var idGetter = propInfo.ElementIdGetter!; - var idType = propInfo.ElementIdType!; - - var count = existingList.Count; - var acObservable = existingList as IAcObservableCollection; - acObservable?.BeginUpdate(); - - try - { - // Build lookup dictionary with capacity hint - Dictionary? existingById = null; - if (count > 0) - { - existingById = new Dictionary(count); - for (var idx = 0; idx < count; idx++) - { - var item = existingList[idx]; - if (item != null) - { - var id = idGetter(item); - if (id != null && !IsDefaultValue(id, idType)) - existingById[id] = item; - } - } - } - - var arrayCount = (int)context.ReadVarUInt(); - var nextDepth = depth + 1; - var elementMetadata = GetTypeMetadata(elementType); - - // Track which IDs we see in source (for orphan removal) - HashSet? sourceIds = context.RemoveOrphanedItems && existingById != null - ? new HashSet(arrayCount) - : null; - - for (int i = 0; i < arrayCount; i++) - { - var itemCode = context.PeekByte(); - if (itemCode != BinaryTypeCode.Object) - { - var value = ReadValue(ref context, elementType, nextDepth); - if (value != null) - existingList.Add(value); - continue; - } - - context.ReadByte(); // consume Object marker - var newItem = CreateInstance(elementType, elementMetadata); - if (newItem == null) continue; - - // Read ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - context.RegisterObject(refId, newItem); - } - - PopulateObject(ref context, newItem, elementMetadata, nextDepth); - - var itemId = idGetter(newItem); - if (itemId != null && !IsDefaultValue(itemId, idType)) - { - // Track this ID as seen in source - sourceIds?.Add(itemId); - - if (existingById != null && existingById.TryGetValue(itemId, out var existingItem)) - { - // Copy properties to existing item - CopyProperties(newItem, existingItem, elementMetadata); - continue; - } - } - - existingList.Add(newItem); - } - - // Remove orphaned items (items in destination but not in source) - if (context.RemoveOrphanedItems && existingById != null && sourceIds != null) - { - // Find items to remove (those not in sourceIds) - var itemsToRemove = new List(); - foreach (var kvp in existingById) - { - if (!sourceIds.Contains(kvp.Key)) - { - itemsToRemove.Add(kvp.Value); - } - } - - // Remove orphaned items - foreach (var item in itemsToRemove) - { - existingList.Remove(item); - } - } - } - finally - { - acObservable?.EndUpdate(); - } - } - - private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata) - { - var props = metadata.PropertiesArray; - for (var i = 0; i < props.Length; i++) - { - var prop = props[i]; - var value = prop.GetValue(source); - if (value != null) - prop.SetValue(target, value); - } - } - - /// - /// Determines if a type is a complex type (not primitive, string, or simple value type). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsComplexType(Type type) - { - if (type.IsPrimitive) return false; - if (ReferenceEquals(type, StringType)) return false; - if (type.IsEnum) return false; - if (ReferenceEquals(type, GuidType)) return false; - if (ReferenceEquals(type, DateTimeType)) return false; - if (ReferenceEquals(type, DecimalType)) return false; - if (ReferenceEquals(type, TimeSpanType)) return false; - if (ReferenceEquals(type, DateTimeOffsetType)) return false; - if (Nullable.GetUnderlyingType(type) != null) return false; - return true; - } - - /// - /// Optimized list populate that reuses existing items when possible. - /// - private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) - { - var elementType = propInfo.ElementType ?? typeof(object); - var count = (int)context.ReadVarUInt(); - var nextDepth = depth + 1; - - var acObservable = existingList as IAcObservableCollection; - acObservable?.BeginUpdate(); - - try - { - var existingCount = existingList.Count; - var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null; - - for (int i = 0; i < count; i++) - { - var peekCode = context.PeekByte(); - - // If we have an existing item at this index and the incoming is an object, reuse it - if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null) - { - var existingItem = existingList[i]; - if (existingItem != null) - { - context.ReadByte(); // consume Object marker - - // Handle ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(refId, existingItem); - } - } - - PopulateObject(ref context, existingItem, elementMetadata, nextDepth); - continue; - } - } - - // Read new value - var value = ReadValue(ref context, elementType, nextDepth); - - if (i < existingCount) - { - // Replace existing item - existingList[i] = value; - } - else - { - // Add new item - existingList.Add(value); - } - } - - // Remove extra items if new list is shorter - while (existingList.Count > count) - { - existingList.RemoveAt(existingList.Count - 1); - } - } - finally - { - acObservable?.EndUpdate(); - } - } - #endregion #region Array Reading @@ -1557,93 +1153,6 @@ public static partial class AcBinaryDeserializer return null; } - private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) - { - var elementType = GetCollectionElementType(listType) ?? typeof(object); - - var acObservable = targetList as IAcObservableCollection; - acObservable?.BeginUpdate(); - - try - { - targetList.Clear(); - - var count = (int)context.ReadVarUInt(); - var nextDepth = depth + 1; - - // ChainMode: Get IId info for element type - var isIId = false; - Type? idType = null; - Func? idGetter = null; - - if (context.IsChainMode) - { - var idInfo = GetIdInfo(elementType); - isIId = idInfo.IsId; - idType = idInfo.IdType; - if (isIId && idType != null) - { - var idProp = elementType.GetProperty("Id"); - if (idProp != null) - idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp); - } - } - - for (int i = 0; i < count; i++) - { - var value = ReadValue(ref context, elementType, nextDepth); - - // ChainMode: Check if we already have this IId object - if (context.IsChainMode && value != null && idGetter != null && idType != null) - { - var id = idGetter(value); - if (id != null && !IsDefaultValue(id, idType)) - { - if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj)) - { - // Use existing object instead of new one - targetList.Add(existingObj); - continue; - } - } - } - - targetList.Add(value); - } - } - finally - { - acObservable?.EndUpdate(); - } - } - - private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) - { - var elementType = GetCollectionElementType(listType) ?? typeof(object); - var (isId, idType) = GetIdInfo(elementType); - - if (!isId || idType == null) - { - // No IId, just replace - PopulateList(ref context, targetList, listType, depth); - return; - } - - // IId merge logic - var idProp = elementType.GetProperty("Id"); - if (idProp == null) - { - PopulateList(ref context, targetList, listType, depth); - return; - } - - var idGetter = CreateCompiledGetter(elementType, idProp); - var propInfo = new BinaryPropertySetterInfo( - "Items", elementType, true, elementType, idType, idGetter); - - MergeIIdCollection(ref context, targetList, propInfo, depth); - } - #endregion #region Dictionary Reading @@ -1830,38 +1339,8 @@ public static partial class AcBinaryDeserializer var propCount = (int)context.ReadVarUInt(); for (int i = 0; i < propCount; i++) { - // Skip property name - but must register in intern table! - if (context.HasMetadata) - { - context.ReadVarUInt(); - } - else - { - var nameCode = context.ReadByte(); - if (nameCode == BinaryTypeCode.String) - { - // CRITICAL FIX: Must register property name in intern table even when skipping! - SkipAndInternString(ref context); - } - else if (nameCode == BinaryTypeCode.StringInterned) - { - // Just read the index, no registration needed - context.ReadVarUInt(); - } - else if (nameCode == BinaryTypeCode.StringInternNew) - { - // New interned string - must register even when skipping! - SkipAndRegisterInternedString(ref context); - } - else if (BinaryTypeCode.IsFixStr(nameCode)) - { - // FixStr: short string, just skip the bytes - var length = BinaryTypeCode.DecodeFixStrLength(nameCode); - if (length > 0) - context.Skip(length); - } - // StringEmpty doesn't need any action - } + // Skip property index (VarUInt) + context.ReadVarUInt(); // Skip value SkipValue(ref context); @@ -1891,9 +1370,40 @@ public static partial class AcBinaryDeserializer #region Type Metadata + /// + /// Gets type metadata with ThreadLocal caching for hot path optimization. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) - => TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); + { + // Fast path: check ThreadLocal cache first + var localCache = t_deserializeTypeMetadataLocalCache; + if (localCache != null && localCache.TryGetValue(type, out var cached)) + { + return cached; + } + + // Slow path: get from global cache and populate local cache + return GetTypeMetadataSlow(type); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static BinaryDeserializeTypeMetadata GetTypeMetadataSlow(Type type) + { + var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); + + // Get or create local cache + var localCache = t_deserializeTypeMetadataLocalCache ??= new Dictionary(); + + // Clear when full - reuses internal array, better than new Dictionary() + if (localCache.Count >= MaxLocalCacheSize) + { + localCache.Clear(); + } + + localCache[type] = metadata; + return metadata; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 64b83f0..4d7223b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -138,6 +138,7 @@ public static partial class AcBinarySerializer _propertyNameList?.Clear(); _internedStringList?.Clear(); + _internedStringUtf8?.Clear(); // Reset cached property indices ResetCachedPropertyIndices(); @@ -186,11 +187,17 @@ public static partial class AcBinarySerializer #region String Interning + /// + /// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections. + /// + private List? _internedStringUtf8; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int RegisterInternedString(string value) { _internedStrings ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal); _internedStringList ??= new List(InitialInternCapacity); + _internedStringUtf8 ??= new List(InitialInternCapacity); // Fast path: check bloom filter first var hash = GetStringHash(value); @@ -200,6 +207,8 @@ public static partial class AcBinarySerializer var newIndex = _internedStringList.Count; _internedStrings[value] = newIndex; _internedStringList.Add(value); + // Cache UTF8 bytes immediately + _internedStringUtf8.Add(GetUtf8BytesCached(value)); BloomFilterAdd(hash); return newIndex; } @@ -213,10 +222,29 @@ public static partial class AcBinarySerializer index = _internedStringList.Count; _internedStringList.Add(value); + // Cache UTF8 bytes immediately + _internedStringUtf8.Add(GetUtf8BytesCached(value)); BloomFilterAdd(hash); return index; } + /// + /// Get UTF8 bytes for a string, optimized for ASCII strings. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte[] GetUtf8BytesCached(string value) + { + // Fast path for ASCII strings - direct char to byte conversion + if (System.Text.Ascii.IsValid(value)) + { + var bytes = new byte[value.Length]; + System.Text.Ascii.FromUtf16(value.AsSpan(), bytes, out _); + return bytes; + } + // Standard path for multi-byte UTF8 + return Utf8NoBom.GetBytes(value); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetStringHash(string value) { @@ -989,14 +1017,27 @@ public static partial class AcBinarySerializer var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 }; var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 }; - // Calculate actual header size first + // Fast path: no header payload needed + if (!hasPropertyNames && !hasInternTable) + { + // Write header flags only + byte flags = BinaryTypeCode.HeaderFlagsBase; + if (UseReferenceHandling) + flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling; + + _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; + _buffer[_headerPosition + 1] = flags; + return; + } + + // Calculate actual header size using cached UTF8 bytes var actualSize = 0; if (hasPropertyNames) { actualSize += GetVarUIntSize((uint)_propertyNameList!.Count); foreach (var name in _propertyNameList) { - var byteCount = Utf8NoBom.GetByteCount(name); + var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name); actualSize += GetVarUIntSize((uint)byteCount) + byteCount; } } @@ -1004,10 +1045,11 @@ public static partial class AcBinarySerializer if (hasInternTable) { actualSize += GetVarUIntSize((uint)_internedStringList!.Count); - foreach (var value in _internedStringList) + // Use cached UTF8 byte lengths + for (var i = 0; i < _internedStringUtf8!.Count; i++) { - var byteCount = Utf8NoBom.GetByteCount(value); - actualSize += GetVarUIntSize((uint)byteCount) + byteCount; + var utf8Bytes = _internedStringUtf8[i]; + actualSize += GetVarUIntSize((uint)utf8Bytes.Length) + utf8Bytes.Length; } } @@ -1031,7 +1073,7 @@ public static partial class AcBinarySerializer } } - // Write header payload directly to buffer (no ArrayBufferWriter allocation) + // Write header payload directly to buffer using cached UTF8 bytes var headerPos = _headerPosition + 2; if (hasPropertyNames) @@ -1039,30 +1081,54 @@ public static partial class AcBinarySerializer headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count); foreach (var name in _propertyNameList) { - headerPos = WriteStringAt(headerPos, name); + headerPos = WriteStringAtOptimized(headerPos, name); } } if (hasInternTable) { headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count); - foreach (var value in _internedStringList) + // Use cached UTF8 bytes - no re-encoding needed! + for (var i = 0; i < _internedStringUtf8!.Count; i++) { - headerPos = WriteStringAt(headerPos, value); + var utf8Bytes = _internedStringUtf8[i]; + headerPos = WriteVarUIntAt(headerPos, (uint)utf8Bytes.Length); + utf8Bytes.CopyTo(_buffer.AsSpan(headerPos)); + headerPos += utf8Bytes.Length; } } // Write header flags - byte flags = BinaryTypeCode.HeaderFlagsBase; + byte flags2 = BinaryTypeCode.HeaderFlagsBase; if (hasPropertyNames) - flags |= BinaryTypeCode.HeaderFlag_Metadata; + flags2 |= BinaryTypeCode.HeaderFlag_Metadata; if (UseReferenceHandling) - flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling; + flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling; if (hasInternTable) - flags |= BinaryTypeCode.HeaderFlag_StringInternTable; + flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable; _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; - _buffer[_headerPosition + 1] = flags; + _buffer[_headerPosition + 1] = flags2; + } + + /// + /// Writes UTF8 string at specific position, optimized for ASCII strings. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int WriteStringAtOptimized(int pos, string value) + { + // Fast path for ASCII strings + if (System.Text.Ascii.IsValid(value)) + { + pos = WriteVarUIntAt(pos, (uint)value.Length); + System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _); + return pos + value.Length; + } + // Standard path for multi-byte UTF8 + var byteCount = Utf8NoBom.GetByteCount(value); + pos = WriteVarUIntAt(pos, (uint)byteCount); + Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount)); + return pos + byteCount; } /// @@ -1080,20 +1146,6 @@ public static partial class AcBinarySerializer return pos; } - /// - /// Writes UTF8 string at specific position (length-prefixed) and returns new position. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int WriteStringAt(int pos, string value) - { - var byteCount = Utf8NoBom.GetByteCount(value); - pos = WriteVarUIntAt(pos, (uint)byteCount); - Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount)); - return pos + byteCount; - } - - // Remove old methods: WriteHeaderVarUInt, WriteHeaderString (no longer needed) - #endregion #region Reference Handling diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs index a1b5605..6d4f4b7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs @@ -1,23 +1,27 @@ using System; -using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; +using AyCode.Core.Serializers; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinarySerializer { - internal sealed class BinaryTypeMetadata : TypeMetadataBase + internal sealed class BinaryTypeMetadata : BinaryTypeMetadataBase { public BinaryPropertyAccessor[] Properties { get; } public BinaryTypeMetadata(Type type) : base(type) { - Properties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false) - .Select(p => new BinaryPropertyAccessor(p, type)) - .ToArray(); + var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false); + + Properties = new BinaryPropertyAccessor[orderedProperties.Length]; + for (var i = 0; i < orderedProperties.Length; i++) + { + var accessor = new BinaryPropertyAccessor(orderedProperties[i], type); + accessor.PropertyIndex = i; + Properties[i] = accessor; + } } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 9f1161b..88a1306 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -26,6 +26,22 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinarySerializer { private static readonly ConcurrentDictionary TypeMetadataCache = new(); + + /// + /// ThreadLocal cache for hot path type metadata lookup. + /// Avoids ConcurrentDictionary overhead for frequently accessed types. + /// + [ThreadStatic] + private static Dictionary? t_typeMetadataLocalCache; + + /// + /// Maximum local cache size before clearing. + /// Clear() is preferred over new Dictionary() because: + /// 1. Reuses already allocated internal array (no new allocation) + /// 2. Reduces GC pressure + /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity + /// + private const int MaxLocalCacheSize = 64; // Pre-computed UTF8 encoder for string operations private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); @@ -190,10 +206,8 @@ public static partial class AcBinarySerializer ScanReferences(value, context, 0); } - if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType)) - { - RegisterMetadataForType(runtimeType, context); - } + // Property index-based serialization - no metadata registration needed! + // PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms // Estimate and reserve header space to avoid body shift later var estimatedHeaderSize = context.EstimateHeaderPayloadSize(); @@ -387,7 +401,7 @@ public static partial class AcBinarySerializer /// /// Optimized primitive writer using TypeCode dispatch. - /// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata. + /// Avoids Nullable.GetUnderlyingType in hot path by using cached type info. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context) @@ -446,13 +460,6 @@ public static partial class AcBinarySerializer return true; } - // Handle nullable types - var underlyingType = Nullable.GetUnderlyingType(type); - if (underlyingType != null) - { - return TryWritePrimitive(value, underlyingType, context); - } - // Handle special types by reference comparison (faster than type equality) if (ReferenceEquals(type, GuidType)) { @@ -475,6 +482,15 @@ public static partial class AcBinarySerializer return true; } + // Handle nullable types - use cached check instead of GetUnderlyingType + // For nullable, value is already unwrapped when boxed, so we can use value.GetType() + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // When boxed, nullable value types are unwrapped to their underlying type + // So we can just call TryWritePrimitive with the actual runtime type + return TryWritePrimitive(value, value.GetType(), context); + } + return false; } @@ -607,10 +623,10 @@ public static partial class AcBinarySerializer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteEnum(object value, BinarySerializationContext context - ) + private static void WriteEnum(object value, BinarySerializationContext context) { - var intValue = Convert.ToInt32(value); + // Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead + var intValue = GetEnumAsInt32Fast(value); if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) { context.WriteByte(BinaryTypeCode.Enum); @@ -622,6 +638,33 @@ public static partial class AcBinarySerializer context.WriteVarInt(intValue); } + /// + /// Fast enum to int conversion avoiding Convert.ToInt32 overhead. + /// Handles all common enum underlying types. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetEnumAsInt32Fast(object enumValue) + { + var type = enumValue.GetType(); + var underlyingType = type.GetEnumUnderlyingType(); + + if (ReferenceEquals(underlyingType, IntType)) + return (int)enumValue; + if (ReferenceEquals(underlyingType, typeof(byte))) + return (byte)enumValue; + if (ReferenceEquals(underlyingType, typeof(sbyte))) + return (sbyte)enumValue; + if (ReferenceEquals(underlyingType, typeof(short))) + return (short)enumValue; + if (ReferenceEquals(underlyingType, typeof(ushort))) + return (ushort)enumValue; + if (ReferenceEquals(underlyingType, typeof(uint))) + return unchecked((int)(uint)enumValue); + + // Fallback for rare cases (long, ulong) + return Convert.ToInt32(enumValue); + } + /// /// Optimized string writer with FixStr for short strings. /// Uses stackalloc for small strings to avoid allocations. @@ -706,18 +749,10 @@ public static partial class AcBinarySerializer if (IsPropertyDefaultOrNull(value, prop)) continue; - // Write property name/index - if (context.UseMetadata) - { - var propIndex = prop.CachedPropertyNameIndex >= 0 - ? prop.CachedPropertyNameIndex - : context.GetPropertyNameIndex(prop.Name); - context.WriteVarUInt((uint)propIndex); - } - else - { - context.WritePreencodedPropertyName(prop.NameUtf8); - } + // Write property index directly - no dictionary lookup needed! + // PropertyIndex is deterministic (based on alphabetical ordering) + // and consistent across all platforms (WASM, iOS, Android, Windows) + context.WriteVarUInt((uint)prop.PropertyIndex); // Write property value WritePropertyValue(value, prop, context, nextDepth); @@ -1059,9 +1094,41 @@ public static partial class AcBinarySerializer return null; } + /// + /// Gets type metadata with ThreadLocal caching for hot path optimization. + /// Falls back to ConcurrentDictionary for cache misses. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BinaryTypeMetadata GetTypeMetadata(Type type) - => TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); + { + // Fast path: check ThreadLocal cache first + var localCache = t_typeMetadataLocalCache; + if (localCache != null && localCache.TryGetValue(type, out var cached)) + { + return cached; + } + + // Slow path: get from global cache and populate local cache + return GetTypeMetadataSlow(type); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static BinaryTypeMetadata GetTypeMetadataSlow(Type type) + { + var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); + + // Get or create local cache + var localCache = t_typeMetadataLocalCache ??= new Dictionary(); + + // Clear when full - reuses internal array, better than new Dictionary() + if (localCache.Count >= MaxLocalCacheSize) + { + localCache.Clear(); + } + + localCache[type] = metadata; + return metadata; + } // Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs index db580f0..2f60f88 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -16,6 +16,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase /// internal int CachedPropertyNameIndex = -1; + /// + /// Deterministic property index based on alphabetical ordering of property names. + /// This is computed once during metadata creation and is consistent across all platforms. + /// Used for fast serialization without dictionary lookup. + /// + public int PropertyIndex { get; internal set; } = -1; + /// /// The accessor type for fast typed getter dispatch. /// diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs new file mode 100644 index 0000000..c85bdbf --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Base class for binary serializer/deserializer type metadata. +/// Provides common functionality for IId detection and property ordering. +/// Properties are ordered alphabetically by name for deterministic serialization across platforms. +/// +public abstract class BinaryTypeMetadataBase : TypeMetadataBase +{ + /// + /// Whether this type implements IId interface. + /// + public bool IsIId { get; } + + /// + /// The Id property type if IsIId is true. + /// + public Type? IdType { get; } + + /// + /// Compiled getter for the Id property (if IsIId is true). + /// + public Func? IdGetter { get; } + + protected BinaryTypeMetadataBase(Type type) : base(type) + { + var (isIId, idType) = GetIdInfo(type); + IsIId = isIId; + IdType = idType; + + if (isIId) + { + var idProp = type.GetProperty("Id"); + if (idProp != null) + IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp); + } + } +} diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index b525169..ac31576 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Reflection; using static AyCode.Core.Helpers.JsonUtilities; @@ -9,6 +10,13 @@ namespace AyCode.Core.Serializers; /// public abstract class TypeMetadataBase { + /// + /// Cache for serializable properties per type. + /// Key: (Type, requiresWrite) - since requiresRead is always true in practice. + /// Value: PropertyInfo[] ordered alphabetically by name for deterministic serialization. + /// + private static readonly ConcurrentDictionary<(Type, bool), PropertyInfo[]> OrderedPropertiesCache = new(); + /// /// Compiled parameterless constructor for the type. /// Null if the type is abstract or has no parameterless constructor. @@ -21,21 +29,29 @@ public abstract class TypeMetadataBase } /// - /// Gets the properties that should be serialized for a type. + /// Gets serializable properties ordered alphabetically by name. + /// Results are cached per type and requiresWrite combination. + /// Ordering ensures deterministic serialization across all platforms (Windows, WASM, iOS, Android). /// /// The type to analyze. - /// Whether the property must be readable. - /// Whether the property must be writable. - /// Enumerable of properties that meet the criteria. - protected static IEnumerable GetSerializableProperties( + /// Whether the property must be readable (always true in practice). + /// Whether the property must be writable (true for deserialization). + /// Array of properties ordered by name (cached). + protected static PropertyInfo[] GetSerializableProperties( Type type, bool requiresRead = true, bool requiresWrite = false) { - return type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => (!requiresRead || p.CanRead) && - (!requiresWrite || p.CanWrite) && - p.GetIndexParameters().Length == 0 && - !HasJsonIgnoreAttribute(p)); + return OrderedPropertiesCache.GetOrAdd((type, requiresWrite), static key => + { + var (t, needsWrite) = key; + return t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && + (!needsWrite || p.CanWrite) && + p.GetIndexParameters().Length == 0 && + !HasJsonIgnoreAttribute(p)) + .OrderBy(static p => p.Name, StringComparer.Ordinal) + .ToArray(); + }); } }