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
@@ -22,6 +22,8 @@ else()
|
||||
"or mesa-libEGL-devel) or configure with -DPLOT_BUILD_TESTS=OFF.")
|
||||
endif()
|
||||
|
||||
find_package(Threads REQUIRED) # std::thread in the push/read race test
|
||||
|
||||
add_executable(plot_tests
|
||||
main.cpp
|
||||
test_transform.cpp
|
||||
@@ -31,7 +33,10 @@ add_executable(plot_tests
|
||||
test_series.cpp
|
||||
egl_headless.cpp
|
||||
)
|
||||
target_link_libraries(plot_tests PRIVATE plot::core ${plot_egl_library})
|
||||
target_link_libraries(plot_tests PRIVATE plot::core ${plot_egl_library} Threads::Threads)
|
||||
# Same switch the library is built with: the decomposition-cost test reads the
|
||||
# counter behind it (see include/plot/data/detail/cover_count.hpp).
|
||||
target_compile_definitions(plot_tests PRIVATE PLOT_PYRAMID_COUNT_COVERS)
|
||||
plot_apply_warnings(plot_tests)
|
||||
|
||||
add_test(NAME plot_tests COMMAND plot_tests)
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "harness.hpp"
|
||||
#include <plot/data/detail/cover_count.hpp>
|
||||
#include <plot/data/minmax_pyramid.hpp>
|
||||
#include <random>
|
||||
|
||||
@@ -104,3 +105,26 @@ PLOT_TEST(pyramid_clear_and_rebuild) {
|
||||
PLOT_CHECK(p.size() == 1);
|
||||
PLOT_CHECK_NEAR(p.reduce(0, 1).max, 7.0, 1e-6);
|
||||
}
|
||||
|
||||
PLOT_TEST(pyramid_cover_cost_is_logarithmic_for_unaligned_starts) {
|
||||
// Answers cannot show this: a decomposition that walks the range one level-0
|
||||
// bucket at a time returns exactly the same min/max as the O(log n) one. So
|
||||
// count the folded aggregates. For this query the bad pass emitted 15624
|
||||
// entries (the level-0 loop consumed the whole remainder); picking a level per
|
||||
// emission leaves 19, at most two per level, and the aligned start stays at 7.
|
||||
MinMaxPyramid p;
|
||||
p.reserve(1'000'000);
|
||||
for (u64 i = 0; i < 1'000'000; ++i) p.append(0.0f);
|
||||
PLOT_CHECK(p.level(0).size() == 1'000'000 / kBucket); // 15625, no partial bucket
|
||||
|
||||
auto folds_for = [&](u64 begin, u64 end) {
|
||||
plot::data::detail::cover_entry_count().store(0, std::memory_order_relaxed);
|
||||
(void)p.reduce_buckets(begin, end);
|
||||
return plot::data::detail::cover_entry_count().load(std::memory_order_relaxed);
|
||||
};
|
||||
const u64 aligned = folds_for(0, 1'000'000); // first whole bucket 0
|
||||
const u64 unaligned = folds_for(1, 1'000'000); // first whole bucket 1: odd
|
||||
PLOT_CHECK(aligned <= 64);
|
||||
PLOT_CHECK(unaligned < 100);
|
||||
PLOT_CHECK(unaligned <= u64{4} * aligned); // misalignment costs entries, not a scan
|
||||
}
|
||||
+53
-1
@@ -4,6 +4,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
PLOT_TEST(array_series_bounds_and_gather) {
|
||||
@@ -69,10 +70,61 @@ PLOT_TEST(array_series_exact_range_queries_match_bruteforce) {
|
||||
|
||||
PLOT_TEST(stream_series_exact_queries_survive_ring_wrap) {
|
||||
plot::data::StreamSeries s(1u << 10, 0.0, 1.0);
|
||||
for (int i = 0; i < 3000; ++i) s.push(f32(i % 97)); // wraps the ring twice
|
||||
constexpr u64 kPushes = 3000;
|
||||
constexpr u64 kFirstRetained = kPushes - (1u << 10); // 1976
|
||||
for (u64 i = 0; i < kPushes; ++i) s.push(f32(i % 97)); // wraps the ring twice
|
||||
s.acquire();
|
||||
const u64 n = s.size();
|
||||
PLOT_CHECK(n == 1u << 10);
|
||||
PLOT_CHECK_NEAR(s.x0(), 1976.0, 1e-9);
|
||||
PLOT_CHECK(s.x_bounds().min == s.x0());
|
||||
PLOT_CHECK(s.pyramid().size() == n);
|
||||
|
||||
const plot::Range r = s.reduce(0, n); // retained window, wrapped
|
||||
PLOT_CHECK_NEAR(r.max, 96.0, 1e-4);
|
||||
PLOT_CHECK_NEAR(r.min, 0.0, 1e-4);
|
||||
|
||||
double sum = 0.0, sumsq = 0.0;
|
||||
for (u64 i = kFirstRetained; i < kPushes; ++i) {
|
||||
const double v = static_cast<double>(i % 97);
|
||||
sum += v;
|
||||
sumsq += v * v;
|
||||
}
|
||||
PLOT_CHECK_NEAR(s.sum(0, n), sum, 1e-2);
|
||||
PLOT_CHECK_NEAR(s.sumsq(0, n), sumsq, 1e-1);
|
||||
|
||||
std::vector<f32> out;
|
||||
s.gather(0, 0, n, out); // level 0: raw samples
|
||||
PLOT_CHECK(out.size() == 2 * n);
|
||||
PLOT_CHECK_NEAR(out[0], s.x0(), 1e-6);
|
||||
PLOT_CHECK_NEAR(out[1], f32(kFirstRetained % 97), 1e-6);
|
||||
PLOT_CHECK_NEAR(out[2 * n - 2], s.x0() + f64(n - 1), 1e-6);
|
||||
}
|
||||
|
||||
PLOT_TEST(stream_series_push_and_read_are_race_free) {
|
||||
// The ring overwrites, so the producer reuses the slot of the snapshot's oldest
|
||||
// retained sample while the consumer may still be reading it. Relaxed atomic
|
||||
// slots make that defined; nothing else in the suite has two threads, so this
|
||||
// is the case ThreadSanitizer actually flags when that is not the case.
|
||||
plot::data::StreamSeries s(1u << 8, 0.0, 1.0);
|
||||
constexpr u64 kPushes = 50'000;
|
||||
std::thread producer([&s] {
|
||||
for (u64 i = 0; i < kPushes; ++i) s.push(f32(i % 971));
|
||||
});
|
||||
u64 reads = 0;
|
||||
while (s.produced() < kPushes) {
|
||||
s.acquire();
|
||||
const u64 n = s.size();
|
||||
if (n == 0) continue;
|
||||
(void)s.reduce(0, n);
|
||||
(void)s.sum(0, n);
|
||||
(void)s.sumsq(0, n);
|
||||
std::vector<f32> out;
|
||||
s.gather(0, 0, n, out); // reads the oldest edge
|
||||
PLOT_CHECK(out.size() == 2 * n);
|
||||
++reads;
|
||||
}
|
||||
producer.join();
|
||||
PLOT_CHECK(s.produced() == kPushes);
|
||||
PLOT_CHECK(reads > 0);
|
||||
}
|
||||
Reference in new issue
Block a user