working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
@@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
//
|
||||
// The status bar and the detail popover: the one-line glance with its activity badge, the popover\n// behind the ⓘ, the short-lived notices, and the starvation detector that says a stream stopped.
|
||||
|
||||
#include "ui_app.hpp"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
namespace ui_impl {
|
||||
|
||||
void on_popover_closed(GtkPopover* popover, gpointer) {
|
||||
// Unparent, one idle later. `gtk_widget_set_parent()` took the reference the popover was created
|
||||
// with (GTK4 widgets come with a floating reference, sunk by the first parent), so unparenting
|
||||
// drops it to zero and destroys the popover -- a following `g_object_unref()` would be a
|
||||
// use-after-free, which is why there is none.
|
||||
//
|
||||
// The *deferral* is not tidiness either: this runs inside the `closed` emission, which GTK raises
|
||||
// from `gtk_popover_popdown()`, and destroying a widget in the middle of its own signal leaves the
|
||||
// popdown path running on freed memory -- a `Gtk-CRITICAL` from `gtk_popover_get_autohide`, on
|
||||
// every menu entry that dismisses its popover, which is every entry there is. The idle holds the
|
||||
// reference (`g_object_ref` in, `g_object_unref` out) so the popover outlives its own emission.
|
||||
g_idle_add_full(
|
||||
G_PRIORITY_DEFAULT_IDLE,
|
||||
+[](gpointer p) -> gboolean {
|
||||
gtk_widget_unparent(GTK_WIDGET(p));
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
g_object_ref(popover), g_object_unref);
|
||||
}
|
||||
|
||||
/// What the activity badge is saying. `Lit`/`Dim` are the two halves of the blink, so
|
||||
/// the badge is only *blinking* while the pipeline is actually running; a steady badge is
|
||||
/// a stopped one, which is the state worth noticing on a glance. `Connecting` is the
|
||||
/// half-way state -- the socket answered but the stream has sent no CONFIG, so there is no
|
||||
/// signal list and nothing to acquire -- and it is steady yellow rather than green,
|
||||
/// because a blinking green badge over a source that has sent nothing claims a working
|
||||
/// pipeline.
|
||||
/// The badge is one glyph in one of four colours. It must stay the *same glyph*: the
|
||||
/// status row sits above the plot, and a badge that changed width as it blinked would
|
||||
/// move the label beside it and re-lay out the plot twice a second.
|
||||
const char* badge_markup(BadgePhase phase) {
|
||||
switch (phase) {
|
||||
case BadgePhase::Lit:
|
||||
return "<span foreground=\"#45E045\">\u25CF</span>";
|
||||
case BadgePhase::Dim:
|
||||
return "<span foreground=\"#1C5A1C\">\u25CF</span>";
|
||||
case BadgePhase::Connecting:
|
||||
return "<span foreground=\"#E0B020\">\u25CF</span>";
|
||||
case BadgePhase::Idle:
|
||||
break;
|
||||
}
|
||||
return "<span foreground=\"#5A5A5A\">\u25CF</span>";
|
||||
}
|
||||
|
||||
/// A short-lived line in front of the status text: the outcome of an action that the
|
||||
/// picture does not show by itself (a drop that changed nothing, an export that wrote a
|
||||
/// file). `tick()` clears it when it expires.
|
||||
void set_notice(App& app, std::string what, int seconds) {
|
||||
app.notice = std::move(what);
|
||||
app.notice_until_us = g_get_monotonic_time() + seconds * G_TIME_SPAN_SECOND;
|
||||
}
|
||||
|
||||
/// The widest window that is worth offering. `--max-window` only: the buffer no longer
|
||||
/// sets the ceiling, because a window too long to store at the wire rate is decimated on
|
||||
/// ingest to fit rather than refused (see `plan_partition()`). It used to be capped to
|
||||
/// `buffered_span()` as well, which is what made a 600 s view of a 1 MS/s stream
|
||||
/// unreachable.
|
||||
double window_limit(const App& app) { return app.model->config().max_window_s; }
|
||||
|
||||
|
||||
/// Re-clamps the window against the current cap. Called once at start-up and again when a
|
||||
/// CONFIG changes how much history one buffer holds -- never on a mere enable or calibration
|
||||
/// change, so those cannot silently re-anchor the view.
|
||||
void refresh_window_limits(App& app) {
|
||||
const double limit = window_limit(app);
|
||||
if (std::fabs(limit - app.window_limit_s) < 1.0e-9) return;
|
||||
app.window_limit_s = limit;
|
||||
apply_time_window(app, app.rows->time_window());
|
||||
}
|
||||
|
||||
/// Sets the popover's seven value labels. Never creates a widget: `popover_rebuilds` exists to
|
||||
/// assert that, because the popover is visible when this runs and a rebuild would relayout it.
|
||||
void update_status_popover(App& app, const udpscope::StatusSnapshot& s) {
|
||||
if (app.popover_label[4] == nullptr) return;
|
||||
char buf[256];
|
||||
|
||||
if (!app.waveform_name.empty()) {
|
||||
// Not "waiting, no config": a file is not a source that has not answered yet.
|
||||
std::snprintf(buf, sizeof(buf), "file: %s", app.waveform_name.c_str());
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "%s, %s", s.connected ? "connected" : "waiting",
|
||||
s.have_config ? "configured" : "no config");
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[4]), buf);
|
||||
|
||||
std::snprintf(buf, sizeof(buf), "packets %llu · frames %llu · gaps %llu",
|
||||
static_cast<unsigned long long>(s.packets),
|
||||
static_cast<unsigned long long>(s.frames),
|
||||
static_cast<unsigned long long>(s.gaps));
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[5]), buf);
|
||||
|
||||
std::snprintf(buf, sizeof(buf), "%zu signals · %zu channels", s.signals, s.channels);
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[6]), buf);
|
||||
|
||||
std::snprintf(buf, sizeof(buf), "%.1f MB · %.0f s span · time x%llu", s.buffer_mb, s.span_s,
|
||||
static_cast<unsigned long long>(s.time_stride));
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[7]), buf);
|
||||
|
||||
const std::string stored = udpscope::format_rate(s.stored_rate_hz);
|
||||
const std::string wire = udpscope::format_rate(s.declared_rate_hz);
|
||||
std::snprintf(buf, sizeof(buf), "stored %s (wire %s)", stored.c_str(), wire.c_str());
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[8]), buf);
|
||||
|
||||
std::snprintf(buf, sizeof(buf), "%d traces · %zu samples", s.plot1_traces, s.plot1_samples);
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[9]), buf);
|
||||
|
||||
std::snprintf(buf, sizeof(buf), "%d", s.reconnects);
|
||||
gtk_label_set_text(GTK_LABEL(app.popover_label[10]), buf);
|
||||
}
|
||||
|
||||
/// Recognises the "the source is serving someone else" state and says so.
|
||||
///
|
||||
/// A UDPStreamer over unicast keeps one session: a second client's CONNECT displaces the
|
||||
/// first, the first sees `silence_timeout_s` of no data and reconnects, and the two take
|
||||
/// turns. Nothing in the client can give both of them the stream -- the wire has one
|
||||
/// destination -- so the useful thing is to name the situation: a reconnect with no frames
|
||||
/// delivered in the same second is that fight, or a server that keeps dropping us, and
|
||||
/// neither is visible in an empty plot.
|
||||
///
|
||||
/// Called once a second from the frame tick.
|
||||
void note_starvation(App& app) {
|
||||
StreamStats& st = app.model->stats();
|
||||
const std::uint64_t frames = st.frames.load(std::memory_order_relaxed);
|
||||
const std::uint64_t reconnects = st.reconnects.load(std::memory_order_relaxed);
|
||||
const bool connected = st.connected.load(std::memory_order_relaxed);
|
||||
|
||||
// "Connected, but nothing is arriving" is a *rate* fact, not a reconnect one: a source that
|
||||
// answers CONNECT and then goes quiet -- the unicast case where another client took the session --
|
||||
// never reconnects while it is silent, so requiring a reconnect made the notice invisible in
|
||||
// exactly the state it describes. What it takes is `frames` not moving across three consecutive
|
||||
// one-second samples: one sample would call a source slower than the sample interval starved, and
|
||||
// three still clears the moment anything arrives.
|
||||
const bool idle = app.starve_have_prev && frames == app.starve_frames;
|
||||
app.starve_still = (connected && !app.acq->paused() && idle) ? app.starve_still + 1 : 0;
|
||||
app.starve_frames = frames;
|
||||
app.starve_have_prev = true;
|
||||
|
||||
if (app.starve_still < 3) {
|
||||
app.starve_reported = false;
|
||||
// **A `return`, not just a branch.** The notice and its line used to sit outside this test, so
|
||||
// they were re-armed every frame and the status bar carried "no data is arriving" while the
|
||||
// stream was arriving perfectly well.
|
||||
return;
|
||||
}
|
||||
// Two diagnoses, split on `have_config`, because they are not the same situation. A source
|
||||
// that sent a CONFIG and then went quiet is the unicast fight: the signal list is here, so
|
||||
// something took the stream away. One that has sent no CONFIG at all never had the stream to
|
||||
// lose -- there is no signal list, so nothing can be enabled and no DATA frame is owed -- and
|
||||
// naming that "connected, but no data is arriving" blamed a second reader that does not exist.
|
||||
// It is still the same stall test: a CONFIG that arrives promptly leaves no notice at all.
|
||||
const bool have_config = st.have_config.load(std::memory_order_relaxed);
|
||||
app.notice =
|
||||
have_config
|
||||
? "connected, but no data is arriving: another reader may hold the source (a unicast "
|
||||
"source serves one client at a time) -- use --multicast to watch from several UIs"
|
||||
: "trying to connect: the source answered, but has sent no configuration, so there is "
|
||||
"no signal list yet -- check that the streamer is running and serving this port";
|
||||
app.notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
||||
if (!app.starve_reported) {
|
||||
app.starve_reported = true;
|
||||
std::fprintf(stderr,
|
||||
have_config
|
||||
? "[udpscope] starved: connected (config=1), reconnects=%llu, no new frames "
|
||||
"in the last second -- a unicast source serves one client at a time\n"
|
||||
: "[udpscope] connecting: connected (config=0), reconnects=%llu, no "
|
||||
"configuration from the stream -- there is no signal list to acquire\n",
|
||||
static_cast<unsigned long long>(reconnects));
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user