AyCode.Core/AyCode.Core/Serializers/Binaries/ArrayBinaryOutput.cs

135 lines
4.9 KiB
C#

using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// High-performance binary output backed by a byte[] from ArrayPool.
/// Only handles buffer lifecycle (Initialize, Grow, result extraction).
/// All write methods live in BinarySerializationContext for zero virtual dispatch.
///
/// This is the fastest output path — use when the result is needed as byte[]/Span.
/// </summary>
public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable
{
private const int DefaultMinBufferSize = 256;
private const int MaxKeepBufferSize = 32 * 1024; // 32KB — below this, keep for reuse
private readonly int _initialCapacity;
private byte[] _rentedBuffer;
public ArrayBinaryOutput(int initialCapacity = 65535)
{
_initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize);
_rentedBuffer = ArrayPool<byte>.Shared.Rent(_initialCapacity);
}
/// <summary>
/// Provides the initial buffer. Position starts at 0, bufferEnd = buffer.Length.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
{
buffer = _rentedBuffer;
position = 0;
bufferEnd = _rentedBuffer.Length;
}
/// <summary>
/// Grows the buffer: rents a bigger one from ArrayPool, copies old data, returns old to pool.
/// Position is unchanged (data was copied to the same offset in the new buffer).
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
{
var required = position + needed;
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;
bufferEnd = newBuffer.Length;
_rentedBuffer = newBuffer;
}
/// <summary>
/// For ArrayBinaryOutput, position IS the total (single contiguous buffer).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetTotalPosition(int currentPosition) => currentPosition;
#region Result Extraction receive buffer/position from context
//TODO: miért nem static a AsSpan?
/// <summary>Returns the written data as a ReadOnlySpan without allocation.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> AsSpan(byte[] buffer, int position) => buffer.AsSpan(0, position);
//TODO: miért nem static a ToArray? Miért nem valami static common osztályban van?
/// <summary>Copies the written data to a new exactly-sized array.</summary>
public byte[] ToArray(byte[] buffer, int position)
{
var result = GC.AllocateUninitializedArray<byte>(position);
buffer.AsSpan(0, position).CopyTo(result);
return result;
}
/// <summary>Copies the written data to an IBufferWriter (single memcpy).</summary>
public void WriteTo(IBufferWriter<byte> writer, byte[] buffer, int position)
{
var span = writer.GetSpan(position);
buffer.AsSpan(0, position).CopyTo(span);
writer.Advance(position);
}
//TODO: miért nem static a DetachResult?
/// <summary>
/// Detaches the internal buffer as a BinarySerializationResult and allocates a fresh buffer.
/// The caller owns the returned result and must dispose it to return the buffer to the pool.
/// </summary>
public AcBinarySerializer.BinarySerializationResult DetachResult(byte[] buffer, int position)
{
var resultBuffer = buffer;
var resultLength = position;
//_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, _initialCapacity));
return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true);
}
/// <summary>
/// Resets for reuse when context is returned to pool.
/// Small buffers (≤ MaxKeepBufferSize): keep as-is (faster than pool round-trip).
/// Large buffers: return to pool, rent a halved one to avoid memory bloat.
/// </summary>
public void Reset()
{
if (_rentedBuffer == null) return;
// Small buffer: keep as-is
if (_rentedBuffer.Length <= MaxKeepBufferSize) return;
// Large buffer: return to pool, rent half size
var nextCapacity = Math.Max(_rentedBuffer.Length / 2, _initialCapacity);
ArrayPool<byte>.Shared.Return(_rentedBuffer);
_rentedBuffer = nextCapacity == _initialCapacity ? null : ArrayPool<byte>.Shared.Rent(nextCapacity);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_rentedBuffer != null)
{
ArrayPool<byte>.Shared.Return(_rentedBuffer);
_rentedBuffer = null!;
}
}
#endregion
}