Class AlignedBuffer
- Namespace
- DatabentoDotNet.Dbn
- Assembly
- DatabentoDotNet.Dbn.dll
A read/write byte buffer backed by ulong[], guaranteeing that its byte view starts
8-byte aligned so records can later be reinterpreted in place over it.
[SuppressMessage("Reliability", "CA1001:Types that own disposable fields should be disposable", Justification = "The disposable field is the ByteView memory manager, and MemoryManager<T> is IDisposable only because managers over native memory need to be. This one projects a managed ulong[] the buffer already owns and its Dispose is empty, so there is nothing to release. Honouring the rule would make AlignedBuffer IDisposable, which forces IDisposable onto DbnFsm, DbnDecoder and the M2 live client in turn — a contract promising cleanup that never happens, spread across the whole public surface.")]
public sealed class AlignedBuffer
- Inheritance
-
AlignedBuffer
- Inherited Members
Remarks
Ported from the Rust dbn crate's AlignedBuffer
(decode/dbn/aligned_buffer.rs), itself forked from the oval ring-buffer crate.
Rust gets 8-byte alignment from Box<[u64]>; the direct .NET analogue is
ulong[] viewed as bytes through AsBytes<T>(Span<T>). Every
array's element storage is aligned to at least its element type's own alignment, so a
ulong[]'s byte view starts 8-byte aligned by construction. A byte[] carries no
such guarantee in .NET — using one here would be silently wrong on x64 today (it happens to
work) and a crash or a torn read on an architecture that faults on misaligned access.
Shifts are explicit. Consume(int) and Fill(int) move only the
internal position/end indices — neither ever copies a byte. Callers reclaim the
consumed prefix by calling Shift() or ShiftForSpace(int) at a point of
their choosing (typically a refill boundary), so the one operation that actually moves memory
is paid there and is visible in a profile, rather than hidden inside every record read.
Invariant, maintained by every member: 0 <= position <= end <= Capacity.
Constructors
AlignedBuffer()
Allocates a buffer with DefaultCapacity usable bytes.
public AlignedBuffer()
AlignedBuffer(int)
Allocates a buffer with at least capacity usable bytes.
public AlignedBuffer(int capacity)
Parameters
capacityintThe minimum number of usable bytes.
Remarks
The actual capacity is rounded up to a multiple of 8 — so the backing ulong[] has
no partial trailing element — and is never allowed below
MaxRecordLength: a buffer smaller than the largest possible
record could never hand the decoder a complete record's worth of contiguous space, no
matter how it is shifted. This floor is a .NET-side addition, not something upstream
enforces: AlignedBuffer::with_capacity allocates exactly what is asked, leaving the
floor to be upheld by callers — the DbnFsm builder docs merely warn that a too-small
buffer "must be at least the size of the largest record" for the zero-copy path to work,
they do not clamp it. Enforcing the floor here removes that foot-gun for every caller at
no cost, since real callers never want a buffer smaller than this anyway.
Exceptions
- ArgumentOutOfRangeException
capacityis negative.
Fields
DefaultCapacity
The capacity used when none is supplied: 64 KiB.
public const int DefaultCapacity = 65536
Field Value
Remarks
Matches upstream's DbnFsm::DEFAULT_BUF_SIZE (fsm.rs:105) — the default main
buffer size the incremental decoder allocates.
Properties
AvailableData
The number of bytes currently available to read.
public int AvailableData { get; }
Property Value
AvailableSpace
The number of bytes currently available to write.
public int AvailableSpace { get; }
Property Value
Capacity
The buffer's total byte capacity.
public int Capacity { get; }
Property Value
Data
The currently readable bytes: the slice from position to end.
public ReadOnlySpan<byte> Data { get; }
Property Value
DataMut
A mutable view of the currently readable bytes.
public Span<byte> DataMut { get; }
Property Value
IsEmpty
true when there is no readable data (position == end).
public bool IsEmpty { get; }
Property Value
Space
The writable tail: the slice from end to Capacity. Does not shift —
call ShiftForSpace(int) or Shift() first for more contiguous room.
public Span<byte> Space { get; }
Property Value
Remarks
Derived from SpaceMemory rather than sliced independently. The two describe the same bytes, and a caller that mixes them — a synchronous read here, an asynchronous read there — must be able to rely on that; deriving one from the other makes it true by construction instead of by a test that has to remember to check.
SpaceMemory
The writable tail as a Memory<T>: exactly the bytes Space describes, in the form asynchronous I/O requires.
public Memory<byte> SpaceMemory { get; }
Property Value
Remarks
Why this exists at all. stream.ReadAsync(buffer.Space) does not compile:
there is no ReadAsync(Span<byte>) overload in .NET, and there cannot be, since
a ref struct may not be preserved across an await. Asynchronous reads take a
Memory<byte> — which has no obvious spelling here either: the backing store is
a ulong[] for alignment (see the remarks on AlignedBuffer),
Cast<TFrom, TTo>(Span<TFrom>) reinterprets spans only, and the
BCL has no Memory equivalent. A MemoryManager<T> is the sanctioned way
to project a byte view over storage the BCL cannot type-pun for you, and that is what backs
this property.
The projection is live, not a snapshot. The manager resolves the array on every call rather than caching it, so a Memory<T> taken before a Grow(int) still resolves to the grown array afterwards — at the same byte offsets, which Grow(int) preserves. A manager that captured the array instead would silently keep writing into the abandoned one.
An outstanding pin is a different matter. Pinning happens per I/O operation, and a MemoryHandle taken before a Grow(int) points into the old array for as long as it lives. Do not grow the buffer while an asynchronous read into it is in flight. Nothing in this codec does: growth happens only while decoding the metadata block, between reads, on the one thread that owns the buffer.
Methods
Consume(int)
Advances the read position by count bytes, capped to
AvailableData. Moves the position index only — never copies a byte.
public int Consume(int count)
Parameters
countintThe number of bytes to consume.
Returns
- int
The number of bytes actually consumed.
Exceptions
- ArgumentOutOfRangeException
countis negative.
Fill(int)
Marks count bytes (capped to AvailableSpace) as written.
The caller must already have written them into Space. Moves the end
index only — never copies a byte.
public int Fill(int count)
Parameters
countintThe number of bytes to mark as filled.
Returns
- int
The number of bytes actually filled.
Exceptions
- ArgumentOutOfRangeException
countis negative.
Grow(int)
Grows the backing storage to at least newSize bytes (rounded up to a
multiple of 8), preserving every existing byte at its current offset. position and
end are unchanged.
public bool Grow(int newSize)
Parameters
newSizeintThe minimum number of usable bytes after growth.
Returns
- bool
true if the buffer was reallocated; false if it was already at least this large, in which case nothing changed.
Exceptions
- ArgumentOutOfRangeException
newSizeis negative.
Reset()
Resets position and end to 0. Keeps the allocated capacity.
public void Reset()
Shift()
Moves the unconsumed bytes ([position, end)) down to offset 0, then sets
position to 0 and end to the moved length. A no-op when position is
already 0. This is the only member of AlignedBuffer that copies memory.
public void Shift()
ShiftForSpace(int)
Reclaims the consumed prefix by calling Shift(), but only if
AvailableSpace is currently less than needed and there is
something to reclaim (position > 0); a no-op otherwise. Never grows the buffer.
public void ShiftForSpace(int needed)
Parameters
neededintThe number of contiguous writable bytes the caller wants.
Exceptions
- ArgumentOutOfRangeException
neededis negative.