feat: the buffer can be searched for the trigger's own condition, and the hits stepped through
This commit is contained in:
1 parent
2cee85c52b
commit
415fceca87
4 files changed
+517
No files matched your search
+264
@@ -25,6 +25,7 @@
|
||||
#include "measure.hpp"
|
||||
#include "model.hpp"
|
||||
#include "plot_host.hpp"
|
||||
#include "search.hpp"
|
||||
|
||||
#include <adwaita.h>
|
||||
#include <gtk/gtk.h>
|
||||
@@ -157,6 +158,15 @@ struct App {
|
||||
/// the stream's own clock advances whether the view is frozen or not and would say nothing
|
||||
/// about how long the user has been looking at a still picture.
|
||||
gint64 pause_started_us = 0;
|
||||
/// The buffer search. Driven a few milliseconds at a time from the frame tick, because its
|
||||
/// samples live behind the model's mutex and the producer needs it back between chunks.
|
||||
BufferSearch search;
|
||||
GtkWidget* search_run_button = nullptr;
|
||||
GtkWidget* search_cancel_button = nullptr;
|
||||
GtkWidget* search_status_label = nullptr;
|
||||
GtkWidget* search_pos_label = nullptr;
|
||||
GtkWidget* search_prev_button = nullptr;
|
||||
GtkWidget* search_next_button = nullptr;
|
||||
GtkWidget* menu_button = nullptr;
|
||||
GtkWidget* cursors_toggle = nullptr;
|
||||
/// Where the panel controls ended up, decided once at build time: the dump runs after the window
|
||||
@@ -2341,6 +2351,76 @@ void on_pause_toggled(GtkToggleButton* button, gpointer user) {
|
||||
: "pause acquisition");
|
||||
}
|
||||
|
||||
/// Freezes the view on a hit, centred, with the hit itself at x = 0.
|
||||
///
|
||||
/// This is the existing pause, not a fifth display mode: the toggle has to end up on, or the
|
||||
/// next frame's `clamp_to_data()` would follow the live edge and walk the frozen window off
|
||||
/// the hit entirely.
|
||||
void search_jump(App& app, double t) {
|
||||
const double w = app.rows->time_window();
|
||||
if (app.pause_button != nullptr &&
|
||||
!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.pause_button))) {
|
||||
// Its handler publishes the *current* window; the publish below replaces it.
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.pause_button), TRUE);
|
||||
}
|
||||
app.model->publish_pause_view(t - w / 2.0, t + w / 2.0, /*x_offset=*/t);
|
||||
app.pause_started_us = g_get_monotonic_time();
|
||||
// Pinned from the snapshot's *own* stamps, never from the window asked for: a cut is
|
||||
// clamped to what the ring holds, so a hit near the oldest sample yields a shorter
|
||||
// snapshot, and pinning the request would leave an empty margin that a zoom moves into.
|
||||
if (std::shared_ptr<const EventSnapshot> snap = app.model->pause_view()) {
|
||||
if (!snap->stamps.empty()) {
|
||||
app.rows->pin_pause_window(static_cast<double>(snap->stamps.front()),
|
||||
static_cast<double>(snap->stamps.back()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_search_panel(App& app) {
|
||||
if (app.search_status_label == nullptr) return;
|
||||
const BufferSearch& s = app.search;
|
||||
const std::size_t n = s.hits().size();
|
||||
char text[192];
|
||||
switch (s.state()) {
|
||||
case BufferSearch::State::Idle:
|
||||
std::snprintf(text, sizeof(text), "not searched");
|
||||
break;
|
||||
case BufferSearch::State::Running:
|
||||
std::snprintf(text, sizeof(text), "scanning %s of %s, %zu found",
|
||||
format_window(s.scanned_s()).c_str(), format_window(s.total_s()).c_str(), n);
|
||||
break;
|
||||
case BufferSearch::State::Done:
|
||||
std::snprintf(text, sizeof(text), "%zu found in %s", n, format_window(s.total_s()).c_str());
|
||||
break;
|
||||
case BufferSearch::State::Truncated:
|
||||
std::snprintf(text, sizeof(text), "%zu found; the stream overtook the oldest samples", n);
|
||||
break;
|
||||
case BufferSearch::State::Capped:
|
||||
std::snprintf(text, sizeof(text), "%zu found (the limit) in the newest %s", n,
|
||||
format_window(s.scanned_s()).c_str());
|
||||
break;
|
||||
case BufferSearch::State::Cancelled:
|
||||
std::snprintf(text, sizeof(text), "stopped after %zu", n);
|
||||
break;
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(app.search_status_label), text);
|
||||
|
||||
char pos[64];
|
||||
if (s.cursor() < 0 || n == 0) {
|
||||
std::snprintf(pos, sizeof(pos), "--");
|
||||
} else {
|
||||
std::snprintf(pos, sizeof(pos), "%d of %zu", s.cursor() + 1, n);
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(app.search_pos_label), pos);
|
||||
|
||||
const bool running = s.state() == BufferSearch::State::Running;
|
||||
gtk_widget_set_sensitive(app.search_run_button, !running);
|
||||
gtk_widget_set_sensitive(app.search_cancel_button, running);
|
||||
gtk_widget_set_sensitive(app.search_prev_button, s.cursor() > 0);
|
||||
gtk_widget_set_sensitive(app.search_next_button,
|
||||
n > 0 && s.cursor() + 1 < static_cast<int>(n));
|
||||
}
|
||||
|
||||
void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
const bool on = gtk_toggle_button_get_active(button);
|
||||
@@ -6014,6 +6094,15 @@ gboolean tick(gpointer user) {
|
||||
app->dbg_measures_pane_w = gtk_widget_get_width(app->measures_list);
|
||||
app->dbg_measures_pane_h = gtk_widget_get_height(app->measures_list);
|
||||
}
|
||||
// A few milliseconds of scanning per frame: enough that a full buffer finishes in a
|
||||
// handful of seconds, little enough that neither the producer (which waits on the mutex
|
||||
// for one chunk) nor the frame budget notices.
|
||||
if (app->search.state() == BufferSearch::State::Running) {
|
||||
const gint64 budget_end_us = g_get_monotonic_time() + 5000;
|
||||
while (app->search.pump(*app->model) && g_get_monotonic_time() < budget_end_us) {
|
||||
}
|
||||
update_search_panel(*app);
|
||||
}
|
||||
if (model.generation() != app->signal_generation) {
|
||||
// A CONFIG re-describes the stream and drops the buffer, so nothing measured before it
|
||||
// belongs in the same series as what comes after.
|
||||
@@ -6852,6 +6941,94 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||
g_signal_connect(app.trigger_mode_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||
|
||||
// Searching *is* the trigger's condition applied to history, so it belongs in this panel
|
||||
// rather than in one of its own: a second place to spell the same condition would be a
|
||||
// second place to get it wrong. Collapsed by default -- it is not part of running a trigger.
|
||||
GtkWidget* search_exp = gtk_expander_new("Search the buffer");
|
||||
gtk_widget_set_tooltip_text(search_exp,
|
||||
"find every place in the retained buffer where the condition "
|
||||
"above holds, and step through them");
|
||||
GtkWidget* sbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_widget_set_margin_top(sbox, 4);
|
||||
|
||||
GtkWidget* srun = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app.search_run_button = gtk_button_new_with_label("Search");
|
||||
gtk_widget_set_hexpand(app.search_run_button, true);
|
||||
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
// Published, not read back off the widgets: the producer's copy is the
|
||||
// one the live trigger uses, so the search asks exactly its question.
|
||||
publish_trigger_fields(a);
|
||||
EventCondition c;
|
||||
int channel = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
c = a->model->trigger().condition;
|
||||
channel = a->model->trigger().channel;
|
||||
}
|
||||
a->search.start(c, channel);
|
||||
update_search_panel(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(srun), app.search_run_button);
|
||||
app.search_cancel_button = gtk_button_new_with_label("Stop");
|
||||
gtk_widget_set_sensitive(app.search_cancel_button, false);
|
||||
g_signal_connect(app.search_cancel_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
a->search.cancel();
|
||||
update_search_panel(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(srun), app.search_cancel_button);
|
||||
gtk_box_append(GTK_BOX(sbox), srun);
|
||||
|
||||
app.search_status_label = gtk_label_new("not searched");
|
||||
gtk_widget_set_halign(app.search_status_label, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(app.search_status_label), true);
|
||||
gtk_widget_add_css_class(app.search_status_label, "dim-label");
|
||||
gtk_box_append(GTK_BOX(sbox), app.search_status_label);
|
||||
|
||||
GtkWidget* snav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app.search_prev_button = gtk_button_new_from_icon_name("go-previous-symbolic");
|
||||
gtk_widget_set_tooltip_text(app.search_prev_button, "the previous hit");
|
||||
gtk_widget_set_sensitive(app.search_prev_button, false);
|
||||
g_signal_connect(app.search_prev_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->search.prev()) search_jump(*a, a->search.hits()[static_cast<std::size_t>(
|
||||
a->search.cursor())]);
|
||||
update_search_panel(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(snav), app.search_prev_button);
|
||||
|
||||
app.search_pos_label = gtk_label_new("--");
|
||||
gtk_widget_set_hexpand(app.search_pos_label, true);
|
||||
gtk_box_append(GTK_BOX(snav), app.search_pos_label);
|
||||
|
||||
app.search_next_button = gtk_button_new_from_icon_name("go-next-symbolic");
|
||||
gtk_widget_set_tooltip_text(app.search_next_button, "the next hit");
|
||||
gtk_widget_set_sensitive(app.search_next_button, false);
|
||||
g_signal_connect(app.search_next_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
// The first press with no cursor goes to the first hit, not the second.
|
||||
if (a->search.cursor() < 0) {
|
||||
a->search.select(0);
|
||||
} else if (!a->search.next()) {
|
||||
update_search_panel(*a);
|
||||
return;
|
||||
}
|
||||
search_jump(*a, a->search.hits()[static_cast<std::size_t>(
|
||||
a->search.cursor())]);
|
||||
update_search_panel(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(snav), app.search_next_button);
|
||||
gtk_box_append(GTK_BOX(sbox), snav);
|
||||
|
||||
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox);
|
||||
gtk_box_append(GTK_BOX(box), search_exp);
|
||||
|
||||
g_signal_connect(app.trigger_type_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_shape_changed), &app);
|
||||
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
|
||||
@@ -8830,6 +9007,90 @@ gboolean condition_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check for the buffer search, and for the claim that makes it worth having:
|
||||
/// a scanned hit is an instant the *live trigger* would have fired on.
|
||||
///
|
||||
/// It arms the same pulse-width condition `condition_probe` arms, waits for a real latch,
|
||||
/// then searches the buffer for that condition and compares the latched t0 with the nearest
|
||||
/// scanned hit. `agree=1` is the assertion; `agree=0` means the two evaluators have drifted
|
||||
/// apart, which is the whole failure mode separating `EventDetector` out was meant to
|
||||
/// prevent. It then steps to a hit and reports the pinned axis against it.
|
||||
int g_search_phase = 0;
|
||||
double g_search_t0 = 0.0;
|
||||
|
||||
gboolean search_probe_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
const BufferSearch& s = a->search;
|
||||
if (s.state() == BufferSearch::State::Running) return G_SOURCE_CONTINUE;
|
||||
|
||||
double nearest = 0.0;
|
||||
double err = 0.0;
|
||||
bool have = false;
|
||||
for (const double h : s.hits()) {
|
||||
const double d = std::fabs(h - g_search_t0);
|
||||
if (!have || d < std::fabs(err)) {
|
||||
have = true;
|
||||
nearest = h;
|
||||
err = h - g_search_t0;
|
||||
}
|
||||
}
|
||||
// One stored sample period is the tolerance: the trigger saw the stored (filtered) stream
|
||||
// and so does the scan, so they must agree to the sample, not merely to the neighbourhood.
|
||||
const double tol = a->model->partition().stored_rate() > 0.0
|
||||
? 1.0 / a->model->partition().stored_rate()
|
||||
: 1.0e-3;
|
||||
std::fprintf(stderr,
|
||||
"[search-probe] state=%d hits=%zu scanned=%.4f total=%.4f chunks=%" G_GUINT64_FORMAT
|
||||
" worst=%.3fms total=%.3fms armed t0=%.6f vs scanned first=%.6f err=%.6g agree=%d\n",
|
||||
int(s.state()), s.hits().size(), s.scanned_s(), s.total_s(), s.chunks(),
|
||||
s.worst_chunk_ms(), s.total_ms(), g_search_t0, have ? nearest : 0.0, err,
|
||||
int(have && std::fabs(err) <= tol));
|
||||
|
||||
if (!s.hits().empty()) {
|
||||
a->search.select(0);
|
||||
search_jump(*a, a->search.hits()[0]);
|
||||
update_search_panel(*a);
|
||||
const plot::Range view = a->rows->view_range();
|
||||
std::fprintf(stderr,
|
||||
"[search-probe] jumped to hit 0 t=%.6f mode=%d axis=[%.4f..%.4f] pos=%d\n",
|
||||
a->search.hits()[0], int(a->model->display_mode()), view.min, view.max,
|
||||
a->search.cursor() + 1);
|
||||
}
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean search_probe_run(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
g_search_t0 = a->model->trigger_time();
|
||||
EventCondition c;
|
||||
int channel = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
c = a->model->trigger().condition;
|
||||
channel = a->model->trigger().channel;
|
||||
}
|
||||
a->search.start(c, channel);
|
||||
std::fprintf(stderr, "[search-probe] started on channel %d, armed t0=%.6f\n", channel,
|
||||
g_search_t0);
|
||||
std::fflush(stderr);
|
||||
g_timeout_add(100, search_probe_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean search_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// Reuse the condition probe's arming entirely: the point is that one condition gives two
|
||||
// evaluators the same answer, so the condition must be set in exactly one place.
|
||||
g_search_phase = 0;
|
||||
condition_probe(a);
|
||||
g_timeout_add(6000, search_probe_run, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check: drives the window control the four ways a user can -- type a number,
|
||||
/// change the unit under it, and step a decade either way -- and prints what each produced.
|
||||
/// Each line reports the typed request, the window the plot took and the trigger's event
|
||||
@@ -10212,6 +10473,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_STATS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIELDS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user