AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Binary...

496 lines
18 KiB
C#

using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
/// <summary>
/// Pool for BinaryDeserializationContext instances.
/// Eliminates per-call heap allocation — mirrors BinarySerializationContextPool pattern.
/// </summary>
private static class DeserializationContextPool<TInput> where TInput : struct, IBinaryInputBase
{
private static readonly ConcurrentQueue<BinaryDeserializationContext<TInput>> Pool = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BinaryDeserializationContext<TInput> Get(AcBinarySerializerOptions options)
{
if (Pool.TryDequeue(out var ctx))
{
ctx.Reset(options);
return ctx;
}
var newCtx = new BinaryDeserializationContext<TInput>();
newCtx.Reset(options);
return newCtx;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(BinaryDeserializationContext<TInput> ctx)
{
if (Pool.Count < ctx.Options.MaxContextPoolSize)
{
ctx.Clear();
Pool.Enqueue(ctx);
}
}
}
/// <summary>
/// Binary deserialization context. Sealed class for pool reuse.
/// Holds all state: buffer, position, caches, options, metadata, interning.
/// Buffer state and read methods are directly in the context (via partial Read.cs)
/// for zero-indirection hot-path access — mirrors the serializer pattern.
/// TInput handles buffer lifecycle (Initialize/AdvanceSegment) — mirrors BinarySerializationContext&lt;TOutput&gt;.
/// </summary>
internal sealed partial class BinaryDeserializationContext<TInput>
: AcSerializerContextBase<BinaryDeserializeTypeMetadata, AcBinarySerializerOptions>
where TInput : struct, IBinaryInputBase
{
/// <summary>
/// Input target — handles only Initialize (startup) and AdvanceSegment (cold path).
/// </summary>
public TInput Input;
// Marker-based interning: sequential cache (no footer needed)
private object?[]? _internCache;
public bool HasMetadata;
public bool IsMergeMode;
public bool RemoveOrphanedItems;
public bool FastWire;
// Options-derived properties
public byte MinStringInternLength => Options.MinStringInternLength;
/// <summary>
/// Chain reference tracker for maintaining object identity across chain operations.
/// Only set when in chain mode (CreateDeserializeChain).
/// </summary>
public AcSerializerCommon.ChainReferenceTracker? ChainTracker;
/// <summary>
/// Returns true if in chain mode (ChainTracker is set).
/// </summary>
public bool IsChainMode => ChainTracker != null;
// Pooled arrays - reused across deserializations, zero steady-state allocation
private int[]? _pooledDupData;
private object?[]? _pooledInternCache;
private int _pooledDupDataLength;
private int _pooledInternCacheLength;
private int _lastInternCacheUsed; // how many slots were used (for targeted Clear)
// Small arrays: keep across calls. Large arrays: return to pool in Clear().
private const int SmallArrayThreshold = 256;
// String cache - for WASM optimization
private Dictionary<int, string>? _stringCache;
// Intern cache index counter
private int _nextCacheIndex;
// Linearized buffer for ReadOnlySequence<byte> input
private byte[]? _linearizedBuffer;
/// <summary>
/// Inline metadata entries flat array.
/// </summary>
private MetadataEntry[]? _metadataEntries;
private int _metadataEntryCount;
/// <summary>
/// Runtime polymorphic slot counter. Indices 0..RuntimeSlotCount-1 stored in _wrapperSlots.
/// Overflow (index >= RuntimeSlotCount) stored in _polyOverflow.
/// </summary>
internal int _nextRuntimeSlot;
/// <summary>
/// Overflow array for polymorphic types beyond RuntimeSlotCount.
/// Only allocated when >RuntimeSlotCount distinct poly types (very rare).
/// Indexed by (polyIndex - RuntimeSlotCount).
/// </summary>
private TypeMetadataWrapper<BinaryDeserializeTypeMetadata>[]? _polyOverflow;
/// <summary>
/// A metadata entry for the deserializer.
/// </summary>
internal struct MetadataEntry
{
public int PropNameHash;
public int[] PropertyHashes;
}
/// <summary>
/// Factory for creating BinaryDeserializeTypeMetadata instances.
/// </summary>
protected override Func<Type, BinaryDeserializeTypeMetadata> MetadataFactory
=> static t => new BinaryDeserializeTypeMetadata(t);
public BinaryDeserializationContext()
{
InitializeWrapperSlots(Volatile.Read(ref AcBinarySerializer.s_nextWrapperSlot));
}
/// <summary>
/// Initializes the context with the given TInput.
/// Calls Input.Initialize to set up buffer/position/length.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InitInput(TInput input)
{
Input = input;
Input.Initialize(out _buffer, out _position, out _bufferLength);
_useStringCaching = Options.UseStringCaching;
_maxCachedStringLength = Options.MaxCachedStringLength;
if (_useStringCaching) GetOrCreateStringCache();
_internCache = null;
HasMetadata = false;
IsMergeMode = false;
RemoveOrphanedItems = false;
FastWire = Options.WireMode == WireMode.Fast;
ChainTracker = null;
}
/// <summary>
/// Initializes the context from a ReadOnlySpan by copying to a pooled linearized buffer,
/// then creating an ArrayBinaryInput. Used when TInput is ArrayBinaryInput.
/// </summary>
public void InitFromSpan(ReadOnlySpan<byte> data)
{
var buffer = RentLinearizedBuffer(data.Length);
data.CopyTo(buffer);
InitInput((TInput)(object)new ArrayBinaryInput(buffer, data.Length));
}
#region Header
public void ReadHeader()
{
if (_position == 0 && IsAtEnd)
{
throw new AcBinaryDeserializationException("Binary payload is too short to contain a header.");
}
var version = ReadByte();
if (version != AcBinarySerializerOptions.FormatVersion)
{
throw new AcBinaryDeserializationException(
$"Unsupported binary format version '{version}'. Expected '{AcBinarySerializerOptions.FormatVersion}'.",
_position - 1);
}
var marker = ReadByte();
var hasPropertyTable = false;
if (marker == BinaryTypeCode.MetadataHeader)
{
hasPropertyTable = true;
Options.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
}
else if (marker == BinaryTypeCode.NoMetadataHeader)
{
Options.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
}
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
{
var flags = (byte)(marker & 0x0F);
hasPropertyTable = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
var hasOnlyId = (flags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
var hasAll = (flags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
Options.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
: hasOnlyId ? ReferenceHandlingMode.OnlyId
: ReferenceHandlingMode.None;
var hasCacheCount = (flags & BinaryTypeCode.HeaderFlag_HasCacheCount) != 0;
if (hasCacheCount)
{
var cacheCount = (int)ReadVarUInt();
if (cacheCount > 0)
{
_internCache = RentInternCache(cacheCount);
SetInternCacheUsed(cacheCount);
}
}
}
else
{
throw new AcBinaryDeserializationException(
$"Unsupported binary header marker '{marker}'.",
_position - 1);
}
HasMetadata = hasPropertyTable;
}
#endregion
#region String Interning
/// <summary>
/// Next intern cache index to assign when registering interned values.
/// </summary>
internal ref int NextCacheIndexRef
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _nextCacheIndex;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterNextInternedValue(object value)
{
_internCache![_nextCacheIndex++] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterInternedValueAt(int cacheIndex, object value)
{
_internCache![cacheIndex] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetInternedString(int cacheIndex)
{
var result = _internCache![cacheIndex];
if (result == null)
{
throw new AcBinaryDeserializationException(
$"Interned string at cache index '{cacheIndex}' was not populated.",
_position);
}
return (string)result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object GetInternedObject(int cacheIndex)
{
var result = _internCache![cacheIndex];
if (result == null)
{
throw new AcBinaryDeserializationException(
$"Interned object at cache index '{cacheIndex}' was not populated.",
_position);
}
return result;
}
#endregion
#region Pooled Arrays & Caches (merged from ContextClass)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Dictionary<int, string> GetOrCreateStringCache()
{
return _stringCache ??= new Dictionary<int, string>(128);
}
/// <summary>
/// Rents a linearized buffer for ReadOnlySequence multi-segment input.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal byte[] RentLinearizedBuffer(int minSize)
{
if (_linearizedBuffer != null && _linearizedBuffer.Length >= minSize)
return _linearizedBuffer;
if (_linearizedBuffer != null)
ArrayPool<byte>.Shared.Return(_linearizedBuffer);
_linearizedBuffer = ArrayPool<byte>.Shared.Rent(minSize);
return _linearizedBuffer;
}
public int[] RentDupData(int minLength)
{
if (_pooledDupData != null && _pooledDupDataLength >= minLength)
return _pooledDupData;
if (_pooledDupData != null)
ArrayPool<int>.Shared.Return(_pooledDupData);
_pooledDupData = ArrayPool<int>.Shared.Rent(minLength);
_pooledDupDataLength = _pooledDupData.Length;
return _pooledDupData;
}
public object?[] RentInternCache(int minLength)
{
if (_pooledInternCache != null && _pooledInternCacheLength >= minLength)
{
if (_lastInternCacheUsed > 0)
{
Array.Clear(_pooledInternCache, 0, _lastInternCacheUsed);
_lastInternCacheUsed = 0;
}
return _pooledInternCache;
}
if (_pooledInternCache != null)
ArrayPool<object?>.Shared.Return(_pooledInternCache, clearArray: true);
_pooledInternCache = ArrayPool<object?>.Shared.Rent(minLength);
_pooledInternCacheLength = _pooledInternCache.Length;
_lastInternCacheUsed = 0;
return _pooledInternCache;
}
public void SetInternCacheUsed(int count)
{
_lastInternCacheUsed = count;
}
#endregion
#region Inline Metadata
public void RegisterInlineMetadata(int propNameHash, int[] propertyHashes)
{
if (_metadataEntries == null || _metadataEntryCount >= _metadataEntries.Length)
{
var newSize = Math.Max((_metadataEntries?.Length ?? 0) * 2, 8);
var newArray = new MetadataEntry[newSize];
if (_metadataEntries != null)
Array.Copy(_metadataEntries, newArray, _metadataEntryCount);
_metadataEntries = newArray;
}
_metadataEntries[_metadataEntryCount++] = new MetadataEntry
{
PropNameHash = propNameHash,
PropertyHashes = propertyHashes
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int[]? FindSourceHashes(int propNameHash)
{
for (var i = 0; i < _metadataEntryCount; i++)
{
if (_metadataEntries![i].PropNameHash == propNameHash)
return _metadataEntries[i].PropertyHashes;
}
return null;
}
#endregion
#region Polymorphic Wrapper Cache
/// <summary>
/// Registers a wrapper in the polymorphic cache (called by ReadObjectWithTypeName/RefFirst).
/// Indices 0..RuntimeSlotCount-1 → _wrapperSlots (fast path, ~1-2ns).
/// Indices RuntimeSlotCount+ → _polyOverflow (still O(1) array access).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RegisterPolymorphicWrapper(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var slot = _nextRuntimeSlot++;
if (slot < AcBinarySerializer.RuntimeSlotCount)
{
SetWrapperSlot(slot, wrapper);
}
else
{
RegisterPolymorphicWrapperOverflow(wrapper, slot - AcBinarySerializer.RuntimeSlotCount);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void RegisterPolymorphicWrapperOverflow(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int overflowIndex)
{
if (_polyOverflow == null || overflowIndex >= _polyOverflow.Length)
{
var newSize = _polyOverflow == null ? 4 : _polyOverflow.Length * 2;
var newArray = new TypeMetadataWrapper<BinaryDeserializeTypeMetadata>[newSize];
if (_polyOverflow != null)
Array.Copy(_polyOverflow, newArray, overflowIndex);
_polyOverflow = newArray;
}
_polyOverflow[overflowIndex] = wrapper;
}
/// <summary>
/// Gets a previously registered polymorphic wrapper by index (called by ReadObjectWithTypeIndex/RefFirst).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TypeMetadataWrapper<BinaryDeserializeTypeMetadata> GetPolymorphicWrapper(int index)
{
if (index < AcBinarySerializer.RuntimeSlotCount)
return GetWrapperSlot(index)!;
return _polyOverflow![index - AcBinarySerializer.RuntimeSlotCount]!;
}
#endregion
#region Reset & Clear
public override void Reset(AcBinarySerializerOptions options)
{
base.Reset(options);
}
public override void Clear()
{
base.Clear();
_metadataEntryCount = 0;
_nextCacheIndex = 0;
// Clear runtime FixObj slots to prevent stale wrapper reuse on pool return.
// Slot-to-type mapping changes between sessions (slot 0 may be TestOrder in session A
// but TestGenericAttribute in session B). Without clearing, ReadObjectFromSlot
// would reuse the stale wrapper → wrong type → stream misalignment.
if (_nextRuntimeSlot > 0)
{
ClearWrapperSlots(Math.Min(_nextRuntimeSlot, AcBinarySerializer.RuntimeSlotCount));
}
_nextRuntimeSlot = 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)
{
if (_pooledInternCacheLength > SmallArrayThreshold)
{
ArrayPool<object?>.Shared.Return(_pooledInternCache, clearArray: true);
_pooledInternCache = null;
_pooledInternCacheLength = 0;
}
else if (_lastInternCacheUsed > 0)
{
Array.Clear(_pooledInternCache, 0, _lastInternCacheUsed);
}
_lastInternCacheUsed = 0;
}
// Dup data: no GC roots (int[]), return large arrays to pool
if (_pooledDupData != null && _pooledDupDataLength > SmallArrayThreshold)
{
ArrayPool<int>.Shared.Return(_pooledDupData);
_pooledDupData = null;
_pooledDupDataLength = 0;
}
// Linearized buffer: no GC roots (byte[]), keep small, return large
if (_linearizedBuffer != null && _linearizedBuffer.Length > SmallArrayThreshold)
{
ArrayPool<byte>.Shared.Return(_linearizedBuffer);
_linearizedBuffer = null;
}
}
#endregion
}
}