Refactor: single-pass ref tracking, restrict IId<T> types

Refactored serialization reference tracking to a single-pass, inline approach, removing the previous two-pass scan. Only int, long, and Guid are now supported as IId<T> types; exotic ID types are no longer allowed. Cleaned up related enum values and code paths, defaulting non-IId types to int-based reference IDs. This simplifies the codebase and improves performance and type safety.
This commit is contained in:
Loretta 2026-01-18 16:25:09 +01:00
parent 8161ddade4
commit 09a61539fa
6 changed files with 87 additions and 133 deletions

View File

@ -511,7 +511,7 @@ public static class JsonUtilities
// FIXED: IsId should be true if IId<T> interface is found, not idType.IsValueType // FIXED: IsId should be true if IId<T> interface is found, not idType.IsValueType
return new IdTypeInfo(true, idType); return new IdTypeInfo(true, idType);
} }
return new IdTypeInfo(false, null); return new IdTypeInfo(false, typeof(int));
}); });
} }

View File

@ -597,16 +597,12 @@ public static class AcSerializerCommon
/// </summary> /// </summary>
public enum IdAccessorType : byte public enum IdAccessorType : byte
{ {
/// <summary>Type does not implement IId.</summary>
None = 0,
/// <summary>Id is int (most common).</summary> /// <summary>Id is int (most common).</summary>
Int32 = 1, Int32 = 1,
/// <summary>Id is long.</summary> /// <summary>Id is long.</summary>
Int64 = 2, Int64 = 2,
/// <summary>Id is Guid.</summary> /// <summary>Id is Guid.</summary>
Guid = 3, Guid = 3,
/// <summary>Id is an exotic type (uses boxing fallback).</summary>
Object = 255
} }
/// <summary> /// <summary>
@ -688,7 +684,6 @@ public static class AcSerializerCommon
IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject), IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject),
IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject), IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject),
IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject), IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject),
IdAccessorType.Object => TryGetObjectOriginal(obj, metadata, out originalObject),
_ => false _ => false
}; };
} }
@ -768,9 +763,6 @@ public static class AcSerializerCommon
case IdAccessorType.Guid: case IdAccessorType.Guid:
RegisterGuid(obj, metadata); RegisterGuid(obj, metadata);
break; break;
case IdAccessorType.Object:
RegisterObject(obj, metadata);
break;
} }
} }

View File

@ -936,7 +936,6 @@ public static partial class AcBinaryDeserializer
AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance), AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance),
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance), AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance), AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
AcSerializerCommon.IdAccessorType.Object when metadata.IdGetter != null => metadata.IdGetter(instance),
_ => null _ => null
}; };

View File

@ -122,6 +122,9 @@ public static partial class AcBinarySerializer
{ {
_refTracker.EnsureInitialized(); _refTracker.EnsureInitialized();
} }
// Reset wrapper tracking state from base class (IId tracking)
base.Reset();
if (_buffer.Length < _initialBufferSize) if (_buffer.Length < _initialBufferSize)
{ {

View File

@ -175,13 +175,8 @@ public static partial class AcBinarySerializer
var context = BinarySerializationContextPool.Get(options); var context = BinarySerializationContextPool.Get(options);
context.WriteHeaderPlaceholder(); context.WriteHeaderPlaceholder();
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(runtimeType)) // Single-pass serialization - no scan phase needed!
{ // Reference tracking happens inline via TryTrack during WriteObject
ScanReferences(value, context, 0);
}
// Property index-based serialization - no metadata registration needed!
// PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
// Estimate and reserve header space to avoid body shift later // Estimate and reserve header space to avoid body shift later
var estimatedHeaderSize = context.EstimateHeaderPayloadSize(); var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
@ -194,73 +189,6 @@ public static partial class AcBinarySerializer
#endregion #endregion
#region Reference Scanning
private static void ScanReferences(object? value, BinarySerializationContext context, int depth)
{
if (value == null || depth > context.MaxDepth) return;
var type = value.GetType();
if (IsPrimitiveOrStringFast(type)) return;
// Get wrapper for IId-aware tracking
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
// OPTIMIZATION: Skip types that don't need reference tracking
// (no IId, no complex properties that could be shared)
if (!metadata.NeedsReferenceTracking) return;
// Use IId-aware tracking if metadata is available and UseReferenceHandling is enabled
if (!context.TrackForScanningWithIId(value, metadata, out _)) return;
if (value is byte[]) return; // byte arrays are value types
if (value is IDictionary dictionary)
{
foreach (DictionaryEntry entry in dictionary)
{
if (entry.Value != null)
ScanReferences(entry.Value, context, depth + 1);
}
return;
}
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{
foreach (var item in enumerable)
{
if (item != null)
ScanReferences(item, context, depth + 1);
}
return;
}
// OPTIMIZATION: Skip if no complex properties to scan
if (!metadata.HasComplexProperties) return;
// Scan only complex properties using pre-computed IsComplexType flag
var properties = metadata.Properties;
for (var i = 0; i < properties.Length; i++)
{
var prop = properties[i];
// Skip primitive properties - use pre-computed flag, no method call!
if (!prop.IsComplexType) continue;
if (!context.ShouldSerializeProperty(value, prop))
{
continue;
}
var propValue = prop.GetValue(value);
if (propValue != null)
ScanReferences(propValue, context, depth + 1);
}
}
#endregion
#region Property Metadata Registration #region Property Metadata Registration
private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null) private static void RegisterMetadataForType(Type type, BinarySerializationContext context, HashSet<Type>? visited = null)
@ -365,15 +293,7 @@ public static partial class AcBinarySerializer
return; return;
} }
// Check for object reference // Handle byte arrays specially (value-like, no reference tracking)
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
{
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(refId);
return;
}
// Handle byte arrays specially
if (value is byte[] byteArray) if (value is byte[] byteArray)
{ {
WriteByteArray(byteArray, context); WriteByteArray(byteArray, context);
@ -394,7 +314,7 @@ public static partial class AcBinarySerializer
return; return;
} }
// Handle complex objects // Handle complex objects with single-pass reference tracking
WriteObject(value, type, context, depth); WriteObject(value, type, context, depth);
} }
@ -711,21 +631,57 @@ public static partial class AcBinarySerializer
private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth) private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth)
{ {
context.WriteByte(BinaryTypeCode.Object);
// Register object reference if needed
if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
{
context.WriteVarInt(refId);
context.MarkAsWritten(value, refId);
}
else if (context.UseReferenceHandling)
{
context.WriteVarInt(-1); // No ref ID
}
var wrapper = context.GetWrapper(type); var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
// Single-pass reference tracking
if (context.UseReferenceHandling)
{
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
if (!context.TryTrack(wrapper, value, out int intId))
{
// Already seen → write reference
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(intId);
return;
}
// First occurrence → write object with refId
context.WriteByte(BinaryTypeCode.Object);
context.WriteVarInt(intId);
break;
case AcSerializerCommon.IdAccessorType.Int64:
if (!context.TryTrack(wrapper, value, out long longId))
{
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarLong(longId);
return;
}
context.WriteByte(BinaryTypeCode.Object);
context.WriteVarLong(longId);
break;
case AcSerializerCommon.IdAccessorType.Guid:
if (!context.TryTrack(wrapper, value, out Guid guidId))
{
context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteGuidBits(guidId);
return;
}
context.WriteByte(BinaryTypeCode.Object);
context.WriteGuidBits(guidId);
break;
}
}
else
{
// No reference handling - just write object marker
context.WriteByte(BinaryTypeCode.Object);
}
// Write properties
var nextDepth = depth + 1; var nextDepth = depth + 1;
var properties = metadata.Properties; var properties = metadata.Properties;
var propCount = properties.Length; var propCount = properties.Length;

View File

@ -144,7 +144,7 @@ public abstract class TypeMetadataBase
SourceType = type; SourceType = type;
_ignorePropertyFilter = ignorePropertyFilter; _ignorePropertyFilter = ignorePropertyFilter;
CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type); CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type);
// Pre-compute property arrays - no dictionary lookup needed later! // Pre-compute property arrays - no dictionary lookup needed later!
// Uses static global cache for unfiltered properties, then applies filter once // Uses static global cache for unfiltered properties, then applies filter once
var allReadable = GetUnfilteredProperties(type, requiresWrite: false) var allReadable = GetUnfilteredProperties(type, requiresWrite: false)
@ -152,40 +152,44 @@ public abstract class TypeMetadataBase
.ToArray(); .ToArray();
ReadableProperties = allReadable; ReadableProperties = allReadable;
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray(); WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
// Cache IId info at construction time - no runtime reflection needed later! // Cache IId info at construction time - no runtime reflection needed later!
var idInfo = GetIdInfo(type); var idInfo = GetIdInfo(type);
IsIId = idInfo.IsId; IsIId = idInfo.IsId;
IdType = idInfo.IdType; IdType = idInfo.IdType;
if (IsIId && IdType != null) if (IsIId)
{ {
var idProp = type.GetProperty("Id"); var idProp = type.GetProperty("Id");
IdPropertyInfo = idProp; // Store for TypeMetadataWrapper IdPropertyInfo = idProp; // Store for TypeMetadataWrapper
if (idProp != null)
// Create typed getter for the three common Id types to avoid boxing
if (ReferenceEquals(IdType, IntType))
{ {
// Create typed getter for the three common Id types to avoid boxing IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
if (ReferenceEquals(IdType, IntType)) _typedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
}
else if (ReferenceEquals(IdType, LongType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Int64;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
}
else if (ReferenceEquals(IdType, GuidType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Guid;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
}
else
{
// Exotic Id types not supported - only int, long, Guid
throw new NotSupportedException($"Unsupported IId type: {IdType.Name}. Only int, long, and Guid are supported.");
}
} }
else if (ReferenceEquals(IdType, LongType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Int64;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
}
else if (ReferenceEquals(IdType, GuidType))
{
IdAccessorType = AcSerializerCommon.IdAccessorType.Guid;
_typedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
}
else
{
throw new NotSupportedException($"Unsupported IId type: {IdType.Name}. Only int, long, and Guid are supported.");
}
}
else
{
// Non-IId types: use RuntimeHelpers.GetHashCode (int)
// RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter()
IdAccessorType = AcSerializerCommon.IdAccessorType.Int32;
// _typedIdGetter remains null - wrapper uses GetHashCode directly
} }
} }