feat: the measurements pane can show how each reading varies, and remembers that it was asked to
This commit is contained in:
1 parent
1a8218887c
commit
4f496b14b7
3 files changed
+166
-1
No files matched your search
@@ -309,6 +309,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
plot_count = std::max(0, std::stoi(value));
|
plot_count = std::max(0, std::stoi(value));
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
}
|
}
|
||||||
|
} else if (key == "measure_stats") {
|
||||||
|
st.measure_stats = value == "true";
|
||||||
} else if (key == "multicast") {
|
} else if (key == "multicast") {
|
||||||
st.multicast = strip_quote(value);
|
st.multicast = strip_quote(value);
|
||||||
} else if (key == "interface") {
|
} else if (key == "interface") {
|
||||||
@@ -431,6 +433,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
|||||||
}
|
}
|
||||||
o << "]\n";
|
o << "]\n";
|
||||||
}
|
}
|
||||||
|
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
|
||||||
for (const auto& row : state.rows) {
|
for (const auto& row : state.rows) {
|
||||||
o << "\n[[plot]]\nsignals = [";
|
o << "\n[[plot]]\nsignals = [";
|
||||||
for (std::size_t i = 0; i < row.signals.size(); ++i) {
|
for (std::size_t i = 0; i < row.signals.size(); ++i) {
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ struct LayoutState {
|
|||||||
/// `math_node_text()` for the field order.
|
/// `math_node_text()` for the field order.
|
||||||
std::vector<MathChannel> math;
|
std::vector<MathChannel> math;
|
||||||
|
|
||||||
|
/// Whether the measurements pane shows its statistics line. The only piece of the
|
||||||
|
/// statistics that is persisted: the accumulated figures themselves describe a session's
|
||||||
|
/// stream and mean nothing restored against another one.
|
||||||
|
bool measure_stats = false;
|
||||||
|
|
||||||
/// The measurements pane, by signal *name* and with the type and options a row was built with.
|
/// The measurements pane, by signal *name* and with the type and options a row was built with.
|
||||||
std::vector<Measure> measures;
|
std::vector<Measure> measures;
|
||||||
};
|
};
|
||||||
|
|||||||
+158
-1
@@ -274,6 +274,13 @@ struct App {
|
|||||||
/// not what the measurement means, and a statistic the user cannot keep across them is
|
/// not what the measurement means, and a statistic the user cannot keep across them is
|
||||||
/// not worth having.
|
/// not worth having.
|
||||||
std::vector<MeasureStatsAccum> measure_stats;
|
std::vector<MeasureStatsAccum> measure_stats;
|
||||||
|
/// The second line of each measurement's row, one per entry of `measures`. Hidden rather
|
||||||
|
/// than removed when the toggle is off, so turning it on costs no rebuild -- and a rebuild
|
||||||
|
/// is what would reset the statistics the user just asked to see.
|
||||||
|
std::vector<GtkWidget*> measure_stats_labels;
|
||||||
|
bool measure_stats_on = false;
|
||||||
|
GtkWidget* measures_stats_check = nullptr;
|
||||||
|
GtkWidget* measures_stats_reset = nullptr;
|
||||||
/// The marks the last computation produced: re-placed every frame, which is what makes the
|
/// The marks the last computation produced: re-placed every frame, which is what makes the
|
||||||
/// measurement cursors read-only.
|
/// measurement cursors read-only.
|
||||||
///
|
///
|
||||||
@@ -746,6 +753,7 @@ LayoutState collect_layout(App& app) {
|
|||||||
// The app's own lists are the pane's source of truth for both of these.
|
// The app's own lists are the pane's source of truth for both of these.
|
||||||
st.math = app.math;
|
st.math = app.math;
|
||||||
st.measures = app.measures;
|
st.measures = app.measures;
|
||||||
|
st.measure_stats = app.measure_stats_on;
|
||||||
// The connection is the session's too: the file's name carries only host and port.
|
// The connection is the session's too: the file's name carries only host and port.
|
||||||
st.multicast = app.source_multicast;
|
st.multicast = app.source_multicast;
|
||||||
st.interface_addr = app.source_interface;
|
st.interface_addr = app.source_interface;
|
||||||
@@ -808,6 +816,11 @@ void apply_layout(App& app, const LayoutState& st) {
|
|||||||
app.math = st.math;
|
app.math = st.math;
|
||||||
apply_math_channels(app);
|
apply_math_channels(app);
|
||||||
app.measures = st.measures;
|
app.measures = st.measures;
|
||||||
|
app.measure_stats_on = st.measure_stats;
|
||||||
|
if (app.measures_stats_check != nullptr) {
|
||||||
|
gtk_check_button_set_active(GTK_CHECK_BUTTON(app.measures_stats_check),
|
||||||
|
app.measure_stats_on);
|
||||||
|
}
|
||||||
rebuild_measures_pane(app);
|
rebuild_measures_pane(app);
|
||||||
|
|
||||||
int rows = static_cast<int>(st.rows.size());
|
int rows = static_cast<int>(st.rows.size());
|
||||||
@@ -2605,6 +2618,10 @@ void open_measure_dialog(App& app, int edit_index);
|
|||||||
/// label is the one widget `update_measures()` rewrites; everything else is rebuilt with the
|
/// label is the one widget `update_measures()` rewrites; everything else is rebuilt with the
|
||||||
/// list.
|
/// list.
|
||||||
GtkWidget* measures_row(App& app, std::size_t index, const Measure& m) {
|
GtkWidget* measures_row(App& app, std::size_t index, const Measure& m) {
|
||||||
|
// The row is two lines: the reading, and the statistics under it. A vertical wrapper
|
||||||
|
// rather than more columns, because the statistics line is four figures wide and would
|
||||||
|
// push the reading itself off the pane.
|
||||||
|
GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
GtkWidget* chip = gtk_label_new("");
|
GtkWidget* chip = gtk_label_new("");
|
||||||
gtk_label_set_markup(GTK_LABEL(chip),
|
gtk_label_set_markup(GTK_LABEL(chip),
|
||||||
@@ -2660,8 +2677,21 @@ GtkWidget* measures_row(App& app, std::size_t index, const Measure& m) {
|
|||||||
}),
|
}),
|
||||||
&app);
|
&app);
|
||||||
gtk_box_append(GTK_BOX(box), edit);
|
gtk_box_append(GTK_BOX(box), edit);
|
||||||
|
gtk_box_append(GTK_BOX(outer), box);
|
||||||
|
|
||||||
|
GtkWidget* stats = gtk_label_new("");
|
||||||
|
gtk_widget_set_halign(stats, GTK_ALIGN_END);
|
||||||
|
gtk_widget_add_css_class(stats, "dim-label");
|
||||||
|
gtk_label_set_selectable(GTK_LABEL(stats), true);
|
||||||
|
gtk_widget_set_tooltip_text(stats,
|
||||||
|
"how this reading has varied since the last reset: the number "
|
||||||
|
"of passes, the mean, the standard deviation, and the extremes");
|
||||||
|
gtk_widget_set_visible(stats, app.measure_stats_on);
|
||||||
|
gtk_box_append(GTK_BOX(outer), stats);
|
||||||
|
|
||||||
app.measure_labels.push_back(value);
|
app.measure_labels.push_back(value);
|
||||||
return box;
|
app.measure_stats_labels.push_back(stats);
|
||||||
|
return outer;
|
||||||
}
|
}
|
||||||
|
|
||||||
void rebuild_measures_pane(App& app) {
|
void rebuild_measures_pane(App& app) {
|
||||||
@@ -2673,6 +2703,7 @@ void rebuild_measures_pane(App& app) {
|
|||||||
child = next;
|
child = next;
|
||||||
}
|
}
|
||||||
app.measure_labels.clear();
|
app.measure_labels.clear();
|
||||||
|
app.measure_stats_labels.clear();
|
||||||
app.measure_text.clear();
|
app.measure_text.clear();
|
||||||
// Editing a measurement changes what the figures *mean*, so the history behind them is no
|
// Editing a measurement changes what the figures *mean*, so the history behind them is no
|
||||||
// longer one series. This is the implicit reset for an add, an edit and a delete alike.
|
// longer one series. This is the implicit reset for an add, an edit and a delete alike.
|
||||||
@@ -3464,6 +3495,22 @@ void update_measures(App& app, bool force) {
|
|||||||
// contributes nothing -- the marks' `measure_last_valid` fallback below is a *drawing*
|
// contributes nothing -- the marks' `measure_last_valid` fallback below is a *drawing*
|
||||||
// convenience, and accumulating it would count one answer as many observations.
|
// convenience, and accumulating it would count one answer as many observations.
|
||||||
if (r.valid && i < app.measure_stats.size()) app.measure_stats[i].add(r.primary);
|
if (r.valid && i < app.measure_stats.size()) app.measure_stats[i].add(r.primary);
|
||||||
|
if (i < app.measure_stats_labels.size() && app.measure_stats_labels[i] != nullptr) {
|
||||||
|
GtkWidget* sl = app.measure_stats_labels[i];
|
||||||
|
gtk_widget_set_visible(sl, app.measure_stats_on);
|
||||||
|
if (app.measure_stats_on && i < app.measure_stats.size()) {
|
||||||
|
const MeasureStatsAccum& s = app.measure_stats[i];
|
||||||
|
std::string text = "no passes yet";
|
||||||
|
if (s.n > 0) {
|
||||||
|
// The same formatter the reading above uses, so the two lines are comparable at a
|
||||||
|
// glance; sd and the extremes carry the measurement's unit, n does not.
|
||||||
|
text = "n " + std::to_string(s.n) + " mean " + format_measure(s.mean, unit) +
|
||||||
|
" sd " + format_measure(s.sd(), unit) + " " +
|
||||||
|
format_measure(s.lo, unit) + " .. " + format_measure(s.hi, unit);
|
||||||
|
}
|
||||||
|
gtk_label_set_text(GTK_LABEL(sl), text.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The marks last, in their own pass over the measurements, so that no `continue` above can skip
|
// The marks last, in their own pass over the measurements, so that no `continue` above can skip
|
||||||
@@ -5081,6 +5128,46 @@ GtkWidget* build_measures_panel(App& app) {
|
|||||||
gtk_box_append(GTK_BOX(head), add);
|
gtk_box_append(GTK_BOX(head), add);
|
||||||
gtk_box_append(GTK_BOX(box), head);
|
gtk_box_append(GTK_BOX(box), head);
|
||||||
|
|
||||||
|
// One toggle for the whole pane, not one per measurement: statistics are either what the
|
||||||
|
// user is looking at or they are not, and a per-row switch is a column of chrome paid for
|
||||||
|
// on every row to answer a question asked once.
|
||||||
|
GtkWidget* stats_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
|
app.measures_stats_check = gtk_check_button_new_with_label("Statistics");
|
||||||
|
gtk_widget_set_hexpand(app.measures_stats_check, true);
|
||||||
|
gtk_widget_set_tooltip_text(app.measures_stats_check,
|
||||||
|
"show how each reading has varied: passes, mean, standard "
|
||||||
|
"deviation and extremes, under the reading itself");
|
||||||
|
g_signal_connect(app.measures_stats_check, "toggled",
|
||||||
|
G_CALLBACK(+[](GtkCheckButton* b, gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
a->measure_stats_on = gtk_check_button_get_active(b);
|
||||||
|
// Deliberately *not* a rebuild: that would reset the statistics the
|
||||||
|
// user just asked to look at. The rows are already there and hidden.
|
||||||
|
for (GtkWidget* w : a->measure_stats_labels) {
|
||||||
|
if (w != nullptr) gtk_widget_set_visible(w, a->measure_stats_on);
|
||||||
|
}
|
||||||
|
gtk_widget_set_sensitive(a->measures_stats_reset, a->measure_stats_on);
|
||||||
|
update_measures(*a, true);
|
||||||
|
schedule_layout_save(*a);
|
||||||
|
}),
|
||||||
|
&app);
|
||||||
|
gtk_box_append(GTK_BOX(stats_row), app.measures_stats_check);
|
||||||
|
|
||||||
|
app.measures_stats_reset = gtk_button_new_with_label("Reset");
|
||||||
|
gtk_widget_add_css_class(app.measures_stats_reset, "flat");
|
||||||
|
gtk_widget_set_tooltip_text(app.measures_stats_reset,
|
||||||
|
"start every measurement's statistics again from the next pass");
|
||||||
|
gtk_widget_set_sensitive(app.measures_stats_reset, false);
|
||||||
|
g_signal_connect(app.measures_stats_reset, "clicked",
|
||||||
|
G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
reset_measure_stats(*a);
|
||||||
|
update_measures(*a, true);
|
||||||
|
}),
|
||||||
|
&app);
|
||||||
|
gtk_box_append(GTK_BOX(stats_row), app.measures_stats_reset);
|
||||||
|
gtk_box_append(GTK_BOX(box), stats_row);
|
||||||
|
|
||||||
app.measures_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
app.measures_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||||
gtk_box_append(GTK_BOX(box), app.measures_list);
|
gtk_box_append(GTK_BOX(box), app.measures_list);
|
||||||
return box;
|
return box;
|
||||||
@@ -8326,6 +8413,73 @@ gboolean measure_trace(gpointer user) {
|
|||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headless self-check for the statistics line: turns the pane-wide toggle on over the
|
||||||
|
/// measurements `measure_probe` added, lets several recomputes land, and reports each
|
||||||
|
/// accumulator. `n` must grow with the passes and never with the samples, and a Reset must
|
||||||
|
/// put every `n` back to zero.
|
||||||
|
///
|
||||||
|
/// Three phases, because "one observation per recompute" needs two different witnesses. Phase 0
|
||||||
|
/// is the running count. Phase 1 is the same count after a **pause held for two seconds**: a
|
||||||
|
/// pinned window recomputes once when it settles and then never again, so `n` must advance by
|
||||||
|
/// *at most one* across the pause -- that is what separates accumulating per recompute from
|
||||||
|
/// accumulating per frame, which would add a hundred and twenty observations of the identical
|
||||||
|
/// window and collapse `sd`. Phase 2 is after a Reset.
|
||||||
|
int g_stats_phase = 0;
|
||||||
|
std::vector<std::uint64_t> g_stats_n_before;
|
||||||
|
|
||||||
|
gboolean stats_probe_report(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
std::fprintf(stderr, "[stats-probe] phase=%d on=%d passes=%" G_GUINT64_FORMAT,
|
||||||
|
g_stats_phase, int(a->measure_stats_on),
|
||||||
|
static_cast<std::uint64_t>(a->measure_passes));
|
||||||
|
for (std::size_t i = 0; i < a->measure_stats.size(); ++i) {
|
||||||
|
const MeasureStatsAccum& s = a->measure_stats[i];
|
||||||
|
std::fprintf(stderr, " | n=%" G_GUINT64_FORMAT " mean=%.6g sd=%.6g lo=%.6g hi=%.6g", s.n,
|
||||||
|
s.mean, s.sd(), s.lo, s.hi);
|
||||||
|
if (g_stats_phase == 1 && i < g_stats_n_before.size()) {
|
||||||
|
std::fprintf(stderr, " dn=%" G_GUINT64_FORMAT, s.n - g_stats_n_before[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "\n");
|
||||||
|
std::fflush(stderr);
|
||||||
|
if (g_stats_phase == 0) {
|
||||||
|
// Hold a pause and come back: `dn` on the next line is the assertion.
|
||||||
|
g_stats_n_before.clear();
|
||||||
|
for (const MeasureStatsAccum& s : a->measure_stats) g_stats_n_before.push_back(s.n);
|
||||||
|
g_stats_phase = 1;
|
||||||
|
if (a->pause_button != nullptr) {
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->pause_button), TRUE);
|
||||||
|
}
|
||||||
|
g_timeout_add(2000, stats_probe_report, a);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
if (g_stats_phase == 1) {
|
||||||
|
// Resume -- a resume must not reset, so phase 2's `n` starts from phase 1's -- then reset
|
||||||
|
// explicitly, which must.
|
||||||
|
if (a->pause_button != nullptr) {
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->pause_button), FALSE);
|
||||||
|
}
|
||||||
|
g_stats_phase = 2;
|
||||||
|
reset_measure_stats(*a);
|
||||||
|
update_measures(*a, true);
|
||||||
|
g_timeout_add(50, stats_probe_report, a);
|
||||||
|
}
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean stats_probe(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
if (a->measures_stats_check != nullptr) {
|
||||||
|
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
|
||||||
|
}
|
||||||
|
g_stats_phase = 0;
|
||||||
|
// Long enough for several throttled recomputes (one per 100 ms while the window moves).
|
||||||
|
g_timeout_add(2000, stats_probe_report, a);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
/// Headless self-check: a gesture must not move a pinned window.
|
/// Headless self-check: a gesture must not move a pinned window.
|
||||||
///
|
///
|
||||||
/// It writes the far-away range the way a gesture does -- on the *member* axis, which
|
/// It writes the far-away range the way a gesture does -- on the *member* axis, which
|
||||||
@@ -10042,6 +10196,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_CONDITION_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_CONDITION_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), condition_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), condition_probe, &app);
|
||||||
}
|
}
|
||||||
|
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_FIELDS_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIELDS_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user