From deffb77de476bfd4dc3e2fd1ac93b631aaba817d Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 18 Feb 2026 18:43:21 +0100 Subject: [PATCH] Add full UseMetadata support to SGen with inline hashes Implements inline type metadata emission in the source generator, matching runtime TypeMetadataBase. Computes FNV-1a hashes for type and property names, stores them in generated code, and emits metadata when UseMetadata is enabled. Adds per-type slot allocation and tracking for first/repeated metadata writes. Removes runtime fallback for UseMetadata, ensuring all logic is handled inline. Updates property filtering/order to match runtime, and optimizes Int32/Int64 skip logic. Thread-safe slot allocation is used for metadata tracking. --- .../AcBinarySourceGenerator.cs | 239 ++++++++++++++---- ...rySerializer.BinarySerializationContext.cs | 24 ++ .../Binaries/AcBinarySerializer.cs | 14 + 3 files changed, 229 insertions(+), 48 deletions(-) diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index f0402ae..d76238b 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -80,6 +80,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator bool hasGenWriter = false; bool propTypeIsIId = false; string? writerClassName = null; + int childTypeNameHash = 0; + int[]? childPropertyHashes = null; if (kind == PropertyTypeKind.Complex) { // Resolve to the actual type symbol (strip nullable annotation for ref types) @@ -104,6 +106,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator writerClassName = string.IsNullOrEmpty(ns) ? $"{flatName}_GeneratedWriter" : $"{ns}.{flatName}_GeneratedWriter"; + + // UseMetadata: compute child type hash-es for inline metadata + childTypeNameHash = ComputeFnvHash(resolvedType.Name); + childPropertyHashes = ComputeChildPropertyHashes(resolvedType); } } @@ -114,6 +120,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator string? elemWriterClassName = null; string? collKind = null; string? elemFullTypeName = null; + int elementTypeNameHash = 0; + int[]? elementPropertyHashes = null; if (kind == PropertyTypeKind.Collection) { var elemType = GetCollectionElementType(p.Type); @@ -161,6 +169,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator elemWriterClassName = string.IsNullOrEmpty(ens) ? $"{elemFlatName}_GeneratedWriter" : $"{ens}.{elemFlatName}_GeneratedWriter"; + + // UseMetadata: compute element type hash-es for inline metadata + elementTypeNameHash = ComputeFnvHash(resolvedElem.Name); + elementPropertyHashes = ComputeChildPropertyHashes(resolvedElem); } } } @@ -173,7 +185,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator kind, p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, - elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName)); + elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName, + childTypeNameHash, childPropertyHashes, + elementTypeNameHash, elementPropertyHashes)); } } @@ -193,7 +207,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); - return new SerializableClassInfo(namespaceName, BuildFlatName(typeSymbol), typeSymbol.ToDisplayString(), properties); + var className = BuildFlatName(typeSymbol); + var typeNameHash = ComputeFnvHash(typeSymbol.Name); + var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); + return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, typeNameHash, propertyNameHashes); } private static void Execute(ImmutableArray classes, SourceProductionContext context) @@ -225,6 +242,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter"); sb.AppendLine("{"); sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();"); + sb.AppendLine($" internal static readonly int s_metadataSlot = AcBinarySerializer.AllocateMetadataSlot();"); + sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};"); + sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ "); + sb.Append(string.Join(", ", ci.PropertyNameHashes)); + sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" public void WriteProperties(object value, AcBinarySerializer.BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase"); sb.AppendLine(" {"); @@ -374,10 +396,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator /// /// Emits direct object write — bypasses GetWrapper + WriteObject entirely. - /// Writes marker bytes + calls child GeneratedWriter.WriteProperties inline. + /// Writes marker bytes + inline metadata (UseMetadata) + calls child GeneratedWriter.WriteProperties. /// IId types: guard ReferenceHandling != None (tracked in OnlyId + All). /// Non-IId types: guard ReferenceHandling == All (tracked only in All mode). - /// Falls back to WriteObjectGenerated when context.IsDirectObjectWrite is false (UseMetadata). + /// No fallback to WriteObjectGenerated — handles both UseMetadata=true and false inline. /// private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i) { @@ -387,24 +409,23 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); } else { - sb.AppendLine($"{i}if (context.IsDirectObjectWrite)"); + sb.AppendLine($"{i}{{"); } - sb.AppendLine($"{i}{{"); - // MaxDepth check — matches WriteObjectGenerated sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); + // UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed) + sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); + // Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior. - // IId types: tracked in OnlyId + All → guard: ReferenceHandling != None - // Non-IId types: tracked only in All → guard: ReferenceHandling == All - // This matches UseTypeReferenceHandling: (IsIId || ReferenceHandling == All) && ReferenceHandling != None var refGuard = p.IsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; @@ -417,52 +438,74 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); + // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst + sb.AppendLine($"{i} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); + EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object + sb.AppendLine($"{i} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); + EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); + } - // Fallback for non-direct mode (UseMetadata=true or HasPropertyFilter=true) - if (p.IsNullable) - sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); - else - { - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); - } + /// + /// Emits inline metadata write: typeNameHash + (if first) propCount + property hashes. + /// All values are compile-time constants. + /// + private static void EmitInlineMetadata(StringBuilder sb, int typeNameHash, int[] propertyHashes, string isFirstVar, string i) + { + sb.AppendLine($"{i}context.WriteRaw({typeNameHash});"); + sb.AppendLine($"{i}if ({isFirstVar})"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} context.WriteVarUInt({(uint)propertyHashes.Length});"); + foreach (var hash in propertyHashes) + sb.AppendLine($"{i} context.WriteRaw({hash});"); + sb.AppendLine($"{i}}}"); } /// /// Emits inline collection write for List<T> / T[] where T is a Complex type with generated writer. /// Bypasses GetWrapper + WriteArray + WriteValue per-element dispatch entirely. /// Wire format: [Array marker][VarUInt count][elem₁ marker+props][elem₂ marker+props]... - /// Falls back to WriteValueGenerated when context.IsDirectObjectWrite is false. + /// Handles both UseMetadata=true and false inline — no fallback to WriteValueGenerated. /// private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i) { var writer = p.ElementWriterClassName; - var elemType = p.ElementFullTypeName; if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); } else { - sb.AppendLine($"{i}if (context.IsDirectObjectWrite)"); + sb.AppendLine($"{i}{{"); } - sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);"); // Get count and iteration based on collection kind @@ -477,7 +520,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } else if (p.CollectionKind == "Counted") { - // HashSet, Queue, ICollection, IReadOnlyCollection, etc. — Count + foreach sb.AppendLine($"{i} var col_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); @@ -494,15 +536,15 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];"); } - // Per-element write — same logic as EmitDirectObjectWrite but for each element + // Per-element write var e = $"elem_{p.Name}"; - // Elements in a collection can be null (runtime writes Null marker) sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); - // Inline ref tracking: guard depends on IId vs non-IId element type to match scan pass. - // IId elements: tracked in OnlyId + All → guard: ReferenceHandling != None - // Non-IId elements: tracked only in All → guard: ReferenceHandling == All + // UseMetadata: register element type for first/repeated tracking + sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); + + // Inline ref tracking var elemRefGuard = p.ElementIsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; @@ -515,28 +557,36 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst + sb.AppendLine($"{i} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object + sb.AppendLine($"{i} if (context.UseMetadata)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); + EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); - - // Fallback for non-direct mode - if (p.IsNullable) - sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); - else - { - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); - } } private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i) @@ -544,13 +594,28 @@ public class AcBinarySourceGenerator : IIncrementalGenerator switch (k) { case PropertyTypeKind.Int32: + { + // Mirrors runtime WritePropertyOrSkip → WriteInt32 (TinyInt optimization) + var s32 = a.Replace(".", "_"); sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt({a}, out var ti_{s32})) context.WriteByte(ti_{s32});"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}"); break; + } case PropertyTypeKind.Int64: + { + // Mirrors runtime WritePropertyOrSkip → WriteInt64 → WriteInt32 (int range + TinyInt) + var s64 = a.Replace(".", "_"); sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if ({a} >= int.MinValue && {a} <= int.MaxValue)"); + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} var iv_{s64} = (int){a};"); + sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(iv_{s64}, out var ti_{s64})) context.WriteByte(ti_{s64});"); + sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{s64}); }}"); + sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}"); break; + } case PropertyTypeKind.Boolean: sb.AppendLine($"{i}if (!{a}) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.True);"); @@ -675,6 +740,64 @@ public class AcBinarySourceGenerator : IIncrementalGenerator return string.Join("_", parts); } + #region FNV-1a Hash (compile-time) + + private static int ComputeFnvHash(string value) + { + uint hash = 2166136261; + for (int i = 0; i < value.Length; i++) + { + hash ^= value[i]; + hash *= 16777619; + } + return (int)hash; + } + + /// + /// Computes FNV-1a hashes for all serializable properties of a child type. + /// Property filtering and ordering matches runtime TypeMetadataBase exactly: + /// public get+set, non-indexer, non-static, no ignore attributes, sorted (Id first if IId, then alphabetical). + /// + private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType) + { + var propNames = new List(); + foreach (var member in resolvedType.GetMembers()) + { + if (member is IPropertySymbol cp && + cp.DeclaredAccessibility == Accessibility.Public && + cp.GetMethod != null && cp.SetMethod != null && + !cp.IsIndexer && !cp.IsStatic) + { + var hasIgnore = cp.GetAttributes().Any(a => + { + var name = a.AttributeClass?.Name ?? ""; + return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; + }); + if (hasIgnore) continue; + propNames.Add(cp.Name); + } + } + + var childIsIId = resolvedType.AllInterfaces.Any(ifc => + ifc.IsGenericType && + ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + + if (childIsIId) + propNames.Sort((a, b) => + { + var ai = a == "Id" ? 0 : 1; + var bi = b == "Id" ? 0 : 1; + if (ai != bi) return ai.CompareTo(bi); + return string.Compare(a, b, StringComparison.Ordinal); + }); + else + propNames.Sort(StringComparer.Ordinal); + + return propNames.Select(ComputeFnvHash).ToArray(); + } + + #endregion + #region Type analysis private static bool IsNullableVT(ITypeSymbol t) => @@ -765,8 +888,12 @@ internal sealed class SerializableClassInfo public string ClassName { get; } public string FullTypeName { get; } public List Properties { get; } - public SerializableClassInfo(string ns, string cn, string ftn, List p) - { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; } + /// FNV-1a hash of ClassName (matches runtime SourceType.Name hash) + public int TypeNameHash { get; } + /// FNV-1a hash of each property name, in property order + public int[] PropertyNameHashes { get; } + public SerializableClassInfo(string ns, string cn, string ftn, List p, int typeNameHash, int[] propertyNameHashes) + { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; } } internal sealed class PropInfo @@ -808,10 +935,22 @@ internal sealed class PropInfo /// Full element type name for generated code (e.g. "SharedTag"). public string? ElementFullTypeName { get; } + // UseMetadata inline hash-ek (Complex/Collection child típushoz) + /// FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter. + public int ChildTypeNameHash { get; } + /// FNV-1a hashes of child type's properties. Only set when HasGeneratedWriter. + public int[]? ChildPropertyHashes { get; } + /// FNV-1a hash of collection element type name. Only set when ElementHasGeneratedWriter. + public int ElementTypeNameHash { get; } + /// FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter. + public int[]? ElementPropertyHashes { get; } + public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false, - string? elementWriterClassName = null, string? collectionKind = null, string? elementFullTypeName = null) + string? elementWriterClassName = null, string? collectionKind = null, string? elementFullTypeName = null, + int childTypeNameHash = 0, int[]? childPropertyHashes = null, + int elementTypeNameHash = 0, int[]? elementPropertyHashes = null) { Name = n; TypeName = tn; @@ -827,6 +966,10 @@ internal sealed class PropInfo ElementWriterClassName = elementWriterClassName; CollectionKind = collectionKind; ElementFullTypeName = elementFullTypeName; + ChildTypeNameHash = childTypeNameHash; + ChildPropertyHashes = childPropertyHashes; + ElementTypeNameHash = elementTypeNameHash; + ElementPropertyHashes = elementPropertyHashes; // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase int flags = 0; if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index fb79237..a8b0051 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -89,6 +89,8 @@ public static partial class AcBinarySerializer #endregion private IdentityMap? _stringInternMap; + private readonly ulong[] _metadataSeenBits = new ulong[4]; // 256 SGen slot (bit per type, first/repeated tracking) + private HashSet? _metadataSeenOverflow; // fallback for slot >= 256 private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex) public int NextFirstIndex; // Next first occurrence index for scan pass. Direct access for performance. @@ -277,6 +279,8 @@ public static partial class AcBinarySerializer } _stringInternMap?.Reset(); + _metadataSeenBits.AsSpan().Clear(); + _metadataSeenOverflow?.Clear(); _nextCacheIndex = 0; NextFirstIndex = 0; ScanVisitIndex = 0; @@ -782,6 +786,26 @@ public static partial class AcBinarySerializer return true; // első előfordulás } + /// + /// SGen inline metadata regisztráció — slot-alapú, nem kell TypeMetadataWrapper. + /// Minden SGen típus kap egy compile-time slot indexet (AllocateMetadataSlot). + /// Első 256 slot: bit művelet (ulong[4]), felette: HashSet fallback. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool RegisterMetadataTypeBySlot(int slot) + { + if (slot < 256) + { + ref var bits = ref _metadataSeenBits[slot >> 6]; + var mask = 1UL << (slot & 63); + if ((bits & mask) != 0) return false; + bits |= mask; + return true; + } + _metadataSeenOverflow ??= new HashSet(); + return _metadataSeenOverflow.Add(slot); + } + /// /// Inline metadata kiírása az ObjectWithMetadata marker után. /// Első előfordulás: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]... diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index d9d1755..654e3c2 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -4,6 +4,7 @@ using AyCode.Core.Serializers.Expressions; using System.Buffers; using System.Collections; using System.Collections.Concurrent; +using System.Threading; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; @@ -250,6 +251,19 @@ public static partial class AcBinarySerializer GeneratedWriterRegistry.Register(type, writer); } + #region UseMetadata Slot Allocation + + private static int s_nextMetadataSlot; + + /// + /// Allocates a unique slot index for SGen UseMetadata first/repeated tracking. + /// Called once per SGen type at startup (ModuleInitializer). Thread-safe. + /// Slot is used by BinarySerializationContext.RegisterMetadataTypeBySlot. + /// + internal static int AllocateMetadataSlot() => Interlocked.Increment(ref s_nextMetadataSlot) - 1; + + #endregion + /// /// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation. ///