What's wrong
AllocateBuffer sizes the backing array to Capacity = NextPower2(Length) (Containers/RingBuffer.cs ~L139), but at most Length slots are live. When the buffer is full, PushBack (~L159) writes Buffer[BackIndex] and advances FrontIndex. It never clears the slot that just dropped out of the live range:
Buffer[BackIndex] = o;
if (Count == Length)
{
FrontIndex = (FrontIndex + 1) & (Capacity - 1);
}
Once the buffer has wrapped, the Capacity - Length slots outside the live window always hold evicted elements. Each one is released only when that slot is overwritten again, and never if pushing stops. This is the same class of leak that #72 fixed for Clear(). That fix did not touch the eviction path.
Failure scenario
new RingBuffer<object>(5) gives Capacity 8. Push a byte[1024], push 5 more items, then run GC.Collect(). The first element has been evicted: it is not in Count or in enumeration. It is still reachable, though, and a WeakReference to it stays alive.
new RingBuffer<object>(1025) gives Capacity 2048. After 10,000 pushes of byte[1024] followed by a full GC: Count=1025, live objects 2048, evicted-but-alive 1023.
For Length = 2^k + 1, roughly twice the logical contents stay alive for the lifetime of the buffer. That is significant for buffers of frames, audio blocks, or other large objects, and it holds on to anything those objects reference.
Suggested fix
Clear the evicted slot before advancing, then write the new item. Writing after the clear also handles Length == Capacity, where FrontIndex == BackIndex:
public void PushBack(T o)
{
if (Count == Length)
{
Buffer[FrontIndex] = default!;
FrontIndex = (FrontIndex + 1) & (Capacity - 1);
}
else
{
Count++;
}
Buffer[BackIndex] = o;
BackIndex = (BackIndex + 1) & (Capacity - 1);
}
Optionally guard the clear with RuntimeHelpers.IsReferenceOrContainsReferences<T>(), the way Clear() does. This fix has been tried locally: the repro above then reports 0 evicted-but-alive objects, and all existing RingBufferTests pass.
Acceptance criteria
What's wrong
AllocateBuffersizes the backing array toCapacity = NextPower2(Length)(Containers/RingBuffer.cs~L139), but at mostLengthslots are live. When the buffer is full,PushBack(~L159) writesBuffer[BackIndex]and advancesFrontIndex. It never clears the slot that just dropped out of the live range:Once the buffer has wrapped, the
Capacity - Lengthslots outside the live window always hold evicted elements. Each one is released only when that slot is overwritten again, and never if pushing stops. This is the same class of leak that #72 fixed forClear(). That fix did not touch the eviction path.Failure scenario
new RingBuffer<object>(5)gives Capacity 8. Push abyte[1024], push 5 more items, then runGC.Collect(). The first element has been evicted: it is not inCountor in enumeration. It is still reachable, though, and aWeakReferenceto it stays alive.new RingBuffer<object>(1025)gives Capacity 2048. After 10,000 pushes ofbyte[1024]followed by a full GC:Count=1025, live objects2048, evicted-but-alive1023.For
Length = 2^k + 1, roughly twice the logical contents stay alive for the lifetime of the buffer. That is significant for buffers of frames, audio blocks, or other large objects, and it holds on to anything those objects reference.Suggested fix
Clear the evicted slot before advancing, then write the new item. Writing after the clear also handles
Length == Capacity, whereFrontIndex == BackIndex:Optionally guard the clear with
RuntimeHelpers.IsReferenceOrContainsReferences<T>(), the wayClear()does. This fix has been tried locally: the repro above then reports 0 evicted-but-alive objects, and all existingRingBufferTestspass.Acceptance criteria
WeakReferencetest like the one added for RingBuffer<T>.Clear() keeps cleared reference-type elements alive (memory leak) #72: push an object into aRingBuffer<object>(5), push 5 more, and after a full GC the evicted object has been collected.