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.
This commit is contained in:
Loretta 2026-02-18 18:43:21 +01:00
parent e2269d3ecf
commit deffb77de4
3 changed files with 229 additions and 48 deletions

View File

@ -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<SerializableClassInfo?> 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<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase");
sb.AppendLine(" {");
@ -374,10 +396,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// <summary>
/// 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.
/// </summary>
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);");
}
/// <summary>
/// Emits inline metadata write: typeNameHash + (if first) propCount + property hashes.
/// All values are compile-time constants.
/// </summary>
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}}}");
}
/// <summary>
/// Emits inline collection write for List&lt;T&gt; / 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.
/// </summary>
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<T>, Queue<T>, ICollection<T>, IReadOnlyCollection<T>, 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;
}
/// <summary>
/// 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).
/// </summary>
private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType)
{
var propNames = new List<string>();
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<T>");
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<PropInfo> Properties { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p)
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; }
/// <summary>FNV-1a hash of ClassName (matches runtime SourceType.Name hash)</summary>
public int TypeNameHash { get; }
/// <summary>FNV-1a hash of each property name, in property order</summary>
public int[] PropertyNameHashes { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> 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
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
public string? ElementFullTypeName { get; }
// UseMetadata inline hash-ek (Complex/Collection child típushoz)
/// <summary>FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter.</summary>
public int ChildTypeNameHash { get; }
/// <summary>FNV-1a hashes of child type's properties. Only set when HasGeneratedWriter.</summary>
public int[]? ChildPropertyHashes { get; }
/// <summary>FNV-1a hash of collection element type name. Only set when ElementHasGeneratedWriter.</summary>
public int ElementTypeNameHash { get; }
/// <summary>FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter.</summary>
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

View File

@ -89,6 +89,8 @@ public static partial class AcBinarySerializer
#endregion
private IdentityMap<string, InternEntry>? _stringInternMap;
private readonly ulong[] _metadataSeenBits = new ulong[4]; // 256 SGen slot (bit per type, first/repeated tracking)
private HashSet<int>? _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
}
/// <summary>
/// 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.
/// </summary>
[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<int>();
return _metadataSeenOverflow.Add(slot);
}
/// <summary>
/// Inline metadata kiírása az ObjectWithMetadata marker után.
/// Első előfordulás: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]...

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
internal static int AllocateMetadataSlot() => Interlocked.Increment(ref s_nextMetadataSlot) - 1;
#endregion
/// <summary>
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
/// </summary>