Refactor type metadata caching for serializers/deserializers
Major overhaul of type metadata caching: - Introduced generic TypeMetadataBase<T> with built-in ThreadLocal and global caching, replacing all per-class static caches. - Updated all serializer/deserializer metadata classes to use the new base, unifying and simplifying cache logic. - Increased ThreadLocal cache size for better performance with many types. - Standardized property collection order and improved property array usage. - Updated documentation and comments to reflect new caching strategy. These changes improve performance, scalability, and maintainability while reducing code duplication.
This commit is contained in:
parent
60ca154c6f
commit
9ad84ec21e
|
|
@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
public static partial class AcBinaryDeserializer
|
public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
internal sealed class BinaryDeserializeTypeMetadata : BinaryTypeMetadataBase
|
internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase<BinaryDeserializeTypeMetadata>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Properties array ordered alphabetically by name for index-based lookup.
|
/// Properties array ordered alphabetically by name for index-based lookup.
|
||||||
|
|
@ -19,7 +19,7 @@ public static partial class AcBinaryDeserializer
|
||||||
public BinaryDeserializeTypeMetadata(Type type) : base(type)
|
public BinaryDeserializeTypeMetadata(Type type) : base(type)
|
||||||
{
|
{
|
||||||
var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
|
var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
|
||||||
|
|
||||||
PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length];
|
PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length];
|
||||||
for (var i = 0; i < orderedProperties.Length; i++)
|
for (var i = 0; i < orderedProperties.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -41,30 +41,13 @@ public class AcBinaryDeserializationException : Exception
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class AcBinaryDeserializer
|
public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
|
||||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ThreadLocal cache for hot path type metadata lookup.
|
|
||||||
/// Avoids ConcurrentDictionary overhead for frequently accessed types.
|
|
||||||
/// </summary>
|
|
||||||
[ThreadStatic]
|
|
||||||
private static Dictionary<Type, BinaryDeserializeTypeMetadata>? t_deserializeTypeMetadataLocalCache;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ThreadLocal cache for type conversion info.
|
/// ThreadLocal cache for type conversion info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
|
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum local cache size before clearing.
|
|
||||||
/// Clear() is preferred over new Dictionary() because:
|
|
||||||
/// 1. Reuses already allocated internal array (no new allocation)
|
|
||||||
/// 2. Reduces GC pressure
|
|
||||||
/// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
|
|
||||||
/// </summary>
|
|
||||||
private const int MaxLocalCacheSize = 64;
|
|
||||||
|
|
||||||
// Type dispatch table for fast ReadValue
|
// Type dispatch table for fast ReadValue
|
||||||
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
|
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
|
||||||
|
|
@ -769,7 +752,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sima string olvasása - NEM regisztrál az intern táblába.
|
/// Sima string olvas<EFBFBD>sa - NEM regisztr<74>l az intern t<>bl<62>ba.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static string ReadPlainString(ref BinaryDeserializationContext context)
|
private static string ReadPlainString(ref BinaryDeserializationContext context)
|
||||||
|
|
@ -780,7 +763,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Új internált string olvasása és regisztrálása az intern táblába.
|
/// <EFBFBD>j intern<72>lt string olvas<61>sa <20>s regisztr<74>l<EFBFBD>sa az intern t<>bl<62>ba.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
|
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||||
|
|
@ -869,9 +852,9 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
// Get or create local cache
|
// Get or create local cache
|
||||||
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
|
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
|
||||||
|
|
||||||
// Clear when full - reuses internal array, better than new Dictionary()
|
// Clear when full - reuses internal array, better than new Dictionary()
|
||||||
if (localCache.Count >= MaxLocalCacheSize)
|
if (localCache.Count >= TypeMetadataBase.MaxLocalCacheSize)
|
||||||
{
|
{
|
||||||
localCache.Clear();
|
localCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
@ -1250,14 +1233,14 @@ public static partial class AcBinaryDeserializer
|
||||||
context.Skip(16);
|
context.Skip(16);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.String:
|
case BinaryTypeCode.String:
|
||||||
// Sima string - nem regisztrálunk
|
// Sima string - nem regisztr<EFBFBD>lunk
|
||||||
SkipPlainString(ref context);
|
SkipPlainString(ref context);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.StringInterned:
|
case BinaryTypeCode.StringInterned:
|
||||||
context.ReadVarUInt();
|
context.ReadVarUInt();
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.StringInternNew:
|
case BinaryTypeCode.StringInternNew:
|
||||||
// Új internált string - regisztrálni kell még skip esetén is
|
// <EFBFBD>j intern<72>lt string - regisztr<74>lni kell m<>g skip eset<65>n is
|
||||||
SkipAndRegisterInternedString(ref context);
|
SkipAndRegisterInternedString(ref context);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.ByteArray:
|
case BinaryTypeCode.ByteArray:
|
||||||
|
|
@ -1285,7 +1268,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sima string kihagyása - NEM regisztrál.
|
/// Sima string kihagy<EFBFBD>sa - NEM regisztr<74>l.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void SkipPlainString(ref BinaryDeserializationContext context)
|
private static void SkipPlainString(ref BinaryDeserializationContext context)
|
||||||
|
|
@ -1298,7 +1281,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Új internált string kihagyása - DE regisztrálni kell!
|
/// <EFBFBD>j intern<72>lt string kihagy<67>sa - DE regisztr<74>lni kell!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
|
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||||
|
|
@ -1369,38 +1352,11 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
||||||
|
/// Uses built-in cache from BinaryDeserializeTypeMetadata base class (zero ref parameter overhead).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
|
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
|
||||||
{
|
=> BinaryDeserializeTypeMetadata.GetOrCreateMetadata(type, static t => new BinaryDeserializeTypeMetadata(t));
|
||||||
// Fast path: check ThreadLocal cache first
|
|
||||||
var localCache = t_deserializeTypeMetadataLocalCache;
|
|
||||||
if (localCache != null && localCache.TryGetValue(type, out var cached))
|
|
||||||
{
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slow path: get from global cache and populate local cache
|
|
||||||
return GetTypeMetadataSlow(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private static BinaryDeserializeTypeMetadata GetTypeMetadataSlow(Type type)
|
|
||||||
{
|
|
||||||
var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
|
|
||||||
|
|
||||||
// Get or create local cache
|
|
||||||
var localCache = t_deserializeTypeMetadataLocalCache ??= new Dictionary<Type, BinaryDeserializeTypeMetadata>();
|
|
||||||
|
|
||||||
// Clear when full - reuses internal array, better than new Dictionary()
|
|
||||||
if (localCache.Count >= MaxLocalCacheSize)
|
|
||||||
{
|
|
||||||
localCache.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
localCache[type] = metadata;
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
|
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
public static partial class AcBinarySerializer
|
public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
internal sealed class BinaryTypeMetadata : BinaryTypeMetadataBase
|
internal sealed class BinaryTypeMetadata : TypeMetadataBase<BinaryTypeMetadata>
|
||||||
{
|
{
|
||||||
public BinaryPropertyAccessor[] Properties { get; }
|
public BinaryPropertyAccessor[] Properties { get; }
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ public static partial class AcBinarySerializer
|
||||||
// This ensures read-only properties (like computed properties) are excluded
|
// This ensures read-only properties (like computed properties) are excluded
|
||||||
// and property indices are consistent between serialization and deserialization
|
// and property indices are consistent between serialization and deserialization
|
||||||
var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
|
var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
|
||||||
|
|
||||||
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
||||||
for (var i = 0; i < orderedProperties.Length; i++)
|
for (var i = 0; i < orderedProperties.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -25,23 +25,6 @@ namespace AyCode.Core.Serializers.Binaries;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class AcBinarySerializer
|
public static partial class AcBinarySerializer
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> TypeMetadataCache = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ThreadLocal cache for hot path type metadata lookup.
|
|
||||||
/// Avoids ConcurrentDictionary overhead for frequently accessed types.
|
|
||||||
/// </summary>
|
|
||||||
[ThreadStatic]
|
|
||||||
private static Dictionary<Type, BinaryTypeMetadata>? t_typeMetadataLocalCache;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum local cache size before clearing.
|
|
||||||
/// Clear() is preferred over new Dictionary() because:
|
|
||||||
/// 1. Reuses already allocated internal array (no new allocation)
|
|
||||||
/// 2. Reduces GC pressure
|
|
||||||
/// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
|
|
||||||
/// </summary>
|
|
||||||
private const int MaxLocalCacheSize = 64;
|
|
||||||
|
|
||||||
// Pre-computed UTF8 encoder for string operations
|
// Pre-computed UTF8 encoder for string operations
|
||||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
@ -1096,39 +1079,11 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
||||||
/// Falls back to ConcurrentDictionary for cache misses.
|
/// Uses built-in cache from BinaryTypeMetadata base class (zero ref parameter overhead).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
private static BinaryTypeMetadata GetTypeMetadata(Type type)
|
||||||
{
|
=> BinaryTypeMetadata.GetOrCreateMetadata(type, static t => new BinaryTypeMetadata(t));
|
||||||
// Fast path: check ThreadLocal cache first
|
|
||||||
var localCache = t_typeMetadataLocalCache;
|
|
||||||
if (localCache != null && localCache.TryGetValue(type, out var cached))
|
|
||||||
{
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slow path: get from global cache and populate local cache
|
|
||||||
return GetTypeMetadataSlow(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private static BinaryTypeMetadata GetTypeMetadataSlow(Type type)
|
|
||||||
{
|
|
||||||
var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
|
|
||||||
|
|
||||||
// Get or create local cache
|
|
||||||
var localCache = t_typeMetadataLocalCache ??= new Dictionary<Type, BinaryTypeMetadata>();
|
|
||||||
|
|
||||||
// Clear when full - reuses internal array, better than new Dictionary()
|
|
||||||
if (localCache.Count >= MaxLocalCacheSize)
|
|
||||||
{
|
|
||||||
localCache.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
localCache[type] = metadata;
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs
|
// Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for binary serializer/deserializer type metadata.
|
|
||||||
/// Extends DeserializeTypeMetadataBase (which provides IId caching).
|
|
||||||
/// Properties are ordered alphabetically by name for deterministic serialization across platforms.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BinaryTypeMetadataBase : DeserializeTypeMetadataBase
|
|
||||||
{
|
|
||||||
protected BinaryTypeMetadataBase(Type type) : base(type)
|
|
||||||
{
|
|
||||||
// IId info is now cached in DeserializeTypeMetadataBase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,20 +7,22 @@ namespace AyCode.Core.Serializers;
|
||||||
/// Base class for deserializer type metadata.
|
/// Base class for deserializer type metadata.
|
||||||
/// Extends TypeMetadataBase with IId detection for efficient chain/merge operations.
|
/// Extends TypeMetadataBase with IId detection for efficient chain/merge operations.
|
||||||
/// Used by both JSON and Binary deserializers.
|
/// Used by both JSON and Binary deserializers.
|
||||||
|
/// Generic version provides built-in ThreadLocal caching with zero ref parameter overhead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DeserializeTypeMetadataBase : TypeMetadataBase
|
public abstract class DeserializeTypeMetadataBase<TMetadata> : TypeMetadataBase<TMetadata>
|
||||||
|
where TMetadata : DeserializeTypeMetadataBase<TMetadata>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this type implements IId interface.
|
/// Whether this type implements IId interface.
|
||||||
/// Cached at metadata creation time to avoid runtime reflection.
|
/// Cached at metadata creation time to avoid runtime reflection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsIId { get; }
|
public bool IsIId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Id property type if IsIId is true, null otherwise.
|
/// The Id property type if IsIId is true, null otherwise.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Type? IdType { get; }
|
public Type? IdType { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compiled getter for the Id property (if IsIId is true).
|
/// Compiled getter for the Id property (if IsIId is true).
|
||||||
/// Pre-compiled delegate avoids reflection overhead during deserialization.
|
/// Pre-compiled delegate avoids reflection overhead during deserialization.
|
||||||
|
|
@ -33,7 +35,7 @@ public abstract class DeserializeTypeMetadataBase : TypeMetadataBase
|
||||||
var idInfo = GetIdInfo(type);
|
var idInfo = GetIdInfo(type);
|
||||||
IsIId = idInfo.IsId;
|
IsIId = idInfo.IsId;
|
||||||
IdType = idInfo.IdType;
|
IdType = idInfo.IdType;
|
||||||
|
|
||||||
if (IsIId)
|
if (IsIId)
|
||||||
{
|
{
|
||||||
var idProp = type.GetProperty("Id");
|
var idProp = type.GetProperty("Id");
|
||||||
|
|
|
||||||
|
|
@ -9,39 +9,37 @@ namespace AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
public static partial class AcJsonDeserializer
|
public static partial class AcJsonDeserializer
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, JsonDeserializeTypeMetadata> TypeMetadataCache = new();
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static JsonDeserializeTypeMetadata GetTypeMetadata(in Type type)
|
private static JsonDeserializeTypeMetadata GetTypeMetadata(in Type type)
|
||||||
=> TypeMetadataCache.GetOrAdd(type, static t => new JsonDeserializeTypeMetadata(t));
|
=> JsonDeserializeTypeMetadata.GetOrCreateMetadata(type, static t => new JsonDeserializeTypeMetadata(t));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JSON deserialization type metadata.
|
/// JSON deserialization type metadata.
|
||||||
/// Extends DeserializeTypeMetadataBase which provides cached IId info.
|
/// Extends DeserializeTypeMetadataBase which provides cached IId info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase
|
private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase<JsonDeserializeTypeMetadata>
|
||||||
{
|
{
|
||||||
public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
|
public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
|
||||||
public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects)
|
public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects)
|
||||||
|
|
||||||
public JsonDeserializeTypeMetadata(Type type) : base(type)
|
public JsonDeserializeTypeMetadata(Type type) : base(type)
|
||||||
{
|
{
|
||||||
var props = GetSerializableProperties(type, requiresRead: true, requiresWrite: true).ToList();
|
var props = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
|
||||||
|
|
||||||
var propertySetters = new Dictionary<string, PropertySetterInfo>(props.Count, StringComparer.OrdinalIgnoreCase);
|
var propertySetters = new Dictionary<string, PropertySetterInfo>(props.Length, StringComparer.OrdinalIgnoreCase);
|
||||||
var propsArray = new PropertySetterInfo[props.Count];
|
var propsArray = new PropertySetterInfo[props.Length];
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
|
||||||
foreach (var prop in props)
|
foreach (var prop in props)
|
||||||
{
|
{
|
||||||
var propInfo = new PropertySetterInfo(prop, type);
|
var propInfo = new PropertySetterInfo(prop, type);
|
||||||
propertySetters[prop.Name] = propInfo;
|
propertySetters[prop.Name] = propInfo;
|
||||||
propsArray[index++] = propInfo;
|
propsArray[index++] = propInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertiesArray = propsArray;
|
PropertiesArray = propsArray;
|
||||||
PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// IId info (IsIId, IdType, IdGetter) is now cached in DeserializeTypeMetadataBase
|
// IId info (IsIId, IdType, IdGetter) is now cached in DeserializeTypeMetadataBase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,11 @@ namespace AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
public static partial class AcJsonSerializer
|
public static partial class AcJsonSerializer
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, JsonTypeMetadata> TypeMetadataCache = new();
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static JsonTypeMetadata GetTypeMetadata(in Type type)
|
private static JsonTypeMetadata GetTypeMetadata(in Type type)
|
||||||
=> TypeMetadataCache.GetOrAdd(type, static t => new JsonTypeMetadata(t));
|
=> JsonTypeMetadata.GetOrCreateMetadata(type, static t => new JsonTypeMetadata(t));
|
||||||
|
|
||||||
private sealed class JsonTypeMetadata : TypeMetadataBase
|
private sealed class JsonTypeMetadata : TypeMetadataBase<JsonTypeMetadata>
|
||||||
{
|
{
|
||||||
public PropertyAccessor[] Properties { get; }
|
public PropertyAccessor[] Properties { get; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers;
|
namespace AyCode.Core.Serializers;
|
||||||
|
|
@ -10,6 +11,22 @@ namespace AyCode.Core.Serializers;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class TypeMetadataBase
|
public abstract class TypeMetadataBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Global shared cache for all metadata types.
|
||||||
|
/// Key: (Type sourceType, Type metadataType) - ensures each type can have multiple metadata representations
|
||||||
|
/// Value: TypeMetadataBase instance (BinaryTypeMetadata, JsonTypeMetadata, etc.)
|
||||||
|
/// This single cache is shared across Binary/JSON Serializers/Deserializers.
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly ConcurrentDictionary<(Type, Type), TypeMetadataBase> GlobalMetadataCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum ThreadLocal cache size before clearing.
|
||||||
|
/// Set to 256 because if a system uses 256+ different types, they're likely all frequently accessed.
|
||||||
|
/// Minimal memory overhead (256 dictionary entries ≈ 8KB) vs significant lookup performance benefit.
|
||||||
|
/// Clear() reuses internal array - no new allocation, just resets count.
|
||||||
|
/// </summary>
|
||||||
|
internal const int MaxLocalCacheSize = 1024;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cache for serializable properties per type.
|
/// Cache for serializable properties per type.
|
||||||
/// Key: (Type, requiresWrite) - since requiresRead is always true in practice.
|
/// Key: (Type, requiresWrite) - since requiresRead is always true in practice.
|
||||||
|
|
@ -51,24 +68,14 @@ public abstract class TypeMetadataBase
|
||||||
{
|
{
|
||||||
var (t, needsWrite) = key;
|
var (t, needsWrite) = key;
|
||||||
|
|
||||||
// Build inheritance hierarchy (base -> derived)
|
// Collect properties from inheritance hierarchy (derived -> base order)
|
||||||
var hierarchy = new Stack<Type>();
|
// Then sort alphabetically - ensures consistent ordering for serialization/deserialization
|
||||||
var currentType = t;
|
|
||||||
while (currentType != null && currentType != typeof(object))
|
|
||||||
{
|
|
||||||
hierarchy.Push(currentType);
|
|
||||||
currentType = currentType.BaseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect properties level by level (base first)
|
|
||||||
var allProperties = new List<PropertyInfo>();
|
var allProperties = new List<PropertyInfo>();
|
||||||
|
|
||||||
while (hierarchy.Count > 0)
|
for (var currentType = t; currentType != null && currentType != typeof(object); currentType = currentType.BaseType)
|
||||||
{
|
{
|
||||||
var levelType = hierarchy.Pop();
|
|
||||||
|
|
||||||
// Get properties declared at this level only
|
// Get properties declared at this level only
|
||||||
var levelProperties = levelType
|
var levelProperties = currentType
|
||||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||||
.Where(p => p.CanRead &&
|
.Where(p => p.CanRead &&
|
||||||
(!needsWrite || p.CanWrite) &&
|
(!needsWrite || p.CanWrite) &&
|
||||||
|
|
@ -83,3 +90,60 @@ 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) : base(type)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue