diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b418660..5afb158 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -29,7 +29,8 @@ "Bash(dotnet exec vstest:*)", "Bash(dotnet new:*)", "Bash(Remove-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Toons\\\\AcToonSerializer.RelationshipDetection.cs\")", - "Bash(find:*)" + "Bash(find:*)", + "Bash(dir:*)" ] } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 2c8e9d9..c7188d9 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -17,7 +17,6 @@ public static partial class AcBinaryDeserializer { private readonly ReadOnlySpan _buffer; private int _position; - private List? _propertyNames; private Dictionary? _stringCache; // Position-based interning: flat int[] for cache-friendly access @@ -74,7 +73,6 @@ public static partial class AcBinaryDeserializer { _buffer = data; _position = 0; - _propertyNames = null; _stringCache = null; // Position-based interning fields (shared: string + IId) @@ -151,17 +149,7 @@ public static partial class AcBinaryDeserializer HasMetadata = hasPropertyTable; - if (hasPropertyTable) - { - var propertyCount = (int)ReadVarUInt(); - _propertyNames = new List(propertyCount); - for (var i = 0; i < propertyCount; i++) - { - _propertyNames.Add(ReadHeaderString()); - } - } - - // Footer-based: read intern indices from footer (string + IId) + // Footer-based: read intern indices and metadata from footer if (hasInternFooter && footerPosition > 0) { ReadFooterIndices(footerPosition); @@ -182,7 +170,7 @@ public static partial class AcBinaryDeserializer // Seek to footer _position = footerPosition; - // Read dup count + // Read dup count (intern entries) var dupCount = (int)ReadVarUInt(); if (dupCount == 0) { @@ -207,10 +195,44 @@ public static partial class AcBinaryDeserializer _nextDupPosition = _dupData[0]; } + // Read UseMetadata footer section (per-type property hashes) + if (HasMetadata && _position < _buffer.Length) + { + ReadMetadataFooter(); + } + // Seek back to data position _position = dataPosition; } + /// + /// UseMetadata footer olvasása: entry-k flat array-be. + /// Formátum: [entryCount (VarUInt)] + /// entry-nként: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]... + /// Entry 0 = root, 1+ = nested object-ek. + /// + private void ReadMetadataFooter() + { + var entryCount = (int)ReadVarUInt(); + for (var i = 0; i < entryCount; i++) + { + EnsureAvailable(4); + var propNameHash = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _buffer[_position])); + _position += 4; + + var propCount = (int)ReadVarUInt(); + var hashes = new int[propCount]; + for (var p = 0; p < propCount; p++) + { + EnsureAvailable(4); + hashes[p] = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _buffer[_position])); + _position += 4; + } + + ContextClass.RegisterFooterEntry(i, propNameHash, hashes); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte ReadByte() => ReadByteInternal(); @@ -551,6 +573,19 @@ public static partial class AcBinaryDeserializer return h.ToHashCode(); } + /// + /// Reads a raw 4-byte int32 (little-endian) without type code. + /// Used for reading type name hashes in UseMetadata mode. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt32Raw() + { + EnsureAvailable(4); + var value = Unsafe.ReadUnaligned(ref Unsafe.AsRef(in _buffer[_position])); + _position += 4; + return value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Skip(int count) { @@ -619,17 +654,6 @@ public static partial class AcBinaryDeserializer return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string GetPropertyName(int index) - { - if (_propertyNames == null || (uint)index >= (uint)_propertyNames.Count) - { - throw new AcBinaryDeserializationException($"Invalid property metadata index '{index}'.", _position); - } - - return _propertyNames[index]; - } - private void EnsureAvailable(int length) { if (_position > _buffer.Length - length) @@ -638,11 +662,5 @@ public static partial class AcBinaryDeserializer } } - private string ReadHeaderString() - { - var byteLength = (int)ReadVarUInt(); - return ReadStringUtf8(byteLength); - } - } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs index 7a4be57..b08bc76 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -1,5 +1,7 @@ using System; using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace AyCode.Core.Serializers.Binaries; @@ -29,6 +31,23 @@ public static partial class AcBinaryDeserializer // Small arrays: keep across calls. Large arrays: return to pool in Clear(). private const int SmallArrayThreshold = 256; + /// + /// Footer metadata bejegyzések flat array-ben. + /// Index = footer entry index (0 = root, 1+ = nested). + /// Minden entry tartalmazza a source propNameHash-t és a source property hash-eket. + /// + private MetadataFooterEntry[]? _footerEntries; + private int _footerEntryCount; + + /// + /// Egy footer bejegyzés a deserializer számára. + /// + internal struct MetadataFooterEntry + { + public int PropNameHash; // source típus propNameHash (FNV-1a) + public int[] PropertyHashes; // source property hash-ek sorrendben + } + public BinaryDeserializationContextClass() { } @@ -84,10 +103,79 @@ public static partial class AcBinaryDeserializer _lastInternCacheUsed = count; } + /// + /// Footer bejegyzés regisztrálása a metadata footer olvasásakor. + /// Az entryIndex a footer-ben lévő sorrend (0 = root). + /// + public void RegisterFooterEntry(int entryIndex, int propNameHash, int[] propertyHashes) + { + // Szükség esetén növeljük a tömböt + if (_footerEntries == null || _footerEntries.Length <= entryIndex) + { + var newSize = Math.Max(entryIndex + 1, 8); + var newArray = new MetadataFooterEntry[newSize]; + if (_footerEntries != null) + Array.Copy(_footerEntries, newArray, _footerEntryCount); + _footerEntries = newArray; + } + + _footerEntries[entryIndex] = new MetadataFooterEntry + { + PropNameHash = propNameHash, + PropertyHashes = propertyHashes + }; + if (entryIndex >= _footerEntryCount) + _footerEntryCount = entryIndex + 1; + } + + /// + /// Footer entry lekérése index alapján. + /// A root mindig a 0. elem. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref MetadataFooterEntry GetFooterEntry(int entryIndex) + { + return ref _footerEntries![entryIndex]; + } + + /// + /// Footer entry-k száma. + /// + public int FooterEntryCount => _footerEntryCount; + + /// + /// Cachemap felépítése: source property hash-ek → target PropertySetter?[] mapping. + /// Null entry = source property-nek nincs megfelelője a target-ben → skip. + /// + public BinaryPropertySetterInfo?[] BuildCacheMap(int footerEntryIndex, BinaryDeserializeTypeMetadata targetMetadata) + { + ref var entry = ref _footerEntries![footerEntryIndex]; + var sourceHashes = entry.PropertyHashes; + var targetProperties = targetMetadata.PropertiesArray; + + var mapping = new BinaryPropertySetterInfo?[sourceHashes.Length]; + for (var i = 0; i < sourceHashes.Length; i++) + { + var sourceHash = sourceHashes[i]; + for (var j = 0; j < targetProperties.Length; j++) + { + if (targetProperties[j].PropertyNameHash == sourceHash) + { + mapping[i] = targetProperties[j]; + break; + } + } + // ha nincs match → mapping[i] marad null → skip + } + return mapping; + } + public override void Clear() { base.Clear(); + _footerEntryCount = 0; + // Intern cache: clear GC roots, return large arrays to pool if (_pooledInternCache != null) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index 746c28c..923428d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -16,6 +16,14 @@ public static partial class AcBinaryDeserializer /// public BinaryPropertySetterInfo[] PropertiesArray { get; } + /// + /// UseMetadata cachemap: source property index → target PropertySetter. + /// Null entry = source property-nek nincs megfelelője a target-ben → skip. + /// Lazy módon épül az első találkozáskor, utána újrahasználódik. + /// A wrapper mindig új vagy pool-ból jön (clear-elve), tehát nem kell invalidálni. + /// + internal BinaryPropertySetterInfo?[]? CacheMap; + /// /// True if this type has a Source Generator generated deserializer available. /// Note: Due to ref struct limitations, the generated code cannot be called via delegates. diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index d71b36c..44c71f2 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -35,6 +35,13 @@ public static partial class AcBinaryDeserializer private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) { var wrapper = context.ContextClass.GetWrapper(targetType); + + // UseMetadata: Populate esetén a root = footer entry 0 + if (context.HasMetadata && wrapper.Metadata.CacheMap == null && context.ContextClass.FooterEntryCount > 0) + { + wrapper.Metadata.CacheMap = context.ContextClass.BuildCacheMap(0, wrapper.Metadata); + } + PopulateObjectCore(ref context, target, wrapper, depth, skipDefaultWrite: false); } @@ -54,14 +61,139 @@ public static partial class AcBinaryDeserializer } /// - /// Populates object properties only - does NOT read hashcode prefix. - /// Used by ReadObject where hashcode is already handled separately. + /// Property-k olvasása. UseMetadata módban cachemap-alapú (marker-vezérelt), + /// egyébként index-alapú (mint eddig). + /// A cachemap a metadata.CacheMap-ben van, a ReadObject/ReadObjectWithMetadata építi fel. /// private static void PopulateObjectProperties( - ref BinaryDeserializationContext context, - object target, - BinaryDeserializeTypeMetadata metadata, - int depth, + ref BinaryDeserializationContext context, + object target, + BinaryDeserializeTypeMetadata metadata, + int depth, + bool skipDefaultWrite) + { + if (context.HasMetadata && metadata.CacheMap != null) + { + // UseMetadata: marker-vezérelt olvasás, cachemap-ből vesszük a setter-t + PopulateWithCacheMap(ref context, target, metadata.CacheMap, metadata, depth, skipDefaultWrite); + } + else + { + PopulateObjectPropertiesIndexed(ref context, target, metadata, depth, skipDefaultWrite); + } + } + + /// + /// UseMetadata olvasás cachemap-pel. Marker-vezérelt, pontosan úgy olvas mint az index-alapú, + /// de a property index-ből a cachemap adja vissza a setter-t. Ha null → skip. + /// + private static void PopulateWithCacheMap( + ref BinaryDeserializationContext context, + object target, + BinaryPropertySetterInfo?[] cacheMap, + BinaryDeserializeTypeMetadata metadata, + int depth, + bool skipDefaultWrite) + { + var nextDepth = depth + 1; + var isMergeMode = context.IsMergeMode; + + for (int i = 0; i < cacheMap.Length; i++) + { + var propInfo = cacheMap[i]; + var peekCode = context.PeekByte(); + + // Nincs megfelelő target property → skip + if (propInfo == null) + { + SkipValue(ref context, metadata); + continue; + } + + // PropertySkip marker - default/null érték + if (peekCode == BinaryTypeCode.PropertySkip) + { + context.ReadByte(); + if (!skipDefaultWrite) + SetPropertyToDefault(target, propInfo); + continue; + } + + // Null érték + if (peekCode == BinaryTypeCode.Null) + { + context.ReadByte(); + propInfo.SetValue(target, null); + continue; + } + + // Kollekció kezelés + if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection is IList existingList) + { + context.ReadByte(); + if (isMergeMode && propInfo.IsIIdCollection) + MergeIIdCollection(ref context, existingList, propInfo, nextDepth); + else + PopulateListOptimized(ref context, existingList, propInfo, nextDepth); + continue; + } + } + + // Nested object kezelés - Object és ObjectWithMetadata marker + if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + // A ReadValue kezeli mindkét markert (Object és ObjectWithMetadata) + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + if (value != null) + { + CopyProperties(value, existingObj, context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata); + } + continue; + } + } + + // Default: érték olvasás és beállítás + var positionBeforeRead = context.Position; + try + { + if (propInfo.AccessorType != PropertyAccessorType.Object && + TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) + continue; + + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + catch (InvalidCastException ex) + { + var targetType = target.GetType(); + throw new AcBinaryDeserializationException( + $"Típushibás property '{propInfo.Name}' (cachemap index {i}), target: '{targetType.Name}'. " + + $"Várt típus: '{propInfo.PropertyType.FullName}'. " + + $"PeekCode: {peekCode} (0x{peekCode:X2}). " + + $"Pozíció: {positionBeforeRead} → {context.Position}. Depth: {depth}. " + + $"Hiba: {ex.Message}", + positionBeforeRead, + propInfo.PropertyType, + ex); + } + } + } + + /// + /// Index-based property populate (original logic, used when UseMetadata is off). + /// Properties are read in order matching the target type's property array. + /// + private static void PopulateObjectPropertiesIndexed( + ref BinaryDeserializationContext context, + object target, + BinaryDeserializeTypeMetadata metadata, + int depth, bool skipDefaultWrite) { var properties = metadata.PropertiesArray; @@ -103,7 +235,7 @@ public static partial class AcBinaryDeserializer if (existingCollection is IList existingList) { context.ReadByte(); // consume Array marker - + // Merge mode with IId collection: use merge logic if (isMergeMode && propInfo.IsIIdCollection) { @@ -119,15 +251,18 @@ public static partial class AcBinaryDeserializer } // Handle nested complex objects - reuse existing if available - if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) + if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) { var existingObj = propInfo.GetValue(target); if (existingObj != null) { - context.ReadByte(); // consume Object marker - - var nestedWrapper = context.ContextClass.GetWrapper(propInfo.PropertyType); - PopulateObjectCore(ref context, existingObj, nestedWrapper, nextDepth, skipDefaultWrite: false); + // ReadValue kezeli mindkét markert + var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth); + if (nestedValue != null) + { + var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata; + CopyProperties(nestedValue, existingObj, nestedMeta); + } continue; } } @@ -138,7 +273,7 @@ public static partial class AcBinaryDeserializer { // Use typed setters for primitives to avoid boxing // Skip method call for Object/String/Collection types - they can't use typed setters - if (propInfo.AccessorType != PropertyAccessorType.Object && + if (propInfo.AccessorType != PropertyAccessorType.Object && TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) continue; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 3242161..433c53c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -72,6 +72,7 @@ public static partial class AcBinaryDeserializer RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe()); RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type)); RegisterReader(BinaryTypeCode.Object, ReadObject); + RegisterReader(BinaryTypeCode.ObjectWithMetadata, ReadObjectWithMetadata); RegisterReader(BinaryTypeCode.ObjectRef, ReadObjectRef); RegisterReader(BinaryTypeCode.Array, ReadArray); RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary); @@ -924,6 +925,21 @@ public static partial class AcBinaryDeserializer /// /// Reads an ObjectRef - looks up previously registered object from shared intern cache. /// Wire format: [ObjectRef][VarUInt cacheIndex] + /// + /// IMPORTANT / BUG FIX TODO: + /// Cross-type deserialization esetén ha egy objektum SkipObject-tel lett átugorva + /// (mert a target típuson nincs megfelelő property), de később ObjectRef hivatkozik rá, + /// az intern cache-ben nincs objektum → exception. + /// + /// Megoldás: SkipObject során a stream pozíciót kell beírni a cache-be (boxolt int). + /// Itt az "is int" check-kel meg kell különböztetni: + /// - Ha a cached value valódi objektum → visszaadjuk (jelenlegi működés) + /// - Ha a cached value boxolt int (stream pozíció) → reposition + ReadObject a target type-ra, + /// majd az eredményt visszaírjuk a cache-be (hogy a következő ref ne olvasson újra) + /// + /// Az "is int" check biztonságos, mert az intern cache-be csak string és class instance + /// kerülhet — egyik sem matchel az "is int"-re. A check az ObjectRef path-ban van, + /// ami ritka eset (2+ referencia), tehát nem lassítja a hot path-ot. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth) @@ -933,11 +949,9 @@ public static partial class AcBinaryDeserializer } /// - /// Reads an Object. - /// Wire format: - /// - IId types: [Object][props 0-tól...] - Id a props-ban, populate után register - /// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode előre olvasva - /// - Ref=Off: [Object][props 0-tól...] - semmi extra + /// Root object olvasása. + /// Wire format: [Object][props 0-tól...] - Id a props-ban, nincs extra + /// UseMetadata esetén a root = footer entry 0 (nincs footer index a body-ban). /// private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth) { @@ -953,36 +967,87 @@ public static partial class AcBinaryDeserializer var wrapper = context.ContextClass.GetWrapper(targetType); var metadata = wrapper.Metadata; - // Wire format: [Object][props...] - no hashcode prefix, no Id prefix - // Position-based footer handles all reference tracking var instance = CreateInstance(targetType, metadata); if (instance == null) return null; - // Register in shared intern cache BEFORE populate (position-based sequential check) - // Instance is populated in-place, so registering early is safe. - // Must happen before reading inner content (strings/objects) that may also need registration. if (context.ContextClass.UseTypeReferenceHandling(metadata)) { context.RegisterInternedValue(instance, streamPosition); } + // UseMetadata: root = footer entry 0, cachemap felépítése ha még nincs + if (context.HasMetadata && context.ContextClass.FooterEntryCount > 0) + { + if (metadata.CacheMap == null) + metadata.CacheMap = context.ContextClass.BuildCacheMap(0, metadata); + } + PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); - // ChainMode: Register/retrieve from chain tracker (separate from reference handling) + // ChainMode kezelés if (context.IsChainMode && metadata.IsIId && metadata.IdType != null) { var id = GetIdBoxed(instance, metadata); if (id != null && !IsDefaultValue(id, metadata.IdType)) { - // Check if we already have this object if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj)) { - // Update existing object's properties and return it CopyProperties(instance, existingObj!, metadata); return existingObj; } + context.ChainTracker.TryRegisterIIdObject(instance); + } + } - // Register this new object + return instance; + } + + /// + /// Nested object olvasása UseMetadata módban. + /// Wire format: [ObjectWithMetadata][footerIndex (VarUInt)][props...] + /// A footer index-ből megkapjuk a source property hash-eket → cachemap felépítése. + /// + private static object? ReadObjectWithMetadata(ref BinaryDeserializationContext context, Type targetType, int depth) + { + var streamPosition = context.Position - 1; + + // Footer index olvasása + var footerIndex = (int)context.ReadVarUInt(); + + // Handle dictionary types + if (IsDictionaryType(targetType, out var keyType, out var valueType)) + { + return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth); + } + + var wrapper = context.ContextClass.GetWrapper(targetType); + var metadata = wrapper.Metadata; + + var instance = CreateInstance(targetType, metadata); + if (instance == null) return null; + + if (context.ContextClass.UseTypeReferenceHandling(metadata)) + { + context.RegisterInternedValue(instance, streamPosition); + } + + // Cachemap felépítése ha még nincs (lazy, 1x per wrapper) + if (metadata.CacheMap == null) + metadata.CacheMap = context.ContextClass.BuildCacheMap(footerIndex, metadata); + + PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); + + // ChainMode kezelés + if (context.IsChainMode && metadata.IsIId && metadata.IdType != null) + { + var id = GetIdBoxed(instance, metadata); + if (id != null && !IsDefaultValue(id, metadata.IdType)) + { + if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj)) + { + CopyProperties(instance, existingObj!, metadata); + return existingObj; + } context.ChainTracker.TryRegisterIIdObject(instance); } } @@ -1292,7 +1357,11 @@ public static partial class AcBinaryDeserializer if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt(); return; case BinaryTypeCode.Object: - SkipObject(ref context, metaData); + SkipObject(ref context, metaData, footerIndex: -1); + return; + case BinaryTypeCode.ObjectWithMetadata: + var skipFooterIndex = (int)context.ReadVarUInt(); + SkipObject(ref context, metaData, skipFooterIndex); return; case BinaryTypeCode.ObjectRef: context.ReadVarInt(); @@ -1351,13 +1420,35 @@ public static partial class AcBinaryDeserializer // } //} - private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) + /// + /// Object kihagyása. UseMetadata módban a footer entry-ből tudjuk a property számot. + /// footerIndex: -1 = root (footer entry 0), >=0 = nested (a body-ból olvasott index). + /// + private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData, int footerIndex) { - // Wire format: [Object][props...] - no hashcode prefix in new format - // NEW FORMAT: Can't skip without knowing property count! + if (context.HasMetadata) + { + // Root object: footer entry 0, nested: a megadott footerIndex + var entryIndex = footerIndex < 0 ? 0 : footerIndex; + if (entryIndex >= context.ContextClass.FooterEntryCount) + { + throw new AcBinaryDeserializationException( + $"SkipObject: Érvénytelen footer index {entryIndex}.", + context.Position); + } + ref var entry = ref context.ContextClass.GetFooterEntry(entryIndex); + var propCount = entry.PropertyHashes.Length; + for (var i = 0; i < propCount; i++) + { + SkipValue(ref context, metaData); + } + return; + } + + // Nincs metadata → nem tudjuk kihagyni az object-et throw new NotSupportedException( - "SkipObject is not supported with SKIP marker format. " + - "Unable to determine property count without type metadata."); + "SkipObject nem támogatott metadata nélkül. " + + "A property szám nem határozható meg típus metadata nélkül."); } private static void SkipArray(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 3a4e771..972b24d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -74,7 +74,13 @@ public static partial class AcBinarySerializer private const int PropertyIndexBufferMaxCache = 512; private const int PropertyStateBufferMaxCache = 512; private const int InitialInternCapacity = 32; - private const int InitialPropertyNameCapacity = 32; + /// + /// UseMetadata footer bejegyzések sorrendben. + /// Minden bejegyzés egy (Type, propertyHashes) pár. + /// A 0. elem mindig a root object. + /// A body-ban a nested object-eknél az index (VarUInt) kerül kiírásra. + /// + private List? _metadataEntries; private byte[] _buffer; private int _position; @@ -100,8 +106,6 @@ public static partial class AcBinarySerializer private IdentityMap? _stringInternMap; private int _nextCacheIndex; // Next dense cache index to assign - private Dictionary? _propertyNames; - private List? _propertyNameList; private int[]? _propertyIndexBuffer; private byte[]? _propertyStateBuffer; @@ -123,9 +127,9 @@ public static partial class AcBinarySerializer // These properties delegate to Options for convenience public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None; /// - /// True if we need footer position in header (string interning OR reference handling). + /// True if we need footer position in header (string interning OR reference handling OR metadata). /// - public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; + public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None || UseMetadata; public bool UseMetadata => Options.UseMetadata; public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; @@ -176,9 +180,7 @@ public static partial class AcBinarySerializer //_refTracker.Reset(); _stringInternMap?.Reset(); - ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4); - - _propertyNameList?.Clear(); + _metadataEntries?.Clear(); _nextCacheIndex = 0; if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache) @@ -362,25 +364,74 @@ public static partial class AcBinarySerializer #endregion - #region Property Name Table + #region UseMetadata Type Tracking - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegisterPropertyName(string name) + /// + /// Egy footer bejegyzés: típus propNameHash + property hash-ek. + /// + internal readonly struct MetadataFooterEntry { - _propertyNames ??= new Dictionary(InitialPropertyNameCapacity, StringComparer.Ordinal); - _propertyNameList ??= new List(InitialPropertyNameCapacity); + public readonly int PropNameHash; // FNV-1a hash a típus nevéből + public readonly int[] PropertyHashes; // property name hash-ek sorrendben - if (!_propertyNames.ContainsKey(name)) + public MetadataFooterEntry(int propNameHash, int[] propertyHashes) { - var index = _propertyNameList.Count; - _propertyNames[name] = index; - _propertyNameList.Add(name); + PropNameHash = propNameHash; + PropertyHashes = propertyHashes; } } + /// + /// Regisztrálja a típust a UseMetadata footer-be. + /// Visszaadja a footer index-et. Ha már regisztrálva van (wrapper.MetadataFooterIndex >= 0), + /// a meglévő index-et adja vissza. Nincs Dictionary lookup — a wrapper tárolja az indexet. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetPropertyNameIndex(string name) - => _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1; + public int RegisterMetadataType(TypeMetadataWrapper wrapper) + { + if (wrapper.MetadataFooterIndex >= 0) + return wrapper.MetadataFooterIndex; + + return RegisterMetadataTypeSlow(wrapper); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int RegisterMetadataTypeSlow(TypeMetadataWrapper wrapper) + { + _metadataEntries ??= new List(); + + // PropNameHash and MetadataPropertyHashes are lazy-computed once per type in metadata. + // Duplicate hash validation also happens once (in MetadataPropertyHashes getter). + var index = _metadataEntries.Count; + _metadataEntries.Add(new MetadataFooterEntry( + wrapper.Metadata.PropNameHash, + wrapper.Metadata.MetadataPropertyHashes)); + wrapper.MetadataFooterIndex = index; + return index; + } + + /// + /// UseMetadata footer kiírása. + /// Formátum: [entryCount (VarUInt)] + /// entry-nként: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]... + /// + public void WriteMetadataFooter() + { + if (_metadataEntries == null || _metadataEntries.Count == 0) return; + + WriteVarUInt((uint)_metadataEntries.Count); + + for (var i = 0; i < _metadataEntries.Count; i++) + { + var entry = _metadataEntries[i]; + WriteRaw(entry.PropNameHash); + WriteVarUInt((uint)entry.PropertyHashes.Length); + for (var j = 0; j < entry.PropertyHashes.Length; j++) + { + WriteRaw(entry.PropertyHashes[j]); + } + } + } #endregion @@ -459,22 +510,7 @@ public static partial class AcBinarySerializer return PropertyFilter(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property) - { - if (PropertyFilter == null) - { - return true; - } - - var context = new BinaryPropertyFilterContext( - null, - property.DeclaringType, - property.Name, - property.PropertyType, - null); - return PropertyFilter(context); - } + public bool CheckDuplicatePropName => Options.CheckDuplicatePropName; #endregion @@ -1005,102 +1041,49 @@ public static partial class AcBinarySerializer // Body: data with StringInterned indices // Footer: interned strings table - /// - /// Estimates header payload size based on registered property names. - /// String interning now uses footer, so no estimation needed for strings. - /// - public int EstimateHeaderPayloadSize() - { - var size = 0; - - // Only property names are in header now - if (UseMetadata && _propertyNameList is { Count: > 0 }) - { - size += GetVarUIntSize((uint)_propertyNameList.Count); - for (var i = 0; i < _propertyNameList.Count; i++) - { - var name = _propertyNameList[i]; - var byteCount = name.Length; // Assume ASCII (common case) - size += GetVarUIntSize((uint)byteCount) + byteCount; - } - } - - return size; - } - public void WriteHeaderPlaceholder() { // Header layout: // [0] version (1 byte) // [1] flags (1 byte) - // [2-5] footer position (4 bytes, if string interning OR reference handling) + // [2-5] footer position (4 bytes, if footer is needed) EnsureCapacity(HasFooter ? 6 : 2); _headerPosition = _position; _position += HasFooter ? 6 : 2; } - /// - /// Reserves space for property name table in header. - /// - public void ReserveHeaderSpace(int estimatedSize) - { - if (estimatedSize <= 0) return; - - EnsureCapacity(estimatedSize); - _position += estimatedSize; - } - public void FinalizeHeaderSections() { - var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 }; var dupCount = GetDupCount(); // Shared counter: string intern + ID tracking var hasInternTable = dupCount > 0; - - // Calculate property names header size (strings go to footer now) - var headerPayloadSize = 0; - if (hasPropertyNames) - { - headerPayloadSize += GetVarUIntSize((uint)_propertyNameList!.Count); - for (var i = 0; i < _propertyNameList.Count; i++) - { - var name = _propertyNameList[i]; - var byteCount = Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name); - headerPayloadSize += GetVarUIntSize((uint)byteCount) + byteCount; - } - } - - // Write property names to header if needed - var headerPayloadStart = _headerPosition + (HasFooter ? 6 : 2); - if (hasPropertyNames) - { - var headerPos = headerPayloadStart; - headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count); - for (var i = 0; i < _propertyNameList.Count; i++) - { - var name = _propertyNameList[i]; - headerPos = WriteStringAtOptimized(headerPos, name); - } - } + var hasMetadataFooter = UseMetadata && _metadataEntries is { Count: > 0 }; // Footer: write merged intern entries (string + ID) var footerPosition = 0; - if (hasInternTable) + if (hasInternTable || hasMetadataFooter) { footerPosition = _position; + + // Intern footer WriteVarUInt((uint)dupCount); - WriteInternedFooter(); + if (hasInternTable) + WriteInternedFooter(); + + // Metadata footer (per-type property hashes) + if (hasMetadataFooter) + WriteMetadataFooter(); } // Write header var flags = BinaryTypeCode.HeaderFlagsBase; - if (hasPropertyNames) + if (hasMetadataFooter) flags |= BinaryTypeCode.HeaderFlag_Metadata; // Encode ReferenceHandlingMode using separate bits if (ReferenceHandling == ReferenceHandlingMode.OnlyId) flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId; else if (ReferenceHandling == ReferenceHandlingMode.All) flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All); - // Set footer position flag if footer is needed (string interning OR ref handling) + // Set footer position flag if footer is needed if (HasFooter) flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition; @@ -1108,34 +1091,12 @@ public static partial class AcBinarySerializer _buffer[_headerPosition + 1] = flags; // Write footer position if footer is needed - // (even if there's no actual interned data - footer position will be 0) if (HasFooter) { Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition); } } - - /// - /// 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 (Ascii.IsValid(value)) - { - pos = WriteVarUIntAt(pos, (uint)value.Length); - 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; - } - /// /// Writes VarUInt at specific position and returns new position. /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs index d601c6e..ab10cbc 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using AyCode.Core.Serializers; @@ -23,7 +24,65 @@ public static partial class AcBinarySerializer /// public Type? GeneratedSerializerType { get; } + /// + /// Lazy-computed FNV-1a hash of the type name (SourceType.Name). + /// Only computed once per type, on first access when UseMetadata=true. + /// + private int _propNameHash; + private bool _propNameHashComputed; + public int PropNameHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (!_propNameHashComputed) + ComputePropNameHash(); + return _propNameHash; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ComputePropNameHash() + { + _propNameHash = FnvHash.ComputeString(SourceType.Name); + _propNameHashComputed = true; + } + + /// + /// Lazy-computed property name hashes array for UseMetadata footer. + /// Includes duplicate hash validation (computed once per type, not per session). + /// + private int[]? _metadataPropertyHashes; + + public int[] MetadataPropertyHashes + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _metadataPropertyHashes ??= ComputeMetadataPropertyHashes(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int[] ComputeMetadataPropertyHashes() + { + var hashes = new int[Properties.Length]; + HashSet? seen = null; + + for (var i = 0; i < Properties.Length; i++) + { + var hash = Properties[i].PropertyNameHash; + seen ??= new HashSet(Properties.Length); + if (!seen.Add(hash)) + { + throw new InvalidOperationException( + $"UseMetadata: Duplikált property name hash a '{SourceType.FullName}' típuson. " + + $"Property '{Properties[i].Name}' FNV-1a hash ütközés. " + + $"Használj [AcPropertyName] attribútumot a feloldáshoz."); + } + hashes[i] = hash; + } + + return hashes; + } public BinarySerializeTypeMetadata(Type type, Func ignorePropertyFilter) : base(type,ignorePropertyFilter) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index dce7683..af9a61d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -90,8 +90,6 @@ public static partial class AcBinarySerializer // Run serialization to trigger callbacks context.WriteHeaderPlaceholder(); - var estimatedHeaderSize = context.EstimateHeaderPayloadSize(); - context.ReserveHeaderSpace(estimatedHeaderSize); WriteValue(value, runtimeType, context, 0); context.FinalizeHeaderSections(); @@ -359,11 +357,8 @@ public static partial class AcBinarySerializer // - No header size estimation needed (strings go to footer) // - No body shifting (footer is appended at the end) // - Reference tracking happens inline via TryTrack during WriteObject - - // Reserve space only for property name table (if metadata is enabled) - var estimatedHeaderSize = context.EstimateHeaderPayloadSize(); - context.ReserveHeaderSpace(estimatedHeaderSize); - + // - UseMetadata: per-type property hashes written to footer + WriteValue(value, runtimeType, context, 0); context.FinalizeHeaderSections(); return context; @@ -371,89 +366,6 @@ public static partial class AcBinarySerializer #endregion - #region Property Metadata Registration - - private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet? visited = null) - { - if (IsPrimitiveOrStringFast(type)) return; - - visited ??= new HashSet(); - if (!visited.Add(type)) return; - - if (IsDictionaryType(type, out var keyType, out var valueType)) - { - if (keyType != null) RegisterMetadataForType(keyType, context, visited); - if (valueType != null) RegisterMetadataForType(valueType, context, visited); - return; - } - - if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType)) - { - var elementType = GetCollectionElementType(type); - if (elementType != null) - { - RegisterMetadataForType(elementType, context, visited); - } - return; - } - - var wrapper = context.GetWrapper(type); - var metadata = wrapper.Metadata; - var properties = metadata.Properties; - - // Use index-based iteration for array access - for (var i = 0; i < properties.Length; i++) - { - var prop = properties[i]; - - if (!context.ShouldIncludePropertyInMetadata(prop)) - { - continue; - } - - context.RegisterPropertyName(prop.Name); - - if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType)) - { - RegisterMetadataForType(nestedType, context, visited); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryResolveNestedMetadataType(Type propertyType, out Type nestedType) - { - nestedType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; - - if (IsPrimitiveOrStringFast(nestedType)) - return false; - - if (IsDictionaryType(nestedType, out var _, out var valueType) && valueType != null) - { - if (!IsPrimitiveOrStringFast(valueType)) - { - nestedType = valueType; - return true; - } - return false; - } - - if (typeof(IEnumerable).IsAssignableFrom(nestedType) && !ReferenceEquals(nestedType, StringType)) - { - var elementType = GetCollectionElementType(nestedType); - if (elementType != null && !IsPrimitiveOrStringFast(elementType)) - { - nestedType = elementType; - return true; - } - return false; - } - - return true; - } - - #endregion - #region Value Writing private static void WriteValue(object? value, Type type, BinarySerializationContext context, int depth) @@ -496,7 +408,7 @@ public static partial class AcBinarySerializer } // Handle complex objects with single-pass reference tracking - WriteObject(value, type, context, depth); + WriteObject(value, type, context, depth, isNested: depth > 0); } /// @@ -829,19 +741,30 @@ public static partial class AcBinarySerializer #region Complex Type Writers - private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth) + private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth, bool isNested = false) { var wrapper = context.GetWrapper(type); var metadata = wrapper.Metadata; // Wire format: - // - IId types: [Object][props 0-tól...] - Id a props-ban, nincs extra - // - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode előre - // - Ref=Off: [Object][props 0-tól...] - semmi extra + // - IId types: [Object/ObjectWithMetadata][props 0-tól...] - Id a props-ban, nincs extra + // - Non-IId + All: [Object/ObjectWithMetadata][props 0-tól...] - no hashcode prefix + // - Ref=Off: [Object/ObjectWithMetadata][props 0-tól...] - no prefix // ObjectRef format: - // - IId: [ObjectRef][Id érték] - // - Non-IId: [ObjectRef][hashcode] - + // - IId: [ObjectRef][cacheIndex] + // - Non-IId: [ObjectRef][cacheIndex] + // + // UseMetadata: + // - Root (depth==0): [Object] marker, footer entry 0 + // - Nested: [ObjectWithMetadata][footerIndex (VarUInt)] + + // UseMetadata: regisztráljuk a típust a footer-be (index kell a marker kiíráshoz) + var metadataFooterIndex = -1; + if (context.UseMetadata) + { + metadataFooterIndex = context.RegisterMetadataType(wrapper); + } + if (context.UseTypeReferenceHandling(metadata)) { if (metadata.IsIId) @@ -852,13 +775,10 @@ public static partial class AcBinarySerializer case IdAccessorType.Int32: if (!context.TryTrackObject(wrapper, value, out int cacheIndex32)) { - // Already seen → ObjectRef + cacheIndex context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarUInt((uint)cacheIndex32); return; } - // First occurrence → Object (no extra data, Id in props) - context.WriteByte(BinaryTypeCode.Object); break; case IdAccessorType.Int64: @@ -868,7 +788,6 @@ public static partial class AcBinarySerializer context.WriteVarUInt((uint)cacheIndex64); return; } - context.WriteByte(BinaryTypeCode.Object); break; case IdAccessorType.Guid: @@ -878,7 +797,6 @@ public static partial class AcBinarySerializer context.WriteVarUInt((uint)cacheIndexGuid); return; } - context.WriteByte(BinaryTypeCode.Object); break; } } @@ -887,18 +805,21 @@ public static partial class AcBinarySerializer // Non-IId + RefHandling=All: track by hashcode if (!context.TryTrackObject(wrapper, value, out int cacheIndexHash)) { - // Already seen → ObjectRef + cacheIndex context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarUInt((uint)cacheIndexHash); return; } - // First occurrence → Object (no extra prefix for Non-IId in new format) - context.WriteByte(BinaryTypeCode.Object); } } + + // Marker kiírása: UseMetadata nested → ObjectWithMetadata + footer index, egyébként Object + if (context.UseMetadata && isNested) + { + context.WriteByte(BinaryTypeCode.ObjectWithMetadata); + context.WriteVarUInt((uint)metadataFooterIndex); + } else { - // No reference handling - just write object marker context.WriteByte(BinaryTypeCode.Object); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index e6d81e1..0b8130a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -76,12 +76,20 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions public int MaxCachedStringLength { get; init; } = 64; /// - /// Whether to include metadata header with property names. - /// NOTE: Currently unused - deserializer uses ordered property indices, not names. - /// Kept for potential future schema evolution support. + /// Whether to include property hash metadata in footer for cross-type deserialization. + /// When enabled, property name hashes (FNV-1a) are written per type in the footer, + /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; init; } = false; + public bool UseMetadata { get; init; } = true; + + /// + /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). + /// Throws exception if FNV-1a hash collision is detected between property names of the same type. + /// Should be enabled during development/testing, can be disabled in production for performance. + /// Default: true (safety first) + /// + public bool CheckDuplicatePropName { get; init; } = true; /// /// Controls how string interning is applied during serialization. diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs index 83f1f55..2727ad2 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs @@ -49,6 +49,7 @@ internal static class BinaryTypeCode public const byte Array = 28; // Start of array/list public const byte Dictionary = 29; // Start of dictionary public const byte ByteArray = 30; // Optimized byte[] storage + public const byte ObjectWithMetadata = 31; // Object with metadata footer index (UseMetadata nested objects) // Special markers (32+, for header/meta) // Header flags byte structure (for values >= 64): diff --git a/AyCode.Core/Serializers/FnvHash.cs b/AyCode.Core/Serializers/FnvHash.cs new file mode 100644 index 0000000..bec042c --- /dev/null +++ b/AyCode.Core/Serializers/FnvHash.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers; + +/// +/// FNV-1a 32-bit hash implementation. +/// Deterministic across processes (unlike string.GetHashCode()). +/// Used for property name hashing in UseMetadata mode. +/// +public static class FnvHash +{ + private const uint FnvOffsetBasis = 2166136261; + private const uint FnvPrime = 16777619; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeString(string value) + { + var hash = FnvOffsetBasis; + for (var i = 0; i < value.Length; i++) + { + hash ^= value[i]; + hash *= FnvPrime; + } + return (int)hash; + } +} diff --git a/AyCode.Core/Serializers/PropertyMetadataBase.cs b/AyCode.Core/Serializers/PropertyMetadataBase.cs index 5c04d03..fa0a9b0 100644 --- a/AyCode.Core/Serializers/PropertyMetadataBase.cs +++ b/AyCode.Core/Serializers/PropertyMetadataBase.cs @@ -74,6 +74,12 @@ public abstract class PropertyMetadataBase /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. /// public bool IsComplexType { get; } + + /// + /// FNV-1a hash of property name. Deterministic across processes. + /// Used for property matching in UseMetadata mode. + /// + public int PropertyNameHash { get; } /// /// The accessor type for fast typed getter/setter dispatch. @@ -100,6 +106,8 @@ public abstract class PropertyMetadataBase // Pre-compute: is this a complex type that needs recursive handling? IsComplexType = !IsPrimitiveOrStringFast(PropertyType); + + PropertyNameHash = FnvHash.ComputeString(Name); AccessorType = DetermineAccessorType(PropertyType); _dynamicGetter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop); diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index bd734ec..c62657a 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -35,6 +35,13 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// internal readonly Func? RefIdGetterGuid; + /// + /// UseMetadata: footer entry index for this type in the current serialization session. + /// -1 = not yet registered. Set by RegisterMetadataType, reset by ResetTracking. + /// Eliminates the need for Dictionary<Type, int> lookup in the serializer hot path. + /// + internal int MetadataFooterIndex = -1; + #region Typed IdentityMaps - No generic type checks in hot path! /// @@ -110,6 +117,8 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetTracking(bool preRentBuckets = false) { + MetadataFooterIndex = -1; + if (SmallIdBitmap != null) Array.Clear(SmallIdBitmap);