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
+1
-1
@@ -518,7 +518,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
||||
time_acc_ += rel;
|
||||
++time_acc_n_;
|
||||
if (tick % g == g - 1) {
|
||||
store.time().push(static_cast<f32>(time_acc_ / static_cast<double>(time_acc_n_)));
|
||||
store.time().push(time_acc_ / static_cast<double>(time_acc_n_));
|
||||
time_acc_ = 0.0;
|
||||
time_acc_n_ = 0;
|
||||
}
|
||||
|
||||
+8
-8
@@ -24,7 +24,7 @@ const plot::data::MinMaxPyramid& null_pyramid() {
|
||||
// --- ChannelBuffer ----------------------------------------------------------
|
||||
|
||||
ChannelBuffer::ChannelBuffer(std::shared_ptr<const plot::data::RingBuffer<f32>> values,
|
||||
std::shared_ptr<const plot::data::RingBuffer<f32>> times, u64 stride,
|
||||
std::shared_ptr<const plot::data::RingBuffer<f64>> times, u64 stride,
|
||||
f64 epoch, f64 nominal_dt)
|
||||
: values_(std::move(values)), times_(std::move(times)), stride_(stride < 1 ? 1 : stride),
|
||||
epoch_(epoch), dt_(nominal_dt) {}
|
||||
@@ -64,11 +64,11 @@ void ChannelBuffer::acquire() noexcept {
|
||||
|
||||
void ChannelBuffer::invalidate() noexcept {
|
||||
published_ = 0;
|
||||
window_x0_ = 0.0f;
|
||||
window_x0_ = 0.0;
|
||||
pyramid_.clear();
|
||||
}
|
||||
|
||||
f32 ChannelBuffer::time_of(u64 absolute_index) const noexcept {
|
||||
f64 ChannelBuffer::time_of(u64 absolute_index) const noexcept {
|
||||
// Channel sample `j` is produced on the tick that also pushes time slot
|
||||
// `j*stride + stride - 1` (see `Acquisition::on_data`): the producer stamps the
|
||||
// time ring every `time_stride` ticks and the channel every `stride` of those,
|
||||
@@ -102,10 +102,10 @@ f32 ChannelBuffer::time_of(u64 absolute_index) const noexcept {
|
||||
// whole trace's geometry collapsing.
|
||||
const u64 base = times_->base();
|
||||
const u64 produced = times_->produced();
|
||||
if (produced == 0) return 0.0f;
|
||||
if (produced == 0) return 0.0;
|
||||
u64 slot = index < base ? base : index;
|
||||
if (slot >= produced) slot = produced - 1;
|
||||
f32 out = 0.0f;
|
||||
f64 out = 0.0;
|
||||
times_->copy_from(slot, 1, &out);
|
||||
return out;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ f64 ChannelBuffer::dx() const {
|
||||
|
||||
f64 ChannelBuffer::x0() const {
|
||||
if (pyramid_.size() == 0) return epoch_;
|
||||
return epoch_ + f64(window_x0_);
|
||||
return epoch_ + window_x0_;
|
||||
}
|
||||
|
||||
f64 ChannelBuffer::x_at(u64 i) const {
|
||||
@@ -131,7 +131,7 @@ f64 ChannelBuffer::x_at(u64 i) const {
|
||||
// ring-span too new made it refuse the whole visible range -- which left the renderer
|
||||
// gathering the entire ring at a coarse level and drawing it off the axis.
|
||||
if (i == 0) return x0();
|
||||
return epoch_ + f64(time_of(absolute(i)));
|
||||
return epoch_ + time_of(absolute(i));
|
||||
}
|
||||
|
||||
bool ChannelBuffer::neighbours(f64 x, u64& i0, u64& i1) const {
|
||||
@@ -171,7 +171,7 @@ u64 ChannelBuffer::snap_index(f64 x) const {
|
||||
plot::Range ChannelBuffer::x_bounds() const {
|
||||
const u64 n = pyramid_.size();
|
||||
if (n == 0) return plot::Range{epoch_, epoch_};
|
||||
return plot::Range{x0(), epoch_ + f64(time_of(absolute(n - 1)))};
|
||||
return plot::Range{x0(), epoch_ + time_of(absolute(n - 1))};
|
||||
}
|
||||
|
||||
plot::Range ChannelBuffer::y_bounds() const {
|
||||
|
||||
+6
-4
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,13 @@ using plot::u64;
|
||||
/// live ring buffers switched by a pending flag, plus a display-epoch shift)
|
||||
/// could not stop.
|
||||
struct EventSnapshot {
|
||||
std::vector<f32> stamps; ///< edge-relative seconds, monotonic
|
||||
/// Seconds, monotonic -- edge-relative for a trigger event, the stream's own for a
|
||||
/// pause view. **f64, like the live ring it is cut from** (see `BufferStore::time()`):
|
||||
/// an event's stamps are small enough that f32 would nearly do, but a *pause* is
|
||||
/// deliberately not rebased, so its stamps carry the full stream time and quantise at
|
||||
/// the same 3.8 us per 32 s of uptime the live ring did. Keeping the two the same type
|
||||
/// also makes the cut in `publish_event()`/`publish_pause_view()` a straight copy.
|
||||
std::vector<f64> stamps;
|
||||
std::vector<std::vector<f32>> values; ///< one array per channel, same length
|
||||
double pre_s = 0.0; ///< seconds of pre-trigger data kept
|
||||
double post_s = 0.0; ///< seconds of post-trigger data kept
|
||||
|
||||
+6
-6
@@ -109,9 +109,9 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
if (snap != nullptr) {
|
||||
// The snapshot is immutable and already index-aligned, so the visible window is two
|
||||
// binary searches over its stamps and the same index range for every channel.
|
||||
const std::vector<f32>& st = snap->stamps;
|
||||
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 std::vector<f64>& st = snap->stamps;
|
||||
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 table;
|
||||
const std::size_t first = static_cast<std::size_t>(lo - st.begin());
|
||||
const std::size_t n = static_cast<std::size_t>(hi - lo);
|
||||
@@ -122,7 +122,7 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
table.time.resize(n);
|
||||
table.stream_time.resize(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
table.time[i] = static_cast<f64>(st[first + i]);
|
||||
table.time[i] = st[first + i];
|
||||
table.stream_time[i] = edge + table.time[i];
|
||||
}
|
||||
for (const int c : chans) {
|
||||
@@ -151,12 +151,12 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
if (n == 0) return table;
|
||||
|
||||
BufferStore& store = model.live();
|
||||
std::vector<f32> stamps(static_cast<std::size_t>(n));
|
||||
std::vector<f64> stamps(static_cast<std::size_t>(n));
|
||||
store.time().copy_from(first, n, stamps.data());
|
||||
table.time.resize(static_cast<std::size_t>(n));
|
||||
table.stream_time.resize(static_cast<std::size_t>(n));
|
||||
for (std::size_t i = 0; i < stamps.size(); ++i) {
|
||||
table.stream_time[i] = static_cast<f64>(stamps[i]);
|
||||
table.stream_time[i] = stamps[i];
|
||||
table.time[i] = epoch + table.stream_time[i];
|
||||
}
|
||||
for (const int c : chans) {
|
||||
|
||||
+27
-29
@@ -42,12 +42,6 @@ constexpr std::uint64_t kMathCostSample = 1024;
|
||||
|
||||
} // namespace
|
||||
|
||||
u64 Partition::total_samples() const noexcept {
|
||||
u64 total = time_capacity;
|
||||
for (u64 c : value_capacity) total += c;
|
||||
return total;
|
||||
}
|
||||
|
||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
|
||||
double target_span_s) {
|
||||
Partition p;
|
||||
@@ -76,11 +70,14 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
||||
for (std::size_t i = 0; i < n; ++i) p.stride[i] = 1;
|
||||
|
||||
const u64 budget_bytes = static_cast<u64>(std::max(1.0, buffer_bytes));
|
||||
const u64 budget = budget_bytes / sizeof(f32);
|
||||
double weight = 1.0; // the time slice itself
|
||||
for (u64 s : p.stride) weight += 1.0 / static_cast<double>(s);
|
||||
// Weighted in *bytes*, not in slots: the shared clock is f64 and the values are f32
|
||||
// (see `BufferStore::time()`), so a time slot costs twice a value sample and the two
|
||||
// slices are not interchangeable. Counting both as one f32 is what would make
|
||||
// `--buffer-mb` quietly understate the time ring by its own size.
|
||||
double weight = static_cast<double>(sizeof(f64)); // the time slice itself
|
||||
for (u64 s : p.stride) weight += static_cast<double>(sizeof(f32)) / static_cast<double>(s);
|
||||
|
||||
const u64 slots = budget / static_cast<u64>(std::llround(weight));
|
||||
const u64 slots = static_cast<u64>(static_cast<double>(budget_bytes) / weight);
|
||||
u64 time_capacity = pow2_floor(std::max<u64>(slots, 128));
|
||||
if (time_capacity == 0) {
|
||||
throw std::runtime_error("buffer too small for one time-base sample per channel");
|
||||
@@ -102,11 +99,13 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
||||
return std::max<u64>(pow2_floor(ticks / stride), 1);
|
||||
};
|
||||
const auto bytes_for = [&](u64 ticks) {
|
||||
u64 total = ticks;
|
||||
for (u64 s : p.stride) total += capacity_for(ticks, s);
|
||||
return total * sizeof(f32);
|
||||
u64 total = ticks * sizeof(f64);
|
||||
for (u64 s : p.stride) total += capacity_for(ticks, s) * sizeof(f32);
|
||||
return total;
|
||||
};
|
||||
while (time_capacity * 2 <= budget && bytes_for(time_capacity * 2) <= budget_bytes) {
|
||||
// `bytes_for` bounds the doubling on its own: it is at least `2 * ticks * sizeof(f64)`,
|
||||
// so a pass means `2 * ticks` is well inside the budget and cannot overflow.
|
||||
while (bytes_for(time_capacity * 2) <= budget_bytes) {
|
||||
time_capacity *= 2;
|
||||
}
|
||||
p.time_capacity = time_capacity;
|
||||
@@ -138,7 +137,7 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
||||
|
||||
BufferStore::BufferStore(const Partition& part, f64 span_s)
|
||||
: part_(part),
|
||||
time_(std::make_shared<plot::data::RingBuffer<f32>>(part.time_capacity)) {
|
||||
time_(std::make_shared<plot::data::RingBuffer<f64>>(part.time_capacity)) {
|
||||
(void)span_s;
|
||||
const std::size_t n = part.value_capacity.size();
|
||||
values_.reserve(n);
|
||||
@@ -452,7 +451,7 @@ BufferStore& ScopeModel::live() { return *store_; }
|
||||
|
||||
void ScopeModel::live_window_locked(double lo_s, double hi_s, u64& first, u64& last) {
|
||||
BufferStore& src = live();
|
||||
plot::data::RingBuffer<f32>& tsrc = src.time();
|
||||
plot::data::RingBuffer<f64>& tsrc = src.time();
|
||||
|
||||
// The slice is one absolute index range, found in the time ring and then shared by
|
||||
// every value ring. Sharing it is what makes a cut's stamps and each of its channels
|
||||
@@ -460,16 +459,15 @@ void ScopeModel::live_window_locked(double lo_s, double hi_s, u64& first, u64& l
|
||||
// truncations that could disagree.
|
||||
const u64 tbase = tsrc.produced() - tsrc.size();
|
||||
const u64 tend = tsrc.produced();
|
||||
// The bounds are rounded to the ring's own `f32` before they are compared against
|
||||
// it. The window arrives in f64 and the stamps are stored as f32, so an f64
|
||||
// comparison decides a sample that sits exactly on a bound by which way its stamp
|
||||
// happened to round -- the same nominal window then keeps or drops its first sample
|
||||
// depending on how large the stream time is (`f32(38.001) < 38.001` but
|
||||
// `f32(18.001) > 18.001`). One sample is invisible at 10 kS/s and a third of the
|
||||
// capture at 1 S/s. Comparing in the stored type makes a bound that lands on a
|
||||
// sample include it, always.
|
||||
const f32 t_lo = static_cast<f32>(lo_s);
|
||||
const f32 t_hi = static_cast<f32>(hi_s);
|
||||
// The bounds are compared in the ring's own type, which is the `f64` they arrive in,
|
||||
// so a bound that lands exactly on a sample includes it. This used to need rounding
|
||||
// *down* to the stored f32 first: an f64 comparison against an f32 stamp decided such
|
||||
// a sample by which way its stamp happened to round, and the same nominal window kept
|
||||
// or dropped its first sample depending on the magnitude of the stream time
|
||||
// (`f32(38.001) < 38.001` but `f32(18.001) > 18.001`). The ring is f64 now and the
|
||||
// question does not arise.
|
||||
const f64 t_lo = lo_s;
|
||||
const f64 t_hi = hi_s;
|
||||
|
||||
// Stream time is non-decreasing, so the bounds are a binary search. Searching the
|
||||
// stamps is the whole point: the window is in seconds and this ring is the only
|
||||
@@ -496,7 +494,7 @@ void ScopeModel::live_window_locked(double lo_s, double hi_s, u64& first, u64& l
|
||||
last = a;
|
||||
|
||||
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||
plot::data::RingBuffer<f32>& from = src.values(c);
|
||||
plot::data::RingBuffer<f32>& from = src.values(c); // values stay f32
|
||||
first = std::max(first, from.produced() - from.size());
|
||||
last = std::min(last, from.produced());
|
||||
}
|
||||
@@ -554,7 +552,7 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
|
||||
void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
|
||||
auto snap = std::make_shared<EventSnapshot>();
|
||||
BufferStore& src = live();
|
||||
plot::data::RingBuffer<f32>& tsrc = src.time();
|
||||
plot::data::RingBuffer<f64>& tsrc = src.time();
|
||||
|
||||
u64 first = 0, last = 0;
|
||||
live_window_locked(edge - std::max(0.0, pre_s), edge + std::max(0.0, post_s), first, last);
|
||||
@@ -566,7 +564,7 @@ void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
|
||||
// Edge-relative from here on: the frozen event reads [-pre, +post] with the
|
||||
// edge at exactly 0, with no axis-level shift any other code path could
|
||||
// disagree with.
|
||||
for (auto& t : snap->stamps) t -= static_cast<f32>(edge);
|
||||
for (auto& t : snap->stamps) t -= edge;
|
||||
}
|
||||
|
||||
snap->values.resize(channels_.size());
|
||||
|
||||
+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);
|
||||
|
||||
+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