1224 lines
43 KiB
C#
1224 lines
43 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
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;
|
|
|
|
public static partial class AcBinarySerializer
|
|
{
|
|
private static class BinarySerializationContextPool
|
|
{
|
|
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
|
|
{
|
|
if (Pool.TryDequeue(out var context))
|
|
{
|
|
context.Reset(options);
|
|
return context;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (Pool.Count < context.Options.MaxContextPoolSize)
|
|
{
|
|
context.Clear();
|
|
Pool.Enqueue(context);
|
|
}
|
|
else
|
|
{
|
|
context.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
public static int GrowBufferCount =>
|
|
#if DEBUG
|
|
BinarySerializationContext.GrowBufferCount;
|
|
#else
|
|
-1;
|
|
#endif
|
|
|
|
public static long GrowBufferTotalBytes =>
|
|
#if DEBUG
|
|
BinarySerializationContext.GrowBufferTotalBytes;
|
|
#else
|
|
-1;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Binary serialization context. Public for generated serializers.
|
|
/// </summary>
|
|
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
|
|
{
|
|
private const int MinBufferSize = 512;
|
|
private const int PropertyIndexBufferMaxCache = 512;
|
|
private const int PropertyStateBufferMaxCache = 512;
|
|
private const int InitialInternCapacity = 32;
|
|
private byte[] _buffer;
|
|
private int _position;
|
|
private int _initialBufferSize;
|
|
|
|
#if DEBUG
|
|
/// <summary>
|
|
/// Counts how many times GrowBuffer was called during serialization.
|
|
/// Used for benchmarking buffer allocation efficiency.
|
|
/// </summary>
|
|
public static int GrowBufferCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Total bytes allocated by GrowBuffer during serialization.
|
|
/// Used for benchmarking buffer allocation efficiency.
|
|
/// </summary>
|
|
public static long GrowBufferTotalBytes { get; set; }
|
|
#endif
|
|
|
|
// Use shared reference tracker from AcSerializerCommon
|
|
//private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
|
|
|
private IdentityMap<string, InternEntry>? _stringInternMap;
|
|
private int _nextCacheIndex; // Next dense cache index to assign
|
|
|
|
private int[]? _propertyIndexBuffer;
|
|
private byte[]? _propertyStateBuffer;
|
|
|
|
#if DEBUG
|
|
/// <summary>
|
|
/// DEBUG ONLY: Current property path being serialized (e.g., "Order.Status").
|
|
/// Used for string interning analysis.
|
|
/// </summary>
|
|
internal string? CurrentPropertyPath;
|
|
|
|
/// <summary>
|
|
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
|
|
/// Parameters: (propertyPath, stringValue)
|
|
/// Use this to analyze which properties have repeated string values.
|
|
/// </summary>
|
|
internal Action<string?, string>? OnStringInterned;
|
|
#endif
|
|
|
|
// These properties delegate to Options for convenience
|
|
public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None;
|
|
/// <summary>
|
|
/// True if we need footer position in header (string interning OR reference handling OR metadata).
|
|
/// </summary>
|
|
/// <summary>
|
|
/// True if we need footer position in header (string interning OR reference handling).
|
|
/// UseMetadata no longer uses footer — metadata is inline in the body.
|
|
/// </summary>
|
|
public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None;
|
|
public bool UseMetadata => Options.UseMetadata;
|
|
public byte MinStringInternLength => Options.MinStringInternLength;
|
|
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
|
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
|
|
|
/// <summary>
|
|
/// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop.
|
|
/// </summary>
|
|
public bool HasPropertyFilter { get; private set; }
|
|
|
|
public int Position => _position;
|
|
|
|
public BinarySerializationContext(AcBinarySerializerOptions options)
|
|
{
|
|
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
|
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
|
Reset(options);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory for creating BinarySerializeTypeMetadata instances.
|
|
/// </summary>
|
|
protected override Func<Type, BinarySerializeTypeMetadata> MetadataFactory
|
|
=> static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute);
|
|
|
|
public override void Reset(AcBinarySerializerOptions options)
|
|
{
|
|
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
|
base.Reset(options);
|
|
|
|
_position = 0;
|
|
_initialBufferSize = Math.Max(Options.InitialBufferCapacity, MinBufferSize);
|
|
HasPropertyFilter = Options.PropertyFilter != null;
|
|
|
|
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
|
|
// Kumulatívan gyűjtjük a benchmark során.
|
|
|
|
if (_buffer.Length < _initialBufferSize)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_buffer);
|
|
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
|
}
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
_position = 0;
|
|
|
|
//_refTracker.Reset();
|
|
_stringInternMap?.Reset();
|
|
_nextCacheIndex = 0;
|
|
|
|
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
|
|
{
|
|
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
|
_propertyIndexBuffer = null;
|
|
}
|
|
|
|
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
|
_propertyStateBuffer = null;
|
|
}
|
|
|
|
// Clear wrapper tracking - returns IdentityMap arrays to pool
|
|
base.Clear();
|
|
}
|
|
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_buffer != null)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_buffer);
|
|
_buffer = null!;
|
|
}
|
|
|
|
if (_propertyIndexBuffer != null)
|
|
{
|
|
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
|
_propertyIndexBuffer = null;
|
|
}
|
|
|
|
if (_propertyStateBuffer != null)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
|
_propertyStateBuffer = null;
|
|
}
|
|
|
|
}
|
|
|
|
#region String Interning
|
|
|
|
/// <summary>
|
|
/// Tries to intern a string. Returns true if string was seen before (write index).
|
|
/// Returns false if first occurrence (write inline).
|
|
/// Uses stream position for 100% reliable deserializer cache matching.
|
|
/// </summary>
|
|
/// <param name="value">The string value to intern</param>
|
|
/// <param name="streamPosition">Current stream position (before writing the string)</param>
|
|
/// <param name="cacheIndex">Output: cache index for 2+ occurrence, -1 for 1st occurrence</param>
|
|
/// <returns>True if 2+ occurrence (write cacheIndex), false if 1st occurrence (write inline)</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TryGetInternedString(string value, int streamPosition, out int cacheIndex)
|
|
{
|
|
_stringInternMap ??= new IdentityMap<string, InternEntry>();
|
|
|
|
if (!_stringInternMap.TryAdd(value, out var slotIndex))
|
|
{
|
|
// 2+ occurrence: assign CacheIndex if first repeat
|
|
ref var entry = ref _stringInternMap.GetValueRef(slotIndex);
|
|
if (entry.CacheIndex < 0)
|
|
{
|
|
entry.CacheIndex = _nextCacheIndex++;
|
|
}
|
|
cacheIndex = entry.CacheIndex;
|
|
return true;
|
|
}
|
|
|
|
// 1st occurrence: store stream position
|
|
ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex);
|
|
newEntry.StreamPosition = streamPosition;
|
|
newEntry.CacheIndex = -1; // Not assigned until 2nd occurrence
|
|
cacheIndex = -1;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if there are any interned strings that occurred more than once.
|
|
/// </summary>
|
|
public bool HasInternedStrings => _stringInternMap != null && _stringInternMap.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Gets the count of strings that occurred more than once (for footer).
|
|
/// </summary>
|
|
public int GetDupCount() => _nextCacheIndex;
|
|
|
|
/// <summary>
|
|
/// Writes the merged footer with (position, cacheIndex) pairs sorted by position.
|
|
/// Collects entries from string interning AND ID tracking (all wrappers).
|
|
/// VarUInt format for compact size, deserializer reads into flat int[].
|
|
/// </summary>
|
|
public void WriteInternedFooter()
|
|
{
|
|
if (_nextCacheIndex == 0) return;
|
|
|
|
// Collect ALL entries with CacheIndex >= 0 (string + ID, all occurred more than once)
|
|
Span<(int Position, int CacheIndex)> entries = _nextCacheIndex <= 64
|
|
? stackalloc (int, int)[_nextCacheIndex]
|
|
: new (int, int)[_nextCacheIndex];
|
|
|
|
var idx = 0;
|
|
|
|
// 1. String intern entries
|
|
if (_stringInternMap != null)
|
|
{
|
|
var count = _stringInternMap.Count;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
ref var entry = ref _stringInternMap.GetValueRefAt(i);
|
|
if (entry.CacheIndex >= 0)
|
|
entries[idx++] = (entry.StreamPosition, entry.CacheIndex);
|
|
}
|
|
}
|
|
|
|
// 2. ID tracking entries from all wrappers
|
|
foreach (var wrapper in GetWrappers())
|
|
{
|
|
CollectInternEntries(wrapper.IdentityMapInt32, ref entries, ref idx);
|
|
CollectInternEntries(wrapper.IdentityMapInt64, ref entries, ref idx);
|
|
CollectInternEntries(wrapper.IdentityMapGuid, ref entries, ref idx);
|
|
}
|
|
|
|
// Sort by StreamPosition (ascending) for deserializer sequential check
|
|
var usedEntries = entries.Slice(0, idx);
|
|
usedEntries.Sort((a, b) => a.Position.CompareTo(b.Position));
|
|
|
|
// Write pairs as VarUInt for compact size
|
|
for (var i = 0; i < idx; i++)
|
|
{
|
|
WriteVarUInt((uint)usedEntries[i].Position);
|
|
WriteVarUInt((uint)usedEntries[i].CacheIndex);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void CollectInternEntries<TKey>(IdentityMap<TKey, InternEntry>? map,
|
|
ref Span<(int Position, int CacheIndex)> entries, ref int idx) where TKey : notnull
|
|
{
|
|
if (map == null) return;
|
|
var count = map.Count;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
ref var entry = ref map.GetValueRefAt(i);
|
|
if (entry.CacheIndex >= 0)
|
|
entries[idx++] = (entry.StreamPosition, entry.CacheIndex);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Object Reference Tracking (IId + Non-IId)
|
|
|
|
/// <summary>
|
|
/// Tries to track an IId object (Int32 Id). Uses shared _nextCacheIndex with string interning.
|
|
/// Returns true if first occurrence, false if already seen (cacheIndex assigned).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TryTrackObject(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, object obj, out int cacheIndex)
|
|
{
|
|
return TryTrack(wrapper, obj, _position, ref _nextCacheIndex, out cacheIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to track an IId object (Int64 Id).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TryTrackObjectLong(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, object obj, out int cacheIndex)
|
|
{
|
|
return TryTrackLong(wrapper, obj, _position, ref _nextCacheIndex, out cacheIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to track an IId object (Guid Id).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TryTrackObjectGuid(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, object obj, out int cacheIndex)
|
|
{
|
|
return TryTrackGuid(wrapper, obj, _position, ref _nextCacheIndex, out cacheIndex);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UseMetadata Type Tracking
|
|
|
|
/// <summary>
|
|
/// Regisztrálja a típust UseMetadata módban.
|
|
/// Visszaadja true-t ha ez az első előfordulás (inline hash-eket kell írni),
|
|
/// false-t ha ismételt (csak propNameHash kell).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool RegisterMetadataType(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
|
|
{
|
|
if (wrapper.MetadataFooterIndex >= 0)
|
|
return false; // ismételt
|
|
|
|
wrapper.MetadataFooterIndex = 0; // jelöljük hogy már regisztrálva
|
|
return true; // első előfordulás
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inline metadata kiírása az ObjectWithMetadata marker után.
|
|
/// Első előfordulás: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]...
|
|
/// Ismételt: [propNameHash (4b)]
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, bool isFirstOccurrence)
|
|
{
|
|
WriteRaw(metadata.PropNameHash);
|
|
|
|
if (isFirstOccurrence)
|
|
{
|
|
var hashes = metadata.MetadataPropertyHashes;
|
|
WriteVarUInt((uint)hashes.Length);
|
|
for (var i = 0; i < hashes.Length; i++)
|
|
{
|
|
WriteRaw(hashes[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Property State Buffer
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public byte[] RentPropertyStateBuffer(int size)
|
|
{
|
|
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
|
|
{
|
|
return _propertyStateBuffer;
|
|
}
|
|
|
|
if (_propertyStateBuffer != null)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
|
|
}
|
|
|
|
_propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
|
|
return _propertyStateBuffer;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void ReturnPropertyStateBuffer(byte[] buffer)
|
|
{
|
|
// Buffer stays cached for reuse.
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Output
|
|
|
|
/// <summary>
|
|
/// Returns the serialized data as a ReadOnlySpan without allocation.
|
|
/// Use this for compression or other processing before final ToArray().
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public ReadOnlySpan<byte> AsSpan() => _buffer.AsSpan(0, _position);
|
|
|
|
public byte[] ToArray()
|
|
{
|
|
var result = GC.AllocateUninitializedArray<byte>(_position);
|
|
_buffer.AsSpan(0, _position).CopyTo(result);
|
|
return result;
|
|
}
|
|
|
|
public void WriteTo(IBufferWriter<byte> writer)
|
|
{
|
|
var span = writer.GetSpan(_position);
|
|
_buffer.AsSpan(0, _position).CopyTo(span);
|
|
writer.Advance(_position);
|
|
}
|
|
|
|
public BinarySerializationResult DetachResult()
|
|
{
|
|
var resultBuffer = _buffer;
|
|
var resultLength = _position;
|
|
|
|
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
|
_position = 0;
|
|
|
|
return new BinarySerializationResult(resultBuffer, resultLength, pooled: true);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Property Filtering
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
|
|
{
|
|
if (PropertyFilter == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var context = new BinaryPropertyFilterContext(
|
|
instance,
|
|
property.DeclaringType,
|
|
property.Name,
|
|
property.PropertyType,
|
|
property.DynamicGetter);
|
|
return PropertyFilter(context);
|
|
}
|
|
|
|
public bool CheckDuplicatePropName => Options.CheckDuplicatePropName;
|
|
|
|
#endregion
|
|
|
|
#region Buffer Helpers
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void EnsureCapacity(int additionalBytes)
|
|
{
|
|
var required = _position + additionalBytes;
|
|
if (required <= _buffer.Length)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GrowBuffer(required);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void GrowBuffer(int required)
|
|
{
|
|
var newSize = Math.Max(_buffer.Length * 2, required);
|
|
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
|
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
|
|
ArrayPool<byte>.Shared.Return(_buffer);
|
|
_buffer = newBuffer;
|
|
|
|
#if DEBUG
|
|
GrowBufferCount++;
|
|
GrowBufferTotalBytes += newSize;
|
|
#endif
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteByte(byte value)
|
|
{
|
|
if (_position >= _buffer.Length)
|
|
{
|
|
GrowBuffer(_position + 1);
|
|
}
|
|
_buffer[_position++] = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write type code byte followed by a raw value. Batches EnsureCapacity call.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
|
|
{
|
|
var size = 1 + Unsafe.SizeOf<T>();
|
|
EnsureCapacity(size);
|
|
_buffer[_position++] = typeCode;
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
|
_position += Unsafe.SizeOf<T>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write two bytes efficiently.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteTwoBytes(byte b1, byte b2)
|
|
{
|
|
EnsureCapacity(2);
|
|
_buffer[_position++] = b1;
|
|
_buffer[_position++] = b2;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteBytes(ReadOnlySpan<byte> data)
|
|
{
|
|
EnsureCapacity(data.Length);
|
|
data.CopyTo(_buffer.AsSpan(_position));
|
|
_position += data.Length;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteRaw<T>(T value) where T : unmanaged
|
|
{
|
|
var size = Unsafe.SizeOf<T>();
|
|
EnsureCapacity(size);
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
|
_position += size;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Specialized Writers
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDecimalBits(decimal value)
|
|
{
|
|
EnsureCapacity(16);
|
|
Span<int> bits = stackalloc int[4];
|
|
decimal.TryGetBits(value, bits, out _);
|
|
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDateTimeBits(DateTime value)
|
|
{
|
|
EnsureCapacity(9);
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
|
_buffer[_position + 8] = (byte)value.Kind;
|
|
_position += 9;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteGuidBits(Guid value)
|
|
{
|
|
EnsureCapacity(16);
|
|
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDateTimeOffsetBits(DateTimeOffset value)
|
|
{
|
|
EnsureCapacity(10);
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
|
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
|
_position += 10;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Patches a previously written VarUInt at the specified position.
|
|
/// Works correctly only if the new value requires the same or fewer bytes.
|
|
/// For property counts < 128, this is always 1 byte.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void PatchVarUInt(int position, uint value)
|
|
{
|
|
// Fast path: single byte (covers 0-127, which is most property counts)
|
|
if (value < 0x80)
|
|
{
|
|
_buffer[position] = (byte)value;
|
|
return;
|
|
}
|
|
|
|
// Multi-byte case - need to shift buffer if new encoding is longer
|
|
// For simplicity, we'll rewrite from the position
|
|
// This is rare for property counts
|
|
PatchVarUIntSlow(position, value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void PatchVarUIntSlow(int position, uint value)
|
|
{
|
|
// Calculate current size at position (read until no continuation bit)
|
|
var currentSize = 0;
|
|
var pos = position;
|
|
while (pos < _position && (_buffer[pos] & 0x80) != 0)
|
|
{
|
|
currentSize++;
|
|
pos++;
|
|
}
|
|
currentSize++; // Include final byte without continuation bit
|
|
|
|
// Calculate new size needed
|
|
var newSize = GetVarUIntSize(value);
|
|
|
|
if (newSize == currentSize)
|
|
{
|
|
// Same size - just overwrite
|
|
var tempPos = position;
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[tempPos++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
_buffer[tempPos] = (byte)value;
|
|
}
|
|
else if (newSize < currentSize)
|
|
{
|
|
// New is smaller - shift data left
|
|
var delta = currentSize - newSize;
|
|
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
|
|
_position -= delta;
|
|
|
|
var tempPos = position;
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[tempPos++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
_buffer[tempPos] = (byte)value;
|
|
}
|
|
else
|
|
{
|
|
// New is larger - shift data right
|
|
var delta = newSize - currentSize;
|
|
EnsureCapacity(delta);
|
|
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
|
|
_position += delta;
|
|
|
|
var tempPos = position;
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[tempPos++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
_buffer[tempPos] = (byte)value;
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int GetVarUIntSize(uint value)
|
|
{
|
|
if (value < 0x80) return 1;
|
|
if (value < 0x4000) return 2;
|
|
if (value < 0x200000) return 3;
|
|
if (value < 0x10000000) return 4;
|
|
return 5;
|
|
}
|
|
|
|
public void WriteVarInt(int value)
|
|
{
|
|
var encoded = (uint)((value << 1) ^ (value >> 31));
|
|
// Fast path for small positive values (0-63 when ZigZag encoded)
|
|
if (encoded < 0x80)
|
|
{
|
|
EnsureCapacity(1);
|
|
_buffer[_position++] = (byte)encoded;
|
|
return;
|
|
}
|
|
EnsureCapacity(5);
|
|
WriteVarUIntInternal(encoded);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarUInt(uint value)
|
|
{
|
|
// Fast path for small values (0-127)
|
|
if (value < 0x80)
|
|
{
|
|
EnsureCapacity(1);
|
|
_buffer[_position++] = (byte)value;
|
|
return;
|
|
}
|
|
EnsureCapacity(5);
|
|
WriteVarUIntInternal(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void WriteVarUIntInternal(uint value)
|
|
{
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[_position++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
|
|
_buffer[_position++] = (byte)value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarLong(long value)
|
|
{
|
|
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
|
// Fast path for small values
|
|
if (encoded < 0x80)
|
|
{
|
|
EnsureCapacity(1);
|
|
_buffer[_position++] = (byte)encoded;
|
|
return;
|
|
}
|
|
EnsureCapacity(10);
|
|
WriteVarULongInternal(encoded);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteVarULong(ulong value)
|
|
{
|
|
// Fast path for small values (0-127)
|
|
if (value < 0x80)
|
|
{
|
|
EnsureCapacity(1);
|
|
_buffer[_position++] = (byte)value;
|
|
return;
|
|
}
|
|
EnsureCapacity(10);
|
|
WriteVarULongInternal(value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void WriteVarULongInternal(ulong value)
|
|
{
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[_position++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
|
|
_buffer[_position++] = (byte)value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteStringUtf8(string value)
|
|
{
|
|
// Fast path for ASCII-only strings using SIMD-optimized check
|
|
if (Ascii.IsValid(value))
|
|
{
|
|
WriteVarUInt((uint)value.Length);
|
|
EnsureCapacity(value.Length);
|
|
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
|
|
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
|
|
_position += value.Length;
|
|
return;
|
|
}
|
|
|
|
// Standard path for multi-byte UTF8
|
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
WriteVarUInt((uint)byteCount);
|
|
EnsureCapacity(byteCount);
|
|
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
|
_position += byteCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if string contains only ASCII characters (0-127).
|
|
/// Optimized loop with early exit.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsAscii(string value)
|
|
{
|
|
var span = value.AsSpan();
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
if (span[i] > 127)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes ASCII string directly to byte buffer (char to byte, no encoding needed).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void WriteAsciiDirect(ReadOnlySpan<char> source, Span<byte> destination)
|
|
{
|
|
for (var i = 0; i < source.Length; i++)
|
|
{
|
|
destination[i] = (byte)source[i];
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
|
|
{
|
|
WriteByte(BinaryTypeCode.String);
|
|
WriteVarUInt((uint)utf8Name.Length);
|
|
WriteBytes(utf8Name);
|
|
}
|
|
|
|
public void WriteInt32ArrayOptimized(int[] array)
|
|
{
|
|
for (var i = 0; i < array.Length; i++)
|
|
{
|
|
var value = array[i];
|
|
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
|
{
|
|
WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.Int32);
|
|
WriteVarInt(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteLongArrayOptimized(long[] array)
|
|
{
|
|
for (var i = 0; i < array.Length; i++)
|
|
{
|
|
var value = array[i];
|
|
if (value >= int.MinValue && value <= int.MaxValue)
|
|
{
|
|
var intValue = (int)value;
|
|
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
|
{
|
|
WriteByte(tiny);
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.Int32);
|
|
WriteVarInt(intValue);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WriteByte(BinaryTypeCode.Int64);
|
|
WriteVarLong(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteDoubleArrayBulk(double[] array)
|
|
{
|
|
EnsureCapacity(array.Length * 9);
|
|
for (var i = 0; i < array.Length; i++)
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.Float64;
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
|
_position += 8;
|
|
}
|
|
}
|
|
|
|
public void WriteFloatArrayBulk(float[] array)
|
|
{
|
|
EnsureCapacity(array.Length * 5);
|
|
for (var i = 0; i < array.Length; i++)
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.Float32;
|
|
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
|
_position += 4;
|
|
}
|
|
}
|
|
|
|
public void WriteGuidArrayBulk(Guid[] array)
|
|
{
|
|
EnsureCapacity(array.Length * 17);
|
|
for (var i = 0; i < array.Length; i++)
|
|
{
|
|
_buffer[_position++] = BinaryTypeCode.Guid;
|
|
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SIMD Bulk Copy
|
|
|
|
/// <summary>
|
|
/// Copy bytes using SIMD when available, otherwise fall back to standard copy.
|
|
/// Optimized for Blazor WASM where Vector operations are supported.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteBytesSimd(ReadOnlySpan<byte> source)
|
|
{
|
|
EnsureCapacity(source.Length);
|
|
var destination = _buffer.AsSpan(_position, source.Length);
|
|
|
|
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
|
|
{
|
|
CopyWithSimd(source, destination);
|
|
}
|
|
else
|
|
{
|
|
source.CopyTo(destination);
|
|
}
|
|
|
|
_position += source.Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// SIMD-optimized memory copy for large buffers.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void CopyWithSimd(ReadOnlySpan<byte> source, Span<byte> destination)
|
|
{
|
|
var vectorSize = Vector<byte>.Count;
|
|
var i = 0;
|
|
var length = source.Length;
|
|
|
|
// Process full vectors
|
|
var vectorCount = length / vectorSize;
|
|
for (var v = 0; v < vectorCount; v++)
|
|
{
|
|
var vec = new Vector<byte>(source.Slice(i, vectorSize));
|
|
vec.CopyTo(destination.Slice(i, vectorSize));
|
|
i += vectorSize;
|
|
}
|
|
|
|
// Copy remaining bytes
|
|
if (i < length)
|
|
{
|
|
source.Slice(i).CopyTo(destination.Slice(i));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write double array using SIMD bulk copy (no per-element type codes).
|
|
/// For use when caller handles type codes separately.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteDoubleBulkRaw(ReadOnlySpan<double> values)
|
|
{
|
|
var byteSpan = MemoryMarshal.AsBytes(values);
|
|
WriteBytesSimd(byteSpan);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write float array using SIMD bulk copy.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteFloatBulkRaw(ReadOnlySpan<float> values)
|
|
{
|
|
var byteSpan = MemoryMarshal.AsBytes(values);
|
|
WriteBytesSimd(byteSpan);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write Guid array using SIMD bulk copy.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteGuidBulkRaw(ReadOnlySpan<Guid> values)
|
|
{
|
|
// Guid is 16 bytes, perfect for SIMD
|
|
var byteLength = values.Length * 16;
|
|
EnsureCapacity(byteLength);
|
|
|
|
for (var i = 0; i < values.Length; i++)
|
|
{
|
|
values[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Header and Metadata
|
|
|
|
private int _headerPosition;
|
|
|
|
// Footer-based string interning: no estimation or shifting needed
|
|
// Header: [version][flags][footerPosition (4 bytes, only if string interning)]
|
|
// Body: data with StringInterned indices
|
|
// Footer: interned strings table
|
|
|
|
public void WriteHeaderPlaceholder()
|
|
{
|
|
// Header layout:
|
|
// [0] version (1 byte)
|
|
// [1] flags (1 byte)
|
|
// [2-5] footer position (4 bytes, if footer is needed)
|
|
EnsureCapacity(HasFooter ? 6 : 2);
|
|
_headerPosition = _position;
|
|
_position += HasFooter ? 6 : 2;
|
|
}
|
|
|
|
public void FinalizeHeaderSections()
|
|
{
|
|
var dupCount = GetDupCount(); // Shared counter: string intern + ID tracking
|
|
var hasInternTable = dupCount > 0;
|
|
|
|
// Footer: write merged intern entries (string + ID)
|
|
// Metadata footer is no longer written here — metadata is inline in the body.
|
|
var footerPosition = 0;
|
|
if (hasInternTable)
|
|
{
|
|
footerPosition = _position;
|
|
|
|
// Intern footer
|
|
WriteVarUInt((uint)dupCount);
|
|
WriteInternedFooter();
|
|
}
|
|
|
|
// Write header
|
|
var flags = BinaryTypeCode.HeaderFlagsBase;
|
|
if (UseMetadata)
|
|
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
|
// Encode ReferenceHandlingMode using separate bits
|
|
if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
|
|
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
|
|
else if (ReferenceHandling == ReferenceHandlingMode.All)
|
|
flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
|
|
// Set footer position flag if footer is needed
|
|
if (HasFooter)
|
|
flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition;
|
|
|
|
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
|
_buffer[_headerPosition + 1] = flags;
|
|
|
|
// Write footer position if footer is needed
|
|
if (HasFooter)
|
|
{
|
|
Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes VarUInt at specific position and returns new position.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private int WriteVarUIntAt(int pos, uint value)
|
|
{
|
|
while (value >= 0x80)
|
|
{
|
|
_buffer[pos++] = (byte)(value | 0x80);
|
|
value >>= 7;
|
|
}
|
|
_buffer[pos++] = (byte)value;
|
|
return pos;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Reference Handling
|
|
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
//public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj);
|
|
|
|
/// <summary>
|
|
/// IId-aware tracking for the scan phase.
|
|
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals.
|
|
/// </summary>
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
|
|
//{
|
|
// if (!ReferenceHandling)
|
|
// {
|
|
// existingRefId = 0;
|
|
// return true; // No tracking needed
|
|
// }
|
|
// return _refTracker.TrackForScanningWithIId(obj, metadata, out existingRefId);
|
|
//}
|
|
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
//public bool ShouldWriteRef(object obj, out int refId) => _refTracker.ShouldWriteId(obj, out refId);
|
|
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
//public void MarkAsWritten(object obj, int refId) => _refTracker.MarkAsWritten(obj, refId);
|
|
|
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
//public bool TryGetExistingRef(object obj, out int refId) => _refTracker.TryGetExistingRef(obj, out refId);
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity)
|
|
where TKey : notnull
|
|
{
|
|
if (dict == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
dict.Clear();
|
|
if (dict.EnsureCapacity(0) > maxCapacity)
|
|
{
|
|
dict.TrimExcess();
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
|
|
{
|
|
if (set == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
set.Clear();
|
|
if (set.EnsureCapacity(0) > maxCapacity)
|
|
{
|
|
set.TrimExcess();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region FixStr Methods
|
|
|
|
/// <summary>
|
|
/// Write short ASCII string using FixStr encoding (type+length in single byte).
|
|
/// Only call when string is ASCII and length <= 31.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteFixStr(string value)
|
|
{
|
|
var length = value.Length;
|
|
EnsureCapacity(1 + length);
|
|
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
|
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
|
_position += length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optimized FixStr write: tries SIMD ASCII conversion, falls back to UTF8.
|
|
/// Single-pass: uses Ascii.FromUtf16 which does validation + copy.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteFixStrDirect(string value)
|
|
{
|
|
var length = value.Length;
|
|
EnsureCapacity(1 + length);
|
|
|
|
// Ascii.FromUtf16: SIMD-optimized ASCII conversion
|
|
// Returns actual bytes written - if less than input length, there was a non-ASCII char
|
|
var destSpan = _buffer.AsSpan(_position + 1, length);
|
|
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
|
|
|
|
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
|
|
{
|
|
// Success - write FixStr header
|
|
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
|
|
_position += 1 + length;
|
|
}
|
|
else
|
|
{
|
|
// Non-ASCII or partial - use standard string encoding
|
|
_buffer[_position++] = BinaryTypeCode.String;
|
|
WriteStringUtf8Internal(value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal string write (after String type code already written).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void WriteStringUtf8Internal(string value)
|
|
{
|
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
WriteVarUInt((uint)byteCount);
|
|
EnsureCapacity(byteCount);
|
|
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
|
_position += byteCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write short UTF8 bytes using FixStr encoding.
|
|
/// Only call when byteLength <= 31.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
|
|
{
|
|
var length = utf8Bytes.Length;
|
|
EnsureCapacity(1 + length);
|
|
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
|
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
|
|
_position += length;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|