#pragma once #include #include #include #include #include #include namespace plot::data { /// Single-producer / single-consumer ring of at most `capacity` elements. /// /// The capacity is rounded up to a power of two so slot addressing is a mask /// instead of a modulo. The producer writes a slot and then publishes the new /// sample count with a release store; a consumer that observes that count with an /// acquire load is guaranteed to see the slot contents written before it. /// /// A push into a full ring silently discards the oldest element: for a live /// plotting stream the newest sample is the interesting one and the retained /// window slides forward. /// /// Two indexing conventions, deliberately named apart: /// - retained-window (operator[], index 0 = oldest element still held) is a /// single-threaded view: it re-derives the window from the live count, so a /// producer pushing concurrently would move it under the reader's feet. /// - absolute (base(), copy_from()) fixes one count for a whole read, which is /// what a consumer holding a snapshot must use. StreamSeries::acquire() takes /// such a snapshot and then reads only absolute indices. template class RingBuffer { public: explicit RingBuffer(u64 capacity) : cap_(std::bit_ceil(capacity < 1 ? u64{1} : capacity)), mask_(cap_ - 1), buf_(static_cast(cap_)) {} /// Producer thread only. void push(T v) { const u64 i = produced_.load(std::memory_order_relaxed); buf_[static_cast(i & mask_)] = v; produced_.store(i + 1, std::memory_order_release); } /// Producer thread only. Equivalent to n calls to push(). void push(const T* vs, u64 n) { for (u64 i = 0; i < n; ++i) push(vs[i]); } /// Consumer: retained element count, saturating at capacity(). u64 size() const noexcept { const u64 p = produced_.load(std::memory_order_acquire); return p < cap_ ? p : cap_; } /// Consumer: total number of elements ever pushed (monotonic). u64 produced() const noexcept { return produced_.load(std::memory_order_acquire); } u64 capacity() const noexcept { return cap_; } /// Consumer: absolute index of the oldest element still retained. 0 until the /// ring has wrapped. u64 base() const noexcept { const u64 p = produced_.load(std::memory_order_acquire); return p > cap_ ? p - cap_ : 0; } /// Single-threaded: retained-window index (0 = oldest retained). `i` must be /// < size(). const T& operator[](u64 i) const noexcept { return buf_[static_cast((base() + i) & mask_)]; } /// Consumer: copies the `n` elements at absolute indices [absolute, absolute+n) /// into `out`, splitting the copy at the ring's wrap point. The caller must hold /// a snapshot that retains those indices; this never consults the live count, so /// a pushing producer cannot shift the read. void copy_from(u64 absolute, u64 n, T* out) const { if (n == 0) return; const u64 s = absolute & mask_; const u64 run = std::min(n, cap_ - s); const auto first = buf_.begin() + static_cast(s); std::copy(first, first + static_cast(run), out); if (run < n) { std::copy(buf_.begin(), buf_.begin() + static_cast(n - run), out + static_cast(run)); } } /// Consumer thread only: discards every element (not safe while a producer is /// pushing). void clear() noexcept { produced_.store(0, std::memory_order_relaxed); } private: u64 cap_; u64 mask_; std::vector buf_; std::atomic produced_{0}; }; } // namespace plot::data