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,7 @@
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -2802,6 +2803,186 @@ gboolean disarm_probe_step1(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_FORCE_MS`: **Force** takes one acquisition at the present moment, with the
|
||||
/// pre/post window the panel is set to, without waiting for the condition to hold.
|
||||
///
|
||||
/// The two legs are the two things the button has to get right, and they need different geometry:
|
||||
///
|
||||
/// - **Leg A** presses it where the ring *can* satisfy the request -- a 2 s window at 50 % pre asks
|
||||
/// for 1 s of history before the edge and 1 s after it, and the probe has let the ring fill past
|
||||
/// that -- so the whole ordinary path is asserted: a latch count that moves by exactly one,
|
||||
/// `display=event(frozen)`, and a snapshot spanning what was asked for.
|
||||
/// - **Leg B** presses it where the ring *cannot*: a 100 s window at 99 % pre asks for 99 s of
|
||||
/// history, and no run this short has it. That is the only state in which "pinned to the
|
||||
/// snapshot, not to the request" is a reading rather than a coincidence, and the axis must come
|
||||
/// out at the snapshot's own `[-pre_got..+post_got]`, a few seconds wide, not at the requested
|
||||
/// hundred.
|
||||
///
|
||||
/// The condition is set to a level the mock never reaches (9 V against +/-1 V signals), so the
|
||||
/// engine cannot latch on its own and every latch the probe sees is the force's -- which is also
|
||||
/// what makes the request *unmet* rather than merely untested: without the button, nothing would be
|
||||
/// captured at all. The `during=` reading is the axis while the forced capture was filling: the
|
||||
/// *requested* window, which is what a capture is framed on before the latch re-pins it.
|
||||
///
|
||||
/// Run it on its own (the trigger must start off, which is the state whose button reading is half
|
||||
/// the point) against the plain mock, with enough time for both legs: `UDPSCOPE_PROBE_FORCE_MS=3000
|
||||
/// UDPSCOPE_EXIT_AFTER_MS=14000`.
|
||||
void force_probe_report(App& a, const char* leg) {
|
||||
const DisplayMode d = a.model->display_mode();
|
||||
auto snap = a.model->event();
|
||||
const RowSet::Row& r = a.rows->row(0);
|
||||
const plot::Range axis = r.plot != nullptr ? r.plot->x_range(r.axis_x) : plot::Range{0.0, 0.0};
|
||||
// `pre_s`/`post_s` are the snapshot's own split as *magnitudes* (`[-pre_s..+post_s]` is the window
|
||||
// it holds), which is the same pair the frame tick states the pin from -- so the axis must equal
|
||||
// `[-pre_s, +post_s]`.
|
||||
const double got_pre = snap ? snap->pre_s : 0.0;
|
||||
const double got_post = snap ? snap->post_s : 0.0;
|
||||
const bool pin_is_snapshot = snap != nullptr &&
|
||||
std::abs(double(axis.min) + got_pre) < 1.0e-3 &&
|
||||
std::abs(double(axis.max) - got_post) < 1.0e-3;
|
||||
// "What was asked for" is satisfied to within a stored interval (the cut's ends are samples), and
|
||||
// leg B *must* fail it: the request is what the ring could not reach back to.
|
||||
const bool matches_request = std::abs(got_pre - a.dbg_force_want_pre) < 0.01 &&
|
||||
std::abs(got_post - a.dbg_force_want_post) < 0.01;
|
||||
std::fprintf(stderr,
|
||||
"[force-probe] leg %s want=[-%.4f..%.4f] during=[%.4f..%.4f] snap=[-%.4f..%.4f] "
|
||||
"n=%llu axis=[%.4f..%.4f] latches=%llu->%llu display=%s matches_request=%d "
|
||||
"pin_is_snapshot=%d\n",
|
||||
leg, a.dbg_force_want_pre, a.dbg_force_want_post, a.dbg_force_capture_lo,
|
||||
a.dbg_force_capture_hi, got_pre, got_post,
|
||||
static_cast<unsigned long long>(snap ? snap->stamps.size() : 0), double(axis.min),
|
||||
double(axis.max),
|
||||
static_cast<unsigned long long>(a.dbg_force_latches_before),
|
||||
static_cast<unsigned long long>(a.model->trigger_latches().load()),
|
||||
d == DisplayMode::Held ? "event(frozen)"
|
||||
: d == DisplayMode::Live ? "live"
|
||||
: d == DisplayMode::Capturing ? "live(capturing)"
|
||||
: "nothing",
|
||||
int(matches_request), int(pin_is_snapshot));
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
gboolean force_probe_poll(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
++a->dbg_force_polls;
|
||||
if (a->model->trigger_pending() && !a->dbg_force_capture_seen) {
|
||||
// The sweep is filling: the axis is the *requested* window now (a capture is framed on what was
|
||||
// asked for, there being no snapshot to measure yet), and this is the one moment it is readable.
|
||||
a->dbg_force_capture_seen = 1;
|
||||
const RowSet::Row& r = a->rows->row(0);
|
||||
const plot::Range ax = r.plot != nullptr ? r.plot->x_range(r.axis_x) : plot::Range{0.0, 0.0};
|
||||
a->dbg_force_capture_lo = ax.min;
|
||||
a->dbg_force_capture_hi = ax.max;
|
||||
}
|
||||
const std::uint64_t latches = a->model->trigger_latches().load();
|
||||
if (latches <= a->dbg_force_latches_before && a->dbg_force_polls < 200) return G_SOURCE_CONTINUE;
|
||||
if (latches <= a->dbg_force_latches_before) {
|
||||
std::fprintf(stderr, "[force-probe] no latch within 10 s of the force (leg %d)\n",
|
||||
a->dbg_force_leg);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
force_probe_report(*a, a->dbg_force_leg == 1 ? "A" : "B");
|
||||
a->dbg_force_polls = 0;
|
||||
if (a->dbg_force_leg == 1) {
|
||||
// Leg B's geometry: a window the ring cannot cover back to the edge, so the cut is clamped and
|
||||
// the pin has to come from the snapshot rather than from the ask. 99 % is the panel's ceiling
|
||||
// (a trigger with no post side at all is not a capture), which leaves 1 s of post window -- the
|
||||
// leg's own latch delay, and the reason the press is a step of its own.
|
||||
a->dbg_force_leg = 2;
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 99.0);
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->window_unit_drop), 2);
|
||||
gtk_editable_set_text(GTK_EDITABLE(a->window_entry), "100");
|
||||
on_window_entry_activate(GTK_ENTRY(a->window_entry), a);
|
||||
g_timeout_add(400, force_probe_press, a);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean force_probe_press(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
a->dbg_force_latches_before = a->model->trigger_latches().load();
|
||||
a->dbg_force_polls = 0;
|
||||
a->dbg_force_capture_seen = 0;
|
||||
a->dbg_force_capture_lo = 0.0;
|
||||
a->dbg_force_capture_hi = 0.0;
|
||||
{
|
||||
// The request the engine will cut by, taken from the producer's own expression rather than
|
||||
// recomputed here, so the reading compares the snapshot against what was actually asked for.
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const double span = std::max(1.0e-3, a->model->event_span());
|
||||
a->dbg_force_want_post = a->model->trigger().post_span_s(span);
|
||||
a->dbg_force_want_pre = span - a->dbg_force_want_post;
|
||||
}
|
||||
// The button's own click, as every other probe drives a control. `label_found` was checked in
|
||||
// step 2: this is the widget the window would press.
|
||||
if (a->force_button != nullptr) g_signal_emit_by_name(a->force_button, "clicked");
|
||||
g_timeout_add(50, force_probe_poll, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean force_probe_step2(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// The trigger-off state, one frame after step 1 forced it: the *sensitivity* is the rule
|
||||
// `sync_arm_buttons()` states (a trigger that is off has no engine to force), the *width* is the
|
||||
// allocation the layout gave the button, and a width read in the tick it was last stated is 0 --
|
||||
// the trap the compact arm control fell into. `label_found` is the same reading at the user's
|
||||
// level: the button the window shows under the word "Force" is this one.
|
||||
a->dbg_force_off_sensitive =
|
||||
a->force_button != nullptr && gtk_widget_get_sensitive(a->force_button) ? 1 : 0;
|
||||
a->dbg_force_off_visible =
|
||||
a->force_button != nullptr && gtk_widget_is_visible(a->force_button) ? 1 : 0;
|
||||
a->dbg_force_off_width = a->force_button != nullptr ? gtk_widget_get_width(a->force_button) : -1;
|
||||
a->dbg_force_arm_width = a->arm_button != nullptr ? gtk_widget_get_width(a->arm_button) : -1;
|
||||
a->dbg_force_label_found =
|
||||
a->force_button != nullptr && find_labelled_button(a->window, "Force") == a->force_button ? 1
|
||||
: 0;
|
||||
// The geometry both legs start from, set through the real widgets: a level no mock signal
|
||||
// reaches, so nothing latches on its own (every latch the probe sees is then the force's, which
|
||||
// is also what makes the request *unmet* rather than merely untested), and a 2 s window at 50 %
|
||||
// pre, which leg A's ring can fill. The window write may re-plan the buffer and clear the
|
||||
// history, which is why the press is 2.5 s later rather than in this tick.
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin), 9.0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 50.0);
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->window_unit_drop), 2);
|
||||
gtk_editable_set_text(GTK_EDITABLE(a->window_entry), "2");
|
||||
on_window_entry_activate(GTK_ENTRY(a->window_entry), a);
|
||||
// A signal has to be *acquired* for the trigger to have a channel at all (the panel lists the
|
||||
// acquired ones, and on a fresh source none is switched on): the mock's `sine`, which with the
|
||||
// level above is a trigger nothing crosses. Set after the level, which is what marks it
|
||||
// hand-edited -- a rebuild re-defaults an untouched level to the middle of the declared range.
|
||||
a->model->set_signal_enabled(1, true);
|
||||
if (a->trigger_toggle != nullptr)
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
a->dbg_force_on_sensitive =
|
||||
a->force_button != nullptr && gtk_widget_get_sensitive(a->force_button) ? 1 : 0;
|
||||
a->dbg_force_on_width = a->force_button != nullptr ? gtk_widget_get_width(a->force_button) : -1;
|
||||
std::fprintf(stderr,
|
||||
"[force-probe] button off: sensitive=%d visible=%d w=%d | on: sensitive=%d w=%d | "
|
||||
"arm w=%d | label_found=%d\n",
|
||||
a->dbg_force_off_sensitive, a->dbg_force_off_visible, a->dbg_force_off_width,
|
||||
a->dbg_force_on_sensitive, a->dbg_force_on_width, a->dbg_force_arm_width,
|
||||
a->dbg_force_label_found);
|
||||
std::fflush(stderr);
|
||||
a->dbg_force_leg = 1;
|
||||
g_timeout_add(2500, force_probe_press, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean force_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || a->trigger_toggle == nullptr) return G_SOURCE_REMOVE;
|
||||
// Trigger mode *off* first, driven through the toggle as a user would: the state whose reading
|
||||
// step 2 takes, and a state this probe states rather than assumes -- a session file remembers the
|
||||
// trigger, so a second run against the same source would otherwise start armed and never read the
|
||||
// off case at all.
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), FALSE);
|
||||
g_timeout_add(300, force_probe_step2, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void pause_probe_report(App& a, const char* what) {
|
||||
double bounds_lo = 0.0;
|
||||
double bounds_hi = 0.0;
|
||||
@@ -5651,5 +5832,234 @@ gboolean tour_surface_probe_step(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// --- persistence (UDPSCOPE_PROBE_PERSIST_MS) ---------------------------------
|
||||
//
|
||||
// The phosphor display: the last N acquisitions drawn behind the current one, dimmer with age, with
|
||||
// `N = 1` -- the default -- drawn exactly like the display without the feature. One leg, and the
|
||||
// depth comes from the environment, because the *reading* is a pair of runs: `..._PERSIST_DEPTH=4`
|
||||
// must show the layers at the right brightnesses and ages with the framing still the current event's,
|
||||
// and the same command at `=1` must show none of them, the same framing, and nothing retained -- and
|
||||
// the second of those is also the baseline the exit dump's `frame cost:` line is read against.
|
||||
|
||||
/// The depth the first leg asks for: `UDPSCOPE_PROBE_PERSIST_DEPTH`, default 4, clamped to the
|
||||
/// panel's own range. **Set it to 1 and the same run is the unpersisted display**, which is the
|
||||
/// baseline the exit dump's `frame cost:` line has to be read against.
|
||||
int persist_probe_depth() {
|
||||
const char* env = g_getenv("UDPSCOPE_PROBE_PERSIST_DEPTH");
|
||||
const int n = env != nullptr ? std::atoi(env) : 4;
|
||||
return std::clamp(n, 1, kMaxPersistDepth);
|
||||
}
|
||||
|
||||
/// The event window the probe arms with: `UDPSCOPE_PROBE_PERSIST_WINDOW` seconds, default 0.5. A
|
||||
/// *long* window on a fast stream is how the **memory** bound is reached -- one retained acquisition
|
||||
/// is a whole window copied out of the ring -- so the run that has to show `dropped` and a `kept`
|
||||
/// below the depth is `=2` against the fast mock.
|
||||
double persist_probe_window() {
|
||||
const char* env = g_getenv("UDPSCOPE_PROBE_PERSIST_WINDOW");
|
||||
const double s = env != nullptr ? std::atof(env) : 0.5;
|
||||
return s > 0.0 ? s : 0.5;
|
||||
}
|
||||
|
||||
/// One leg's reading: what the model retained, what the rows drew, and whether the framing is still
|
||||
/// the event on screen and nothing else.
|
||||
void persist_probe_report(App& a, const char* what) {
|
||||
int depth = 1;
|
||||
int snap_n = 0;
|
||||
double snap_lo = 0.0;
|
||||
double snap_hi = 0.0;
|
||||
std::size_t kept = 0;
|
||||
std::size_t bytes = 0;
|
||||
std::size_t budget = 0;
|
||||
std::size_t signals = 0;
|
||||
std::size_t channels = 0;
|
||||
unsigned long long dropped = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
depth = a.model->persist_depth_locked();
|
||||
kept = a.model->persist_count();
|
||||
bytes = a.model->persist_bytes();
|
||||
budget = a.model->persist_budget_bytes();
|
||||
dropped = static_cast<unsigned long long>(a.model->persist_dropped());
|
||||
signals = a.model->signals().size();
|
||||
channels = a.model->channels().size();
|
||||
if (const std::shared_ptr<const EventSnapshot> snap = a.model->event()) {
|
||||
snap_lo = -snap->pre_s;
|
||||
snap_hi = snap->post_s;
|
||||
snap_n = static_cast<int>(snap->stamps.size());
|
||||
}
|
||||
}
|
||||
// The decay schedule in use, oldest first: the reading's point is that the numbers descend by half
|
||||
// and that the *oldest* is the dimmest.
|
||||
const std::vector<std::pair<int, float>> layers = a.rows->row_layers(0);
|
||||
std::fprintf(stderr,
|
||||
"[persist-probe] %s depth=%d kept=%zu bytes=%zu budget=%zu dropped=%llu signals=%zu "
|
||||
"channels=%zu layers=%d want=%d placed=%d alphas=[",
|
||||
what, depth, kept, bytes, budget, dropped, signals, channels, a.rows->layer_slots(),
|
||||
a.persist_want, a.persist_placed);
|
||||
for (std::size_t i = 0; i < layers.size(); ++i) {
|
||||
std::fprintf(stderr, "%s%.4f", i != 0 ? "," : "", static_cast<double>(layers[i].second));
|
||||
}
|
||||
std::fprintf(stderr, "] ages=[");
|
||||
for (std::size_t i = 0; i < layers.size(); ++i) {
|
||||
std::fprintf(stderr, "%s%d", i != 0 ? "," : "", layers[i].first);
|
||||
}
|
||||
std::fprintf(stderr, "] row0=[");
|
||||
const RowSet::Row& r0 = a.rows->row(0);
|
||||
bool first = true;
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
if (r0.layer[si] == 0) continue;
|
||||
std::fprintf(stderr, "%s%d:'%s'@%.4f", first ? "" : " ", s, r0.signal_name[si].c_str(),
|
||||
static_cast<double>(r0.slot_alpha[si]));
|
||||
first = false;
|
||||
}
|
||||
// How many layer slots are actually *bound* to a retained acquisition. It is not the same number
|
||||
// as `layers=`/`placed=`: a slot can be planned for an age the retention budget refused, and it
|
||||
// then draws nothing at all -- so the two pairs together say which of the two bounds bit.
|
||||
int drawn = 0;
|
||||
for (int r = 0; r < a.rows->row_count(); ++r) {
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
const RowSet::Row& row_state = a.rows->row(r);
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
if (row_state.layer[si] == 0) continue;
|
||||
if (row_state.slots[si].bound_event() != nullptr) ++drawn;
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr, "] drawn=%d\n", drawn);
|
||||
// The framing. `ev` is what a held event's pin is re-stated from (`event_data_window()`), and it
|
||||
// must be the *snapshot's* own window: layers are bound event series on the same rows, so a union
|
||||
// that had grown with them reads here as `ev_is_snap=0`.
|
||||
const plot::Range axis = a.rows->view_range();
|
||||
const plot::Range pin = a.rows->pinned_range();
|
||||
double ev_lo = 0.0;
|
||||
double ev_hi = 0.0;
|
||||
const bool have_ev = a.rows->event_data_window(ev_lo, ev_hi);
|
||||
const bool ev_is_snap =
|
||||
have_ev && std::abs(ev_lo - snap_lo) < 1.0e-6 && std::abs(ev_hi - snap_hi) < 1.0e-6;
|
||||
const bool pin_is_snap = a.rows->window_pinned() && std::abs(pin.min - snap_lo) < 1.0e-6 &&
|
||||
std::abs(pin.max - snap_hi) < 1.0e-6;
|
||||
std::fprintf(stderr,
|
||||
"[persist-probe] %s axis=[%.4f..%.4f] pin=[%.4f..%.4f] snap=[%.4f..%.4f] n=%d "
|
||||
"ev=[%.4f..%.4f] ev_is_snap=%d pin_is_snap=%d\n",
|
||||
what, double(axis.min), double(axis.max), double(pin.min), double(pin.max), snap_lo,
|
||||
snap_hi, snap_n, ev_lo, ev_hi, ev_is_snap ? 1 : 0, pin_is_snap ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
/// Sets the panel's own depth control and publishes it: the *widget* is the path a user takes, so the
|
||||
/// probe exercises the handler that reads it and not the model setter.
|
||||
void persist_probe_set_depth(App& a, int depth) {
|
||||
if (a.trigger_persist_spin != nullptr) {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a.trigger_persist_spin), double(depth));
|
||||
}
|
||||
publish_trigger_fields(&a);
|
||||
}
|
||||
|
||||
/// Step 1: enable the signals **itself** -- one call, the way a restored session does it, and
|
||||
/// deliberately not `UDPSCOPE_ENABLE_ALL_MS`: that probe ends by switching one signal off two seconds
|
||||
/// later, so a run that relied on it would see its first row's signal leave the plots (and its base
|
||||
/// slot go with it) in the middle of the measurement. Then one signal per row -- the arrangement the
|
||||
/// app itself gives an opened file, and the one with room for every age (a row of four signals has one
|
||||
/// free slot per signal, a row of one has seven) -- the depth through the panel's own spin, and the
|
||||
/// trigger armed on the mock's `square`, which crosses its level several times a second so that several
|
||||
/// acquisitions land in a couple of seconds.
|
||||
gboolean persist_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
{
|
||||
const std::vector<SignalInfo>& sigs = a->model->signals();
|
||||
std::vector<std::size_t> all(sigs.size());
|
||||
for (std::size_t i = 0; i < all.size(); ++i) all[i] = i;
|
||||
if (!all.empty()) a->model->set_signals_enabled(all, true);
|
||||
}
|
||||
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
const int rows = std::min<int>(RowSet::kMaxRows, static_cast<int>(channels.size()));
|
||||
if (rows > 0) {
|
||||
a->rows->set_row_count(rows);
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) a->rows->assign(r, s, "", "", 0);
|
||||
a->rows->assign(r, 0, channels[static_cast<std::size_t>(r)].name,
|
||||
channels[static_cast<std::size_t>(r)].name,
|
||||
signal_color_index(signals, channels[static_cast<std::size_t>(r)].signal));
|
||||
}
|
||||
a->relayout_pending = true;
|
||||
}
|
||||
a->dbg_persist_depth = persist_probe_depth();
|
||||
persist_probe_set_depth(*a, a->dbg_persist_depth);
|
||||
int sel = -1;
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
if (channels[i].name == "square") sel = static_cast<int>(i);
|
||||
}
|
||||
if (sel >= 0) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop), static_cast<guint>(sel));
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), 0);
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop), 0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin), 0.0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0);
|
||||
const double window_s = persist_probe_window();
|
||||
apply_time_window(*a, window_s);
|
||||
publish_trigger_fields(a);
|
||||
std::fprintf(stderr, "[persist-probe] %d row(s), one signal each, depth=%d, window=%.3f s, arming on '%s'\n",
|
||||
rows, a->dbg_persist_depth, window_s,
|
||||
sel >= 0 ? channels[static_cast<std::size_t>(sel)].name.c_str() : "(none)");
|
||||
std::fflush(stderr);
|
||||
if (a->trigger_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
}
|
||||
a->dbg_persist_polls = 0;
|
||||
g_timeout_add(25, persist_probe_wait, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Waits for the retention to be *full*: an event on screen and as many older ones kept as the depth
|
||||
/// asks for. The trigger's cadence is the stream's, so a fixed delay would report whatever happened
|
||||
/// to have latched; the 15 s cap is for a stream that never latches, and the probe still reports.
|
||||
///
|
||||
/// A full set is not always *reachable*: the retention budget can keep fewer acquisitions than the
|
||||
/// depth asks for, and then the condition above can never come true -- which is the state the memory
|
||||
/// bound is read in, so waiting for it would report nothing at all. The wait therefore also ends when
|
||||
/// the set has **settled** (nothing new retained for two seconds) with something in it: that is the
|
||||
/// budget speaking, and the report's `kept`/`dropped`/`drawn` are what say so.
|
||||
///
|
||||
/// The depth is **left where the first leg put it**: the run's own tail is then the display at that
|
||||
/// depth, which is what the exit dump's `frame cost:` line is a mean over -- and the run to read it
|
||||
/// against is the same command with `UDPSCOPE_PROBE_PERSIST_DEPTH=1`, whose two legs are both the
|
||||
/// display without the feature. Reading it in one run that turns the depth off partway would have
|
||||
/// meant comparing two means over different displays.
|
||||
gboolean persist_probe_wait(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
++a->dbg_persist_polls;
|
||||
const bool held = a->model->display_mode() == DisplayMode::Held;
|
||||
const std::size_t kept = a->model->persist_count();
|
||||
const std::size_t want = static_cast<std::size_t>(std::max(0, a->dbg_persist_depth - 1));
|
||||
if (kept != a->dbg_persist_last_kept) {
|
||||
a->dbg_persist_last_kept = kept;
|
||||
a->dbg_persist_stable_polls = 0;
|
||||
} else if (++a->dbg_persist_stable_polls > 160) { // 4 s at 25 ms a poll
|
||||
a->dbg_persist_settled = true;
|
||||
}
|
||||
const bool full = kept >= want;
|
||||
// ... and the rows must already be *bound* to it: the report reads what the frame bound, and the
|
||||
// mode can flip to `Held` between two ticks (a latch lands mid-frame), so a reading taken on the
|
||||
// tick boundary can describe a display whose bindings are still the live ring's. `Held`, bound and
|
||||
// full -- the state every reading below is about -- is what the wait is for.
|
||||
double lo = 0.0;
|
||||
double hi = 0.0;
|
||||
const bool bound = a->rows->event_data_window(lo, hi);
|
||||
if ((held && bound && (full || (a->dbg_persist_settled && kept > 0))) ||
|
||||
a->dbg_persist_polls > 600) {
|
||||
if (!full) {
|
||||
std::fprintf(stderr, "[persist-probe] settled: kept=%zu of %zu after %d polls\n", kept, want,
|
||||
a->dbg_persist_polls);
|
||||
}
|
||||
persist_probe_report(*a, "held");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user