Correct downsampling and better layout
This commit is contained in:
1 parent
0d741c401b
commit
30db44eaa0
13 files changed
+1511
-252
No files matched your search
+59
-11
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -29,7 +30,8 @@ u64 Partition::total_samples() const noexcept {
|
||||
return total;
|
||||
}
|
||||
|
||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes) {
|
||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
|
||||
double target_span_s) {
|
||||
Partition p;
|
||||
const std::size_t n = channel_dt.size();
|
||||
p.stride.assign(n, 1);
|
||||
@@ -48,16 +50,12 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
||||
if (!(base_dt > 0.0)) base_dt = 1.0e-3;
|
||||
p.base_dt = base_dt;
|
||||
|
||||
// No per-channel decimation, and none on the shared clock either. A stride of
|
||||
// `round(dt_i / dt_fastest)` throws away all but one sample per stride, and when
|
||||
// that stride equals the signal's own period -- a 2 kHz sine on a 1 MHz clock,
|
||||
// stride 500 -- every kept sample sits at the same phase: the channel plots as a
|
||||
// flat line. Decimating the shared clock to make a 600 s window fit did the same
|
||||
// to *every* channel. The LOD min/max pyramid is what preserves the extremes at
|
||||
// draw time, and it needs the real samples; the buffer simply holds as many as
|
||||
// it holds, and the window dropdown is capped to that span.
|
||||
// No *per-channel* decimation. A stride of `round(dt_i / dt_fastest)` keeps one sample
|
||||
// in every stride, and when that stride equals the signal's own period -- a 2 kHz sine
|
||||
// on a 1 MHz clock, stride 500 -- every kept sample sits at the same phase and the
|
||||
// channel plots as a flat line. The shared clock is decimated below instead, which
|
||||
// affects every channel equally and goes through the ingest filter.
|
||||
for (std::size_t i = 0; i < n; ++i) p.stride[i] = 1;
|
||||
p.time_stride = 1;
|
||||
|
||||
const u64 budget_bytes = static_cast<u64>(std::max(1.0, buffer_bytes));
|
||||
const u64 budget = budget_bytes / sizeof(f32);
|
||||
@@ -97,6 +95,23 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
p.value_capacity[i] = capacity_for(p.time_capacity, p.stride[i]);
|
||||
}
|
||||
|
||||
// Only now, with the ring as long as the budget can buy, decimate the shared clock
|
||||
// enough for the requested window to fit in it: deciding this earlier would over-
|
||||
// decimate by whatever factor the growth loop was about to add. A power of two keeps
|
||||
// the ring capacities (themselves powers of two) in step and keeps `tick % period`
|
||||
// cheap. The cap stops a nonsense request -- a window far past `--max-window` -- from
|
||||
// spinning here; at 2^20 the stored rate is six decades below the wire rate and the
|
||||
// picture is averages of averages anyway.
|
||||
u64 m = 1;
|
||||
if (target_span_s > 0.0) {
|
||||
while (m < (u64(1) << 20) &&
|
||||
static_cast<double>(time_capacity) * static_cast<double>(m) * base_dt <
|
||||
target_span_s) {
|
||||
m *= 2;
|
||||
}
|
||||
}
|
||||
p.time_stride = m;
|
||||
p.span_s = static_cast<double>(p.time_capacity) * static_cast<double>(p.time_stride) * base_dt;
|
||||
return p;
|
||||
}
|
||||
@@ -184,12 +199,37 @@ void ScopeModel::rebuild_locked() {
|
||||
return;
|
||||
}
|
||||
|
||||
part_ = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0);
|
||||
part_ = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0,
|
||||
target_span_.load(std::memory_order_relaxed));
|
||||
store_ = std::make_unique<BufferStore>(part_, part_.span_s);
|
||||
|
||||
if (trigger_.channel >= static_cast<int>(channels_.size())) trigger_.channel = -1;
|
||||
}
|
||||
|
||||
bool ScopeModel::set_target_span(double seconds) {
|
||||
if (!(seconds > 0.0)) return false;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const double previous = target_span_.exchange(seconds, std::memory_order_relaxed);
|
||||
if (channels_.empty()) return false;
|
||||
// Re-plan only if the factor moves. The planner is cheap, so ask it rather than
|
||||
// reimplementing its arithmetic here -- two expressions for the same decision is how
|
||||
// the buffer and the view end up disagreeing about the stored rate.
|
||||
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);
|
||||
if (want.time_stride == part_.time_stride) return false;
|
||||
std::fprintf(stderr,
|
||||
"[udpscope] window %.4f s -> %.4f s: decimation %llu -> %llu, stored rate "
|
||||
"%.4g -> %.4g S/s (anti-alias %.4g Hz); buffer rebuilt\n",
|
||||
previous, seconds, static_cast<unsigned long long>(part_.time_stride),
|
||||
static_cast<unsigned long long>(want.time_stride), part_.stored_rate(),
|
||||
want.stored_rate(), want.antialias_hz());
|
||||
std::fflush(stderr);
|
||||
rebuild_locked();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScopeModel::is_time_signal(const std::vector<SignalInfo>& signals,
|
||||
std::size_t signal) {
|
||||
if (signal >= signals.size()) return false;
|
||||
@@ -331,15 +371,23 @@ void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
|
||||
|
||||
snap->values.resize(channels_.size());
|
||||
snap->y.resize(channels_.size(), plot::Range{0.0, 0.0});
|
||||
snap->pyramids.resize(channels_.size());
|
||||
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||
auto& vals = snap->values[c];
|
||||
vals.resize(static_cast<std::size_t>(n));
|
||||
if (n > 0) src.values(c).copy_from(first, n, vals.data());
|
||||
f32 lo = vals.empty() ? 0.0f : vals.front();
|
||||
f32 hi = lo;
|
||||
// The LOD index is folded into the bounds pass that was already here, so the
|
||||
// snapshot costs one O(n) walk per channel rather than two. Without a pyramid
|
||||
// the renderer's `level_for()` descends to level 0 and the whole capture is
|
||||
// drawn raw on every frame the event is held: see `EventSnapshot::pyramids`.
|
||||
auto& pyr = snap->pyramids[c];
|
||||
pyr.reserve(n);
|
||||
for (f32 v : vals) {
|
||||
lo = std::min(lo, v);
|
||||
hi = std::max(hi, v);
|
||||
pyr.append(v);
|
||||
}
|
||||
snap->y[c] = plot::Range{lo, hi};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user