feat: a non-finite aggregate can be amended additively at every level

This commit is contained in:
Martino Ferrari committed 2026-10-08 19:18:51 +02:00
1 parent 37e414bcc5
commit 2ebf2be571
3 files changed
+117

No files matched your search

+67
View File
@@ -6,6 +6,7 @@
#include <plot/data/detail/merge_count.hpp>
#include <plot/data/minmax_pyramid.hpp>
#include <algorithm>
#include <limits>
#include <random>
using plot::data::kBucket;
@@ -342,3 +343,69 @@ PLOT_TEST(pyramid_level_cap_keeps_the_span_shift_defined) {
p.append(1.0f);
PLOT_CHECK(p.level_count() >= 1);
}
PLOT_TEST(amend_folds_a_backfilled_sample_into_every_covering_level) {
// A hole is appended as NaN -- which the pyramid already stores as an empty aggregate --
// and amended later when the packet that owned it finally arrives. The amendment has to
// reach every level that covers the index, because the renderer reads whichever level the
// zoom picked: repairing level 0 alone would make a trace whole when zoomed in and still
// broken when zoomed out.
MinMaxPyramid p;
p.reserve(8 * kBucket);
for (u64 i = 0; i < 8 * kBucket; ++i) {
// One NaN, at an index comfortably inside the first bucket of every level.
p.append(i == 5 ? std::numeric_limits<f32>::quiet_NaN() : 1.0f);
}
// Before: every covering bucket aggregates kBucket-1 ones and knows nothing of index 5.
PLOT_CHECK(p.bucket_count(0, 0) == kBucket - 1);
PLOT_CHECK_NEAR(p.bucket(0, 0).max, 1.0);
p.amend(5, 7.0f);
// Level 0 gained the sample.
PLOT_CHECK(p.bucket_count(0, 0) == kBucket);
PLOT_CHECK_NEAR(p.bucket(0, 0).max, 7.0);
PLOT_CHECK_NEAR(p.bucket_sum(0, 0), f64(kBucket - 1) + 7.0);
PLOT_CHECK_NEAR(p.bucket_sumsq(0, 0), f64(kBucket - 1) + 49.0);
// And so did every coarser level that covers it.
for (u32 l = 1; l < p.level_count(); ++l) {
if (p.level_size(l) == 0) continue;
const u64 span = u64{kBucket} << l;
const u64 b = 5 / span;
if (b < p.level_base(l) || b - p.level_base(l) >= p.level_size(l)) continue;
PLOT_CHECK_NEAR(p.bucket(l, b).max, 7.0);
PLOT_CHECK(p.bucket_count(l, b) == span);
}
}
PLOT_TEST(amend_makes_an_all_nan_bucket_present_again) {
// A bucket whose every sample was non-finite is stored with count 0, which every reader
// treats as absent. One amendment has to make it present -- min and max become the value
// rather than widening from a meaningless zero, which is the case a plain fold gets wrong.
MinMaxPyramid p;
p.reserve(4 * kBucket);
const f32 nan = std::numeric_limits<f32>::quiet_NaN();
for (u64 i = 0; i < 2 * kBucket; ++i) p.append(i < kBucket ? nan : 1.0f);
PLOT_CHECK(p.bucket_count(0, 0) == 0);
p.amend(3, -2.0f);
PLOT_CHECK(p.bucket_count(0, 0) == 1);
PLOT_CHECK_NEAR(p.bucket(0, 0).min, -2.0);
PLOT_CHECK_NEAR(p.bucket(0, 0).max, -2.0);
}
PLOT_TEST(amend_outside_the_window_is_a_no_op) {
// An index the window has already dropped, or one not yet appended, has nowhere to go.
// Silently ignoring it is what lets the caller amend without first proving the ring still
// holds the slot -- a race it cannot win, because the window slides on the other thread.
MinMaxPyramid p;
p.reserve(4 * kBucket);
for (u64 i = 0; i < 4 * kBucket; ++i) p.append(1.0f);
p.drop_before(2 * kBucket);
const u64 before = p.bucket_count(0, 2);
p.amend(5, 99.0f); // dropped
p.amend(100 * kBucket, 99.0f); // never appended
PLOT_CHECK(p.bucket_count(0, 2) == before);
PLOT_CHECK_NEAR(p.bucket(0, 2).max, 1.0);
}