improved perfs and event management

This commit is contained in:
Martino Ferrari committed 2026-10-13 23:00:42 +02:00
1 parent 667ed0c100
commit bb99be5889
17 files changed
+626 -59

No files matched your search

+58
View File
@@ -109,6 +109,23 @@ struct Partition {
/// Nyquist. The boxcar the ingest path averages with has its first null at the stored
/// rate itself, so everything that would fold back is attenuated.
double antialias_hz() const noexcept { return 0.5 * stored_rate(); }
/// Two plans that describe the **same storage**: the same rings, of the same lengths, at
/// the same decimation, over the same clock. This is the test `rebuild_locked()` uses to
/// decide whether it can clear the buffer it has instead of allocating another one.
///
/// `base_dt` is compared although it buys no byte, because a reused store keeps the
/// `Partition` *it* was built with and hands `slot_dt()` to every `ChannelBuffer` as the
/// nominal grid `dx()` reports -- and `dx()` is what picks the LOD level. Two channel sets
/// can want the same capacities off a different base tick, and reusing across that would
/// leave every channel describing a grid the samples are not on. It is an exact compare of
/// a value that is copied, never recomputed, from the fastest channel's `dt`.
/// `span_s` needs no compare of its own: it is the product of the three fields above.
bool same_storage_as(const Partition& o) const noexcept {
return time_capacity == o.time_capacity && time_stride == o.time_stride &&
abs_time == o.abs_time && base_dt == o.base_dt && stride == o.stride &&
value_capacity == o.value_capacity;
}
};
/// Plans a partition for `channel_dt` cadences within `buffer_bytes`, covering at least
@@ -156,6 +173,27 @@ class BufferStore {
}
const Partition& partition() const noexcept { return part_; }
/// Drops every sample and leaves a store indistinguishable from a freshly built one with
/// the same `Partition` -- the state `rebuild_locked()` wants when the plan did not change.
///
/// Why it exists: constructing a store *is* its cost. A ring is a
/// `vector<std::atomic<T>>`, which is value-initialised, so building one touches every
/// byte of the user's `--buffer-mb` -- about 300 ms per GB, nearly all of it first-touch
/// page faults. Clearing is a count store per ring. Most structural changes (a
/// calibration, a re-enable of what was just disabled, going back to live) re-plan to the
/// *same* partition, and paying a second full allocation for them is what made a large
/// buffer take ten seconds to start and an event take seconds to leave.
///
/// It must leave **nothing** of the old history behind, in either copy: the rings are
/// emptied and every `ChannelBuffer` is `invalidate()`d, which drops the published window
/// and the pyramid built over it. The epoch goes back to 0 for the same reason -- a fresh
/// store has none, and a reused one keeping the old event's epoch would re-base the next
/// axis against a trigger that no longer exists.
///
/// Caller's thread: the consumer, under `ScopeModel::mutex()` -- the same lock the
/// producer's pushes hold. `RingBuffer::clear()` is not safe against a concurrent push.
void reset() noexcept;
/// Consumer: publishes every channel's new samples.
void acquire_all();
@@ -356,6 +394,17 @@ class ScopeModel {
static bool is_time_signal(const std::vector<SignalInfo>& signals,
std::size_t signal);
void set_signal_enabled(std::size_t signal, bool on);
/// The same switch thrown for several signals as **one** structural change.
///
/// Enabling a signal changes the channel count, so it re-plans the partition and the new
/// plan really is a different buffer -- there is no reusing the old store for it (see
/// `BufferStore::reset()`). A caller that enables n signals one at a time therefore pays n
/// full allocations to reach a state one allocation describes, and every one of them drops
/// the history the one before it started collecting. That is what a restored session, a
/// loaded waveform and an event group all do, and at a 1 GB buffer it is seconds of a
/// blank window: the plots are bound to a store that is replaced again before it holds a
/// screenful. Indices the model has no signal for are ignored.
void set_signals_enabled(const std::vector<std::size_t>& signals, bool on);
/// Stores the calibration for a signal's *name* and applies it to that signal's
/// live channels. Keyed by name, not index: a CONFIG renumbers and an
/// enable/disable rebuilds the channel list, and `rebuild_channels()` re-applies
@@ -587,6 +636,15 @@ class ScopeModel {
private:
void rebuild_channels();
/// One signal's enable bookkeeping -- `enabled_` and the math channel's own flag -- with no
/// re-plan. Returns whether the state actually moved. The two halves of
/// `set_signal_enabled()` are split so that a batch can do n of these and one of the
/// rebuild below.
bool enable_locked(std::size_t signal, bool on);
/// The re-plan an enable needs: carry the trigger's channel across the renumbering,
/// rebuild the channels and the buffers, and publish the new generation. Caller holds
/// `mutex()` and has already moved `enabled_`.
void rebuild_enabled_locked();
/// Re-appends one synthetic signal per math channel, after the stream's own. Called under the
/// lock by both `set_signals()` (a CONFIG) and `set_math_channels()` (an edit).
void append_math_signals_locked();