The completion check was scheduling-dependent and the test could lose its TSan overlap window. The producer now signals done with a release store, the consumer reads in a do/while keyed on that flag so at least one read is guaranteed by construction, and a started/reading handshake makes the producer's remaining pushes provably overlap the reader's loop. Bounded to 4096 pushes on the 256-slot ring, and the assertions moved after join() so a failure reports instead of terminating. With the pre-fix ring restored, four TSan runs still flag push vs copy_from, so the case keeps its teeth.
162 lines
5.8 KiB
C++
162 lines
5.8 KiB
C++
#include "harness.hpp"
|
|
#include <plot/data/array_series.hpp>
|
|
#include <plot/data/stream_series.hpp>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <random>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
PLOT_TEST(array_series_bounds_and_gather) {
|
|
std::vector<f32> y{1, 5, -2, 3};
|
|
plot::data::ArraySeries s(std::move(y), /*x0=*/10.0, /*dx=*/0.5);
|
|
PLOT_CHECK(s.size() == 4);
|
|
PLOT_CHECK_NEAR(s.x_bounds().min, 10.0, 1e-9);
|
|
PLOT_CHECK_NEAR(s.x_bounds().max, 11.5, 1e-9);
|
|
PLOT_CHECK_NEAR(s.y_bounds().min, -2.0, 1e-6);
|
|
std::vector<f32> out;
|
|
s.gather(0, 0, 4, out);
|
|
PLOT_CHECK(out.size() == 8); // interleaved x,y
|
|
PLOT_CHECK_NEAR(out[0], 10.0, 1e-6);
|
|
PLOT_CHECK_NEAR(out[1], 1.0, 1e-6);
|
|
PLOT_CHECK_NEAR(out[6], 11.5, 1e-6);
|
|
}
|
|
|
|
PLOT_TEST(stream_series_acquire_publishes_snapshot) {
|
|
plot::data::StreamSeries s(1 << 12, /*x0=*/0.0, /*dx=*/1e-6);
|
|
for (int i = 0; i < 100; ++i) s.push(f32(i));
|
|
PLOT_CHECK(s.produced() == 100);
|
|
s.acquire();
|
|
PLOT_CHECK(s.size() == 100);
|
|
// Pyramid must track the published samples.
|
|
PLOT_CHECK_NEAR(s.reduce(0, 100).max, 99.0, 1e-4);
|
|
for (int i = 100; i < 250; ++i) s.push(f32(i));
|
|
s.acquire();
|
|
PLOT_CHECK(s.size() == 250);
|
|
}
|
|
|
|
PLOT_TEST(stream_series_drops_oldest_when_full) {
|
|
plot::data::StreamSeries s(256, 0.0, 1.0);
|
|
for (int i = 0; i < 1000; ++i) s.push(f32(i));
|
|
s.acquire();
|
|
PLOT_CHECK(s.size() <= 256);
|
|
PLOT_CHECK_NEAR(s.y_bounds().max, 999.0, 1e-4);
|
|
}
|
|
|
|
PLOT_TEST(array_series_exact_range_queries_match_bruteforce) {
|
|
// The strong test for the exactness split: arbitrary, deliberately
|
|
// unaligned ranges must match a brute-force scan of the raw samples.
|
|
std::mt19937 rng(7);
|
|
std::uniform_real_distribution<float> d(-5.0f, 5.0f);
|
|
std::vector<f32> y(4000);
|
|
for (auto& v : y) v = d(rng);
|
|
plot::data::ArraySeries s(y, 0.0, 1.0);
|
|
for (u64 a = 0; a < 4000; a += 173) {
|
|
for (u64 b = a + 1; b <= 4000; b += 331) {
|
|
float lo = y[a], hi = y[a];
|
|
double sum = 0.0, sq = 0.0;
|
|
for (u64 i = a; i < b; ++i) {
|
|
lo = std::min(lo, y[i]); hi = std::max(hi, y[i]);
|
|
sum += y[i]; sq += double(y[i]) * double(y[i]);
|
|
}
|
|
const plot::Range r = s.reduce(a, b);
|
|
PLOT_CHECK_NEAR(r.min, lo, 1e-4);
|
|
PLOT_CHECK_NEAR(r.max, hi, 1e-4);
|
|
PLOT_CHECK_NEAR(s.sum(a, b), sum, 1e-2);
|
|
PLOT_CHECK_NEAR(s.sumsq(a, b), sq, 1e-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
PLOT_TEST(stream_series_exact_queries_survive_ring_wrap) {
|
|
plot::data::StreamSeries s(1u << 10, 0.0, 1.0);
|
|
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 the slots are plain.
|
|
//
|
|
// The two flags make the overlap structural, not scheduling luck: the reader
|
|
// waits until the producer is already pushing, acknowledges, and only then does
|
|
// the producer push the rest -- so the reader's window spans pushes that are in
|
|
// flight by construction. The reader then loops until the producer reports done,
|
|
// which also makes "at least one read" true by construction instead of by
|
|
// winning a race with a fast producer.
|
|
plot::data::StreamSeries s(1u << 8, 0.0, 1.0); // one read spans reused slots
|
|
constexpr u64 kStride = 512; // pushes before the reader starts
|
|
constexpr u64 kPushes = 4'096; // total: a few thousand only
|
|
std::atomic<bool> started{false};
|
|
std::atomic<bool> reading{false};
|
|
std::atomic<bool> done{false};
|
|
|
|
std::thread producer([&] {
|
|
for (u64 i = 0; i < kStride; ++i) s.push(f32(i % 971));
|
|
started.store(true, std::memory_order_release);
|
|
while (!reading.load(std::memory_order_acquire)) {
|
|
}
|
|
for (u64 i = kStride; i < kPushes; ++i) s.push(f32(i % 971));
|
|
done.store(true, std::memory_order_release);
|
|
});
|
|
|
|
while (!started.load(std::memory_order_acquire)) {
|
|
}
|
|
reading.store(true, std::memory_order_release);
|
|
|
|
u64 reads = 0;
|
|
bool sizes_ok = true;
|
|
bool finished = false;
|
|
do {
|
|
finished = done.load(std::memory_order_acquire);
|
|
s.acquire();
|
|
const u64 n = s.size();
|
|
if (n > 0) {
|
|
(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
|
|
if (out.size() != 2 * n) sizes_ok = false;
|
|
}
|
|
++reads;
|
|
} while (!finished);
|
|
|
|
producer.join();
|
|
// Checked only after the join: a throw while the producer is joinable would
|
|
// std::terminate instead of reporting the failure.
|
|
PLOT_CHECK(s.produced() == kPushes);
|
|
PLOT_CHECK(reads >= 1);
|
|
PLOT_CHECK(sizes_ok);
|
|
}
|