diff --git a/AyCode.Core/Serializers/AcSerializeBase.cs b/AyCode.Core/Serializers/AcSerializeBase.cs new file mode 100644 index 0000000..f1d36c0 --- /dev/null +++ b/AyCode.Core/Serializers/AcSerializeBase.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers; + +/// +/// 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. +/// +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. +} diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index a59ce38..ed92ead 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -878,23 +878,27 @@ public static class AcSerializerCommon /// /// 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) /// 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? _seenHashes; + + // Multi-referenced hashes + private HashSet? _multiRefHashes; + + // Written refs: hash → refId + private Dictionary? _writtenRefs; - private Dictionary? _scanOccurrences; - private Dictionary? _writtenRefs; - private Dictionary<(Type, object), int>? _iidWrittenRefs; // IId-based written refs - private HashSet? _multiReferenced; private int _nextRefId = 1; - - // IId tracker for same-IId-different-instance deduplication - private IIdReferenceTracker? _iidTracker; /// /// Resets the tracker for reuse. @@ -902,104 +906,153 @@ public static class AcSerializerCommon public void Reset() { _nextRefId = 1; - _scanOccurrences?.Clear(); + _bloom0 = _bloom1 = _bloom2 = _bloom3 = 0; + _seenHashes?.Clear(); + _multiRefHashes?.Clear(); _writtenRefs?.Clear(); - _iidWrittenRefs?.Clear(); - _multiReferenced?.Clear(); - _iidTracker?.Reset(); } /// /// Ensures internal collections are initialized. - /// Call once before scanning when reference handling is enabled. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void EnsureInitialized() { - _scanOccurrences ??= new Dictionary(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); - _writtenRefs ??= new Dictionary(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); - _iidWrittenRefs ??= new Dictionary<(Type, object), int>(InitialMultiRefCapacity); - _multiReferenced ??= new HashSet(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance); + _seenHashes ??= new HashSet(InitialCapacity); + _multiRefHashes ??= new HashSet(); + _writtenRefs ??= new Dictionary(InitialCapacity); } /// - /// 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 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeHash(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); + } + + /// + /// Tracks an object during reference scanning phase (non-IId version). /// [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); } /// - /// 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). /// - /// Object to track - /// Type metadata with IId info - /// If returning false, contains the refId of the existing object (unused, kept for API compatibility) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TrackForScanningWithIId(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)) + // Skip default IId values (Id = 0) + if (metadata.IsIId && hash == 0) return true; + + return TrackHash(hash); + } + + /// + /// Core tracking logic using Bloom filter + HashSet. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TrackHash(int hash) + { + // Bloom filter check - fast "definitely new" detection + var segment = (hash >> 6) & 3; + var bit = hash & 63; + var mask = 1UL << bit; + + var bloomHit = segment switch { - // 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; + 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(InitialCapacity); + _seenHashes.Add(hash); + return true; } - // 2. ReferenceEquals check (same instance) - ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists); - if (exists) + // Possible duplicate - check HashSet + _seenHashes ??= new HashSet(InitialCapacity); + if (!_seenHashes.Add(hash)) { - count++; - _multiReferenced!.Add(obj); + // Already seen - multi-referenced! + _multiRefHashes ??= new HashSet(); + _multiRefHashes.Add(hash); return false; } - count = 1; - - // 3. Register IId for future lookups - if (metadata.IsIId) - { - _iidTracker ??= new IIdReferenceTracker(); - _iidTracker.Register(obj, metadata); - } return true; } /// /// Checks if object needs a reference ID during serialization. - /// Returns true if object is multi-referenced and hasn't been written yet. /// [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); + } + + /// + /// Checks if object needs a reference ID (IId-aware version). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldWriteIdForIId(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)) { - refId = _nextRefId++; - return true; + _writtenRefs ??= new Dictionary(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; - } - } - - // Nem IId → ReferenceEquals alapján - _writtenRefs![obj] = refId; + var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000); + _writtenRefs![hash] = refId; + } + + /// + /// Marks object as written (IId-aware version). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkAsWrittenForIId(object obj, TMetadata metadata, int refId) + where TMetadata : TypeMetadataBase + { + var hash = ComputeHash(obj, metadata); + _writtenRefs ??= new Dictionary(InitialCapacity); + _writtenRefs[hash] = refId; } /// @@ -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); } /// - /// Gets the (Type, Id) key for IId-based lookup. + /// Tries to get existing reference ID (IId-aware version). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (Type, object)? GetIIdKey(object obj, Type type, Type idType) + public bool TryGetExistingRefForIId(object obj, TMetadata metadata, out int refId) + where TMetadata : TypeMetadataBase { - var idProp = type.GetProperty("Id"); - if (idProp == null) return null; - - var id = idProp.GetValue(obj); - if (id == null || JsonUtilities.IsDefaultValue(id, idType)) return null; - - return (type, id); + var hash = ComputeHash(obj, metadata); + return TryGetExistingRefForHash(hash, out refId); + } + + [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; } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializationException.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializationException.cs new file mode 100644 index 0000000..c3fbaac --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializationException.cs @@ -0,0 +1,17 @@ +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Exception thrown when binary deserialization fails. +/// +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; + } +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index a6ef251..57fda80 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -12,22 +12,6 @@ using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; -/// -/// Exception thrown when binary deserialization fails. -/// -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; - } -} - /// /// High-performance binary deserializer matching AcBinarySerializer. /// Features: @@ -1412,21 +1396,4 @@ public static partial class AcBinaryDeserializer #endregion } -/// -/// Cached type conversion info. Using readonly struct to avoid heap allocation. -/// -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 diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index c9a3a0e..d28775b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -47,7 +47,7 @@ public static partial class AcBinarySerializer /// /// Binary serialization context. Public for generated serializers. /// - internal sealed class BinarySerializationContext : IDisposable + internal sealed class BinarySerializationContext : AcSerializeBase, IDisposable { private const int MinBufferSize = 256; private const int PropertyIndexBufferMaxCache = 512; diff --git a/AyCode.Core/Serializers/Binaries/TypeConversionInfo.cs b/AyCode.Core/Serializers/Binaries/TypeConversionInfo.cs new file mode 100644 index 0000000..65e7fbf --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/TypeConversionInfo.cs @@ -0,0 +1,18 @@ +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Cached type conversion info. Using readonly struct to avoid heap allocation. +/// +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; + } +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializationException.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializationException.cs new file mode 100644 index 0000000..4fea839 --- /dev/null +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializationException.cs @@ -0,0 +1,17 @@ +namespace AyCode.Core.Serializers.Jsons; + +/// +/// Exception thrown when JSON deserialization fails. +/// +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; + } +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs index f1e78d4..60aec2b 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs @@ -9,22 +9,6 @@ using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Jsons; -/// -/// Exception thrown when JSON deserialization fails. -/// -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; - } -} - /// /// High-performance custom JSON deserializer optimized for IId<T> reference handling. /// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach). diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs index 95e25df..07013f8 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs @@ -35,7 +35,7 @@ public static partial class AcJsonSerializer } } - private sealed class JsonSerializationContext : IDisposable + private sealed class JsonSerializationContext : AcSerializeBase, IDisposable { private readonly ArrayBufferWriter _buffer; public Utf8JsonWriter Writer { get; private set; } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs index ae6420d..4e064cd 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs @@ -47,7 +47,7 @@ public static partial class AcToonSerializer /// Pooled context for Toon serialization. /// Handles output building, indentation, and reference tracking. /// - private sealed class ToonSerializationContext + private sealed class ToonSerializationContext : AcSerializeBase { private readonly StringBuilder _builder; private Dictionary? _scanOccurrences;