Skip to content

RingBuffer keeps up to Length−1 evicted elements alive when Length is not a power of two #85

Description

@matt-edmondson

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions