fix(data): level-per-emission pyramid cover, relaxed-atomic ring slots
cover_buckets chose one level per pass, so an odd first whole bucket (half of all measurement ranges) never reached a level above 0 and folded every remaining bucket at level 0 -- 15624 entries for a 1e6-sample pyramid. Pick the largest aligned, in-range, materialised level at each cursor instead; the new cost test counts folded entries through a PLOT_PYRAMID_COUNT_COVERS-only hook (absent from normal builds) and fails on the old body. Ring slots are now relaxed std::atomic: overwrite-on-full means the producer reuses the slot of a snapshot's oldest sample while the consumer reads it, which was a data race, not just a stale value. copy_from loads explicitly instead of std::copy; push stays lock-free. A new producer/consumer case is the one TSan flags without this. Also extends the wrapped-window test with x0/bounds/pyramid/sum/sumsq/gather checks, and corrects the StreamSeries threading note to the sample-count bound.
This commit is contained in:
1 parent
138fa478c3
commit
d9909f2115
8 files changed
+190
-38
No files matched your search
+29
-10
@@ -1,3 +1,4 @@
|
||||
#include <plot/data/detail/cover_count.hpp>
|
||||
#include <plot/data/minmax_pyramid.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -26,22 +27,40 @@ constexpr u64 ceil_to_bucket(u64 v) {
|
||||
/// A level-l entry is emitted only when the whole level-l bucket exists (all
|
||||
/// kBucket << l samples are present, so its aggregate is final). Callers
|
||||
/// guarantee b1 <= ceil(size() / kBucket), so the only possible remainder is the
|
||||
/// single trailing partial level-0 bucket, which is emitted at level 0. The
|
||||
/// decomposition has O(log) entries, so every query below is O(log n).
|
||||
/// single trailing partial level-0 bucket, which is emitted at level 0.
|
||||
///
|
||||
/// The level is chosen per *emission*, not once per pass: at each cursor the
|
||||
/// largest level is taken whose bucket is aligned with the cursor, lies wholly
|
||||
/// inside [b0, b1) and has already been materialised. A cursor that starts
|
||||
/// misaligned (an odd first whole bucket, i.e. half of all measurement ranges)
|
||||
/// therefore burns at most one level-0 bucket and then resumes the coarse
|
||||
/// levels, so the decomposition is at most two entries per level and every query
|
||||
/// below is O(log n) for any start. Choosing the level once per pass instead left
|
||||
/// the level-0 loop to consume the whole remainder one bucket at a time, because
|
||||
/// no coarser level is ever reachable from an odd cursor.
|
||||
template <typename Fn>
|
||||
void cover_buckets(const MinMaxPyramid& p, u64 b0, u64 b1, Fn&& fn) {
|
||||
const u64 complete = p.size() / kBucket;
|
||||
const u64 limit = std::min(b1, complete);
|
||||
const u32 top = p.level_count() - 1; // level_count() is at least 1
|
||||
u64 b = b0;
|
||||
for (u32 li = p.level_count(); li-- > 0 && b < b1;) {
|
||||
const u64 lb = u64{1} << li; // level-0 buckets per level-li bucket
|
||||
const u64 avail = p.level(li).size();
|
||||
while (b < b1 && (b % lb) == 0 && b + lb <= b1 && b + lb <= complete &&
|
||||
(b / lb) < avail) {
|
||||
fn(li, b / lb);
|
||||
b += lb;
|
||||
while (b < b1) {
|
||||
u32 li = top;
|
||||
for (;;) {
|
||||
const u64 lb = u64{1} << li;
|
||||
if ((b % lb) == 0 && b + lb <= limit &&
|
||||
(b / lb) < static_cast<u64>(p.level(li).size())) {
|
||||
break;
|
||||
}
|
||||
if (li == 0) break;
|
||||
--li;
|
||||
}
|
||||
#ifdef PLOT_PYRAMID_COUNT_COVERS
|
||||
detail::cover_entry_count().fetch_add(1, std::memory_order_relaxed);
|
||||
#endif
|
||||
fn(li, b >> li);
|
||||
b += u64{1} << li;
|
||||
}
|
||||
if (b < b1) fn(0u, b); // trailing partial level-0 bucket
|
||||
}
|
||||
|
||||
/// Folds the decomposition of [b0, b1) into a min/max Range. Returns a
|
||||
|
||||
Reference in new issue
Block a user