feat(data): SPSC ring buffer, incremental min/max LOD pyramid, series types

This commit is contained in:
Martino Ferrari committed 2026-09-27 21:29:59 +02:00
1 parent c93c98002b
commit 138fa478c3
15 files changed
+1182 -8

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
#include <plot/data/stream_series.hpp>
#include <algorithm>
#include <cstddef>
namespace plot::data {
namespace {
/// Samples copied per ring read on the ingest path, so rebuilding a saturated
/// window costs no allocation.
constexpr u64 kReadChunk = 256;
} // namespace
StreamSeries::StreamSeries(u64 capacity, f64 x0, f64 dx)
: ring_(capacity), x0_(x0), dx_(dx) {}
void StreamSeries::push(f32 y) { ring_.push(y); }
void StreamSeries::push(const f32* ys, u64 n) { ring_.push(ys, n); }
u64 StreamSeries::produced() const { return ring_.produced(); }
void StreamSeries::acquire() {
// One atomic read: every absolute index below is relative to this single count,
// so the producer may keep pushing without the snapshot shifting under us.
const u64 p = ring_.produced();
const u64 cap = ring_.capacity();
const u64 base = p > cap ? p - cap : 0; // oldest index the ring still holds
const u64 n = p - base; // retained, published sample count
if (base != base_) {
// The window slid: samples the pyramid covered are gone, and because bucket
// boundaries are absolute, extending it incrementally would mis-align every
// bucket. Rebuild from the retained samples instead.
pyramid_.clear();
pyramid_.reserve(n);
f32 lo = 0.0f, hi = 0.0f;
bool first = true;
f32 chunk[kReadChunk];
for (u64 i = 0; i < n;) {
const u64 take = std::min<u64>(kReadChunk, n - i);
ring_.copy_from(base + i, take, chunk);
for (u64 k = 0; k < take; ++k) {
const f32 v = chunk[k];
pyramid_.append(v);
if (first) {
lo = v;
hi = v;
first = false;
} else {
lo = std::min(lo, v);
hi = std::max(hi, v);
}
}
i += take;
}
y_bounds_ = first ? Range{0.0, 0.0} : Range{f64(lo), f64(hi)};
base_ = base;
} else {
// Contiguous: only the samples produced since the last acquire() are new.
if (n == 0) y_bounds_ = Range{0.0, 0.0};
f32 chunk[kReadChunk];
for (u64 i = end_ - base_; i < n;) {
const u64 take = std::min<u64>(kReadChunk, n - i);
ring_.copy_from(base + i, take, chunk);
for (u64 k = 0; k < take; ++k) {
const f32 v = chunk[k];
pyramid_.append(v);
if (i + k == 0) {
y_bounds_ = Range{f64(v), f64(v)};
} else {
y_bounds_.min = std::min(y_bounds_.min, f64(v));
y_bounds_.max = std::max(y_bounds_.max, f64(v));
}
}
i += take;
}
}
end_ = p;
}
u64 StreamSeries::size() const { return end_ - base_; }
f64 StreamSeries::x0() const { return x0_ + f64(base_) * dx_; }
f64 StreamSeries::dx() const { return dx_; }
Range StreamSeries::x_bounds() const {
const f64 n = f64(size());
return n == 0.0 ? Range{x0(), x0()} : Range{x0(), x0() + (n - 1.0) * dx_};
}
Range StreamSeries::y_bounds() const { return y_bounds_; }
const MinMaxPyramid& StreamSeries::pyramid() const { return pyramid_; }
void StreamSeries::gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy) const {
gather_impl(level, begin, end, out_xy);
}
void StreamSeries::copy_raw(u64 begin, u64 end, f32* out) const {
const u64 want = end > begin ? end - begin : 0;
if (want == 0) return;
const u64 n = size();
const u64 b = std::min(begin, n);
const u64 e = std::min(std::max(end, b), n);
// Retained index b is absolute base_ + b: fixed for the whole copy, so a
// pushing producer cannot shift the window midway.
ring_.copy_from(base_ + b, e - b, out);
std::fill(out + static_cast<std::ptrdiff_t>(e - b),
out + static_cast<std::ptrdiff_t>(want), 0.0f);
}
} // namespace plot::data