Optimize serialization: precompute type metadata, remove caches

Refactored serialization for performance:
- Precompute type metadata (primitive, collection, element info) in TypeMetadataBase
- Remove runtime type caches from JsonUtilities
- Rewrite primitive/collection checks to use direct logic or metadata
- Update scan pass and serialization hot path to use wrappers/metadata
- Improve buffer management (halve oversized buffers)
- Increase profiler warmup iterations, comment out deserialization in hot path
- Clean up code and clarify documentation/comments

Reduces runtime overhead and memory usage, streamlines hot path execution.
This commit is contained in:
Loretta 2026-02-08 08:13:34 +01:00
parent 5a174ced4c
commit b37d873792
7 changed files with 202 additions and 136 deletions

View File

@ -68,7 +68,7 @@ public static class Program
} }
// Profiler mode: warmup only, then exit (for memory profiler analysis) // Profiler mode: warmup only, then exit (for memory profiler analysis)
if (mode == "profiler") //if (mode == "profiler")
{ {
RunProfilerMode(); RunProfilerMode();
return; return;
@ -132,7 +132,7 @@ public static class Program
byte[] bytes = AcBinarySerializer.Serialize(order, options); byte[] bytes = AcBinarySerializer.Serialize(order, options);
// Warmup (fills caches) // Warmup (fills caches)
System.Console.WriteLine("Warming up (10 iterations)..."); System.Console.WriteLine("Warming up (1000 iterations)...");
for (var i = 0; i < 1000; i++) for (var i = 0; i < 1000; i++)
{ {
_ = AcBinarySerializer.Serialize(order, options); _ = AcBinarySerializer.Serialize(order, options);
@ -148,7 +148,7 @@ public static class Program
for (var i = 0; i < 1000; i++) for (var i = 0; i < 1000; i++)
{ {
_ = AcBinarySerializer.Serialize(order, options); _ = AcBinarySerializer.Serialize(order, options);
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes); //_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
} }
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)..."); System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");

View File

@ -86,23 +86,17 @@ public static class JsonUtilities
typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ushort), typeof(int), typeof(uint), typeof(long),
typeof(ulong), typeof(float), typeof(double), typeof(char) typeof(ulong), typeof(float), typeof(double), typeof(char)
}.ToFrozenSet(); }.ToFrozenSet();
#endregion #endregion
#region Type Caches #region Type Caches
private static readonly ConcurrentDictionary<Type, IdTypeInfo> IdInfoCache = new();
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCollectionCache = new();
private static readonly ConcurrentDictionary<PropertyInfo, bool> JsonIgnoreCache = new();
private static readonly ConcurrentDictionary<Type, Func<int, IList>> ListFactoryCache = new(); private static readonly ConcurrentDictionary<Type, Func<int, IList>> ListFactoryCache = new();
#endregion #endregion
#region UTF8 Buffer Pool #region UTF8 Buffer Pool
/// <summary> /// <summary>
/// Rents a UTF8 byte buffer from the shared pool. /// Rents a UTF8 byte buffer from the shared pool.
/// </summary> /// </summary>
@ -368,19 +362,16 @@ public static class JsonUtilities
} }
/// <summary> /// <summary>
/// Fast primitive check using type code. /// Checks if type is primitive, string, or nullable primitive.
/// Delegates to IsPrimitiveOrStringFast with nullable unwrapping.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveOrString(Type type) public static bool IsPrimitiveOrString(Type type)
{ {
return IsPrimitiveCache.GetOrAdd(type, static t => if (IsPrimitiveOrStringFast(type)) return true;
{ if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableGenericType)
if (t.IsPrimitive || PrimitiveTypes.Contains(t)) return true; return IsPrimitiveOrStringFast(type.GetGenericArguments()[0]);
if (t.IsGenericType && t.GetGenericTypeDefinition() == NullableGenericType) return false;
return IsPrimitiveOrString(t.GetGenericArguments()[0]);
if (t.IsEnum) return true;
return false;
});
} }
/// <summary> /// <summary>
@ -402,35 +393,33 @@ public static class JsonUtilities
/// <summary> /// <summary>
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.) /// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
/// Only called at metadata/config creation time — not in hot path.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGenericCollectionType(in Type type) public static bool IsGenericCollectionType(in Type type)
{ {
return IsCollectionCache.GetOrAdd(type, static t => if (ReferenceEquals(type, StringType) || type.IsPrimitive) return false;
if (type.IsArray) return true;
if (type.IsGenericType)
{ {
if (ReferenceEquals(t, StringType) || t.IsPrimitive) return false; var genericDef = type.GetGenericTypeDefinition();
if (t.IsArray) return true; if (ReferenceEquals(genericDef, ListGenericType) ||
ReferenceEquals(genericDef, IListGenericType) ||
if (t.IsGenericType) genericDef == typeof(ICollection<>) ||
{ ReferenceEquals(genericDef, IEnumerableGenericType) ||
var genericDef = t.GetGenericTypeDefinition(); ReferenceEquals(genericDef, ObservableCollectionType) ||
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, CollectionType))
ReferenceEquals(genericDef, IListGenericType) || return true;
genericDef == typeof(ICollection<>) || }
ReferenceEquals(genericDef, IEnumerableGenericType) ||
ReferenceEquals(genericDef, ObservableCollectionType) || foreach (var iface in type.GetInterfaces())
ReferenceEquals(genericDef, CollectionType)) {
return true; if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
} return true;
}
foreach (var iface in t.GetInterfaces())
{ return typeof(IEnumerable).IsAssignableFrom(type);
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return true;
}
return typeof(IEnumerable).IsAssignableFrom(t);
});
} }
/// <summary> /// <summary>
@ -469,62 +458,57 @@ public static class JsonUtilities
/// <summary> /// <summary>
/// Gets the element type of a collection. /// Gets the element type of a collection.
/// Only called at metadata/config creation time — not in hot path.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Type? GetCollectionElementType(in Type collectionType) public static Type? GetCollectionElementType(in Type collectionType)
{ {
return CollectionElementCache.GetOrAdd(collectionType, static type => if (collectionType.IsArray)
return collectionType.GetElementType();
if (collectionType.IsGenericType)
{ {
if (type.IsArray) var genericDef = collectionType.GetGenericTypeDefinition();
return type.GetElementType(); if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
return collectionType.GetGenericArguments()[0];
}
foreach (var iface in collectionType.GetInterfaces())
{
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return iface.GetGenericArguments()[0];
}
if (type.IsGenericType) return typeof(object);
{
var genericDef = type.GetGenericTypeDefinition();
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
return type.GetGenericArguments()[0];
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return iface.GetGenericArguments()[0];
}
return typeof(object);
});
} }
/// <summary> /// <summary>
/// Gets IId info for a type. Returns struct to avoid allocation. /// Gets IId info for a type. Returns struct to avoid allocation.
/// Only called at metadata creation time — not in hot path.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IdTypeInfo GetIdInfo(in Type type) public static IdTypeInfo GetIdInfo(in Type type)
{ {
return IdInfoCache.GetOrAdd(type, static t => foreach (var iface in type.GetInterfaces())
{ {
foreach (var iface in t.GetInterfaces()) if (!iface.IsGenericType) continue;
{ if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
if (!iface.IsGenericType) continue; var idType = iface.GetGenericArguments()[0];
if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue; return new IdTypeInfo(true, idType);
var idType = iface.GetGenericArguments()[0]; }
// FIXED: IsId should be true if IId<T> interface is found, not idType.IsValueType return new IdTypeInfo(false, typeof(int));
return new IdTypeInfo(true, idType);
}
return new IdTypeInfo(false, typeof(int));
});
} }
/// <summary> /// <summary>
/// Checks if property has JsonIgnore attribute. /// Checks if property has JsonIgnore attribute.
/// Only called at metadata creation time — not in hot path.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool HasJsonIgnoreAttribute(PropertyInfo prop) public static bool HasJsonIgnoreAttribute(PropertyInfo prop)
{ {
return JsonIgnoreCache.GetOrAdd(prop, static p => return Attribute.IsDefined(prop, typeof(JsonIgnoreAttribute)) ||
Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) || Attribute.IsDefined(prop, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute));
Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)));
} }
/// <summary> /// <summary>
@ -534,11 +518,6 @@ public static class JsonUtilities
public static bool HasToonIgnoreAttribute(PropertyInfo prop) public static bool HasToonIgnoreAttribute(PropertyInfo prop)
{ {
return false; return false;
//return JsonIgnoreCache.GetOrAdd(prop, static p => Attribute.IsDefined(p, typeof(ToonIgnoreAttribute)));
return JsonIgnoreCache.GetOrAdd(prop, static p =>
Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) ||
Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)));
} }
/// <summary> /// <summary>
@ -561,26 +540,24 @@ public static class JsonUtilities
/// <summary> /// <summary>
/// Checks if collection contains primitive elements. /// Checks if collection contains primitive elements.
/// Only called at metadata/config creation time — not in hot path.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveElementCollection(in Type type) public static bool IsPrimitiveElementCollection(in Type type)
{ {
return IsPrimitiveCollectionCache.GetOrAdd(type, static t => if (ReferenceEquals(type, StringType)) return false;
Type? elementType = null;
if (type.IsArray)
elementType = type.GetElementType();
else if (type.IsGenericType && typeof(System.Collections.IEnumerable).IsAssignableFrom(type))
{ {
if (ReferenceEquals(t, StringType)) return false; var genericArgs = type.GetGenericArguments();
if (genericArgs.Length == 1) elementType = genericArgs[0];
}
Type? elementType = null; if (elementType == null) return false;
if (t.IsArray) return IsPrimitiveOrString(elementType) || elementType.IsEnum;
elementType = t.GetElementType();
else if (t.IsGenericType && typeof(IEnumerable).IsAssignableFrom(t))
{
var genericArgs = t.GetGenericArguments();
if (genericArgs.Length == 1) elementType = genericArgs[0];
}
if (elementType == null) return false;
return IsPrimitiveOrString(elementType) || elementType.IsEnum;
});
} }
/// <summary> /// <summary>

View File

@ -71,6 +71,7 @@ public static partial class AcBinarySerializer
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
{ {
private const int MinBufferSize = 512; private const int MinBufferSize = 512;
private const int BufferHalvingThreshold = 4; // Halve buffer when > _initialBufferSize * this
private const int PropertyIndexBufferMaxCache = 512; private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512; private const int PropertyStateBufferMaxCache = 512;
private const int InitialInternCapacity = 32; private const int InitialInternCapacity = 32;
@ -186,7 +187,7 @@ public static partial class AcBinarySerializer
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt! // NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
// Kumulatívan gyűjtjük a benchmark során. // Kumulatívan gyűjtjük a benchmark során.
if (_buffer.Length < _initialBufferSize) if (_buffer.Length < _initialBufferSize || _buffer.Length > _initialBufferSize * BufferHalvingThreshold)
{ {
ArrayPool<byte>.Shared.Return(_buffer); ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize); _buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
@ -214,6 +215,14 @@ public static partial class AcBinarySerializer
_propertyStateBuffer = null; _propertyStateBuffer = null;
} }
// Halve oversized output buffer (IdentityMap pattern: gradual shrink after spike)
if (_buffer.Length > _initialBufferSize * BufferHalvingThreshold)
{
var nextSize = Math.Max(_buffer.Length / 2, _initialBufferSize);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(nextSize);
}
// Clear wrapper tracking - returns IdentityMap arrays to pool // Clear wrapper tracking - returns IdentityMap arrays to pool
base.Clear(); base.Clear();
} }

View File

@ -141,7 +141,7 @@ public static partial class AcBinarySerializer
// 1. It's IId (can be deduplicated by Id) // 1. It's IId (can be deduplicated by Id)
// 2. It has complex properties (children could be shared) // 2. It has complex properties (children could be shared)
// 3. It's not a primitive/string (could be referenced multiple times) // 3. It's not a primitive/string (could be referenced multiple times)
NeedsReferenceTracking = IsIId || HasComplexProperties || !IsPrimitiveOrStringFast(type); NeedsReferenceTracking = IsIId || HasComplexProperties || !IsPrimitiveType;
// Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute // Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute
if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false)) if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))

View File

@ -1,4 +1,5 @@
using System.Collections; using System.Collections;
using System.Runtime.CompilerServices;
using static AyCode.Core.Helpers.JsonUtilities; using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;
@ -8,7 +9,10 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// First pass: scans object graph to identify duplicates (strings + objects). /// First pass: scans object graph to identify duplicates (strings + objects).
/// Only traverses reference properties (complex types + strings). /// Only traverses reference properties (complex types + strings).
/// Stops traversing an object after its 2nd occurrence. /// Stops traversing children after 2nd occurrence of an IId object:
/// - Prevents infinite recursion on circular references
/// - Consistent with write pass which writes ObjectRef (no children) for 2nd occurrence
/// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed). /// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
/// </summary> /// </summary>
private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context) private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context)
@ -16,41 +20,43 @@ public static partial class AcBinarySerializer
if (!context.HasCaching) if (!context.HasCaching)
return; return;
ScanValue(value, type, context, 0); var wrapper = context.GetWrapper(type);
// No AssignCacheIndicesInOrder() needed - CacheIndex assigned inline on 2nd occurrence ScanValue(value, wrapper, context, 0);
} }
private static void ScanValue(object? value, Type type, BinarySerializationContext context, int depth) private static void ScanValue(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext context, int depth)
{ {
if (value == null || depth > context.MaxDepth) if (value == null || depth > context.MaxDepth)
return; return;
// String → intern tracking (with length check to match serialize pass) var metadata = wrapper.Metadata;
if (value is string str)
{ // Skip primitives (pre-computed field, no Type.GetTypeCode() call)
if (context.UseStringInterning && context.IsValidForInterningString(str.Length)) if (metadata.IsPrimitiveType)
{
context.ScanInternString(str);
}
return;
}
// Skip primitives
if (IsPrimitiveOrStringFast(type))
return; return;
// Collection → iterate elements // Collection → iterate elements using IList fast path (no IEnumerator alloc)
if (value is IEnumerable enumerable) if (metadata.IsCollection)
{ {
var elementType = GetCollectionElementType(type) ?? typeof(object); if (metadata.ElementNeedsScan)
if (!IsPrimitiveOrStringFast(elementType) || elementType == typeof(string))
{ {
var nextDepth = depth + 1; var nextDepth = depth + 1;
foreach (var item in enumerable) if (value is IList list)
{ {
if (item != null) for (var i = 0; i < list.Count; i++)
ScanValue(item, item.GetType(), context, nextDepth); {
var item = list[i];
if (item != null)
ScanItem(item, context, nextDepth);
}
}
else if (value is IEnumerable enumerable)
{
foreach (var item in enumerable)
{
if (item != null)
ScanItem(item, context, nextDepth);
}
} }
} }
@ -58,10 +64,12 @@ public static partial class AcBinarySerializer
} }
// Object → ref tracking + recursive scan // Object → ref tracking + recursive scan
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
// Reference tracking for IId types (or all types when ReferenceHandling == All) // Reference tracking for IId types (or all types when ReferenceHandling == All)
// 2nd occurrence → skip children because:
// 1. Write pass writes ObjectRef (no children) → strings/objects here are never in output
// 2. Prevents infinite recursion on circular references (A→B→A→...)
// 3. Nested objects reachable from other paths are scanned through those paths
if (context.UseTypeReferenceHandling(metadata)) if (context.UseTypeReferenceHandling(metadata))
{ {
// Direct tracking call - avoid extra indirection through context // Direct tracking call - avoid extra indirection through context
@ -86,7 +94,7 @@ public static partial class AcBinarySerializer
} }
if (!isFirst) if (!isFirst)
return; // 2nd occurrence → skip children return; // 2nd occurrence → skip children (symmetric with write pass ObjectRef)
} }
// Recursive scan on reference properties only // Recursive scan on reference properties only
@ -105,11 +113,32 @@ public static partial class AcBinarySerializer
} }
else else
{ {
// Object property: use generic getter // Object property: use generic getter, get wrapper for property type
var propValue = prop.GetValue(value); var propValue = prop.GetValue(value);
if (propValue != null) if (propValue != null)
ScanValue(propValue, prop.PropertyType, context, nextDepth2); {
var propWrapper = context.GetWrapper(prop.PropertyType);
ScanValue(propValue, propWrapper, context, nextDepth2);
}
} }
} }
} }
/// <summary>
/// Scans a collection item. Handles string fast path and gets wrapper for the runtime type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ScanItem(object item, BinarySerializationContext context, int depth)
{
// String fast path — avoid GetWrapper entirely
if (item is string str)
{
if (context.UseStringInterning && context.IsValidForInterningString(str.Length))
context.ScanInternString(str);
return;
}
var itemWrapper = context.GetWrapper(item.GetType());
ScanValue(item, itemWrapper, context, depth);
}
} }

View File

@ -432,15 +432,18 @@ public static partial class AcBinarySerializer
return; return;
} }
// Get wrapper once — used by both WriteArray and WriteObject
var wrapper = context.GetWrapper(type);
// Handle collections/arrays // Handle collections/arrays
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{ {
WriteArray(enumerable, type, context, depth); WriteArray(enumerable, wrapper, context, depth);
return; return;
} }
// Handle complex objects with single-pass reference tracking // Handle complex objects with single-pass reference tracking
WriteObject(value, type, context, depth, isNested: depth > 0); WriteObject(value, wrapper, context, depth, isNested: depth > 0);
} }
/// <summary> /// <summary>
@ -789,9 +792,8 @@ public static partial class AcBinarySerializer
#region Complex Type Writers #region Complex Type Writers
private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth, bool isNested = false) private static void WriteObject(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext context, int depth, bool isNested = false)
{ {
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
// Wire format: // Wire format:
@ -1269,14 +1271,17 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// Optimized array writer with specialized paths for primitive arrays. /// Optimized array writer with specialized paths for primitive arrays.
/// </summary> /// </summary>
private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth) private static void WriteArray(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext context, int depth)
{ {
context.WriteByte(BinaryTypeCode.Array); context.WriteByte(BinaryTypeCode.Array);
var nextDepth = depth + 1; var nextDepth = depth + 1;
// Use pre-computed metadata — no GetWrapper or GetCollectionElementType needed
var metadata = wrapper.Metadata;
var elementType = metadata.CollectionElementType;
// Optimized path for primitive arrays // Optimized path for primitive arrays
var elementType = GetCollectionElementType(type); if (elementType != null && metadata.SourceType.IsArray)
if (elementType != null && type.IsArray)
{ {
if (TryWritePrimitiveArray(enumerable, elementType, context)) if (TryWritePrimitiveArray(enumerable, elementType, context))
return; return;

View File

@ -1,6 +1,7 @@
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using System; using System;
using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -123,6 +124,37 @@ public abstract class TypeMetadataBase
/// </summary> /// </summary>
public bool NeedsReferenceTracking { get; protected set; } public bool NeedsReferenceTracking { get; protected set; }
/// <summary>
/// True if this type is a primitive, string, enum, Guid, DateTime, etc.
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
/// </summary>
public bool IsPrimitiveType { get; }
/// <summary>
/// True if this type implements IEnumerable (excluding string and Dictionary).
/// Pre-computed to replace IsCollectionCache and IsGenericCollectionType() lookups.
/// </summary>
public bool IsCollection { get; }
/// <summary>
/// The element type if IsCollection is true, null otherwise.
/// Pre-computed to replace CollectionElementCache lookups.
/// </summary>
public Type? CollectionElementType { get; }
/// <summary>
/// True if collection elements need scanning (are complex types or strings).
/// Pre-computed: !IsPrimitiveOrStringFast(elementType) || elementType == typeof(string).
/// Only meaningful when IsCollection is true.
/// </summary>
public bool ElementNeedsScan { get; }
/// <summary>
/// True if collection elements are all primitive types (no scanning needed at all).
/// Pre-computed to replace IsPrimitiveCollectionCache.
/// </summary>
public bool IsPrimitiveElementCollection { get; }
#endregion #endregion
/// <summary> /// <summary>
@ -158,6 +190,20 @@ public abstract class TypeMetadataBase
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray(); WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
IsComplexType = IsComplexType2(type); IsComplexType = IsComplexType2(type);
IsPrimitiveType = !IsComplexType;
// Pre-compute collection info — replaces CollectionElementCache / IsCollectionCache lookups
if (!IsPrimitiveType && !ReferenceEquals(type, StringType) && typeof(IEnumerable).IsAssignableFrom(type))
{
CollectionElementType = GetCollectionElementType(type);
IsCollection = CollectionElementType != null;
if (IsCollection)
{
var elemIsPrimitive = IsPrimitiveOrStringFast(CollectionElementType!);
ElementNeedsScan = !elemIsPrimitive || ReferenceEquals(CollectionElementType, StringType);
IsPrimitiveElementCollection = elemIsPrimitive || CollectionElementType!.IsEnum;
}
}
// Cache IId info at construction time - no runtime reflection needed later! // Cache IId info at construction time - no runtime reflection needed later!
var idInfo = GetIdInfo(type); var idInfo = GetIdInfo(type);