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>
/// Common reference tracking for serialization.
/// Used by both JSON and Binary serializers to track multi-referenced objects.
/// Supports both ReferenceEquals-based tracking and IId-based tracking.
/// Uses int IDs for efficiency (no string allocation).
/// Uses unified Bloom filter + HashSet for both IId and Reference tracking.
/// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
/// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
/// </summary>
public sealed class SerializationReferenceTracker
{
private const int InitialReferenceCapacity = 64;
private const int InitialMultiRefCapacity = 32;
private const int InitialCapacity = 128;
// 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;
// IId tracker for same-IId-different-instance deduplication
private IIdReferenceTracker? _iidTracker;
/// <summary>
/// Resets the tracker for reuse.
/// </summary>
public void Reset()
{
_nextRefId = 1;
_scanOccurrences?.Clear();
_bloom0 = _bloom1 = _bloom2 = _bloom3 = 0;
_seenHashes?.Clear();
_multiRefHashes?.Clear();
_writtenRefs?.Clear();
_iidWrittenRefs?.Clear();
_multiReferenced?.Clear();
_iidTracker?.Reset();
}
/// <summary>
/// Ensures internal collections are initialized.
/// Call once before scanning when reference handling is enabled.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EnsureInitialized()
{
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
_writtenRefs ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
_iidWrittenRefs ??= new Dictionary<(Type, object), int>(InitialMultiRefCapacity);
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
_seenHashes ??= new HashSet<int>(InitialCapacity);
_multiRefHashes ??= new HashSet<int>();
_writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
}
/// <summary>
/// Tracks an object during reference scanning phase.
/// Returns true if this is the first occurrence (continue scanning).
/// Returns false if already seen (object is multi-referenced, stop scanning this branch).
/// Computes the tracking hash for an object.
/// IId objects: positive hash from Id
/// 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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj)
{
if (_scanOccurrences == null) return true;
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;
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
return TrackHash(hash);
}
/// <summary>
/// Extended tracking with IId support.
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals.
/// Returns true if this is the first occurrence (continue scanning children).
/// Returns false if already seen (stop scanning this branch).
/// Tracks an object during reference scanning phase with IId support.
/// Returns true if first occurrence (continue scanning).
/// Returns false if already seen (multi-referenced, stop scanning).
/// </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)]
public bool TrackForScanningWithIId<TMetadata>(object obj, TMetadata metadata, out int existingRefId)
where TMetadata : TypeMetadataBase
{
existingRefId = 0;
if (_scanOccurrences == null) return true;
var hash = ComputeHash(obj, metadata);
// 1. IId check first (different instance, same Id)
if (metadata.IsIId && _iidTracker != null && _iidTracker.TryGetOriginalObject(obj, metadata, out var originalObject))
{
// Same IId already seen → mark BOTH original and current as multi-referenced
_multiReferenced!.Add(originalObject!); // Original object
_multiReferenced.Add(obj); // Current object (duplicate)
return false;
// Skip default IId values (Id = 0)
if (metadata.IsIId && hash == 0) return true;
return TrackHash(hash);
}
// 2. ReferenceEquals check (same instance)
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists)
/// <summary>
/// Core tracking logic using Bloom filter + HashSet.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TrackHash(int hash)
{
count++;
_multiReferenced!.Add(obj);
return false;
}
count = 1;
// Bloom filter check - fast "definitely new" detection
var segment = (hash >> 6) & 3;
var bit = hash & 63;
var mask = 1UL << bit;
// 3. Register IId for future lookups
if (metadata.IsIId)
var bloomHit = segment switch
{
_iidTracker ??= new IIdReferenceTracker();
_iidTracker.Register(obj, metadata);
0 => (_bloom0 & mask) != 0,
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;
@ -991,16 +1023,37 @@ public static class AcSerializerCommon
/// <summary>
/// Checks if object needs a reference ID during serialization.
/// Returns true if object is multi-referenced and hasn't been written yet.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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++;
return true;
}
}
refId = 0;
return false;
}
@ -1011,22 +1064,20 @@ public static class AcSerializerCommon
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, int refId)
{
var type = obj.GetType();
var idInfo = JsonUtilities.GetIdInfo(type);
// 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;
}
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
_writtenRefs![hash] = refId;
}
// Nem IId → ReferenceEquals alapján
_writtenRefs![obj] = refId;
/// <summary>
/// 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>
@ -1035,42 +1086,30 @@ public static class AcSerializerCommon
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out int refId)
{
var type = obj.GetType();
var idInfo = JsonUtilities.GetIdInfo(type);
// 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;
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
return TryGetExistingRefForHash(hash, out refId);
}
/// <summary>
/// Gets the (Type, Id) key for IId-based lookup.
/// Tries to get existing reference ID (IId-aware version).
/// </summary>
[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");
if (idProp == null) return null;
var hash = ComputeHash(obj, metadata);
return TryGetExistingRefForHash(hash, out refId);
}
var id = idProp.GetValue(obj);
if (id == null || JsonUtilities.IsDefaultValue(id, idType)) return null;
return (type, id);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetExistingRefForHash(int hash, out int refId)
{
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;
/// <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>
/// High-performance binary deserializer matching AcBinarySerializer.
/// Features:
@ -1412,21 +1396,4 @@ public static partial class AcBinaryDeserializer
#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

View File

@ -47,7 +47,7 @@ public static partial class AcBinarySerializer
/// <summary>
/// Binary serialization context. Public for generated serializers.
/// </summary>
internal sealed class BinarySerializationContext : IDisposable
internal sealed class BinarySerializationContext : AcSerializeBase, IDisposable
{
private const int MinBufferSize = 256;
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;
/// <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>
/// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling.
/// 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;
public Utf8JsonWriter Writer { get; private set; }

View File

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