From 8161ddade4d72cf59f5bc14a955dc349d2d80aad Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 18 Jan 2026 15:31:45 +0100 Subject: [PATCH] Refactor: unify metadata and tracking for serializer contexts Major refactor of serialization infrastructure: - Removed AcSerializeBase; replaced with AcSerializerContextBase for unified context management. - Added TypeMetadataWrapper to combine metadata and per-context tracking state. - All serializer contexts now inherit from AcSerializerContextBase and use context.GetWrapper(type) for metadata and tracking. - Reference tracking for IId types is now type-safe and efficient (bitmaps for small int IDs, generic identity maps for others). - Removed generic ThreadLocal caching from TypeMetadataBase; caching now uses global ConcurrentDictionary. - Updated all type metadata classes to inherit from non-generic base. - Added IdPropertyInfo and MetadataType to TypeMetadataBase. - Added stub context base classes for JSON and Toon. This centralizes and optimizes metadata/tracking, improves performance, and prepares for future extensibility. --- AyCode.Core/Serializers/AcSerializeBase.cs | 30 --- AyCode.Core/Serializers/AcSerializerCommon.cs | 46 +++++ .../Serializers/AcSerializerContextBase.cs | 187 ++++++++++++++++++ ...erializer.BinaryDeserializeTypeMetadata.cs | 2 +- .../Binaries/AcBinaryDeserializer.cs | 9 +- ...rySerializer.BinarySerializationContext.cs | 9 +- ...ySerializer.BinarySerializeTypeMetadata.cs | 2 +- .../Binaries/AcBinarySerializer.cs | 18 +- .../DeserializeTypeMetadataBase.cs | 4 +- .../Serializers/Jsons/AcJsonContextBase.cs | 4 + ...Deserializer.JsonDeserializationContext.cs | 13 +- ...eserializer.JsonDeserializeTypeMetadata.cs | 6 +- .../Serializers/Jsons/AcJsonDeserializer.cs | 8 + ...JsonSerializer.JsonSerializationContext.cs | 8 +- ...sonSerializer.JsonSerializeTypeMetadata.cs | 6 +- .../Serializers/Jsons/AcJsonSerializer.cs | 6 +- .../Serializers/SerializeTypeMetadataBase.cs | 5 +- .../Serializers/Toons/AcToonContextBase.cs | 4 + ...ToonSerializer.ToonSerializationContext.cs | 8 +- ...oonSerializer.ToonSerializeTypeMetadata.cs | 2 +- .../Serializers/Toons/AcToonSerializer.cs | 10 +- AyCode.Core/Serializers/TypeMetadataBase.cs | 73 ++----- .../Serializers/TypeMetadataWrapper.cs | 95 +++++++++ 23 files changed, 417 insertions(+), 138 deletions(-) delete mode 100644 AyCode.Core/Serializers/AcSerializeBase.cs create mode 100644 AyCode.Core/Serializers/AcSerializerContextBase.cs create mode 100644 AyCode.Core/Serializers/Jsons/AcJsonContextBase.cs create mode 100644 AyCode.Core/Serializers/Toons/AcToonContextBase.cs create mode 100644 AyCode.Core/Serializers/TypeMetadataWrapper.cs diff --git a/AyCode.Core/Serializers/AcSerializeBase.cs b/AyCode.Core/Serializers/AcSerializeBase.cs deleted file mode 100644 index f1d36c0..0000000 --- a/AyCode.Core/Serializers/AcSerializeBase.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 ed92ead..87ca528 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -609,6 +609,52 @@ public static class AcSerializerCommon Object = 255 } + /// + /// Interface for identity maps used in serialization tracking. + /// Enables type-safe Reset() without knowing the generic type parameter. + /// + public interface IIdentityMap + { + /// + /// Resets the identity map for reuse between serializations. + /// + void Reset(); + } + + /// + /// Generic identity map for tracking IId values during serialization. + /// Uses JIT-optimized EqualityComparer for maximum performance with common ID types. + /// + /// The ID type (int, long, Guid) + public sealed class IdentityMap : IIdentityMap where TId : notnull + { + private readonly HashSet _seenIds; + + public IdentityMap() + { + _seenIds = new HashSet(EqualityComparer.Default); + } + + /// + /// Tries to add an ID to the tracking set. + /// Returns true if this is the first occurrence (ID was added). + /// Returns false if already seen. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(TId id) + { + return _seenIds.Add(id); + } + + /// + /// Resets the identity map for reuse. + /// + public void Reset() + { + _seenIds.Clear(); + } + } + /// /// IId-based reference tracking for serialization. /// Supplements (not replaces) the ReferenceEquals-based SerializationReferenceTracker. diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs new file mode 100644 index 0000000..051a9da --- /dev/null +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers; + +/// +/// Base class for all serializer contexts. +/// Provides GetWrapper for type metadata access with per-context tracking state. +/// GlobalMetadataCache stores metadata (thread-safe), wrappers store per-context tracking. +/// +/// The concrete metadata type. +public abstract class AcSerializerContextBase where TMetadata : TypeMetadataBase +{ + /// + /// Global shared cache for metadata (thread-safe, shared across all contexts). + /// Generic specialization ensures separate cache per TMetadata type. + /// + private static readonly ConcurrentDictionary GlobalMetadataCache = new(); + + /// + /// Per-context wrappers containing metadata + tracking state. + /// + private readonly Dictionary> _wrappers = new(); + + /// + /// Factory function to create metadata. Implemented by derived class. + /// + protected abstract Func MetadataFactory { get; } + + private const int BitArraySize = 1024; + private const int MaxSmallId = BitArraySize * 64; + + #region Wrapper Access + + /// + /// Gets or creates a wrapper for the specified type. + /// The wrapper contains metadata (from GlobalMetadataCache) + per-context tracking state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TypeMetadataWrapper GetWrapper(Type type) + { + if (_wrappers.TryGetValue(type, out var wrapper)) + return wrapper; + + return GetWrapperSlow(type); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private TypeMetadataWrapper GetWrapperSlow(Type type) + { + // Get metadata from global cache (thread-safe) + var metadata = GlobalMetadataCache.GetOrAdd(type, MetadataFactory); + + // Create wrapper with metadata + tracking state (per-context) + var wrapper = new TypeMetadataWrapper(metadata); + _wrappers[type] = wrapper; + return wrapper; + } + + #endregion + + #region Tracking API - int + + /// + /// Tries to track an object with int RefId. + /// Use when wrapper.Metadata.IdAccessorType == Int32. + /// Returns true if first occurrence. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryTrack(TypeMetadataWrapper wrapper, object obj, out int refId) + { + Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32); + + var getter = (Func)wrapper.RefIdGetter; + refId = getter(obj); + + // BitArray fast path for small positive IDs + if (refId >= 0 && refId < MaxSmallId) + { + return TryTrackSmallId(wrapper, refId); + } + + // IdentityMap for large/negative IDs + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + return map.TryAdd(refId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryTrackSmallId(TypeMetadataWrapper wrapper, int id) + { + wrapper.EnsureSmallIdBitmap(); + var bitmap = wrapper.SmallIdBitmap!; + + uint idx = (uint)id; + uint wordIdx = idx >> 6; + int bitIdx = (int)(idx & 63); + + ulong mask = 1UL << bitIdx; + ref ulong word = ref bitmap[wordIdx]; + + if ((word & mask) == 0) + { + word |= mask; + return true; + } + + return false; + } + + #endregion + + #region Tracking API - long + + /// + /// Tries to track an object with long RefId. + /// Use when wrapper.Metadata.IdAccessorType == Int64. + /// Returns true if first occurrence. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryTrack(TypeMetadataWrapper wrapper, object obj, out long refId) + { + Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64); + + var getter = (Func)wrapper.RefIdGetter; + refId = getter(obj); + + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + return map.TryAdd(refId); + } + + #endregion + + #region Tracking API - Guid + + /// + /// Tries to track an object with Guid RefId. + /// Use when wrapper.Metadata.IdAccessorType == Guid. + /// Returns true if first occurrence. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryTrack(TypeMetadataWrapper wrapper, object obj, out Guid refId) + { + Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid); + + var getter = (Func)wrapper.RefIdGetter; + refId = getter(obj); + + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + return map.TryAdd(refId); + } + + #endregion + + #region Reset + + /// + /// Resets all wrapper tracking states for reuse. + /// Does not remove wrappers - keeps them for next operation. + /// + public virtual void Reset() + { + foreach (var wrapper in _wrappers.Values) + { + wrapper.ResetTracking(); + } + } + + #endregion +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index c523a00..746c28c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { - internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase + internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase { /// /// Properties array ordered alphabetically by name for index-based lookup. diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 57fda80..ca0f687 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -1358,13 +1358,12 @@ public static partial class AcBinaryDeserializer #region Type Metadata - /// - /// Gets type metadata with ThreadLocal caching for hot path optimization. - /// Uses built-in cache from BinaryDeserializeTypeMetadata base class (zero ref parameter overhead). - /// + // Temporary: own cache until ref struct is removed + private static readonly ConcurrentDictionary MetadataCache = new(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) - => BinaryDeserializeTypeMetadata.GetOrCreateMetadata(type, static t => new BinaryDeserializeTypeMetadata(t)); + => MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index d28775b..a93563f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Text; +using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; @@ -47,7 +48,7 @@ public static partial class AcBinarySerializer /// /// Binary serialization context. Public for generated serializers. /// - internal sealed class BinarySerializationContext : AcSerializeBase, IDisposable + internal sealed class BinarySerializationContext : AcSerializerContextBase, IDisposable { private const int MinBufferSize = 256; private const int PropertyIndexBufferMaxCache = 512; @@ -99,6 +100,12 @@ public static partial class AcBinarySerializer Reset(options); } + /// + /// Factory for creating BinarySerializeTypeMetadata instances. + /// + protected override Func MetadataFactory + => static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute); + public void Reset(AcBinarySerializerOptions options) { _position = 0; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs index a82cb85..d601c6e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs @@ -9,7 +9,7 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinarySerializer { - internal sealed class BinarySerializeTypeMetadata : SerializeTypeMetadataBase + internal sealed class BinarySerializeTypeMetadata : SerializeTypeMetadataBase { public BinaryPropertyAccessor[] Properties { get; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 65b7ac3..14b1757 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -203,8 +203,9 @@ public static partial class AcBinarySerializer var type = value.GetType(); if (IsPrimitiveOrStringFast(type)) return; - // Get metadata for IId-aware tracking - var metadata = GetTypeMetadata(type); + // 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) @@ -286,7 +287,8 @@ public static partial class AcBinarySerializer return; } - var metadata = GetTypeMetadata(type); + var wrapper = context.GetWrapper(type); + var metadata = wrapper.Metadata; var properties = metadata.Properties; // Use index-based iteration for array access @@ -722,7 +724,8 @@ public static partial class AcBinarySerializer context.WriteVarInt(-1); // No ref ID } - var metadata = GetTypeMetadata(type); + var wrapper = context.GetWrapper(type); + var metadata = wrapper.Metadata; var nextDepth = depth + 1; var properties = metadata.Properties; var propCount = properties.Length; @@ -1248,13 +1251,6 @@ public static partial class AcBinarySerializer return null; } - /// - /// Gets type metadata with ThreadLocal caching for hot path optimization. - /// Uses built-in cache from BinarySerializeTypeMetadata base class (zero ref parameter overhead). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BinarySerializeTypeMetadata GetTypeMetadata(Type type) - => BinarySerializeTypeMetadata.GetOrCreateMetadata(type, static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute)); // Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs diff --git a/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs b/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs index a1cd6cd..e1071c1 100644 --- a/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs +++ b/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs @@ -6,11 +6,9 @@ namespace AyCode.Core.Serializers; /// Base class for deserializer type metadata. /// Extends TypeMetadataBase for deserializer-specific functionality. /// Used by both JSON and Binary deserializers. -/// Generic version provides built-in ThreadLocal caching with zero ref parameter overhead. /// Note: IId detection (IsIId, IdType, IdGetter) is now in TypeMetadataBase for use by both serializers and deserializers. /// -public abstract class DeserializeTypeMetadataBase : TypeMetadataBase - where TMetadata : DeserializeTypeMetadataBase +public abstract class DeserializeTypeMetadataBase : TypeMetadataBase { protected DeserializeTypeMetadataBase(Type type, Func ignorePropertyFilter) : base(type, ignorePropertyFilter) { diff --git a/AyCode.Core/Serializers/Jsons/AcJsonContextBase.cs b/AyCode.Core/Serializers/Jsons/AcJsonContextBase.cs new file mode 100644 index 0000000..373b4a1 --- /dev/null +++ b/AyCode.Core/Serializers/Jsons/AcJsonContextBase.cs @@ -0,0 +1,4 @@ +public class AcJsonContextBase +{ + +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs index b66b9aa..dab5fd5 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs @@ -45,7 +45,7 @@ public static partial class AcJsonDeserializer public readonly int RefId = refId; } - private sealed class DeserializationContext + private sealed class DeserializationContext : AcSerializerContextBase { // Use shared reference tracker from AcSerializerCommon private readonly AcSerializerCommon.DeserializationReferenceTracker _refTracker = new(); @@ -66,12 +66,18 @@ public static partial class AcJsonDeserializer /// public bool IsChainMode => ChainTracker != null; + /// + /// Factory for creating JsonDeserializeTypeMetadata instances. + /// + protected override Func MetadataFactory + => static t => new JsonDeserializeTypeMetadata(t); + public DeserializationContext(in AcJsonSerializerOptions options) { Reset(options); } - public void Reset(in AcJsonSerializerOptions options) + public new void Reset(in AcJsonSerializerOptions options) { UseReferenceHandling = options.UseReferenceHandling; MaxDepth = options.MaxDepth; @@ -80,8 +86,9 @@ public static partial class AcJsonDeserializer _refTracker.Reset(); } - public void Clear() + public new void Clear() { + base.Reset(); _refTracker.Reset(); _propertiesToResolve?.Clear(); ChainTracker = null; diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs index 2b00f17..b59965e 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs @@ -11,15 +11,11 @@ namespace AyCode.Core.Serializers.Jsons; public static partial class AcJsonDeserializer { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static JsonDeserializeTypeMetadata GetTypeMetadata(in Type type) - => JsonDeserializeTypeMetadata.GetOrCreateMetadata(type, static t => new JsonDeserializeTypeMetadata(t)); - /// /// JSON deserialization type metadata. /// Extends DeserializeTypeMetadataBase which provides cached IId info. /// - private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase + private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase { public FrozenDictionary PropertySettersFrozen { get; } public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects) diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs index 60aec2b..b909a7e 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Concurrent; using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Text; @@ -19,6 +20,13 @@ public static partial class AcJsonDeserializer private static readonly byte[] RefPropertyUtf8 = "$ref"u8.ToArray(); private static readonly byte[] IdPropertyUtf8 = "$id"u8.ToArray(); + // Global metadata cache + private static readonly ConcurrentDictionary MetadataCache = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static JsonDeserializeTypeMetadata GetTypeMetadata(Type type) + => MetadataCache.GetOrAdd(type, static t => new JsonDeserializeTypeMetadata(t)); + #region Public API /// diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs index 07013f8..1e839ef 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 : AcSerializeBase, IDisposable + private sealed class JsonSerializationContext : AcSerializerContextBase, IDisposable { private readonly ArrayBufferWriter _buffer; public Utf8JsonWriter Writer { get; private set; } @@ -59,6 +59,12 @@ public static partial class AcJsonSerializer Reset(options); } + /// + /// Factory for creating JsonSerializeTypeMetadata instances. + /// + protected override Func MetadataFactory + => static t => new JsonSerializeTypeMetadata(t); + public void Reset(in AcJsonSerializerOptions options) { UseReferenceHandling = options.UseReferenceHandling; diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializeTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializeTypeMetadata.cs index a740f90..ddc0906 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializeTypeMetadata.cs @@ -8,11 +8,7 @@ namespace AyCode.Core.Serializers.Jsons; public static partial class AcJsonSerializer { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static JsonSerializeTypeMetadata GetTypeMetadata(in Type type) - => JsonSerializeTypeMetadata.GetOrCreateMetadata(type, static t => new JsonSerializeTypeMetadata(t)); - - private sealed class JsonSerializeTypeMetadata : SerializeTypeMetadataBase + private sealed class JsonSerializeTypeMetadata : SerializeTypeMetadataBase { public PropertyAccessor[] Properties { get; } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs index 67215bb..ed48862 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs @@ -139,7 +139,8 @@ public static partial class AcJsonSerializer return; } - var metadata = GetTypeMetadata(type); + var wrapper = context.GetWrapper(type); + var metadata = wrapper.Metadata; var props = metadata.Properties; var propCount = props.Length; for (var i = 0; i < propCount; i++) @@ -190,7 +191,8 @@ public static partial class AcJsonSerializer context.MarkAsWritten(value, id); } - var metadata = GetTypeMetadata(type); + var wrapper = context.GetWrapper(type); + var metadata = wrapper.Metadata; var props = metadata.Properties; var propCount = props.Length; var nextDepth = depth + 1; diff --git a/AyCode.Core/Serializers/SerializeTypeMetadataBase.cs b/AyCode.Core/Serializers/SerializeTypeMetadataBase.cs index 0c2bf28..b40542a 100644 --- a/AyCode.Core/Serializers/SerializeTypeMetadataBase.cs +++ b/AyCode.Core/Serializers/SerializeTypeMetadataBase.cs @@ -6,11 +6,8 @@ namespace AyCode.Core.Serializers; /// Base class for serializer type metadata. /// Extends TypeMetadataBase for serializer-specific functionality. /// Used by Binary, JSON, and Toon serializers. -/// Generic version provides built-in ThreadLocal caching with zero ref parameter overhead. /// -/// The concrete metadata type (must inherit from this class). -public abstract class SerializeTypeMetadataBase : TypeMetadataBase - where TMetadata : SerializeTypeMetadataBase +public abstract class SerializeTypeMetadataBase : TypeMetadataBase { protected SerializeTypeMetadataBase(Type type, Func ignorePropertyFilter) : base(type, ignorePropertyFilter) diff --git a/AyCode.Core/Serializers/Toons/AcToonContextBase.cs b/AyCode.Core/Serializers/Toons/AcToonContextBase.cs new file mode 100644 index 0000000..fed9fac --- /dev/null +++ b/AyCode.Core/Serializers/Toons/AcToonContextBase.cs @@ -0,0 +1,4 @@ +public class AcToonContextBase //: AcSerializerContextBase +{ + +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs index 4e064cd..90fc903 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 : AcSerializeBase + private sealed class ToonSerializationContext : AcSerializerContextBase { private readonly StringBuilder _builder; private Dictionary? _scanOccurrences; @@ -68,6 +68,12 @@ public static partial class AcToonSerializer Reset(options); } + /// + /// Factory for creating ToonSerializeTypeMetadata instances. + /// + protected override Func MetadataFactory + => static t => new ToonSerializeTypeMetadata(t); + public void Reset(AcToonSerializerOptions options) { Options = options; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializeTypeMetadata.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializeTypeMetadata.cs index f2e2f14..6ddf81d 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializeTypeMetadata.cs @@ -12,7 +12,7 @@ public static partial class AcToonSerializer /// Cached metadata for a type including properties, type name, and descriptions. /// Uses SerializeTypeMetadataBase infrastructure for shared caching across all serializers. /// - private sealed class ToonSerializeTypeMetadata : SerializeTypeMetadataBase + private sealed class ToonSerializeTypeMetadata : SerializeTypeMetadataBase { public string TypeName { get; } public string ShortTypeName { get; } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs index 1382ccd..406502d 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; @@ -321,13 +322,12 @@ public static partial class AcToonSerializer #region Type Metadata - /// - /// Gets or creates ToonSerializeTypeMetadata using TypeMetadataBase infrastructure. - /// This uses the shared GlobalMetadataCache and ThreadLocal cache for optimal performance. - /// + // Temporary: own cache for static methods without context + private static readonly ConcurrentDictionary MetadataCache = new(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ToonSerializeTypeMetadata GetTypeMetadata(Type type) - => ToonSerializeTypeMetadata.GetOrCreateMetadata(type, static t => new ToonSerializeTypeMetadata(t)); + => MetadataCache.GetOrAdd(type, static t => new ToonSerializeTypeMetadata(t)); #endregion } diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 47433c4..e3fc007 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -88,6 +88,17 @@ public abstract class TypeMetadataBase /// public AcSerializerCommon.IdAccessorType IdAccessorType { get; } + /// + /// The Id property info if IsIId is true, null otherwise. + /// Used by TypeMetadataWrapper to create typed RefIdGetter. + /// + public PropertyInfo? IdPropertyInfo { get; } + + /// + /// Public accessor for the type. Used by wrapper for typed getter creation. + /// + public Type MetadataType => SourceType; + /// /// Typed getter delegate for IId.Id property. /// Type depends on IdAccessorType (Func<object, int>, Func<object, long>, or Func<object, Guid>). @@ -150,6 +161,7 @@ public abstract class TypeMetadataBase if (IsIId && IdType != null) { var idProp = type.GetProperty("Id"); + IdPropertyInfo = idProp; // Store for TypeMetadataWrapper if (idProp != null) { // Create typed getter for the three common Id types to avoid boxing @@ -170,9 +182,8 @@ public abstract class TypeMetadataBase } else { - // Fallback for exotic Id types - uses boxing - IdAccessorType = AcSerializerCommon.IdAccessorType.Object; - IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp); + // Exotic Id types not supported - only int, long, Guid + throw new NotSupportedException($"Unsupported IId type: {IdType.Name}. Only int, long, and Guid are supported."); } } } @@ -227,59 +238,3 @@ public abstract class TypeMetadataBase }); } } - -/// -/// Generic base class for type metadata with built-in ThreadLocal caching. -/// Provides better performance than TypeMetadataBase by eliminating ref parameter overhead. -/// Each TMetadata type gets its own ThreadStatic cache instance automatically. -/// -/// The concrete metadata type (must inherit from this class). -public abstract class TypeMetadataBase : TypeMetadataBase where TMetadata : TypeMetadataBase -{ - /// - /// ThreadLocal cache for this specific metadata type. - /// Each TMetadata type gets its own static cache due to generic specialization. - /// - [ThreadStatic] - private static Dictionary? t_localCache; - - protected TypeMetadataBase(Type type, Func ignorePropertyFilter) : base(type, ignorePropertyFilter) - { - } - - /// - /// Gets or creates metadata for the specified type using two-level cache: - /// 1. ThreadLocal cache (fast path, no lock, no ref parameter) - /// 2. Global cache (slow path, ConcurrentDictionary with lock) - /// - /// The type to get metadata for. - /// Factory function to create metadata if not cached. - /// Cached or newly created metadata instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static TMetadata GetOrCreateMetadata(Type sourceType, Func factory) - { - // Fast path: check ThreadLocal cache first (no ref parameter overhead!) - if (t_localCache != null && t_localCache.TryGetValue(sourceType, out var cached)) - return cached; - - // Slow path: get from global cache and populate local cache - return GetOrCreateMetadataSlow(sourceType, factory); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static TMetadata GetOrCreateMetadataSlow(Type sourceType, Func factory) - { - // Get or create from global cache - var key = (sourceType, typeof(TMetadata)); - var metadata = (TMetadata)GlobalMetadataCache.GetOrAdd(key, _ => factory(sourceType)); - - // Populate ThreadLocal cache - t_localCache ??= new Dictionary(); - - if (t_localCache.Count >= MaxLocalCacheSize) - t_localCache.Clear(); - - t_localCache[sourceType] = metadata; - return metadata; - } -} diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs new file mode 100644 index 0000000..31d8e36 --- /dev/null +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.CompilerServices; +using AyCode.Core.Helpers; + +namespace AyCode.Core.Serializers; + +/// +/// Wrapper that combines metadata with tracking state. +/// Each context has one wrapper per type - contains all type-specific info and state. +/// Not generic on TRefId - uses runtime typed Delegate and object for flexibility. +/// +/// The concrete metadata type (BinarySerializeTypeMetadata, etc.) +public sealed class TypeMetadataWrapper where TMetadata : TypeMetadataBase +{ + /// + /// The cached metadata reference (from GlobalMetadataCache). + /// + public readonly TMetadata Metadata; + + /// + /// Typed getter for reference ID. Runtime type is Func<object, int/long/Guid>. + /// Use IdAccessorType to determine the actual type. + /// + internal readonly Delegate RefIdGetter; + + /// + /// Identity map for tracking. Runtime type is IdentityMap<int/long/Guid>. + /// + internal AcSerializerCommon.IIdentityMap? IdentityMap; + + /// + /// BitArray for tracking small int32 IDs (0-65535). + /// Only used when IdAccessorType == Int32. + /// + internal ulong[]? SmallIdBitmap; + + private const int BitArraySize = 1024; // 1024 * 64 = 65,536 IDs + + /// + /// Creates a new wrapper for the given metadata. + /// Initializes RefIdGetter based on IdAccessorType. + /// + public TypeMetadataWrapper(TMetadata metadata) + { + Metadata = metadata; + + // Create typed RefIdGetter based on IdAccessorType + RefIdGetter = CreateRefIdGetter(metadata); + } + + private static Delegate CreateRefIdGetter(TMetadata metadata) + { + if (metadata.IsIId && metadata.IdPropertyInfo != null) + { + // IId type - create typed getter from Id property + return metadata.IdAccessorType switch + { + AcSerializerCommon.IdAccessorType.Int32 => + AcSerializerCommon.CreateTypedGetter(metadata.MetadataType, metadata.IdPropertyInfo), + AcSerializerCommon.IdAccessorType.Int64 => + AcSerializerCommon.CreateTypedGetter(metadata.MetadataType, metadata.IdPropertyInfo), + AcSerializerCommon.IdAccessorType.Guid => + AcSerializerCommon.CreateTypedGetter(metadata.MetadataType, metadata.IdPropertyInfo), + _ => throw new NotSupportedException($"Unsupported IdAccessorType: {metadata.IdAccessorType}") + }; + } + else + { + // Non-IId type - use RuntimeHelpers.GetHashCode + return new Func(RuntimeHelpers.GetHashCode); + } + } + + /// + /// Resets tracking state for reuse between serializations. + /// Does not deallocate - just clears for reuse. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ResetTracking() + { + if (SmallIdBitmap != null) + Array.Clear(SmallIdBitmap); + + IdentityMap?.Reset(); + } + + /// + /// Ensures SmallIdBitmap is allocated (lazy allocation). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void EnsureSmallIdBitmap() + { + SmallIdBitmap ??= new ulong[BitArraySize]; + } +}