feat: relative mode keeps the stream's absolute clock for the export
This commit is contained in:
1 parent
d11e7d48c4
commit
57e2f6aa8b
6 files changed
+87
-9
No files matched your search
@@ -135,6 +135,7 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
|
||||
n = std::max(n, udps_signal_num_elements(&frame->signals[i]));
|
||||
}
|
||||
stamps_.assign(n, 0.0);
|
||||
abs_stamps_.assign(n, 0.0);
|
||||
|
||||
// Relative mode: the stamp is a pure function of position and the *declared* rate. Nothing
|
||||
// below this block runs -- not the arrival-cadence override, not the degeneracy guard, not
|
||||
@@ -168,6 +169,21 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
|
||||
stamps_[k] = double(slot_base_ + k) * dt;
|
||||
}
|
||||
slot_base_ += n;
|
||||
// The absolute clock is still read, for export only. When the stream carries no usable
|
||||
// time signal there is no absolute clock to record and the relative stamp is the honest
|
||||
// answer -- an export then says what the counter says, which is all anyone had.
|
||||
if (time_signal >= 0 && time_signal < static_cast<int>(frame->num_signals)) {
|
||||
const udps_signal_t& ts = frame->signals[time_signal];
|
||||
const bool scalar_time = udps_signal_num_elements(&ts) == 1;
|
||||
for (std::uint32_t k = 0; k < n; ++k) {
|
||||
abs_stamps_[k] =
|
||||
scalar_time
|
||||
? udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), k, 0)
|
||||
: udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), 0, k);
|
||||
}
|
||||
} else {
|
||||
for (std::uint32_t k = 0; k < n; ++k) abs_stamps_[k] = stamps_[k];
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -413,6 +429,10 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
|
||||
for (std::uint32_t k = 0; k < n; ++k) stamps_[k] *= unit_scale;
|
||||
}
|
||||
|
||||
// Stream mode: `time()` already holds the absolute clock, so the parallel ring repeats it
|
||||
// rather than carrying a second timeline. One rule for the export either way.
|
||||
for (std::uint32_t k = 0; k < n; ++k) abs_stamps_[k] = stamps_[k];
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -476,6 +496,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
||||
value_acc_n_.assign(channels.size(), 0);
|
||||
math_sample_.assign(channels.size(), std::nanf(""));
|
||||
time_acc_ = 0.0;
|
||||
abs_time_acc_ = 0.0;
|
||||
time_acc_n_ = 0;
|
||||
}
|
||||
|
||||
@@ -595,10 +616,17 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
||||
// boxcar's group delay puts the averaged value, and stamping it at the end of the
|
||||
// group would shift every trace half a stored sample late.
|
||||
time_acc_ += rel;
|
||||
abs_time_acc_ += abs_stamps_[k] - epoch;
|
||||
++time_acc_n_;
|
||||
if (tick % g == g - 1) {
|
||||
store.time().push(time_acc_ / static_cast<double>(time_acc_n_));
|
||||
time_acc_ = 0.0;
|
||||
// The absolute clock on the same slot and under the same group mean, so an export can
|
||||
// pair value, relative stamp and absolute stamp by index with no interpolation.
|
||||
if (plot::data::RingBuffer<f64>* abs = store.abs_time(); abs != nullptr) {
|
||||
abs->push(abs_time_acc_ / static_cast<double>(time_acc_n_));
|
||||
}
|
||||
abs_time_acc_ = 0.0;
|
||||
time_acc_n_ = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ class Acquisition {
|
||||
/// Latches the one warning about a frame whose length disagrees with the stream's first.
|
||||
bool warned_batch_ = false;
|
||||
std::vector<double> stamps_;
|
||||
std::vector<double> abs_stamps_;
|
||||
/// The decimation filter's state: a running sum per channel (and one for the shared
|
||||
/// clock) over the group of base samples that becomes one stored sample. A boxcar
|
||||
/// average, which is what turns decimation into a *resampling* rather than a stride:
|
||||
@@ -114,6 +115,7 @@ class Acquisition {
|
||||
std::vector<f32> math_sample_;
|
||||
std::vector<std::uint64_t> value_acc_n_;
|
||||
double time_acc_ = 0.0;
|
||||
double abs_time_acc_ = 0.0;
|
||||
std::uint64_t time_acc_n_ = 0;
|
||||
/// The partition the accumulators were sized for, so a re-plan resets them rather than
|
||||
/// averaging across the boundary.
|
||||
|
||||
@@ -32,6 +32,10 @@ struct EventSnapshot {
|
||||
/// (see `BufferStore::time()`), which also makes the cut in `publish_event()`/
|
||||
/// `publish_pause_view()` a straight copy.
|
||||
std::vector<f64> stamps;
|
||||
/// Absolute stream seconds on the same indices as `stamps`, or empty when the store had no
|
||||
/// absolute ring. **Not** rebased by `x_offset` or by the trigger edge: a snapshot outlives
|
||||
/// the epoch that produced it, so it stores the absolute value outright.
|
||||
std::vector<f64> abs_stamps;
|
||||
std::vector<std::vector<f32>> values; ///< one array per channel, same length
|
||||
/// The stream time of this snapshot's x = 0, so `x_offset + stamps[i]` is the clock the
|
||||
/// data arrived on: the edge for a trigger event, the newest sample at the moment of the
|
||||
|
||||
+11
-2
@@ -127,7 +127,9 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
table.abs_seconds.resize(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
table.time[i] = st[first + i];
|
||||
table.abs_seconds[i] = abs_base + table.time[i];
|
||||
table.abs_seconds[i] = snap->abs_stamps.empty()
|
||||
? abs_base + table.time[i]
|
||||
: snap->abs_stamps[first + i];
|
||||
}
|
||||
for (const int c : chans) {
|
||||
const std::size_t ch = static_cast<std::size_t>(c);
|
||||
@@ -159,9 +161,16 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
||||
store.time().copy_from(first, n, stamps.data());
|
||||
table.time.resize(static_cast<std::size_t>(n));
|
||||
table.abs_seconds.resize(static_cast<std::size_t>(n));
|
||||
std::vector<f64> abs_stamps;
|
||||
if (const plot::data::RingBuffer<f64>* abs_ring = store.abs_time(); abs_ring != nullptr) {
|
||||
abs_stamps.resize(static_cast<std::size_t>(n));
|
||||
abs_ring->copy_from(first, n, abs_stamps.data());
|
||||
}
|
||||
for (std::size_t i = 0; i < stamps.size(); ++i) {
|
||||
table.time[i] = stamps[i] - s0;
|
||||
table.abs_seconds[i] = abs_base + table.time[i];
|
||||
table.abs_seconds[i] = abs_stamps.empty()
|
||||
? abs_base + table.time[i]
|
||||
: model.epoch() + abs_stamps[i];
|
||||
}
|
||||
for (const int c : chans) {
|
||||
const std::size_t ch = static_cast<std::size_t>(c);
|
||||
|
||||
+24
-5
@@ -43,7 +43,7 @@ constexpr std::uint64_t kMathCostSample = 1024;
|
||||
} // namespace
|
||||
|
||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
|
||||
double target_span_s) {
|
||||
double target_span_s, bool with_abs_time) {
|
||||
Partition p;
|
||||
const std::size_t n = channel_dt.size();
|
||||
p.stride.assign(n, 1);
|
||||
@@ -69,12 +69,14 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
||||
// affects every channel equally and goes through the ingest filter.
|
||||
for (std::size_t i = 0; i < n; ++i) p.stride[i] = 1;
|
||||
|
||||
p.abs_time = with_abs_time;
|
||||
|
||||
const u64 budget_bytes = static_cast<u64>(std::max(1.0, buffer_bytes));
|
||||
// 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
|
||||
double weight = static_cast<double>(sizeof(f64)) * (with_abs_time ? 2.0 : 1.0); // the time slice(s)
|
||||
for (u64 s : p.stride) weight += static_cast<double>(sizeof(f32)) / static_cast<double>(s);
|
||||
|
||||
const u64 slots = static_cast<u64>(static_cast<double>(budget_bytes) / weight);
|
||||
@@ -99,7 +101,7 @@ 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 * sizeof(f64);
|
||||
u64 total = ticks * sizeof(f64) * (with_abs_time ? 2u : 1u);
|
||||
for (u64 s : p.stride) total += capacity_for(ticks, s) * sizeof(f32);
|
||||
return total;
|
||||
};
|
||||
@@ -139,6 +141,11 @@ BufferStore::BufferStore(const Partition& part, f64 span_s)
|
||||
: part_(part),
|
||||
time_(std::make_shared<plot::data::RingBuffer<f64>>(part.time_capacity)) {
|
||||
(void)span_s;
|
||||
// Same capacity as the clock it parallels: the two are written on the same slots, so a
|
||||
// smaller ring would make an export's stamps run out before its values do.
|
||||
if (part.abs_time) {
|
||||
abs_time_ = std::make_shared<plot::data::RingBuffer<f64>>(part.time_capacity);
|
||||
}
|
||||
const std::size_t n = part.value_capacity.size();
|
||||
values_.reserve(n);
|
||||
channels_.reserve(n);
|
||||
@@ -340,7 +347,7 @@ void ScopeModel::rebuild_locked() {
|
||||
}
|
||||
|
||||
part_ = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0,
|
||||
target_span_.load(std::memory_order_relaxed));
|
||||
target_span_.load(std::memory_order_relaxed), cfg_.relative_time);
|
||||
store_ = std::make_unique<BufferStore>(part_, part_.span_s);
|
||||
|
||||
if (trigger_.channel >= static_cast<int>(channels_.size())) trigger_.channel = -1;
|
||||
@@ -358,7 +365,7 @@ bool ScopeModel::set_target_span(double seconds) {
|
||||
std::vector<double> dts;
|
||||
dts.reserve(channels_.size());
|
||||
for (const ChannelInfo& c : channels_) dts.push_back(c.dt);
|
||||
const Partition want = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0, seconds);
|
||||
const Partition want = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0, seconds, cfg_.relative_time);
|
||||
if (want.time_stride == part_.time_stride) return false;
|
||||
std::fprintf(stderr,
|
||||
"[udpscope] window %.4f s -> %.4f s: decimation %llu -> %llu, stored rate "
|
||||
@@ -524,6 +531,12 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s, double x_offset) {
|
||||
src.time().copy_from(first, n, snap->stamps.data());
|
||||
for (f64& t : snap->stamps) t -= x_offset;
|
||||
}
|
||||
if (const plot::data::RingBuffer<f64>* abs = src.abs_time(); abs != nullptr) {
|
||||
snap->abs_stamps.resize(static_cast<std::size_t>(n));
|
||||
abs->copy_from(first, n, snap->abs_stamps.data());
|
||||
// Absolute seconds outright -- deliberately not rebased like `stamps`.
|
||||
for (f64& t : snap->abs_stamps) t += epoch_;
|
||||
}
|
||||
snap->values.resize(channels_.size());
|
||||
snap->y.resize(channels_.size(), plot::Range{0.0, 0.0});
|
||||
snap->pyramids.resize(channels_.size());
|
||||
@@ -588,6 +601,12 @@ void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
|
||||
// disagree with.
|
||||
for (auto& t : snap->stamps) t -= edge;
|
||||
}
|
||||
if (const plot::data::RingBuffer<f64>* abs = src.abs_time(); abs != nullptr) {
|
||||
snap->abs_stamps.resize(static_cast<std::size_t>(n));
|
||||
abs->copy_from(first, n, snap->abs_stamps.data());
|
||||
// Absolute seconds outright -- deliberately not rebased like `stamps`.
|
||||
for (f64& t : snap->abs_stamps) t += epoch_;
|
||||
}
|
||||
|
||||
snap->values.resize(channels_.size());
|
||||
snap->y.resize(channels_.size(), plot::Range{0.0, 0.0});
|
||||
|
||||
+18
-2
@@ -93,6 +93,11 @@ struct Partition {
|
||||
double base_dt = 1.0; ///< seconds per base tick
|
||||
double span_s = 0.0; ///< stream seconds one buffer covers
|
||||
|
||||
/// True when a second f64 ring parallel to the clock was planned and paid for in the
|
||||
/// byte budget. Relative mode only -- in stream mode `time()` already *is* the absolute
|
||||
/// clock and a copy would cost ~22% of the user's `--buffer-mb` for nothing.
|
||||
bool abs_time = false;
|
||||
|
||||
/// 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.
|
||||
@@ -109,9 +114,10 @@ struct Partition {
|
||||
/// Plans a partition for `channel_dt` cadences within `buffer_bytes`, covering at least
|
||||
/// `target_span_s` seconds of stream -- decimating (with the ingest filter) by whatever
|
||||
/// power of two that takes. Throws std::runtime_error when the budget cannot hold even
|
||||
/// one sample per channel.
|
||||
/// one sample per channel. `with_abs_time` adds a second f64 per time slot to the budget
|
||||
/// and sets `Partition::abs_time`.
|
||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
|
||||
double target_span_s);
|
||||
double target_span_s, bool with_abs_time = false);
|
||||
|
||||
/// One of the two identical sample buffers: a shared time-base ring plus one
|
||||
/// value ring and one `ChannelBuffer` per channel.
|
||||
@@ -132,6 +138,15 @@ class BufferStore {
|
||||
/// 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_; }
|
||||
/// The stream's **absolute** clock, parallel to `time()` and on the same slots, or
|
||||
/// `nullptr` when the store was planned without it.
|
||||
///
|
||||
/// There is exactly one time ring per store, shared by every channel, and relative mode
|
||||
/// fills it with counter-derived stamps -- so without this ring, selecting relative mode
|
||||
/// would silently drop absolute time from an export, the one output `todo.md` item 2 says
|
||||
/// must keep it. Allocated in relative mode only; read only by the export path.
|
||||
plot::data::RingBuffer<f64>* abs_time() noexcept { return abs_time_.get(); }
|
||||
const plot::data::RingBuffer<f64>* abs_time() const noexcept { return abs_time_.get(); }
|
||||
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
|
||||
@@ -147,6 +162,7 @@ class BufferStore {
|
||||
private:
|
||||
Partition part_;
|
||||
std::shared_ptr<plot::data::RingBuffer<f64>> time_;
|
||||
std::shared_ptr<plot::data::RingBuffer<f64>> abs_time_;
|
||||
std::vector<std::shared_ptr<plot::data::RingBuffer<f32>>> values_;
|
||||
std::vector<std::shared_ptr<ChannelBuffer>> channels_;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user