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:
1 parent
eb4372242c
commit
ec93a9831d
10 files changed
+159
-81
No files matched your search
+12
-7
@@ -93,8 +93,6 @@ struct Partition {
|
||||
double base_dt = 1.0; ///< seconds per base tick
|
||||
double span_s = 0.0; ///< stream seconds one buffer covers
|
||||
|
||||
u64 total_samples() const noexcept;
|
||||
u64 total_bytes() const noexcept { return total_samples() * sizeof(f32); }
|
||||
/// Seconds between two time-base slots (the sample interval of every channel).
|
||||
double slot_dt() const noexcept { return base_dt * static_cast<double>(time_stride); }
|
||||
/// The rate samples are actually stored at, in samples per second.
|
||||
@@ -125,8 +123,15 @@ class BufferStore {
|
||||
/// axis read seconds from the trigger event.
|
||||
void set_epoch(f64 epoch) noexcept;
|
||||
|
||||
plot::data::RingBuffer<f32>& time() noexcept { return *time_; }
|
||||
const plot::data::RingBuffer<f32>& time() const noexcept { return *time_; }
|
||||
/// The shared clock, in **f64**. The values are f32 and the clock is not: a stamp is
|
||||
/// `stream_time - epoch` and the epoch is fixed at the first sample ever seen, so the
|
||||
/// magnitude grows without bound over a run's life while the interval that has to be
|
||||
/// resolved stays the sample period. f32 has 24 bits of mantissa, so its ULP at 32 s is
|
||||
/// 3.8 us -- four consecutive samples of a 1 MS/s stream then share one stamp, and at
|
||||
/// 600 s uptime 61 of them do. A value's precision is a property of the signal and 24
|
||||
/// bits is plenty; a *timestamp's* is a property of how long the scope has been running.
|
||||
plot::data::RingBuffer<f64>& time() noexcept { return *time_; }
|
||||
const plot::data::RingBuffer<f64>& time() const noexcept { return *time_; }
|
||||
plot::data::RingBuffer<f32>& values(std::size_t channel) { return *values_[channel]; }
|
||||
ChannelBuffer& channel(std::size_t channel) { return *channels_[channel]; }
|
||||
/// Shared handle to a channel view: the caller keeps the storage alive even if
|
||||
@@ -141,7 +146,7 @@ class BufferStore {
|
||||
|
||||
private:
|
||||
Partition part_;
|
||||
std::shared_ptr<plot::data::RingBuffer<f32>> time_;
|
||||
std::shared_ptr<plot::data::RingBuffer<f64>> time_;
|
||||
std::vector<std::shared_ptr<plot::data::RingBuffer<f32>>> values_;
|
||||
std::vector<std::shared_ptr<ChannelBuffer>> channels_;
|
||||
};
|
||||
@@ -278,8 +283,8 @@ class ScopeModel {
|
||||
/// The live buffer's absolute index range covering `[lo_s, hi_s]` in stream seconds,
|
||||
/// clamped to what the time ring and *every* value ring still hold. One range for the
|
||||
/// time ring and all the channels, which is what keeps a cut index-aligned by
|
||||
/// construction; the bounds are compared in the ring's own `f32`, so a bound that
|
||||
/// lands on a sample includes it whatever the magnitude of the stream time.
|
||||
/// construction; the bounds are compared in the ring's own type, which is now the
|
||||
/// `f64` they arrive in, so a bound that lands on a sample includes it exactly.
|
||||
///
|
||||
/// Caller holds `mutex()` (the acquisition thread already does when it latches).
|
||||
void live_window_locked(double lo_s, double hi_s, u64& first, u64& last);
|
||||
|
||||
Reference in new issue
Block a user