AyCode.Core/AyCode.Core/Serializers/Binaries/SequenceBinaryInput.cs

185 lines
6.8 KiB
C#

using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AyCode.Core.Serializers.Binaries;
/// <summary>
/// Binary input that reads directly from a ReadOnlySequence (e.g. SignalR pipe, network stream).
/// Iterates segments lazily via TryGet — no upfront ArraySegment[] allocation.
///
/// The context's _buffer always points to the current segment's backing byte[] (zero-copy).
/// Cross-boundary values (straddling two+ segments) are copied into a small ArrayPool scratch buffer.
/// After the scratch read, _afterCrossBoundary restores the context to the segment's backing array.
///
/// Typical overhead for a 225KB payload with 4096-byte segments:
/// ~224.5KB zero-copy reads, ~500 bytes scratch copies (at ~55 segment boundaries).
/// </summary>
public struct SequenceBinaryInput : IBinaryInputBase
{
private ReadOnlySequence<byte> _sequence;
private SequencePosition _nextPosition;
// ArrayPool scratch for cross-boundary reads — lazy rent, reused across boundaries
private byte[]? _scratchBuffer;
private bool _afterCrossBoundary;
// After cross-boundary: saved state of the last touched segment for restore
private byte[]? _savedBuffer;
private int _savedPosition;
private int _savedBufferLength;
/// <summary>
/// Creates a SequenceBinaryInput from a ReadOnlySequence.
/// Does NOT pre-extract segments — iterates lazily via TryGet.
/// </summary>
public SequenceBinaryInput(ReadOnlySequence<byte> sequence)
{
_sequence = sequence;
_nextPosition = sequence.Start;
_scratchBuffer = null;
_afterCrossBoundary = false;
_savedBuffer = null;
_savedPosition = 0;
_savedBufferLength = 0;
}
/// <summary>
/// Provides the first segment's buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Initialize(out byte[] buffer, out int position, out int bufferLength)
{
if (!_sequence.TryGet(ref _nextPosition, out var memory))
throw new AcBinaryDeserializationException("Empty sequence — no segments to read.");
ExtractArray(memory, out buffer, out position, out bufferLength);
}
/// <summary>
/// Advances to the next segment when the current one is exhausted.
/// Handles cross-boundary reads by copying overlapping bytes to a scratch buffer.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public bool TryAdvanceSegment(ref byte[] buffer, ref int position, ref int bufferLength, int needed)
{
// After cross-boundary scratch read: restore to the last touched segment's backing array
if (_afterCrossBoundary)
{
_afterCrossBoundary = false;
buffer = _savedBuffer!;
position = _savedPosition;
bufferLength = _savedBufferLength;
return position < bufferLength;
}
var remaining = bufferLength - position;
if (remaining > 0 && remaining < needed)
{
// Cross-boundary: value spans segment boundary
return TryReadCrossBoundary(ref buffer, ref position, ref bufferLength, needed, remaining);
}
// Current segment fully consumed — advance to next
return TryLoadNextSegment(ref buffer, ref position, ref bufferLength);
}
/// <summary>
/// Returns the ArrayPool scratch buffer if one was rented.
/// Must be called after deserialization completes.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Release()
{
if (_scratchBuffer != null)
{
ArrayPool<byte>.Shared.Return(_scratchBuffer);
_scratchBuffer = null;
}
}
/// <summary>
/// Loads the next segment from the sequence via TryGet.
/// Extracts the backing byte[] for zero-copy access.
/// </summary>
private bool TryLoadNextSegment(ref byte[] buffer, ref int position, ref int bufferLength)
{
if (!_sequence.TryGet(ref _nextPosition, out var memory) || memory.Length == 0)
return false;
ExtractArray(memory, out buffer, out position, out bufferLength);
return true;
}
/// <summary>
/// Handles a read that spans N segments by copying the overlapping bytes
/// into an ArrayPool scratch buffer. After this read, the next TryAdvanceSegment
/// restores the context to the last touched segment's backing array.
/// </summary>
private bool TryReadCrossBoundary(ref byte[] buffer, ref int position, ref int bufferLength, int needed, int remaining)
{
// Rent scratch (or reuse if large enough)
if (_scratchBuffer == null || _scratchBuffer.Length < needed)
{
if (_scratchBuffer != null)
ArrayPool<byte>.Shared.Return(_scratchBuffer);
_scratchBuffer = ArrayPool<byte>.Shared.Rent(needed);
}
// 1) Copy tail of current segment
Buffer.BlockCopy(buffer, position, _scratchBuffer, 0, remaining);
var filled = remaining;
// 2) Copy from subsequent segments until we have enough
while (filled < needed)
{
if (!_sequence.TryGet(ref _nextPosition, out var memory) || memory.Length == 0)
return false;
ExtractArray(memory, out var segArray, out var segOffset, out var segBufferLength);
var segCount = segBufferLength - segOffset;
var take = Math.Min(needed - filled, segCount);
Buffer.BlockCopy(segArray, segOffset, _scratchBuffer, filled, take);
filled += take;
// Save last touched segment for _afterCrossBoundary restore
_savedBuffer = segArray;
_savedPosition = segOffset + take;
_savedBufferLength = segBufferLength;
}
// Context reads from scratch buffer
buffer = _scratchBuffer;
position = 0;
bufferLength = filled;
_afterCrossBoundary = true;
return true;
}
/// <summary>
/// Extracts the backing byte[] from a ReadOnlyMemory segment.
/// Array-backed (99.9%): zero-copy reference to backing array.
/// Non-array-backed (native memory): copies to a managed byte[].
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ExtractArray(ReadOnlyMemory<byte> memory, out byte[] buffer, out int position, out int bufferLength)
{
if (MemoryMarshal.TryGetArray(memory, out var segment))
{
buffer = segment.Array!;
position = segment.Offset;
bufferLength = segment.Offset + segment.Count;
}
else
{
var temp = new byte[memory.Length];
memory.Span.CopyTo(temp);
buffer = temp;
position = 0;
bufferLength = temp.Length;
}
}
}