496 lines
18 KiB
C#
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<TOutput>.
|
|
/// </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
|
|
}
|
|
}
|