BufferLease Utilization
On this page
BufferLease is the cornerstone of memory management in Nalix. It provides a reference-counted, pooled abstraction over raw byte arrays, enabling zero-allocation networking pipelines.
1. The Core Concept
In high-performance networking, the Garbage Collector (GC) is your primary bottleneck. Allocating new byte[] for every outgoing packet creates “heap noise” that leads to GC pauses.
Nalix solves this by using a Pooled Buffer Lease (BufferLease backed by BufferPoolManager and ArrayPool<byte>.Shared). Instead of allocating new memory, you rent an existing buffer, use it, and return it to the pool. BufferLease automates this lifecycle with atomic reference counting and optional leak tracking (BufferOptions).
The Buffer Hierarchy
graph TD
A[BufferLease] -->|Rents Shell| B[ObjectPoolManager]
A -->|Rents Array| C[BufferPoolManager]
C -->|Delegates| D["ArrayPool<byte>.Shared"]
C -->|Optional Leak Tracking| E[BufferOptions Leak Tracker]2. Constructing Outgoing Packets
To send data without allocations, follow the Rent-Serialize-Commit-Send pattern.
Step-by-Step Implementation
public async ValueTask BroadcastUpdate(IConnection connection, GameUpdate packet)
{
// 1. RENT: Obtain a lease from the pool.
// This is O(1) and does not allocate on the heap.
using var lease = BufferLease.Rent(packet.Length);
// 2. SERIALIZE: Write directly into the full capacity of the lease.
// lease.SpanFull gives you access to the entire rented array.
int written = packet.Serialize(lease.SpanFull);
// 3. COMMIT: Update the lease length to match the actual data written.
// This ensures downstream components (compression, encryption)
// only process valid bytes.
lease.CommitLength(written);
// 4. SEND: Pass the ReadOnlyMemory<byte> to the transport.
// The transport will transmit the data. The 'using' block
// ensures the buffer returns to the pool even if sending fails.
await connection.TCP.SendAsync(lease.Memory);
}
3. Lifecycle & Reference Counting
BufferLease supports reference counting via Retain() and Dispose().
Retention Pattern
If you need to pass a lease to an asynchronous background task or a broadcast queue, you must call Retain().
public void HandleIncoming(IBufferLease lease)
{
// We want to process this lease in the background
lease.Retain(); // Increment ref count to 2
_ = Task.Run(() =>
{
try
{
ProcessLongRunning(lease.Memory);
}
finally
{
lease.Dispose(); // Decrement ref count
}
});
// Original lease is disposed here by the framework (ref count goes to 1)
}
4. Advanced: Ownership Transfer
In some scenarios, you might want to “detach” the underlying array from the lease (e.g., if you are passing it to a legacy API that manages its own memory).
if (lease.ReleaseOwnership(out byte[]? buffer, out int start, out int length))
{
// The lease is now 'empty' and won't return the buffer to the pool on Dispose.
// YOU are now responsible for returning the 'buffer' to the pool manually!
bufferPool.Return(buffer);
}
5. Performance Checklist
| Action | Impact | Why? |
|---|---|---|
Use using var | Critical | Ensures deterministic return to the pool. |
Avoid lease.Span in async | Critical | Span<T> cannot be used across await boundaries. |
Prefer lease.Memory | High | ReadOnlyMemory<byte> is safe for async and zero-allocation. |
| Match Capacity | Medium | Renting a size close to your packet length reduces internal fragmentation. |