File: src\Shared\CircularBuffer.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Diagnostics;
using System.Runtime.InteropServices;
 
namespace Aspire.Dashboard.Otlp.Storage;
 
/// <summary>
/// The circular buffer starts with an empty list and grows to a maximum size.
/// When the buffer is full, adding or inserting a new item removes the first item in the buffer.
/// </summary>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(CircularBuffer<>.CircularBufferDebugView))]
internal sealed class CircularBuffer<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable
{
    // Internal for testing.
    internal readonly List<T> _buffer;
    internal int _start;
    internal int _end;
 
    public event Action<T>? ItemRemovedForCapacity;
 
    public CircularBuffer(int capacity) : this(new List<T>(), capacity, start: 0, end: 0)
    {
    }
 
    internal CircularBuffer(List<T> buffer, int capacity, int start, int end)
    {
        if (capacity < 1)
        {
            throw new ArgumentException("Circular buffer must have a capacity greater than 0.", nameof(capacity));
        }
 
        _buffer = buffer;
        Capacity = capacity;
        _start = start;
        _end = end;
    }
 
    public int Capacity { get; }
 
    public bool IsFull => Count == Capacity;
 
    public bool IsEmpty => Count == 0;
 
    public int Count => _buffer.Count;
 
    public bool IsReadOnly { get; }
 
    public bool IsFixedSize { get; } = true;
 
    public object SyncRoot { get; } = new object();
 
    public bool IsSynchronized { get; }
 
    public int IndexOf(T item)
    {
        for (var index = 0; index < Count; ++index)
        {
            if (Equals(this[index], item))
            {
                return index;
            }
        }
        return -1;
    }
 
    public void Insert(int index, T item)
    {
        // TODO: There are a lot of branches in this method. Look into simplifying it.
        if (index == Count)
        {
            Add(item);
            return;
        }
 
        ValidateIndexInRange(index);
 
        if (IsFull)
        {
            if (index == 0)
            {
                // When full, the item inserted at 0 is always the "last" in the buffer and is removed.
                ItemRemovedForCapacity?.Invoke(item);
                return;
            }
            else
            {
                ItemRemovedForCapacity?.Invoke(this[0]);
            }
 
            var internalIndex = InternalIndex(index);
 
            var data = CollectionsMarshal.AsSpan(_buffer);
 
            // Shift data to make remove for insert.
            if (internalIndex == 0)
            {
                data.Slice(0, _end).CopyTo(data.Slice(1));
            }
            else if (internalIndex > _end)
            {
                // Data is shifted forward so save the last item to copy to the front.
                var overflowItem = data[data.Length - 1];
 
                var shiftLength = data.Length - internalIndex - 1;
                data.Slice(internalIndex, shiftLength).CopyTo(data.Slice(internalIndex + 1));
                if (shiftLength > 0 || internalIndex == _buffer.Count - 1)
                {
                    data.Slice(0, _end).CopyTo(data.Slice(1));
                }
                data[0] = overflowItem;
            }
            else if (internalIndex < _end && _end < _buffer.Count - 1)
            {
                data.Slice(internalIndex, _end - internalIndex).CopyTo(data.Slice(_end));
            }
            else
            {
                data.Slice(internalIndex, data.Length - internalIndex - 1).CopyTo(data.Slice(internalIndex + 1));
            }
 
            // Set the actual item.
            data[internalIndex] = item;
 
            Increment(ref _end);
            _start = _end;
        }
        else
        {
            var internalIndex = index + _start;
            if (internalIndex > _buffer.Count)
            {
                internalIndex = internalIndex % _buffer.Count;
            }
 
            _buffer.Insert(internalIndex, item);
            if (internalIndex < _end)
            {
                Increment(ref _end);
                if (_end != _buffer.Count)
                {
                    _start = _end;
                }
            }
        }
    }
 
    public void RemoveAt(int index)
    {
        ValidateIndexInRange(index);
 
        var internalIndex = InternalIndex(index);
        _buffer.RemoveAt(internalIndex);
        if (internalIndex < _end)
        {
            Decrement(ref _end);
            if (_start > 0)
            {
                _start = _end;
            }
        }
    }
 
    private void ValidateIndexInRange(int index)
    {
        if (index >= Count)
        {
            throw new InvalidOperationException($"Cannot access index {index}. Buffer size is {Count}");
        }
    }
 
    public bool Remove(T item) => throw new NotImplementedException();
 
    public T this[int index]
    {
        get
        {
            ValidateIndexInRange(index);
            return _buffer[InternalIndex(index)];
        }
        set
        {
            ValidateIndexInRange(index);
            _buffer[InternalIndex(index)] = value;
        }
    }
 
    public void Add(T item)
    {
        if (IsFull)
        {
            ItemRemovedForCapacity?.Invoke(this[0]);
 
            _buffer[_end] = item;
            Increment(ref _end);
            _start = _end;
        }
        else
        {
            _buffer.Insert(_end, item);
            Increment(ref _end);
            if (_end != _buffer.Count)
            {
                _start = _end;
            }
        }
    }
 
    public void Clear()
    {
        _start = 0;
        _end = 0;
        _buffer.Clear();
    }
 
    public bool Contains(T item) => IndexOf(item) != -1;
 
    public void CopyTo(T[] array, int arrayIndex)
    {
        if (array.Length - arrayIndex < Count)
        {
            throw new ArgumentException("Array does not contain enough space for items");
        }
 
        for (var index = 0; index < Count; ++index)
        {
            array[index + arrayIndex] = this[index];
        }
    }
 
    public T[] ToArray()
    {
        if (IsEmpty)
        {
            return Array.Empty<T>();
        }
 
        var array = new T[Count];
        for (var index = 0; index < Count; ++index)
        {
            array[index] = this[index];
        }
 
        return array;
    }
 
    public IEnumerator<T> GetEnumerator()
    {
        for (var i = 0; i < Count; ++i)
        {
            yield return this[i];
        }
    }
 
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
    private int InternalIndex(int index)
    {
        return (_start + index) % _buffer.Count;
    }
 
    private void Increment(ref int index)
    {
        if (++index < Capacity)
        {
            return;
        }
 
        index = 0;
    }
 
    private void Decrement(ref int index)
    {
        if (index <= 0)
        {
            index = Capacity - 1;
        }
 
        --index;
    }
 
    private sealed class CircularBufferDebugView(CircularBuffer<T> collection)
    {
        private readonly CircularBuffer<T> _collection = collection;
 
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public T[] Items => _collection.ToArray();
    }
}