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
@@ -30,15 +30,23 @@ namespace plot::data {
|
||||
///
|
||||
/// THREADING NOTE. The ring overwrites the oldest samples -- that is what keeps a
|
||||
/// live stream showing the newest data, and what the drop test pins down -- so a
|
||||
/// producer racing past capacity() can write the very slot the consumer is reading
|
||||
/// at the window's oldest edge. That read is not ordered against the write, so a
|
||||
/// race detector flags it: the value is still a single f32 (no tearing on
|
||||
/// supported targets) and the worst case is one stale or too-new sample at the
|
||||
/// oldest edge, corrected by the next acquire() once it sees the base moved. The
|
||||
/// pyramid itself never mixes windows, because acquire() derives the whole window
|
||||
/// from one count. Ingest that is race-free even at the edge needs the producer to
|
||||
/// stop before unread data (drop-on-full) or a claim protocol, i.e. a different
|
||||
/// contract from the one this class specifies.
|
||||
/// producer can overwrite the slots a consumer is reading even while it holds a
|
||||
/// snapshot. Those slots are relaxed atomics (see RingBuffer), so this is not a
|
||||
/// data race and no value tears; what it is not is a frozen window.
|
||||
///
|
||||
/// The bound is the sample count, not the geometry. A snapshot count fixes *which
|
||||
/// absolute indices* a read walks, not that the slots still hold those samples:
|
||||
/// every push after the snapshot overwrites exactly one slot, the one belonging to
|
||||
/// the sample capacity() positions behind it. So if the producer pushes k samples
|
||||
/// while a read is in flight, the first k positions of that read window can hold
|
||||
/// one of those k newer samples (or a still-newer sample, once the producer has
|
||||
/// lapped again) instead of the sample the snapshot names. Nothing else moves: the
|
||||
/// count, the absolute index -> x mapping and the newest end of the window are
|
||||
/// derived from the one published count, and the next acquire() rebuilds from the
|
||||
/// base it observes, so the pyramid never mixes two windows. Ingest with no reuse
|
||||
/// underneath a read at all needs the producer to stop before unread data
|
||||
/// (drop-on-full) or a claim protocol, i.e. a different contract from the one this
|
||||
/// class specifies.
|
||||
class StreamSeries final : public Series {
|
||||
public:
|
||||
explicit StreamSeries(u64 capacity, f64 x0, f64 dx);
|
||||
|
||||
Reference in new issue
Block a user