Refactor: unify metadata and tracking for serializer contexts

Major refactor of serialization infrastructure:
- Removed AcSerializeBase; replaced with AcSerializerContextBase<TMetadata> for unified context management.
- Added TypeMetadataWrapper<TMetadata> 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.
This commit is contained in:
Loretta 2026-01-18 15:31:45 +01:00
parent 2ab640b375
commit 8161ddade4
23 changed files with 417 additions and 138 deletions

View File

@ -1,30 +0,0 @@
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

@ -609,6 +609,52 @@ public static class AcSerializerCommon
Object = 255
}
/// <summary>
/// Interface for identity maps used in serialization tracking.
/// Enables type-safe Reset() without knowing the generic type parameter.
/// </summary>
public interface IIdentityMap
{
/// <summary>
/// Resets the identity map for reuse between serializations.
/// </summary>
void Reset();
}
/// <summary>
/// Generic identity map for tracking IId values during serialization.
/// Uses JIT-optimized EqualityComparer for maximum performance with common ID types.
/// </summary>
/// <typeparam name="TId">The ID type (int, long, Guid)</typeparam>
public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
{
private readonly HashSet<TId> _seenIds;
public IdentityMap()
{
_seenIds = new HashSet<TId>(EqualityComparer<TId>.Default);
}
/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAdd(TId id)
{
return _seenIds.Add(id);
}
/// <summary>
/// Resets the identity map for reuse.
/// </summary>
public void Reset()
{
_seenIds.Clear();
}
}
/// <summary>
/// IId-based reference tracking for serialization.
/// Supplements (not replaces) the ReferenceEquals-based SerializationReferenceTracker.

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TMetadata">The concrete metadata type.</typeparam>
public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeMetadataBase
{
/// <summary>
/// Global shared cache for metadata (thread-safe, shared across all contexts).
/// Generic specialization ensures separate cache per TMetadata type.
/// </summary>
private static readonly ConcurrentDictionary<Type, TMetadata> GlobalMetadataCache = new();
/// <summary>
/// Per-context wrappers containing metadata + tracking state.
/// </summary>
private readonly Dictionary<Type, TypeMetadataWrapper<TMetadata>> _wrappers = new();
/// <summary>
/// Factory function to create metadata. Implemented by derived class.
/// </summary>
protected abstract Func<Type, TMetadata> MetadataFactory { get; }
private const int BitArraySize = 1024;
private const int MaxSmallId = BitArraySize * 64;
#region Wrapper Access
/// <summary>
/// Gets or creates a wrapper for the specified type.
/// The wrapper contains metadata (from GlobalMetadataCache) + per-context tracking state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeMetadataWrapper<TMetadata> GetWrapper(Type type)
{
if (_wrappers.TryGetValue(type, out var wrapper))
return wrapper;
return GetWrapperSlow(type);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private TypeMetadataWrapper<TMetadata> 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<TMetadata>(metadata);
_wrappers[type] = wrapper;
return wrapper;
}
#endregion
#region Tracking API - int
/// <summary>
/// Tries to track an object with int RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int32.
/// Returns true if first occurrence.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out int refId)
{
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
var getter = (Func<object, int>)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<int>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<int>();
wrapper.IdentityMap = map;
}
return map.TryAdd(refId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> 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
/// <summary>
/// Tries to track an object with long RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int64.
/// Returns true if first occurrence.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out long refId)
{
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
var getter = (Func<object, long>)wrapper.RefIdGetter;
refId = getter(obj);
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<long>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<long>();
wrapper.IdentityMap = map;
}
return map.TryAdd(refId);
}
#endregion
#region Tracking API - Guid
/// <summary>
/// Tries to track an object with Guid RefId.
/// Use when wrapper.Metadata.IdAccessorType == Guid.
/// Returns true if first occurrence.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out Guid refId)
{
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
var getter = (Func<object, Guid>)wrapper.RefIdGetter;
refId = getter(obj);
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<Guid>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<Guid>();
wrapper.IdentityMap = map;
}
return map.TryAdd(refId);
}
#endregion
#region Reset
/// <summary>
/// Resets all wrapper tracking states for reuse.
/// Does not remove wrappers - keeps them for next operation.
/// </summary>
public virtual void Reset()
{
foreach (var wrapper in _wrappers.Values)
{
wrapper.ResetTracking();
}
}
#endregion
}

View File

@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase<BinaryDeserializeTypeMetadata>
internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase
{
/// <summary>
/// Properties array ordered alphabetically by name for index-based lookup.

View File

@ -1358,13 +1358,12 @@ public static partial class AcBinaryDeserializer
#region Type Metadata
/// <summary>
/// Gets type metadata with ThreadLocal caching for hot path optimization.
/// Uses built-in cache from BinaryDeserializeTypeMetadata base class (zero ref parameter overhead).
/// </summary>
// Temporary: own cache until ref struct is removed
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> 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)

View File

@ -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
/// <summary>
/// Binary serialization context. Public for generated serializers.
/// </summary>
internal sealed class BinarySerializationContext : AcSerializeBase, IDisposable
internal sealed class BinarySerializationContext : AcSerializerContextBase<BinarySerializeTypeMetadata>, IDisposable
{
private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
@ -99,6 +100,12 @@ public static partial class AcBinarySerializer
Reset(options);
}
/// <summary>
/// Factory for creating BinarySerializeTypeMetadata instances.
/// </summary>
protected override Func<Type, BinarySerializeTypeMetadata> MetadataFactory
=> static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute);
public void Reset(AcBinarySerializerOptions options)
{
_position = 0;

View File

@ -9,7 +9,7 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
internal sealed class BinarySerializeTypeMetadata : SerializeTypeMetadataBase<BinarySerializeTypeMetadata>
internal sealed class BinarySerializeTypeMetadata : SerializeTypeMetadataBase
{
public BinaryPropertyAccessor[] Properties { get; }

View File

@ -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;
}
/// <summary>
/// Gets type metadata with ThreadLocal caching for hot path optimization.
/// Uses built-in cache from BinarySerializeTypeMetadata base class (zero ref parameter overhead).
/// </summary>
[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

View File

@ -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.
/// </summary>
public abstract class DeserializeTypeMetadataBase<TMetadata> : TypeMetadataBase<TMetadata>
where TMetadata : DeserializeTypeMetadataBase<TMetadata>
public abstract class DeserializeTypeMetadataBase : TypeMetadataBase
{
protected DeserializeTypeMetadataBase(Type type, Func<PropertyInfo, bool> ignorePropertyFilter) : base(type, ignorePropertyFilter)
{

View File

@ -0,0 +1,4 @@
public class AcJsonContextBase
{
}

View File

@ -45,7 +45,7 @@ public static partial class AcJsonDeserializer
public readonly int RefId = refId;
}
private sealed class DeserializationContext
private sealed class DeserializationContext : AcSerializerContextBase<JsonDeserializeTypeMetadata>
{
// Use shared reference tracker from AcSerializerCommon
private readonly AcSerializerCommon.DeserializationReferenceTracker _refTracker = new();
@ -66,12 +66,18 @@ public static partial class AcJsonDeserializer
/// </summary>
public bool IsChainMode => ChainTracker != null;
/// <summary>
/// Factory for creating JsonDeserializeTypeMetadata instances.
/// </summary>
protected override Func<Type, JsonDeserializeTypeMetadata> 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;

View File

@ -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));
/// <summary>
/// JSON deserialization type metadata.
/// Extends DeserializeTypeMetadataBase which provides cached IId info.
/// </summary>
private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase<JsonDeserializeTypeMetadata>
private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase
{
public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects)

View File

@ -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<Type, JsonDeserializeTypeMetadata> MetadataCache = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static JsonDeserializeTypeMetadata GetTypeMetadata(Type type)
=> MetadataCache.GetOrAdd(type, static t => new JsonDeserializeTypeMetadata(t));
#region Public API
/// <summary>

View File

@ -35,7 +35,7 @@ public static partial class AcJsonSerializer
}
}
private sealed class JsonSerializationContext : AcSerializeBase, IDisposable
private sealed class JsonSerializationContext : AcSerializerContextBase<JsonSerializeTypeMetadata>, IDisposable
{
private readonly ArrayBufferWriter<byte> _buffer;
public Utf8JsonWriter Writer { get; private set; }
@ -59,6 +59,12 @@ public static partial class AcJsonSerializer
Reset(options);
}
/// <summary>
/// Factory for creating JsonSerializeTypeMetadata instances.
/// </summary>
protected override Func<Type, JsonSerializeTypeMetadata> MetadataFactory
=> static t => new JsonSerializeTypeMetadata(t);
public void Reset(in AcJsonSerializerOptions options)
{
UseReferenceHandling = options.UseReferenceHandling;

View File

@ -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<JsonSerializeTypeMetadata>
private sealed class JsonSerializeTypeMetadata : SerializeTypeMetadataBase
{
public PropertyAccessor[] Properties { get; }

View File

@ -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;

View File

@ -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.
/// </summary>
/// <typeparam name="TMetadata">The concrete metadata type (must inherit from this class).</typeparam>
public abstract class SerializeTypeMetadataBase<TMetadata> : TypeMetadataBase<TMetadata>
where TMetadata : SerializeTypeMetadataBase<TMetadata>
public abstract class SerializeTypeMetadataBase : TypeMetadataBase
{
protected SerializeTypeMetadataBase(Type type, Func<PropertyInfo, bool> ignorePropertyFilter)
: base(type, ignorePropertyFilter)

View File

@ -0,0 +1,4 @@
public class AcToonContextBase //: AcSerializerContextBase<ToonMeta>
{
}

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 : AcSerializeBase
private sealed class ToonSerializationContext : AcSerializerContextBase<ToonSerializeTypeMetadata>
{
private readonly StringBuilder _builder;
private Dictionary<object, int>? _scanOccurrences;
@ -68,6 +68,12 @@ public static partial class AcToonSerializer
Reset(options);
}
/// <summary>
/// Factory for creating ToonSerializeTypeMetadata instances.
/// </summary>
protected override Func<Type, ToonSerializeTypeMetadata> MetadataFactory
=> static t => new ToonSerializeTypeMetadata(t);
public void Reset(AcToonSerializerOptions options)
{
Options = options;

View File

@ -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.
/// </summary>
private sealed class ToonSerializeTypeMetadata : SerializeTypeMetadataBase<ToonSerializeTypeMetadata>
private sealed class ToonSerializeTypeMetadata : SerializeTypeMetadataBase
{
public string TypeName { get; }
public string ShortTypeName { get; }

View File

@ -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
/// <summary>
/// Gets or creates ToonSerializeTypeMetadata using TypeMetadataBase infrastructure.
/// This uses the shared GlobalMetadataCache and ThreadLocal cache for optimal performance.
/// </summary>
// Temporary: own cache for static methods without context
private static readonly ConcurrentDictionary<Type, ToonSerializeTypeMetadata> 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
}

View File

@ -88,6 +88,17 @@ public abstract class TypeMetadataBase
/// </summary>
public AcSerializerCommon.IdAccessorType IdAccessorType { get; }
/// <summary>
/// The Id property info if IsIId is true, null otherwise.
/// Used by TypeMetadataWrapper to create typed RefIdGetter.
/// </summary>
public PropertyInfo? IdPropertyInfo { get; }
/// <summary>
/// Public accessor for the type. Used by wrapper for typed getter creation.
/// </summary>
public Type MetadataType => SourceType;
/// <summary>
/// Typed getter delegate for IId.Id property.
/// Type depends on IdAccessorType (Func&lt;object, int&gt;, Func&lt;object, long&gt;, or Func&lt;object, Guid&gt;).
@ -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
});
}
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TMetadata">The concrete metadata type (must inherit from this class).</typeparam>
public abstract class TypeMetadataBase<TMetadata> : TypeMetadataBase where TMetadata : TypeMetadataBase<TMetadata>
{
/// <summary>
/// ThreadLocal cache for this specific metadata type.
/// Each TMetadata type gets its own static cache due to generic specialization.
/// </summary>
[ThreadStatic]
private static Dictionary<Type, TMetadata>? t_localCache;
protected TypeMetadataBase(Type type, Func<PropertyInfo, bool> ignorePropertyFilter) : base(type, ignorePropertyFilter)
{
}
/// <summary>
/// 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)
/// </summary>
/// <param name="sourceType">The type to get metadata for.</param>
/// <param name="factory">Factory function to create metadata if not cached.</param>
/// <returns>Cached or newly created metadata instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static TMetadata GetOrCreateMetadata(Type sourceType, Func<Type, TMetadata> 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<Type, TMetadata> 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<Type, TMetadata>();
if (t_localCache.Count >= MaxLocalCacheSize)
t_localCache.Clear();
t_localCache[sourceType] = metadata;
return metadata;
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Runtime.CompilerServices;
using AyCode.Core.Helpers;
namespace AyCode.Core.Serializers;
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam>
public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase
{
/// <summary>
/// The cached metadata reference (from GlobalMetadataCache).
/// </summary>
public readonly TMetadata Metadata;
/// <summary>
/// Typed getter for reference ID. Runtime type is Func&lt;object, int/long/Guid&gt;.
/// Use IdAccessorType to determine the actual type.
/// </summary>
internal readonly Delegate RefIdGetter;
/// <summary>
/// Identity map for tracking. Runtime type is IdentityMap&lt;int/long/Guid&gt;.
/// </summary>
internal AcSerializerCommon.IIdentityMap? IdentityMap;
/// <summary>
/// BitArray for tracking small int32 IDs (0-65535).
/// Only used when IdAccessorType == Int32.
/// </summary>
internal ulong[]? SmallIdBitmap;
private const int BitArraySize = 1024; // 1024 * 64 = 65,536 IDs
/// <summary>
/// Creates a new wrapper for the given metadata.
/// Initializes RefIdGetter based on IdAccessorType.
/// </summary>
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<int>(metadata.MetadataType, metadata.IdPropertyInfo),
AcSerializerCommon.IdAccessorType.Int64 =>
AcSerializerCommon.CreateTypedGetter<long>(metadata.MetadataType, metadata.IdPropertyInfo),
AcSerializerCommon.IdAccessorType.Guid =>
AcSerializerCommon.CreateTypedGetter<Guid>(metadata.MetadataType, metadata.IdPropertyInfo),
_ => throw new NotSupportedException($"Unsupported IdAccessorType: {metadata.IdAccessorType}")
};
}
else
{
// Non-IId type - use RuntimeHelpers.GetHashCode
return new Func<object, int>(RuntimeHelpers.GetHashCode);
}
}
/// <summary>
/// Resets tracking state for reuse between serializations.
/// Does not deallocate - just clears for reuse.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetTracking()
{
if (SmallIdBitmap != null)
Array.Clear(SmallIdBitmap);
IdentityMap?.Reset();
}
/// <summary>
/// Ensures SmallIdBitmap is allocated (lazy allocation).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void EnsureSmallIdBitmap()
{
SmallIdBitmap ??= new ulong[BitArraySize];
}
}