Refactor serialization reference tracking and contexts

- Rework SerializationReferenceTracker to use a unified Bloom filter + HashSet for both IId and reference-based tracking, improving efficiency and reducing allocations.
- Introduce AcSerializeBase as a common base class for serialization contexts; update Binary, JSON, and Toon contexts to inherit from it.
- Move AcBinaryDeserializationException, AcJsonDeserializationException, and TypeConversionInfo to separate files for better organization.
- Remove obsolete code and update documentation to reflect new reference tracking logic.
This commit is contained in:
Loretta 2026-01-17 10:06:46 +01:00
parent 858d43b881
commit 2ab640b375
10 changed files with 237 additions and 165 deletions

View File

@ -0,0 +1,30 @@
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers;
/// <summary>
/// Abstract base class for serialization operations.
/// Contains serialize-specific logic that may need override capability.
///
/// Responsibilities:
/// - Reference scanning (ScanReferences)
/// - Reference tracking during write (TrackForScanning, ShouldWriteId, MarkAsWritten)
/// - IId-aware serialization logic
///
/// Derived classes:
/// - BinarySerializationContext (or similar) for Binary serialization
/// - JsonSerializationContext (or similar) for JSON serialization
/// - ToonSerializationContext for Toon serialization
///
/// Note: Currently SerializationReferenceTracker remains in AcSerializerCommon.
/// As patterns emerge, serialize-specific methods can be moved here.
/// </summary>
public abstract class AcSerializeBase
{
// Future: Move serialize-specific logic here
// - SerializationReferenceTracker (or make it a protected property)
// - Virtual ComputeHash method (for IId vs Reference distinction)
// - Virtual TrackForScanning method
// - Virtual ShouldWriteRef method
// - etc.
}

View File

@ -878,112 +878,144 @@ public static class AcSerializerCommon
/// <summary> /// <summary>
/// Common reference tracking for serialization. /// Common reference tracking for serialization.
/// Used by both JSON and Binary serializers to track multi-referenced objects. /// Uses unified Bloom filter + HashSet for both IId and Reference tracking.
/// Supports both ReferenceEquals-based tracking and IId-based tracking. /// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
/// Uses int IDs for efficiency (no string allocation). /// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
/// </summary> /// </summary>
public sealed class SerializationReferenceTracker public sealed class SerializationReferenceTracker
{ {
private const int InitialReferenceCapacity = 64; private const int InitialCapacity = 128;
private const int InitialMultiRefCapacity = 32;
// Unified Bloom filter (256 bits = 4 x 64-bit)
private ulong _bloom0, _bloom1, _bloom2, _bloom3;
// Unified HashSet for seen hashes (both IId and Reference)
private HashSet<int>? _seenHashes;
// Multi-referenced hashes
private HashSet<int>? _multiRefHashes;
// Written refs: hash → refId
private Dictionary<int, int>? _writtenRefs;
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
private Dictionary<(Type, object), int>? _iidWrittenRefs; // IId-based written refs
private HashSet<object>? _multiReferenced;
private int _nextRefId = 1; private int _nextRefId = 1;
// IId tracker for same-IId-different-instance deduplication
private IIdReferenceTracker? _iidTracker;
/// <summary> /// <summary>
/// Resets the tracker for reuse. /// Resets the tracker for reuse.
/// </summary> /// </summary>
public void Reset() public void Reset()
{ {
_nextRefId = 1; _nextRefId = 1;
_scanOccurrences?.Clear(); _bloom0 = _bloom1 = _bloom2 = _bloom3 = 0;
_seenHashes?.Clear();
_multiRefHashes?.Clear();
_writtenRefs?.Clear(); _writtenRefs?.Clear();
_iidWrittenRefs?.Clear();
_multiReferenced?.Clear();
_iidTracker?.Reset();
} }
/// <summary> /// <summary>
/// Ensures internal collections are initialized. /// Ensures internal collections are initialized.
/// Call once before scanning when reference handling is enabled.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EnsureInitialized() public void EnsureInitialized()
{ {
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); _seenHashes ??= new HashSet<int>(InitialCapacity);
_writtenRefs ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); _multiRefHashes ??= new HashSet<int>();
_iidWrittenRefs ??= new Dictionary<(Type, object), int>(InitialMultiRefCapacity); _writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
} }
/// <summary> /// <summary>
/// Tracks an object during reference scanning phase. /// Computes the tracking hash for an object.
/// Returns true if this is the first occurrence (continue scanning). /// IId objects: positive hash from Id
/// Returns false if already seen (object is multi-referenced, stop scanning this branch). /// Non-IId objects: negative hash from RuntimeHelpers.GetHashCode
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ComputeHash<TMetadata>(object obj, TMetadata metadata) where TMetadata : TypeMetadataBase
{
if (metadata.IsIId)
{
return metadata.IdAccessorType switch
{
IdAccessorType.Int32 => metadata.GetIdInt32(obj),
IdAccessorType.Int64 => (int)(metadata.GetIdInt64(obj) ^ (metadata.GetIdInt64(obj) >> 32)),
IdAccessorType.Guid => metadata.GetIdGuid(obj).GetHashCode() & 0x7FFFFFFF,
_ => metadata.IdGetter?.Invoke(obj)?.GetHashCode() ?? 0
};
}
// Non-IId: use RuntimeHelpers identity hash with high bit set (negative)
return RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
}
/// <summary>
/// Tracks an object during reference scanning phase (non-IId version).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj) public bool TrackForScanning(object obj)
{ {
if (_scanOccurrences == null) return true; var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
return TrackHash(hash);
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists)
{
count++;
_multiReferenced!.Add(obj);
return false;
}
count = 1;
return true;
} }
/// <summary> /// <summary>
/// Extended tracking with IId support. /// Tracks an object during reference scanning phase with IId support.
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals. /// Returns true if first occurrence (continue scanning).
/// Returns true if this is the first occurrence (continue scanning children). /// Returns false if already seen (multi-referenced, stop scanning).
/// Returns false if already seen (stop scanning this branch).
/// </summary> /// </summary>
/// <param name="obj">Object to track</param>
/// <param name="metadata">Type metadata with IId info</param>
/// <param name="existingRefId">If returning false, contains the refId of the existing object (unused, kept for API compatibility)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanningWithIId<TMetadata>(object obj, TMetadata metadata, out int existingRefId) public bool TrackForScanningWithIId<TMetadata>(object obj, TMetadata metadata, out int existingRefId)
where TMetadata : TypeMetadataBase where TMetadata : TypeMetadataBase
{ {
existingRefId = 0; existingRefId = 0;
if (_scanOccurrences == null) return true; var hash = ComputeHash(obj, metadata);
// 1. IId check first (different instance, same Id) // Skip default IId values (Id = 0)
if (metadata.IsIId && _iidTracker != null && _iidTracker.TryGetOriginalObject(obj, metadata, out var originalObject)) if (metadata.IsIId && hash == 0) return true;
{
// Same IId already seen → mark BOTH original and current as multi-referenced return TrackHash(hash);
_multiReferenced!.Add(originalObject!); // Original object
_multiReferenced.Add(obj); // Current object (duplicate)
return false;
} }
// 2. ReferenceEquals check (same instance) /// <summary>
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); /// Core tracking logic using Bloom filter + HashSet.
if (exists) /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TrackHash(int hash)
{ {
count++; // Bloom filter check - fast "definitely new" detection
_multiReferenced!.Add(obj); var segment = (hash >> 6) & 3;
return false; var bit = hash & 63;
} var mask = 1UL << bit;
count = 1;
// 3. Register IId for future lookups var bloomHit = segment switch
if (metadata.IsIId)
{ {
_iidTracker ??= new IIdReferenceTracker(); 0 => (_bloom0 & mask) != 0,
_iidTracker.Register(obj, metadata); 1 => (_bloom1 & mask) != 0,
2 => (_bloom2 & mask) != 0,
_ => (_bloom3 & mask) != 0
};
if (!bloomHit)
{
// Definitely new - add to bloom and set
switch (segment)
{
case 0: _bloom0 |= mask; break;
case 1: _bloom1 |= mask; break;
case 2: _bloom2 |= mask; break;
default: _bloom3 |= mask; break;
}
_seenHashes ??= new HashSet<int>(InitialCapacity);
_seenHashes.Add(hash);
return true;
}
// Possible duplicate - check HashSet
_seenHashes ??= new HashSet<int>(InitialCapacity);
if (!_seenHashes.Add(hash))
{
// Already seen - multi-referenced!
_multiRefHashes ??= new HashSet<int>();
_multiRefHashes.Add(hash);
return false;
} }
return true; return true;
@ -991,16 +1023,37 @@ public static class AcSerializerCommon
/// <summary> /// <summary>
/// Checks if object needs a reference ID during serialization. /// Checks if object needs a reference ID during serialization.
/// Returns true if object is multi-referenced and hasn't been written yet.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteId(object obj, out int refId) public bool ShouldWriteId(object obj, out int refId)
{ {
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj)) var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
return ShouldWriteIdForHash(hash, out refId);
}
/// <summary>
/// Checks if object needs a reference ID (IId-aware version).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteIdForIId<TMetadata>(object obj, TMetadata metadata, out int refId)
where TMetadata : TypeMetadataBase
{
var hash = ComputeHash(obj, metadata);
return ShouldWriteIdForHash(hash, out refId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldWriteIdForHash(int hash, out int refId)
{
if (_multiRefHashes != null && _multiRefHashes.Contains(hash))
{
_writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
if (!_writtenRefs.ContainsKey(hash))
{ {
refId = _nextRefId++; refId = _nextRefId++;
return true; return true;
} }
}
refId = 0; refId = 0;
return false; return false;
} }
@ -1011,22 +1064,20 @@ public static class AcSerializerCommon
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, int refId) public void MarkAsWritten(object obj, int refId)
{ {
var type = obj.GetType(); var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
var idInfo = JsonUtilities.GetIdInfo(type); _writtenRefs![hash] = refId;
// IId típus → IId alapján tároljuk
if (idInfo.IsId && idInfo.IdType != null)
{
var key = GetIIdKey(obj, type, idInfo.IdType);
if (key.HasValue)
{
_iidWrittenRefs![key.Value] = refId;
return;
}
} }
// Nem IId → ReferenceEquals alapján /// <summary>
_writtenRefs![obj] = refId; /// Marks object as written (IId-aware version).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWrittenForIId<TMetadata>(object obj, TMetadata metadata, int refId)
where TMetadata : TypeMetadataBase
{
var hash = ComputeHash(obj, metadata);
_writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
_writtenRefs[hash] = refId;
} }
/// <summary> /// <summary>
@ -1035,42 +1086,30 @@ public static class AcSerializerCommon
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out int refId) public bool TryGetExistingRef(object obj, out int refId)
{ {
var type = obj.GetType(); var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
var idInfo = JsonUtilities.GetIdInfo(type); return TryGetExistingRefForHash(hash, out refId);
// IId típus → IId alapján keresünk
if (idInfo.IsId && idInfo.IdType != null && _iidWrittenRefs != null)
{
var key = GetIIdKey(obj, type, idInfo.IdType);
if (key.HasValue && _iidWrittenRefs.TryGetValue(key.Value, out refId))
{
return true;
}
}
// Nem IId → ReferenceEquals alapján
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
{
return true;
}
refId = 0;
return false;
} }
/// <summary> /// <summary>
/// Gets the (Type, Id) key for IId-based lookup. /// Tries to get existing reference ID (IId-aware version).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static (Type, object)? GetIIdKey(object obj, Type type, Type idType) public bool TryGetExistingRefForIId<TMetadata>(object obj, TMetadata metadata, out int refId)
where TMetadata : TypeMetadataBase
{ {
var idProp = type.GetProperty("Id"); var hash = ComputeHash(obj, metadata);
if (idProp == null) return null; return TryGetExistingRefForHash(hash, out refId);
}
var id = idProp.GetValue(obj); [MethodImpl(MethodImplOptions.AggressiveInlining)]
if (id == null || JsonUtilities.IsDefaultValue(id, idType)) return null; private bool TryGetExistingRefForHash(int hash, out int refId)
{
return (type, id); if (_writtenRefs != null && _writtenRefs.TryGetValue(hash, out refId))
{
return true;
}
refId = 0;
return false;
} }
} }

View File

@ -0,0 +1,17 @@
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Exception thrown when binary deserialization fails.
/// </summary>
public class AcBinaryDeserializationException : Exception
{
public int Position { get; }
public Type? TargetType { get; }
public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null)
: base(message, innerException)
{
Position = position;
TargetType = targetType;
}
}

View File

@ -12,22 +12,6 @@ using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Exception thrown when binary deserialization fails.
/// </summary>
public class AcBinaryDeserializationException : Exception
{
public int Position { get; }
public Type? TargetType { get; }
public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null)
: base(message, innerException)
{
Position = position;
TargetType = targetType;
}
}
/// <summary> /// <summary>
/// High-performance binary deserializer matching AcBinarySerializer. /// High-performance binary deserializer matching AcBinarySerializer.
/// Features: /// Features:
@ -1412,21 +1396,4 @@ public static partial class AcBinaryDeserializer
#endregion #endregion
} }
/// <summary>
/// Cached type conversion info. Using readonly struct to avoid heap allocation.
/// </summary>
readonly struct TypeConversionInfo
{
public readonly Type UnderlyingType;
public readonly TypeCode TypeCode;
public readonly bool IsEnum;
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
{
UnderlyingType = underlyingType;
TypeCode = typeCode;
IsEnum = isEnum;
}
}
// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs // Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs

View File

@ -47,7 +47,7 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// Binary serialization context. Public for generated serializers. /// Binary serialization context. Public for generated serializers.
/// </summary> /// </summary>
internal sealed class BinarySerializationContext : IDisposable internal sealed class BinarySerializationContext : AcSerializeBase, IDisposable
{ {
private const int MinBufferSize = 256; private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512; private const int PropertyIndexBufferMaxCache = 512;

View File

@ -0,0 +1,18 @@
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Cached type conversion info. Using readonly struct to avoid heap allocation.
/// </summary>
readonly struct TypeConversionInfo
{
public readonly Type UnderlyingType;
public readonly TypeCode TypeCode;
public readonly bool IsEnum;
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
{
UnderlyingType = underlyingType;
TypeCode = typeCode;
IsEnum = isEnum;
}
}

View File

@ -0,0 +1,17 @@
namespace AyCode.Core.Serializers.Jsons;
/// <summary>
/// Exception thrown when JSON deserialization fails.
/// </summary>
public class AcJsonDeserializationException : Exception
{
public string? Json { get; }
public Type? TargetType { get; }
public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null)
: base(message, innerException)
{
Json = json?.Length > 500 ? json[..500] + "..." : json;
TargetType = targetType;
}
}

View File

@ -9,22 +9,6 @@ using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Serializers.Jsons;
/// <summary>
/// Exception thrown when JSON deserialization fails.
/// </summary>
public class AcJsonDeserializationException : Exception
{
public string? Json { get; }
public Type? TargetType { get; }
public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null)
: base(message, innerException)
{
Json = json?.Length > 500 ? json[..500] + "..." : json;
TargetType = targetType;
}
}
/// <summary> /// <summary>
/// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling. /// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling.
/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach). /// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).

View File

@ -35,7 +35,7 @@ public static partial class AcJsonSerializer
} }
} }
private sealed class JsonSerializationContext : IDisposable private sealed class JsonSerializationContext : AcSerializeBase, IDisposable
{ {
private readonly ArrayBufferWriter<byte> _buffer; private readonly ArrayBufferWriter<byte> _buffer;
public Utf8JsonWriter Writer { get; private set; } public Utf8JsonWriter Writer { get; private set; }

View File

@ -47,7 +47,7 @@ public static partial class AcToonSerializer
/// Pooled context for Toon serialization. /// Pooled context for Toon serialization.
/// Handles output building, indentation, and reference tracking. /// Handles output building, indentation, and reference tracking.
/// </summary> /// </summary>
private sealed class ToonSerializationContext private sealed class ToonSerializationContext : AcSerializeBase
{ {
private readonly StringBuilder _builder; private readonly StringBuilder _builder;
private Dictionary<object, int>? _scanOccurrences; private Dictionary<object, int>? _scanOccurrences;