[LOADED_DOCS: 3 files, no new loads]
Refactor AsyncPipeWriterOutput buffer management Refactored AsyncPipeWriterOutput to lazily allocate and reuse the fallback ArrayPool<byte> buffer across a serialize lifecycle, releasing it only once at the end. Replaced the _ownedBuffer boolean with _hasOwnedBuffer and a nullable _ownedBuffer field. Centralized buffer release logic, updated diagnostics, and improved chunk acquisition to minimize ArrayPool churn and clarify buffer ownership semantics.
This commit is contained in:
parent
910b0deab8
commit
96a2f90535
|
|
@ -85,7 +85,14 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
||||||
private readonly TimeSpan _flushTimeout;
|
private readonly TimeSpan _flushTimeout;
|
||||||
private int _committedBytes;
|
private int _committedBytes;
|
||||||
private int _currentChunkStart;
|
private int _currentChunkStart;
|
||||||
private bool _ownedBuffer;
|
// Whether the current chunk's buffer is the owned ArrayPool fallback (true) or a zero-copy
|
||||||
|
// PipeWriter slab (false). Used by CommitCurrentChunk to pick the commit strategy.
|
||||||
|
private bool _hasOwnedBuffer;
|
||||||
|
// Lazy-allocated, long-lived ArrayPool buffer used as the fallback when PipeWriter.GetMemory
|
||||||
|
// returns non-array-backed memory (custom non-byte[] PipeWriter — practically never on
|
||||||
|
// production writers). Reused across chunks within a single serialize lifecycle; returned
|
||||||
|
// to the pool exactly once in Flush(). Stays null entirely on the zero-copy path.
|
||||||
|
private byte[]? _ownedBuffer;
|
||||||
private ValueTask<FlushResult> _lastFlush;
|
private ValueTask<FlushResult> _lastFlush;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -133,7 +140,8 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
||||||
_serializeFlushAndAcquire = StreamPipeWriterType.IsInstanceOfType(pipeWriter);
|
_serializeFlushAndAcquire = StreamPipeWriterType.IsInstanceOfType(pipeWriter);
|
||||||
|
|
||||||
_committedBytes = 0;
|
_committedBytes = 0;
|
||||||
_ownedBuffer = false;
|
_hasOwnedBuffer = false;
|
||||||
|
_ownedBuffer = null;
|
||||||
_lastFlush = default;
|
_lastFlush = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,17 +241,43 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
||||||
SyncAwaitFlush(_lastFlush);
|
SyncAwaitFlush(_lastFlush);
|
||||||
|
|
||||||
CommitCurrentChunk(buffer, position);
|
CommitCurrentChunk(buffer, position);
|
||||||
|
|
||||||
|
// End of serialize lifecycle — return the owned fallback buffer to ArrayPool exactly
|
||||||
|
// once (NOT per chunk). The buffer was reused across all chunks in this lifecycle;
|
||||||
|
// releasing it now avoids per-chunk rent/return churn even when the fallback path
|
||||||
|
// actually fires.
|
||||||
|
ReleaseOwnedBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// No-op for PipeWriter-based output — chunks are owned by PipeWriter, not us.
|
/// Releases the lazy-allocated owned fallback buffer (if any) back to <see cref="ArrayPool{T}"/>.
|
||||||
|
/// Idempotent — safe to call multiple times. Called from <see cref="Flush"/> at the end of a
|
||||||
|
/// successful serialize cycle, and also from <see cref="Reset"/> for defensive cleanup if a
|
||||||
|
/// caller reuses the struct.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset() { }
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ReleaseOwnedBuffer()
|
||||||
|
{
|
||||||
|
if (_ownedBuffer != null)
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(_ownedBuffer);
|
||||||
|
_ownedBuffer = null;
|
||||||
|
}
|
||||||
|
_hasOwnedBuffer = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defensive cleanup — releases the owned fallback buffer if one was retained. Normally a
|
||||||
|
/// no-op for PipeWriter-based output (chunks are owned by PipeWriter), but covers the rare
|
||||||
|
/// case where the same struct instance is reused after a serialize without going through Flush.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset() => ReleaseOwnedBuffer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Commits the current chunk to the PipeWriter. In framed mode, patches the reserved
|
/// Commits the current chunk to the PipeWriter. In framed mode, patches the reserved
|
||||||
/// <c>[201][UINT16 dataBytes]</c> header before Advance; in raw mode, simply Advances the data.
|
/// <c>[201][UINT16 dataBytes]</c> header before Advance; in raw mode, simply Advances the data.
|
||||||
/// For owned buffers, copies to PipeWriter first.
|
/// For owned buffers, copies to PipeWriter first (without releasing the buffer — Flush() does
|
||||||
|
/// that once at the end).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private void CommitCurrentChunk(byte[] buffer, int position)
|
private void CommitCurrentChunk(byte[] buffer, int position)
|
||||||
|
|
@ -258,32 +292,35 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(headerStart + 1, 2), (ushort)dataBytes);
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(headerStart + 1, 2), (ushort)dataBytes);
|
||||||
|
|
||||||
EmitDiagnostic($"CommitCurrentChunk[framed]: dataBytes={dataBytes} headerStart={headerStart} _currentChunkStart={_currentChunkStart} position={position} _ownedBuffer={_ownedBuffer} → Advance({HeaderSize + dataBytes})");
|
EmitDiagnostic($"CommitCurrentChunk[framed]: dataBytes={dataBytes} headerStart={headerStart} _currentChunkStart={_currentChunkStart} position={position} _hasOwnedBuffer={_hasOwnedBuffer} → Advance({HeaderSize + dataBytes})");
|
||||||
|
|
||||||
if (_ownedBuffer) FlushOwnedBuffer(buffer, headerStart, HeaderSize + dataBytes);
|
if (_hasOwnedBuffer) CopyOwnedToPipeWriter(buffer, headerStart, HeaderSize + dataBytes);
|
||||||
else _pipeWriter.Advance(HeaderSize + dataBytes);
|
else _pipeWriter.Advance(HeaderSize + dataBytes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
EmitDiagnostic($"CommitCurrentChunk[raw]: dataBytes={dataBytes} _currentChunkStart={_currentChunkStart} position={position} _ownedBuffer={_ownedBuffer} → Advance({dataBytes})");
|
EmitDiagnostic($"CommitCurrentChunk[raw]: dataBytes={dataBytes} _currentChunkStart={_currentChunkStart} position={position} _hasOwnedBuffer={_hasOwnedBuffer} → Advance({dataBytes})");
|
||||||
|
|
||||||
if (_ownedBuffer) FlushOwnedBuffer(buffer, _currentChunkStart, dataBytes);
|
if (_hasOwnedBuffer) CopyOwnedToPipeWriter(buffer, _currentChunkStart, dataBytes);
|
||||||
else _pipeWriter.Advance(dataBytes);
|
else _pipeWriter.Advance(dataBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
_committedBytes += dataBytes; // only count data bytes, not framing
|
_committedBytes += dataBytes; // only count data bytes, not framing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the owned-buffer chunk into the PipeWriter (since GetMemory returned non-array-backed
|
||||||
|
/// memory and the serializer needs <c>byte[]</c>). Does NOT return the buffer to the pool —
|
||||||
|
/// the <see cref="_ownedBuffer"/> reference stays alive across chunks within this serialize
|
||||||
|
/// lifecycle; <see cref="Flush"/> releases it exactly once at the end.
|
||||||
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private void FlushOwnedBuffer(byte[] buffer, int start, int length)
|
private void CopyOwnedToPipeWriter(byte[] buffer, int start, int length)
|
||||||
{
|
{
|
||||||
var span = _pipeWriter.GetSpan(length);
|
var span = _pipeWriter.GetSpan(length);
|
||||||
|
|
||||||
buffer.AsSpan(start, length).CopyTo(span);
|
buffer.AsSpan(start, length).CopyTo(span);
|
||||||
_pipeWriter.Advance(length);
|
_pipeWriter.Advance(length);
|
||||||
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
_ownedBuffer = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AcquireChunk(int requestSize, out byte[] buffer, out int position, out int bufferEnd)
|
private void AcquireChunk(int requestSize, out byte[] buffer, out int position, out int bufferEnd)
|
||||||
|
|
@ -303,19 +340,32 @@ public struct AsyncPipeWriterOutput : IBinaryOutputBase
|
||||||
buffer = segment.Array;
|
buffer = segment.Array;
|
||||||
position = segment.Offset + headerOffset;
|
position = segment.Offset + headerOffset;
|
||||||
bufferEnd = segment.Offset + headerOffset + dataSize;
|
bufferEnd = segment.Offset + headerOffset + dataSize;
|
||||||
_ownedBuffer = false;
|
_hasOwnedBuffer = false;
|
||||||
|
|
||||||
EmitDiagnostic($"AcquireChunk[zc]: segment.Array.Length={segment.Array.Length} segment.Offset={segment.Offset} segment.Count={segment.Count} → buffer[{position}..{bufferEnd}]");
|
EmitDiagnostic($"AcquireChunk[zc]: segment.Array.Length={segment.Array.Length} segment.Offset={segment.Offset} segment.Count={segment.Count} → buffer[{position}..{bufferEnd}]");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var owned = ArrayPool<byte>.Shared.Rent(totalRequest);
|
// Lazy reuse: keep the same ArrayPool buffer across chunks within this serialize
|
||||||
buffer = owned;
|
// lifecycle. Only re-rent if the existing buffer is too small for the next chunk.
|
||||||
|
// Released exactly once in Flush() — no per-chunk rent/return churn even when this
|
||||||
|
// (rare) path actually fires.
|
||||||
|
if (_ownedBuffer == null || _ownedBuffer.Length < totalRequest)
|
||||||
|
{
|
||||||
|
if (_ownedBuffer != null) ArrayPool<byte>.Shared.Return(_ownedBuffer);
|
||||||
|
_ownedBuffer = ArrayPool<byte>.Shared.Rent(totalRequest);
|
||||||
|
|
||||||
|
EmitDiagnostic($"AcquireChunk[ob-rent]: rented={_ownedBuffer.Length} (totalRequest={totalRequest})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmitDiagnostic($"AcquireChunk[ob-reuse]: reused={_ownedBuffer.Length} (totalRequest={totalRequest})");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = _ownedBuffer;
|
||||||
position = headerOffset;
|
position = headerOffset;
|
||||||
bufferEnd = headerOffset + dataSize;
|
bufferEnd = headerOffset + dataSize;
|
||||||
_ownedBuffer = true;
|
_hasOwnedBuffer = true;
|
||||||
|
|
||||||
EmitDiagnostic($"AcquireChunk[ob]: rented={owned.Length} → buffer[{position}..{bufferEnd}]");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue