High-perf IId tracking: custom IdentityMap, async cleanup

Refactor IId reference tracking with a new allocation-free, high-performance IdentityMap<TId> using bitmaps and pooled hash tables. Add async context cleanup for serializers, with pre-rented arrays for improved hot-path performance. Update AcSerializerOptions and context classes for better pooling, immutability, and platform support. Centralize and optimize array pooling and clearing to reduce memory pressure and GC impact.
This commit is contained in:
Loretta 2026-01-30 18:12:45 +01:00
parent 946148cc3d
commit dbacc2da80
12 changed files with 664 additions and 525 deletions

View File

@ -339,11 +339,6 @@ public static class Program
}
}
private static TestDataSet CreateDeepNestedTestData()
{
TestDataFactory.ResetIdCounter();

View File

@ -137,16 +137,21 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
#region Reset
/// <summary>
/// Resets all wrapper tracking states for reuse.
/// Does not remove wrappers - keeps them for next operation.
/// Resets context for new operation. Sets options only.
/// </summary>
public virtual void Reset(TOptions options)
{
Options = options;
}
/// <summary>
/// Clears tracking state for pool return. Returns IdentityMap arrays to pool.
/// </summary>
public virtual void Clear()
{
foreach (var wrapper in _wrappers.Values)
{
wrapper.ResetTracking();
wrapper.ResetTracking(Options.UseAsync);
}
}

View File

@ -1,4 +1,5 @@
using System.Reflection;
using System;
using System.Reflection;
namespace AyCode.Core.Serializers;
@ -11,7 +12,29 @@ public abstract class AcSerializerOptions
/// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented)
/// Note: Binary serializer supports OnlyId mode for IId-only tracking.
/// </summary>
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
public ReferenceHandlingMode ReferenceHandling
{
get => _referenceHandling;
set => _referenceHandling = value;
}
private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.OnlyId;
private readonly byte _maxDepth = byte.MaxValue;
private readonly bool _throwOnCircularReference = true;
private readonly PropertyMapperDelegate? _propertyMapper;
private bool _useAsync = false;
public bool UseAsync
{
get => _useAsync && ReferenceHandling != ReferenceHandlingMode.None;
init => _useAsync = !DetectedIsWasm && value;
}
/// <summary>
/// Cached platform detection - true if running in WebAssembly/Browser environment.
/// </summary>
protected static readonly bool DetectedIsWasm = OperatingSystem.IsBrowser();
/// <summary>
/// Maximum depth for serialization/deserialization.
@ -20,7 +43,11 @@ public abstract class AcSerializerOptions
/// byte.MaxValue (255) = effectively unlimited
/// Default: byte.MaxValue
/// </summary>
public byte MaxDepth { get; init; } = byte.MaxValue;
public byte MaxDepth
{
get => _maxDepth;
init => _maxDepth = value;
}
/// <summary>
/// Throw exception on circular reference detection for non-IId types.
@ -29,7 +56,11 @@ public abstract class AcSerializerOptions
/// Default: true (production safety)
/// Note: IId types are always tracked when ReferenceHandling != None.
/// </summary>
public bool ThrowOnCircularReference { get; init; } = true;
public bool ThrowOnCircularReference
{
get => _throwOnCircularReference;
init => _throwOnCircularReference = value;
}
/// <summary>
/// Optional callback for custom property mapping during cross-type operations.
@ -45,7 +76,11 @@ public abstract class AcSerializerOptions
///
/// Performance: ZERO overhead on same-type operations (Deserialize&lt;T&gt;).
/// </summary>
public PropertyMapperDelegate? PropertyMapper { get; init; }
public PropertyMapperDelegate? PropertyMapper
{
get => _propertyMapper;
init => _propertyMapper = value;
}
}
public enum AcSerializerType : byte

View File

@ -21,8 +21,12 @@ public static partial class AcBinaryDeserializer
public BinaryDeserializationContextClass()
{
// Initialize with default options - will be reset with actual options before use
Reset(AcBinarySerializerOptions.Default);
}
public override void Reset(AcBinarySerializerOptions options)
{
Clear();
base.Reset(options);
}
/// <summary>

View File

@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Text;
using System.Threading;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
@ -30,6 +31,12 @@ public static partial class AcBinarySerializer
return new BinarySerializationContext(options);
}
public static void ReturnAsync(BinarySerializationContext context)
{
// 🔥 FIRE-AND-FORGET: cleanup háttérben
ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(BinarySerializationContext context)
{
@ -170,7 +177,7 @@ public static partial class AcBinarySerializer
}
}
public void Clear()
public override void Clear()
{
_position = 0;
@ -193,9 +200,8 @@ public static partial class AcBinarySerializer
_propertyStateBuffer = null;
}
// NOTE: GrowBufferCount <20>s GrowBufferTotalBytes nem null<6C>z<EFBFBD>dik itt,
// hogy a m<>r<EFBFBD>sek v<>g<EFBFBD>n ki tudj<64>k <20>rni az <20>rt<72>keket.
// Csak a Reset() met<65>dusban null<6C>z<EFBFBD>dnak minden <20>j fut<75>s elej<65>n.
// Clear wrapper tracking - returns IdentityMap arrays to pool
base.Clear();
}

View File

@ -254,7 +254,8 @@ public static partial class AcBinarySerializer
}
finally
{
BinarySerializationContextPool.Return(context);
if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context);
else BinarySerializationContextPool.Return(context);
}
}
@ -295,7 +296,8 @@ public static partial class AcBinarySerializer
}
finally
{
BinarySerializationContextPool.Return(context);
if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context);
else BinarySerializationContextPool.Return(context);
}
}
@ -315,7 +317,8 @@ public static partial class AcBinarySerializer
}
finally
{
BinarySerializationContextPool.Return(context);
if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context);
else BinarySerializationContextPool.Return(context);
}
}
@ -338,7 +341,8 @@ public static partial class AcBinarySerializer
}
finally
{
BinarySerializationContextPool.Return(context);
if (context.Options.UseAsync) BinarySerializationContextPool.ReturnAsync(context);
else BinarySerializationContextPool.Return(context);
}
}

View File

@ -8,11 +8,6 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary>
public sealed class AcBinarySerializerOptions : AcSerializerOptions
{
/// <summary>
/// Cached platform detection - true if running in WebAssembly/Browser environment.
/// </summary>
private static readonly bool DetectedIsWasm = OperatingSystem.IsBrowser();
public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Binary;
/// <summary>

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,10 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Threading;
namespace AyCode.Core.Serializers.Jsons;
@ -24,6 +26,13 @@ public static partial class AcJsonSerializer
return new JsonSerializationContext(options);
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnAsync(JsonSerializationContext context)
{
// 🔥 FIRE-AND-FORGET: cleanup háttérben
ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(JsonSerializationContext context)
{
@ -32,6 +41,10 @@ public static partial class AcJsonSerializer
context.Clear();
Pool.Enqueue(context);
}
else
{
context.Dispose(); // ✅ Utf8JsonWriter + ArrayBufferWriter cleanup!
}
}
}
@ -75,11 +88,14 @@ public static partial class AcJsonSerializer
}
}
public void Clear()
public override void Clear()
{
Writer.Reset();
_buffer.Clear();
_refTracker.Reset();
// Clear wrapper tracking - returns IdentityMap arrays to pool
base.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -60,13 +60,14 @@ public static partial class AcJsonSerializer
{
if (options.ReferenceHandling != ReferenceHandlingMode.None)
ScanReferences(actualValue, context, 0);
WriteValue(actualValue, context, 0);
return context.GetResult();
}
finally
{
SerializationContextPool.Return(context);
if (context.Options.UseAsync) SerializationContextPool.ReturnAsync(context);
else SerializationContextPool.Return(context);
}
}

View File

@ -15,6 +15,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
where TMetadata : TypeMetadataBase
where TOptions : AcSerializerOptions
{
private bool _useSmallInt = false;
private const int BitArraySize = 1024;
private const int MaxSmallId = BitArraySize * 64;
@ -43,7 +44,7 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
refId = wrapper.RefIdGetterInt32!(obj);
// BitArray fast path for small positive IDs
return refId is >= 0 and < MaxSmallId ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId);
return (_useSmallInt && refId is >= 0 and < MaxSmallId) ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -115,15 +115,16 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// Resets tracking state for reuse between serializations.
/// Does not deallocate - just clears for reuse (pool-friendly).
/// </summary>
/// <param name="preRentBuckets">If true, pre-rent arrays at next capacity (useful for async Clear)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetTracking()
public void ResetTracking(bool preRentBuckets = false)
{
if (SmallIdBitmap != null)
Array.Clear(SmallIdBitmap);
IdentityMapInt32?.Reset();
IdentityMapInt64?.Reset();
IdentityMapGuid?.Reset();
IdentityMapInt32?.Reset(preRentBuckets);
IdentityMapInt64?.Reset(preRentBuckets);
IdentityMapGuid?.Reset(preRentBuckets);
}
#region Direct Int32 Operations - No type check, no generic overhead