perf(data): absolute-space pyramid so acquire() is O(produced), not O(window)
Frame cost was O(capacity x levels) once the ring saturated: acquire() saw the window base move (it always moves once the ring wraps), cleared the pyramid and re-appended all 1,048,576 retained samples, and append() walked every live level with no early stop -- 13,647,936 bucket merges and 73.8 ms per frame at a 1M-sample window, scaling with the window, which is what capped the 1 MS/s demo at 32-58 ms/frame while it was still filling. Index the pyramid in absolute bucket space instead. Level l bucket k covers the samples [k * (kBucket << l), (k + 1) * (kBucket << l)), so a bucket's span never depends on where the window starts and the window can slide over the aggregate instead of forcing it to be rebuilt. Each level keeps its complete buckets in a deque plus the absolute base of its first entry; append() folds a sample into the single in-progress level-0 bucket and, only when that bucket completes, cascades upward, stopping at the first level that gains no new entry; a new drop_before() slides the window by discarding leading buckets and remembers where the window begins. acquire() is now: drop to the new base, then append exactly what was produced -- 256 merges and 0.034 ms per frame for 8192 new samples, independent of the window, and memory bounded by the window (32,767 entries after 50M samples through a 1M ring, the same as after 10M). A producer that laps the consumer by more than capacity() between two acquires restarts the window rather than splicing a gap. Interface: `level(l) -> std::span<const MinMax>` cannot survive a ring and is replaced by level_base/level_size/bucket in absolute bucket coordinates, plus window_base/window_end/drop_before. The *_buckets queries stay in window coordinates (index 0 = oldest retained sample), so Series keeps its window semantics for callers; its exactness split now aligns in absolute space (window_base() + the ceil/floor) because the bucket grid is absolute. The sums move from per-level prefix sums to per-bucket ones -- the only representation a sliding front can keep exact -- and StreamSeries::y_bounds() becomes reduce(0, size()), exact in O(log n + kBucket) per frame instead of a scan of the window. The pyramid owns the published window now, so there is no second copy of base/end to keep in step, and gather() emits only buckets that are both complete and wholly retained. Tests: test-only merge counters (mirroring PLOT_PYRAMID_COUNT_COVERS, absent from every non-test build) pin an acquire proportional to production and independent of capacity, an append cost amortised over 16 live levels, and bounded memory; brute-force checks pin absolute bucket aggregates across slides, sliding-window reduce/sum/sumsq over unaligned ranges, and the level-1 gather mapping. The demo now draws 946 samples/px at 1.03 ms/frame (967 fps) where the saturated window cost 54.96 ms/frame (18 fps).
This commit is contained in:
1 parent
4b649a2783
commit
0ac5d47fa6
13 files changed
+769
-244
No files matched your search
+35
-57
@@ -6,14 +6,18 @@
|
||||
namespace plot::data {
|
||||
namespace {
|
||||
|
||||
/// Samples copied per ring read on the ingest path, so rebuilding a saturated
|
||||
/// window costs no allocation.
|
||||
/// Samples copied per ring read on the ingest path, so appending the samples
|
||||
/// produced between two frames costs no allocation.
|
||||
constexpr u64 kReadChunk = 256;
|
||||
|
||||
} // namespace
|
||||
|
||||
StreamSeries::StreamSeries(u64 capacity, f64 x0, f64 dx)
|
||||
: ring_(capacity), x0_(x0), dx_(dx) {}
|
||||
: ring_(capacity), x0_(x0), dx_(dx) {
|
||||
// Size the pyramid's level table for a full window up front: the ring never
|
||||
// holds more than capacity() samples, so appending never grows it.
|
||||
pyramid_.reserve(capacity);
|
||||
}
|
||||
|
||||
void StreamSeries::push(f32 y) { ring_.push(y); }
|
||||
|
||||
@@ -27,62 +31,35 @@ void StreamSeries::acquire() {
|
||||
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;
|
||||
}
|
||||
// Slide the pyramid's window: the buckets that covered samples the ring has
|
||||
// overwritten are dropped, and the rest stay exactly as they were built --
|
||||
// their boundaries are absolute, so nothing has to be recomputed. This is also
|
||||
// where a producer that lapped the consumer by more than the ring holds is
|
||||
// handled: the pyramid restarts at the new base.
|
||||
pyramid_.drop_before(base);
|
||||
|
||||
// Append exactly the samples produced since the last acquire(). Normally that
|
||||
// is one frame's worth; after a lapped producer it is the whole window.
|
||||
f32 chunk[kReadChunk];
|
||||
for (u64 a = pyramid_.window_end(); a < p;) {
|
||||
const u64 take = std::min<u64>(kReadChunk, p - a);
|
||||
ring_.copy_from(a, take, chunk);
|
||||
for (u64 k = 0; k < take; ++k) pyramid_.append(chunk[k]);
|
||||
a += take;
|
||||
}
|
||||
end_ = p;
|
||||
|
||||
// Exact bounds for the published window, in the same O(log n + kBucket) as any
|
||||
// other exact query over it: the bucket-aligned interior comes from the
|
||||
// pyramid and the two partial edge spans are scanned through copy_raw(). An
|
||||
// incrementally extended bound cannot work here -- it would have to know
|
||||
// whether the extreme it is holding has just slid out of the window.
|
||||
y_bounds_ = reduce(0, size());
|
||||
}
|
||||
|
||||
u64 StreamSeries::size() const { return end_ - base_; }
|
||||
u64 StreamSeries::size() const { return pyramid_.size(); }
|
||||
|
||||
f64 StreamSeries::x0() const { return x0_ + f64(base_) * dx_; }
|
||||
f64 StreamSeries::x0() const { return x0_ + f64(pyramid_.window_base()) * dx_; }
|
||||
|
||||
f64 StreamSeries::dx() const { return dx_; }
|
||||
|
||||
@@ -105,9 +82,10 @@ void StreamSeries::copy_raw(u64 begin, u64 end, f32* out) const {
|
||||
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);
|
||||
// Retained index b is absolute window_base() + b: fixed for the whole copy, so
|
||||
// a pushing producer cannot shift the window midway (the pyramid is written by
|
||||
// this thread alone, so its window cannot move under the read either).
|
||||
ring_.copy_from(pyramid_.window_base() + b, e - b, out);
|
||||
std::fill(out + static_cast<std::ptrdiff_t>(e - b),
|
||||
out + static_cast<std::ptrdiff_t>(want), 0.0f);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user