trigger: a phosphor display keeps the last N acquisitions; and a Force button
Two changes in the trigger pane, landed together. **Persistence.** Trigger mode keeps the last N published acquisitions and draws them fading toward the background, N being a panel control (1..8, default 1) with N=1 meaning today's behaviour exactly, so the knob is the switch. The layers are the accumulate path's own -- one layer per (signal, age) in the row's free trace slots, named with the accumulate vocabulary so `layer_base_name()` strips the suffix and `signal_color()` resolves the base signal -- so a layer is not a signal: the signal list, the colours and the measurements see one row per signal. `ScopeModel` retains the outgoing snapshot in seven atomic `shared_ptr` slots (no lock, no copy; the snapshot already exists) and a layer's channel is resolved **by name inside the snapshot**, because the channel list is renumbered by any structural change since that acquisition was cut. `RowSet::sync_layers()` re-plans from the rows and the depth alone, so a latch costs no rebuild, and `event_data_window()` skips layer slots so an older acquisition can never re-pin the axis. The schedule is 0.5^age -- the acquisition on screen at full brightness and each older one half the previous, independent of N, so raising the depth adds dimmer acquisitions rather than rescaling the ones already on screen. Memory is bounded by the ring's own `--buffer-mb` rather than by the depth, which is the bound that cannot be driven into an OOM, and the panel says when either bound bites. This also wired up `set_trace_alpha()`, which the accumulate path was setting and nothing was reading -- so the documented 0.35 translucency of an accumulation was opaque in fact. **Force.** In the trigger pane beside Arm/Disarm, shown only while trigger mode is on: one acquisition now, with the configured window. It is a synthetic edge rather than a second publisher -- the engine opens one capture at the present moment and everything else is the ordinary latch, so a forced acquisition is cut, pinned, framed, recorded and cached exactly like a latched one, and the button owns no window and no publish. The pre-trigger gate is deliberately skipped (capture now is a request for the history that exists, and the snapshot reports its own shorter pre_s), a capture already filling is refused rather than restarted, and the state afterwards is the mode's: Normal stays armed, Single disarms. The request is one atomic flag written under the model mutex with `armed` -- the producer reads `armed` at the top of a packet and the request at the bottom, so a request written between the two would be dropped -- and consumed at the end of a packet, after the channel loop and before the pending publish. **A defect fixed in passing, in the function Force was modelled on:** `on_arm_clicked()` passed `TriggerConfig::channel` (an index into `channels()`) to `set_signal_enabled()` (an index into `signals()`). With one signal acquired, arming switched on the stream's clock signal and left the watched signal untouched -- and since a signal the panel was not showing is a change, it rebuilt the buffer under the click, dropping the very pre-trigger window the arming, and now the Force, was about to capture. Measured before: `0.0100 s of history behind it`; after: `2.5299 s`. `trigger_signal_index()` is now the one resolution, used by both buttons.
This commit is contained in:
1 parent
fc4700d1c9
commit
e14e1ee069
22 files changed
+1652
-26
No files matched your search
@@ -10,6 +10,8 @@
|
||||
|
||||
#include <plot/data/ring_buffer.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
@@ -511,6 +513,55 @@ class ScopeModel {
|
||||
/// while the latch count keeps answering "how many events fired".
|
||||
std::uint64_t event_seq() const noexcept { return event_seq_.load(std::memory_order_acquire); }
|
||||
|
||||
// --- persistence ----------------------------------------------------------
|
||||
/// The acquisitions retained for the persistence display, newest first: `persist_event(1)` is
|
||||
/// the event one latch back, `(2)` the one before that. Null for an age nobody kept.
|
||||
///
|
||||
/// Read **without a lock**, like `event()`: the producer publishes these in `publish_event()`
|
||||
/// with the same atomic-`shared_ptr` discipline, and a snapshot is immutable and shared, so a
|
||||
/// reader holds it alive for as long as it draws it. What the *ring* would have given is not
|
||||
/// needed afterwards -- a retained snapshot is a whole window copied out of it.
|
||||
std::shared_ptr<const EventSnapshot> persist_event(std::size_t age) const noexcept {
|
||||
if (age < 1 || age > persist_.size()) return nullptr;
|
||||
return persist_[age - 1].load(std::memory_order_acquire);
|
||||
}
|
||||
/// How many acquisitions are retained right now: what the depth asks for, or fewer -- never more
|
||||
/// -- when the retention budget cut the set short. A reading, so it is counted as the slots that
|
||||
/// hold a snapshot rather than derived from the depth.
|
||||
std::size_t persist_count() const noexcept {
|
||||
return persist_count_.load(std::memory_order_relaxed);
|
||||
}
|
||||
/// What those snapshots hold in bytes, and how many acquisitions the *budget* -- as against the
|
||||
/// depth -- has dropped. Both are producer-written and read as readings.
|
||||
std::size_t persist_bytes() const noexcept {
|
||||
return persist_bytes_.load(std::memory_order_relaxed);
|
||||
}
|
||||
std::uint64_t persist_dropped() const noexcept {
|
||||
return persist_dropped_.load(std::memory_order_relaxed);
|
||||
}
|
||||
/// The bytes persistence may retain, which is the ring's own byte budget (`--buffer-mb`): the
|
||||
/// snapshots are copies *of* what the rings hold, so holding as much again as the buffer does is
|
||||
/// the honest ceiling for a display of it -- and it is the bound that cannot be driven into an
|
||||
/// OOM by a long window on a fast stream, whatever depth the panel asks for.
|
||||
std::size_t persist_budget_bytes() const noexcept {
|
||||
const int mb = cfg_.buffer_mb > 0 ? cfg_.buffer_mb : 0;
|
||||
return static_cast<std::size_t>(mb) * 1024u * 1024u;
|
||||
}
|
||||
|
||||
/// Sets how many acquisitions the persistence display keeps, clamped to `kMaxPersistDepth`, and
|
||||
/// prunes the retained set to it **at once**: lowering the depth is a user asking to stop paying
|
||||
/// for the layers, and the next latch -- when the retention would otherwise catch up -- may never
|
||||
/// come on a trigger that has stopped firing.
|
||||
///
|
||||
/// Takes `mutex()` itself; do not call it holding the lock.
|
||||
void set_persist_depth(int depth);
|
||||
|
||||
/// How many acquisitions the persistence display keeps: the depth, clamped to the cap. Reading
|
||||
/// `trigger()` needs the lock, which every caller of this already holds.
|
||||
int persist_depth_locked() const noexcept {
|
||||
return std::clamp(trigger_.persist_depth, 1, kMaxPersistDepth);
|
||||
}
|
||||
|
||||
/// Publishes the live buffer's new samples to its `ChannelBuffer`s.
|
||||
void acquire_all();
|
||||
|
||||
@@ -742,6 +793,19 @@ class ScopeModel {
|
||||
std::atomic<double> capture_edge_{0.0};
|
||||
std::atomic<std::shared_ptr<const EventSnapshot>> event_; ///< the event on screen
|
||||
std::atomic<std::shared_ptr<const EventSnapshot>> pause_view_; ///< the view frozen at pause
|
||||
/// The retention behind `persist_event()`: one slot per *previous* acquisition, newest first, so
|
||||
/// slot 0 is the event one latch back. `kMaxPersistDepth - 1` of them, because the acquisition on
|
||||
/// screen is `event_` itself and is not retained a second time. Written by the producer in
|
||||
/// `publish_event()` and by `set_persist_depth()`, both under `mutex()`; read without it.
|
||||
std::array<std::atomic<std::shared_ptr<const EventSnapshot>>, kMaxPersistDepth - 1> persist_{};
|
||||
/// The readings `persist_count()`/`persist_bytes()`/`persist_dropped()` answer with, maintained
|
||||
/// where the slots are written.
|
||||
std::atomic<std::size_t> persist_count_{0};
|
||||
std::atomic<std::size_t> persist_bytes_{0};
|
||||
std::atomic<std::uint64_t> persist_dropped_{0};
|
||||
/// Drops the retained snapshots past `keep` (0 empties the set) and recounts the readings.
|
||||
/// Caller holds `mutex()`.
|
||||
void prune_persist_locked(std::size_t keep);
|
||||
std::atomic<bool> paused_{false};
|
||||
/// Per-signal-name calibration (see `set_calibration`).
|
||||
std::unordered_map<std::string, std::pair<double, double>> calibration_;
|
||||
|
||||
Reference in new issue
Block a user