185 lines
6.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|