Table of Contents

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

capacity int

The 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

capacity is negative.

Fields

DefaultCapacity

The capacity used when none is supplied: 64 KiB.

public const int DefaultCapacity = 65536

Field Value

int

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

int

AvailableSpace

The number of bytes currently available to write.

public int AvailableSpace { get; }

Property Value

int

Capacity

The buffer's total byte capacity.

public int Capacity { get; }

Property Value

int

Data

The currently readable bytes: the slice from position to end.

public ReadOnlySpan<byte> Data { get; }

Property Value

ReadOnlySpan<byte>

DataMut

A mutable view of the currently readable bytes.

public Span<byte> DataMut { get; }

Property Value

Span<byte>

IsEmpty

true when there is no readable data (position == end).

public bool IsEmpty { get; }

Property Value

bool

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

Span<byte>

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

Memory<byte>

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

count int

The number of bytes to consume.

Returns

int

The number of bytes actually consumed.

Exceptions

ArgumentOutOfRangeException

count is 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

count int

The number of bytes to mark as filled.

Returns

int

The number of bytes actually filled.

Exceptions

ArgumentOutOfRangeException

count is 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

newSize int

The 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

newSize is 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

needed int

The number of contiguous writable bytes the caller wants.

Exceptions

ArgumentOutOfRangeException

needed is negative.