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
+21
-19
@@ -2030,18 +2030,18 @@ bool visible_samples(App& app, std::size_t channel, std::vector<double>& time,
|
||||
|
||||
if (std::shared_ptr<const EventSnapshot> snap = app.model->display_snapshot()) {
|
||||
if (snap == nullptr || channel >= snap->values.size()) return false;
|
||||
const std::vector<f32>& st = snap->stamps;
|
||||
const std::vector<f64>& st = snap->stamps;
|
||||
const std::vector<f32>& vals = snap->values[channel];
|
||||
if (vals.size() != st.size()) return false;
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), static_cast<f32>(view.min));
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), static_cast<f32>(view.max));
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), view.min);
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), view.max);
|
||||
const std::size_t count = static_cast<std::size_t>(hi - lo);
|
||||
if (count == 0) return false;
|
||||
const std::size_t off = static_cast<std::size_t>(lo - st.begin());
|
||||
time.resize(count);
|
||||
value.resize(count);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
time[i] = static_cast<double>(st[off + i]);
|
||||
time[i] = st[off + i];
|
||||
value[i] = static_cast<double>(vals[off + i]);
|
||||
}
|
||||
return true;
|
||||
@@ -2058,21 +2058,23 @@ bool visible_samples(App& app, std::size_t channel, std::vector<double>& time,
|
||||
if (n == 0) return false;
|
||||
time.resize(static_cast<std::size_t>(n));
|
||||
value.resize(static_cast<std::size_t>(n));
|
||||
// In bounded chunks rather than through two whole-window temporaries: the rings hold f32 and
|
||||
// the engine reads f64, so the widening is unavoidable, but the *copies* are not -- a full
|
||||
// window is millions of samples, and the temporaries cost a full write and read of it on top
|
||||
// of the result. `kChunk` keeps the scratch in cache.
|
||||
// In bounded chunks rather than through two whole-window temporaries: the value ring holds
|
||||
// f32 and the engine reads f64, so that widening is unavoidable, but the *copies* are not --
|
||||
// a full window is millions of samples, and the temporaries cost a full write and read of it
|
||||
// on top of the result. `kChunk` keeps both scratches in cache. Two of them, because the
|
||||
// clock ring is f64 and the values f32 (see `BufferStore::time()`).
|
||||
constexpr u64 kChunk = 8192;
|
||||
std::vector<f32> scratch(kChunk);
|
||||
std::vector<f64> t_scratch(kChunk);
|
||||
std::vector<f32> v_scratch(kChunk);
|
||||
for (u64 at = 0; at < n; at += kChunk) {
|
||||
const u64 take = std::min<u64>(kChunk, n - at);
|
||||
store.time().copy_from(first + at, take, scratch.data());
|
||||
store.time().copy_from(first + at, take, t_scratch.data());
|
||||
for (u64 i = 0; i < take; ++i) {
|
||||
time[static_cast<std::size_t>(at + i)] = epoch + static_cast<double>(scratch[i]);
|
||||
time[static_cast<std::size_t>(at + i)] = epoch + t_scratch[i];
|
||||
}
|
||||
store.values(channel).copy_from(first + at, take, scratch.data());
|
||||
store.values(channel).copy_from(first + at, take, v_scratch.data());
|
||||
for (u64 i = 0; i < take; ++i) {
|
||||
value[static_cast<std::size_t>(at + i)] = static_cast<double>(scratch[i]);
|
||||
value[static_cast<std::size_t>(at + i)] = static_cast<double>(v_scratch[i]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -2098,10 +2100,10 @@ class MeasureWindowReader {
|
||||
const plot::Range view = r0.plot->x_range(r0.axis_x);
|
||||
if ((snap_ = app.model->display_snapshot()) != nullptr) {
|
||||
if (channel >= snap_->values.size()) return;
|
||||
const std::vector<f32>& st = snap_->stamps;
|
||||
const std::vector<f64>& st = snap_->stamps;
|
||||
if (snap_->values[channel].size() != st.size()) return;
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), static_cast<f32>(view.min));
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), static_cast<f32>(view.max));
|
||||
const auto lo = std::lower_bound(st.begin(), st.end(), view.min);
|
||||
const auto hi = std::upper_bound(st.begin(), st.end(), view.max);
|
||||
if (hi <= lo) return;
|
||||
off_ = static_cast<std::size_t>(lo - st.begin());
|
||||
count_ = static_cast<std::size_t>(hi - lo);
|
||||
@@ -2174,10 +2176,10 @@ class MeasureWindowReader {
|
||||
|
||||
/// Window sample `i`'s stream time. One sample at a time on purpose: see the class comment.
|
||||
double time_of(std::size_t i) const {
|
||||
if (snap_vals_ != nullptr) return static_cast<double>(snap_->stamps[off_ + i]);
|
||||
f32 stamp = 0.0f;
|
||||
if (snap_vals_ != nullptr) return snap_->stamps[off_ + i];
|
||||
f64 stamp = 0.0;
|
||||
store_->time().copy_from(first_ + i, 1, &stamp);
|
||||
return epoch_ + static_cast<double>(stamp);
|
||||
return epoch_ + stamp;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
Reference in new issue
Block a user