fix: store the stream clock as f64, so a stamp's precision does not decay with uptime

The time ring was f32 like the values, holding each stamp relative to an epoch
fixed at the first sample ever seen -- so the number stored grew for as long as
the app ran while the interval it had to resolve stayed one sample period. At
1 MS/s the f32 ULP reaches the sample period after about sixteen seconds, so two
to four consecutive samples rounded onto one stamp after half a minute and eight
after a minute. On screen that is a plateau the LOD envelope draws as a
zero-width segment -- a vertical spike where the waveform should be -- and in an
export it was three distinct diffs at 2, 2.5 and 3 ULP instead of one clock.

A value's precision is a property of the signal; a timestamp's is a property of
how long the scope has been running. Only the clock widens: the values are the
memory budget. That makes the two slices different sizes per slot, so
plan_partition() weighs its growth loop in bytes rather than in slots (and
bytes_for() is what bounds the doubling); live_window_locked() loses the f32
round-trip of its bounds, which existed only to keep a sample sitting exactly on
one; and EventSnapshot::stamps is f64 because a pause view is not rebased and so
carries full stream time.

Verified against the fast mock: np.diff over two million exported rows now holds
exactly one step (1e-06, 1999999 times), no duplicate or backwards stamps, and
the 200 MB ring still spans 8.39 s. Every documented probe assertion unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-05 10:41:57 +02:00
1 parent eb4372242c
commit ec93a9831d
10 files changed
+159 -81

No files matched your search

+6 -4
View File
@@ -34,8 +34,10 @@ using plot::u64;
/// undecimated case and the common one.
class ChannelBuffer final : public plot::data::Series {
public:
/// `values` is f32 and `times` is f64: see `BufferStore::time()` for why the clock
/// alone needs the wider type.
ChannelBuffer(std::shared_ptr<const plot::data::RingBuffer<f32>> values,
std::shared_ptr<const plot::data::RingBuffer<f32>> times, u64 stride, f64 epoch,
std::shared_ptr<const plot::data::RingBuffer<f64>> times, u64 stride, f64 epoch,
f64 nominal_dt);
ChannelBuffer(const ChannelBuffer&) = delete;
@@ -94,10 +96,10 @@ class ChannelBuffer final : public plot::data::Series {
/// Absolute (channel) index of retained-window index `i`.
u64 absolute(u64 i) const noexcept { return pyramid_.window_base() + i; }
/// Timestamp of channel sample `absolute_index`, in seconds since `epoch_`.
f32 time_of(u64 absolute_index) const noexcept;
f64 time_of(u64 absolute_index) const noexcept;
std::shared_ptr<const plot::data::RingBuffer<f32>> values_;
std::shared_ptr<const plot::data::RingBuffer<f32>> times_;
std::shared_ptr<const plot::data::RingBuffer<f64>> times_;
u64 stride_ = 1;
f64 epoch_ = 0.0;
f64 dt_ = 1.0;
@@ -114,7 +116,7 @@ class ChannelBuffer final : public plot::data::Series {
/// the "live plot flickers as if there were no data" defect. The cached value is also the
/// *right* answer rather than merely a stable one: it is the stamp sample `window_base`
/// actually carries, where the clamp answered with whatever slot happened to be oldest now.
f32 window_x0_ = 0.0f;
f64 window_x0_ = 0.0;
plot::data::MinMaxPyramid pyramid_;
};