diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index dc0d113..3b5ad3a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -11,22 +11,21 @@ public static partial class AcBinaryDeserializer { /// /// Binary deserialization context. Public for generated serializers. - /// Uses composition with BinaryDeserializationContextClass for IId-based tracking. + /// Uses composition with pooled BinaryDeserializationContextClass for zero-alloc deserialization. + /// String cache and intern cache index are delegated to ContextClass for pool reuse. /// internal ref struct BinaryDeserializationContext { private readonly ReadOnlySpan _buffer; private int _position; - private Dictionary? _stringCache; // Marker-based interning: sequential cache (no footer needed) // StringInternFirst/ObjectRefFirst markers register values in order private object?[]? _internCache; // Shared cache for interned strings AND IId objects - private int _nextCacheIndex; // Next index to assign when registering /// /// Heap-allocated context class for IId-based reference tracking. - /// Also holds Options - all options-derived properties delegate to ContextClass.Options. + /// Pooled via DeserializationContextClassPool — holds caches, options, metadata. /// public readonly BinaryDeserializationContextClass ContextClass; @@ -56,33 +55,23 @@ public static partial class AcBinaryDeserializer /// public readonly bool IsChainMode => ChainTracker != null; - public BinaryDeserializationContext(ReadOnlySpan data) - : this(data, AcBinarySerializerOptions.Default, new BinaryDeserializationContextClass()) - { - } - - public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options) - : this(data, options, new BinaryDeserializationContextClass()) - { - } - - public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options, BinaryDeserializationContextClass contextClass) + /// + /// Creates a deserialization context with a pooled ContextClass. + /// ContextClass must already be Reset() with options before passing in. + /// + public BinaryDeserializationContext(ReadOnlySpan data, BinaryDeserializationContextClass contextClass) { _buffer = data; _position = 0; - _stringCache = null; // Marker-based interning fields _internCache = null; - _nextCacheIndex = 0; HasMetadata = false; IsMergeMode = false; RemoveOrphanedItems = false; ChainTracker = null; ContextClass = contextClass; - // Reset ContextClass with options - this sets Options and clears any previous state - ContextClass.Reset(options); } public void ReadHeader() @@ -422,9 +411,9 @@ public static partial class AcBinaryDeserializer var slice = _buffer.Slice(_position, length); var hash = ComputeStringHashFull(slice); - _stringCache ??= new Dictionary(128); + var stringCache = ContextClass.GetOrCreateStringCache(); - if (_stringCache.TryGetValue(hash, out var cached)) + if (stringCache.TryGetValue(hash, out var cached)) { // Hash includes all bytes for short strings, so collision is extremely unlikely // For longer strings, we still verify length as a sanity check @@ -437,7 +426,7 @@ public static partial class AcBinaryDeserializer } var value = Utf8NoBom.GetString(slice); - _stringCache[hash] = value; + stringCache[hash] = value; _position += length; return value; } @@ -513,7 +502,7 @@ public static partial class AcBinaryDeserializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RegisterNextInternedValue(object value) { - _internCache![_nextCacheIndex++] = value; + _internCache![ContextClass.NextCacheIndexRef++] = value; } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs index 366187c..e5d056a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -7,11 +8,44 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { + /// + /// Pool for BinaryDeserializationContextClass instances. + /// Eliminates per-call heap allocation — mirrors BinarySerializationContextPool pattern. + /// + private static class DeserializationContextClassPool + { + private static readonly ConcurrentQueue Pool = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BinaryDeserializationContextClass Get(AcBinarySerializerOptions options) + { + if (Pool.TryDequeue(out var ctx)) + { + ctx.Reset(options); + return ctx; + } + + var newCtx = new BinaryDeserializationContextClass(); + newCtx.Reset(options); + return newCtx; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(BinaryDeserializationContextClass ctx) + { + if (Pool.Count < ctx.Options.MaxContextPoolSize) + { + ctx.Clear(); + Pool.Enqueue(ctx); + } + } + } + /// /// Heap-allocated context class for binary deserialization. /// Inherits from AcSerializerContextBase for unified metadata caching and IId-based reference tracking. /// Used in composition with the ref struct BinaryDeserializationContext. - /// Holds pooled arrays for intern cache reuse across deserializations. + /// Holds pooled arrays and caches for reuse across deserializations. /// internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase { @@ -31,6 +65,55 @@ public static partial class AcBinaryDeserializer // Small arrays: keep across calls. Large arrays: return to pool in Clear(). private const int SmallArrayThreshold = 256; + // String cache - moved from ref struct for pool reuse (WASM optimization) + private Dictionary? _stringCache; + + // Intern cache index counter - moved from ref struct for pool reuse + private int _nextCacheIndex; + + // Linearized buffer for ReadOnlySequence input + private byte[]? _linearizedBuffer; + + /// + /// String cache for WASM optimization. Reused across deserializations. + /// + internal Dictionary? StringCache => _stringCache; + + /// + /// Gets or lazily creates the string cache with initial capacity. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Dictionary GetOrCreateStringCache() + { + return _stringCache ??= new Dictionary(128); + } + + /// + /// Next intern cache index to assign when registering interned values. + /// + internal ref int NextCacheIndexRef + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _nextCacheIndex; + } + + /// + /// Rents a linearized buffer for ReadOnlySequence multi-segment input. + /// Buffer is pooled and reused across deserializations. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal byte[] RentLinearizedBuffer(int minSize) + { + if (_linearizedBuffer != null && _linearizedBuffer.Length >= minSize) + return _linearizedBuffer; + + if (_linearizedBuffer != null) + ArrayPool.Shared.Return(_linearizedBuffer); + + _linearizedBuffer = ArrayPool.Shared.Rent(minSize); + return _linearizedBuffer; + } + /// /// Inline metadata bejegyzések flat array-ben. /// A propNameHash alapján lineárisan keresünk (kis számú típus per stream). @@ -146,6 +229,10 @@ public static partial class AcBinaryDeserializer base.Clear(); _metadataEntryCount = 0; + _nextCacheIndex = 0; + + // String cache: clear content but keep dictionary allocated for reuse + _stringCache?.Clear(); // Intern cache: clear GC roots, return large arrays to pool if (_pooledInternCache != null) @@ -172,6 +259,13 @@ public static partial class AcBinaryDeserializer _pooledDupData = null; _pooledDupDataLength = 0; } + + // Linearized buffer: no GC roots (byte[]), keep small, return large + if (_linearizedBuffer != null && _linearizedBuffer.Length > SmallArrayThreshold) + { + ArrayPool.Shared.Return(_linearizedBuffer); + _linearizedBuffer = null; + } } public override void Reset(AcBinarySerializerOptions options) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index 4bc79be..a496ae6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -55,7 +55,8 @@ public static partial class AcBinaryDeserializer // Cross-type path: use index mapping var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper); - var context = new BinaryDeserializationContext(data, options); + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(data, ctxClass); try { @@ -73,6 +74,10 @@ public static partial class AcBinaryDeserializer $"Failed to deserialize binary data from '{sourceType.Name}' to '{destType.Name}': {ex.Message}", context.Position, destType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } /// @@ -136,7 +141,8 @@ public static partial class AcBinaryDeserializer // Cross-type path: use index mapping var indexMapping = GetIndexMapping(sourceType, destType, options.PropertyMapper); - var context = new BinaryDeserializationContext(data, options); + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(data, ctxClass); try { @@ -171,6 +177,10 @@ public static partial class AcBinaryDeserializer $"Failed to populate target of type '{destType.Name}' with data from '{sourceType.Name}': {ex.Message}", context.Position, destType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index d66fb5a..f92f6f3 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections; using System.Collections.Concurrent; using System.Collections.Frozen; @@ -135,7 +136,8 @@ public static partial class AcBinaryDeserializer return (T?)(object?)DeserializeExpression(data, targetType, options); } - var context = new BinaryDeserializationContext(data, options); + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(data, ctxClass); try { @@ -154,6 +156,10 @@ public static partial class AcBinaryDeserializer $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", context.Position, targetType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } /// @@ -176,7 +182,8 @@ public static partial class AcBinaryDeserializer return DeserializeExpression(data, targetType, options); } - var context = new BinaryDeserializationContext(data, options); + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(data, ctxClass); try { @@ -195,6 +202,144 @@ public static partial class AcBinaryDeserializer $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", context.Position, targetType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } + } + + /// + /// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes). + /// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer. + /// + public static T? Deserialize(ReadOnlySequence data) + => Deserialize(data, AcBinarySerializerOptions.Default); + + /// + /// Deserialize binary data from a ReadOnlySequence with options. + /// + public static T? Deserialize(ReadOnlySequence data, AcBinarySerializerOptions options) + { + if (data.Length == 0) return default; + + if (data.IsSingleSegment) + return Deserialize(data.FirstSpan, options); + + var ctxClass = DeserializationContextClassPool.Get(options); + try + { + var buffer = ctxClass.RentLinearizedBuffer((int)data.Length); + data.CopyTo(buffer); + return Deserialize(buffer.AsSpan(0, (int)data.Length), ctxClass); + } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } + } + + /// + /// Deserialize binary data from a ReadOnlySequence to specified type. + /// + public static object? Deserialize(ReadOnlySequence data, Type targetType) + => Deserialize(data, targetType, AcBinarySerializerOptions.Default); + + /// + /// Deserialize binary data from a ReadOnlySequence to specified type with options. + /// + public static object? Deserialize(ReadOnlySequence data, Type targetType, AcBinarySerializerOptions options) + { + if (data.Length == 0) return null; + + if (data.IsSingleSegment) + return Deserialize(data.FirstSpan, targetType, options); + + var ctxClass = DeserializationContextClassPool.Get(options); + try + { + var buffer = ctxClass.RentLinearizedBuffer((int)data.Length); + data.CopyTo(buffer); + return Deserialize(buffer.AsSpan(0, (int)data.Length), targetType, ctxClass); + } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } + } + + /// + /// Internal: Deserialize with pre-pooled ContextClass (used by ReadOnlySequence multi-segment path). + /// + private static T? Deserialize(ReadOnlySpan data, BinaryDeserializationContextClass ctxClass) + { + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; + + var targetType = typeof(T); + if (AcSerializerCommon.IsExpressionType(targetType)) + return (T?)(object?)DeserializeExpression(data, targetType, ctxClass); + + var context = new BinaryDeserializationContext(data, ctxClass); + try + { + context.ReadHeader(); + return (T?)ReadValue(ref context, targetType, 0); + } + catch (AcBinaryDeserializationException) { throw; } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); + } + } + + /// + /// Internal: Deserialize with pre-pooled ContextClass (used by ReadOnlySequence multi-segment path). + /// + private static object? Deserialize(ReadOnlySpan data, Type targetType, BinaryDeserializationContextClass ctxClass) + { + if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null; + + if (AcSerializerCommon.IsExpressionType(targetType)) + return DeserializeExpression(data, targetType, ctxClass); + + var context = new BinaryDeserializationContext(data, ctxClass); + try + { + context.ReadHeader(); + return ReadValue(ref context, targetType, 0); + } + catch (AcBinaryDeserializationException) { throw; } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}", + context.Position, targetType, ex); + } + } + + /// + /// Internal: DeserializeExpression with pre-pooled ContextClass. + /// + private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, BinaryDeserializationContextClass ctxClass) + { + var context = new BinaryDeserializationContext(data, ctxClass); + try + { + context.ReadHeader(); + var node = (AcExpressionNode?)ReadValue(ref context, typeof(AcExpressionNode), 0); + if (node == null) return null; + + var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType); + return AcExpressionRebuilder.FromNode(node, entityType); + } + catch (AcBinaryDeserializationException) { throw; } + catch (Exception ex) + { + throw new AcBinaryDeserializationException( + $"Failed to deserialize Expression from binary data: {ex.Message}", + context.Position, targetExpressionType, ex); + } } /// @@ -203,7 +348,8 @@ public static partial class AcBinaryDeserializer /// private static Expression? DeserializeExpression(ReadOnlySpan data, Type targetExpressionType, AcBinarySerializerOptions options) { - var context = new BinaryDeserializationContext(data, options); + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(data, ctxClass); try { @@ -225,6 +371,10 @@ public static partial class AcBinaryDeserializer $"Failed to deserialize Expression from binary data: {ex.Message}", context.Position, targetExpressionType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } /// @@ -255,7 +405,8 @@ public static partial class AcBinaryDeserializer if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return; var targetType = target.GetType(); - var context = new BinaryDeserializationContext(data, options); + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(data, ctxClass); try { @@ -297,6 +448,10 @@ public static partial class AcBinaryDeserializer $"Failed to populate object of type '{targetType.Name}': {ex.Message}", context.Position, targetType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } /// @@ -320,7 +475,8 @@ public static partial class AcBinaryDeserializer var opts = options ?? AcBinarySerializerOptions.Default; var targetType = target.GetType(); - var context = new BinaryDeserializationContext(data, opts) + var ctxClass = DeserializationContextClassPool.Get(opts); + var context = new BinaryDeserializationContext(data, ctxClass) { IsMergeMode = true, RemoveOrphanedItems = opts.RemoveOrphanedItems @@ -382,6 +538,10 @@ public static partial class AcBinaryDeserializer $"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}", context.Position, targetType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } #endregion @@ -408,7 +568,8 @@ public static partial class AcBinaryDeserializer // Copy data to array for chain storage var dataArray = data.ToArray(); var chainTracker = new AcSerializerCommon.ChainReferenceTracker(); - var context = new BinaryDeserializationContext(dataArray, options) { ChainTracker = chainTracker }; + var ctxClass = DeserializationContextClassPool.Get(options); + var context = new BinaryDeserializationContext(dataArray, ctxClass) { ChainTracker = chainTracker }; try { @@ -417,9 +578,9 @@ public static partial class AcBinaryDeserializer // Position-based string interning - no validation needed return new BinaryDeserializeChain(dataArray, options, chainTracker, (T?)result); } - catch + finally { - throw; + DeserializationContextClassPool.Return(ctxClass); } } @@ -453,7 +614,8 @@ public static partial class AcBinaryDeserializer ThrowIfDisposed(); var targetType = typeof(TResult); - var context = new BinaryDeserializationContext(_data, _options) { ChainTracker = _chainTracker }; + var ctxClass = DeserializationContextClassPool.Get(_options); + var context = new BinaryDeserializationContext(_data, ctxClass) { ChainTracker = _chainTracker }; try { @@ -469,6 +631,10 @@ public static partial class AcBinaryDeserializer $"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}", 0, targetType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } public IDeserializeChain ThenPopulate(object target) @@ -477,7 +643,8 @@ public static partial class AcBinaryDeserializer ThrowIfDisposed(); var targetType = target.GetType(); - var context = new BinaryDeserializationContext(_data, _options) { ChainTracker = _chainTracker }; + var ctxClass = DeserializationContextClassPool.Get(_options); + var context = new BinaryDeserializationContext(_data, ctxClass) { ChainTracker = _chainTracker }; try { @@ -517,6 +684,10 @@ public static partial class AcBinaryDeserializer $"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}", 0, targetType, ex); } + finally + { + DeserializationContextClassPool.Return(ctxClass); + } } private void ThrowIfDisposed() diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 927b822..f58ef46 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; init; } = true; + public bool UseMetadata { get; init; } = false; /// /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).