Budget the sample buffer in samples per signal, under a hard memory limit
--buffer-samples (k/M/G) replaces --buffer-mb: the request is rounded down to a power of two for every signal and the shared time ring. A separate memory limit (--memory-limit-mb, a Preferences row, default 8 GiB) halves the plan until the real ring bytes fit and says so on stderr and in the status line. The per-source key buffer_mb becomes buffer_samples; an old buffer_mb is ignored. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
616cde5b13
commit
034c82f7d7
20 files changed
+516
-134
No files matched your search
+73
-27
@@ -7,7 +7,9 @@
|
||||
|
||||
#include "help_window.hpp"
|
||||
#include "layout_store.hpp"
|
||||
#include "model.hpp" // `buffer_ticks()` / `planned_bytes()`: what a request becomes
|
||||
#include "settings.hpp"
|
||||
#include "status_text.hpp" // `format_samples()` and friends
|
||||
#include "source/registry.hpp"
|
||||
// The app's shared chrome -- `ui_impl::menu_button()`/`menu_toggle()` and `install_css()` -- so the
|
||||
// chooser's help menu is the *same* menu the window's Help view is, not a second one that merely
|
||||
@@ -21,6 +23,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -47,7 +50,7 @@ void apply_source(Config& cfg, const KnownSource& src) {
|
||||
// The configuration's own startup settings. They are read *before* the model exists, which is the
|
||||
// only moment a buffer size can be applied at all -- and the reason they are stored per
|
||||
// configuration rather than only passed on the command line.
|
||||
if (src.buffer_mb > 0.0) cfg.buffer_mb = src.buffer_mb;
|
||||
if (src.buffer_samples > 0) cfg.buffer_samples = src.buffer_samples;
|
||||
// The list's own copy of the clock, but only where nothing newer has said: with the preferences
|
||||
// open from this dialog a user can state relative time a minute before clicking the row, and that
|
||||
// statement must survive the click that follows it.
|
||||
@@ -59,8 +62,9 @@ void apply_source(Config& cfg, const KnownSource& src) {
|
||||
void report_sources(const std::vector<KnownSource>& known) {
|
||||
std::fprintf(stderr, "[sources] %zu known:", known.size());
|
||||
for (const KnownSource& s : known) {
|
||||
std::fprintf(stderr, " %s:%u/%s plots=%d signals=%d buffer=%.6g relative=%d capture=%u",
|
||||
s.host.c_str(), s.port, s.config.c_str(), s.rows, s.signals, s.buffer_mb,
|
||||
std::fprintf(stderr, " %s:%u/%s plots=%d signals=%d samples=%llu relative=%d capture=%u",
|
||||
s.host.c_str(), s.port, s.config.c_str(), s.rows, s.signals,
|
||||
static_cast<unsigned long long>(s.buffer_samples),
|
||||
s.relative_time ? 1 : 0, s.capture_packets);
|
||||
if (!s.comment.empty()) std::fprintf(stderr, " note='%s'", s.comment.c_str());
|
||||
if (!s.multicast.empty()) std::fprintf(stderr, " multicast=%s", s.multicast.c_str());
|
||||
@@ -405,9 +409,7 @@ void chooser_populate(Chooser& c) {
|
||||
|
||||
std::string sub = std::to_string(s.rows) + (s.rows == 1 ? " plot, " : " plots, ") +
|
||||
std::to_string(s.signals) + (s.signals == 1 ? " signal" : " signals");
|
||||
if (s.buffer_mb > 0.0) {
|
||||
sub += ", buffer " + std::to_string(static_cast<int>(s.buffer_mb)) + " MB";
|
||||
}
|
||||
if (s.buffer_samples > 0) sub += ", buffer " + format_samples(s.buffer_samples) + " samples";
|
||||
if (s.relative_time) sub += ", relative time";
|
||||
if (s.capture_packets > 0) {
|
||||
sub += ", capture " + std::to_string(s.capture_packets) + " packets";
|
||||
@@ -442,6 +444,32 @@ void chooser_populate(Chooser& c) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The form's sample budget as a count: the spin is in millions, and a count is what is stored.
|
||||
std::uint64_t form_buffer_samples(const Chooser& c) {
|
||||
const double m = adw_spin_row_get_value(c.form_buffer);
|
||||
return static_cast<std::uint64_t>(std::max(128.0, std::round(m * 1.0e6)));
|
||||
}
|
||||
|
||||
/// Says under the "Samples per signal" row what the number will become: the rings are powers of two,
|
||||
/// so the request is a ceiling, and the memory limit can lower it further. The limit is judged for
|
||||
/// *one* signal here -- the channel count is not known until a stream declares it -- so a clip shown
|
||||
/// is certain, and the status line says what a real stream got. The cost line gives the two figures
|
||||
/// the user can multiply by their own signal count.
|
||||
void update_buffer_note(Chooser& c) {
|
||||
if (c.form_buffer == nullptr) return;
|
||||
const std::uint64_t requested = form_buffer_samples(c);
|
||||
const bool relative = adw_switch_row_get_active(c.form_relative) != FALSE;
|
||||
const std::uint64_t limit = c.cfg->memory_limit_mb * 1024u * 1024u;
|
||||
const std::uint64_t wanted = buffer_ticks(requested);
|
||||
std::uint64_t used = wanted;
|
||||
while (used > 128 && planned_bytes(used, 1, relative) > limit) used /= 2;
|
||||
const std::string note =
|
||||
describe_buffer(requested, used, used < wanted, limit) + ". The rings take " +
|
||||
format_bytes(used * sizeof(double) * (relative ? 2 : 1)) + " for the clock and " +
|
||||
format_bytes(used * sizeof(float)) + " per signal.";
|
||||
adw_action_row_set_subtitle(ADW_ACTION_ROW(c.form_buffer), note.c_str());
|
||||
}
|
||||
|
||||
void form_close(Chooser& c) {
|
||||
// The form's own `closed` handler clears the pointer, so this is the only place that has to know how
|
||||
// an `AdwDialog` closes.
|
||||
@@ -497,7 +525,7 @@ void on_form_connect(GtkButton*, gpointer user) {
|
||||
const std::string iface = udps ? scfg.get("interface") : std::string();
|
||||
std::string name = gtk_editable_get_text(GTK_EDITABLE(c.form_name));
|
||||
const std::string note = gtk_editable_get_text(GTK_EDITABLE(c.form_note));
|
||||
const double buffer_mb = adw_spin_row_get_value(c.form_buffer);
|
||||
const std::uint64_t buffer_samples = form_buffer_samples(c);
|
||||
const bool relative = adw_switch_row_get_active(c.form_relative) != FALSE;
|
||||
if (name.empty()) name = "default";
|
||||
|
||||
@@ -513,7 +541,7 @@ void on_form_connect(GtkButton*, gpointer user) {
|
||||
}
|
||||
// The startup settings. Applied here because this runs *before* `ScopeModel` exists, which is the
|
||||
// only moment a buffer size can be applied at all -- and stored, so the next run gets them back.
|
||||
c.cfg->buffer_mb = buffer_mb;
|
||||
c.cfg->buffer_samples = buffer_samples;
|
||||
c.cfg->relative_time = relative;
|
||||
|
||||
LayoutState st;
|
||||
@@ -523,7 +551,7 @@ void on_form_connect(GtkButton*, gpointer user) {
|
||||
load_layout(configuration_path(c.edit_host, c.edit_port, c.edit_config), st);
|
||||
}
|
||||
st.comment = note;
|
||||
st.buffer_mb = buffer_mb;
|
||||
st.buffer_samples = buffer_samples;
|
||||
st.relative_time = relative;
|
||||
st.multicast = group;
|
||||
st.interface_addr = iface;
|
||||
@@ -541,9 +569,9 @@ void on_form_connect(GtkButton*, gpointer user) {
|
||||
c.saved_config = name;
|
||||
c.saved_note = note;
|
||||
std::fprintf(stderr,
|
||||
"[sources] configuration %s: %s:%u/%s note='%s' buffer=%.6g relative=%d\n",
|
||||
"[sources] configuration %s: %s:%u/%s note='%s' samples=%llu relative=%d\n",
|
||||
editing ? "saved" : "created", host.c_str(), port, name.c_str(), note.c_str(),
|
||||
buffer_mb, relative ? 1 : 0);
|
||||
static_cast<unsigned long long>(buffer_samples), relative ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
} else {
|
||||
std::fprintf(stderr, "[sources] configuration write failed: %s:%u/%s\n", host.c_str(), port,
|
||||
@@ -624,12 +652,24 @@ void open_form(Chooser& c, int edit_index = -1) {
|
||||
// the rows' subtitles, so an empty field still says what empty means.
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name), src != nullptr ? src->config.c_str() : "default");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), src != nullptr ? src->comment.c_str() : "");
|
||||
adw_spin_row_set_value(c.form_buffer, src != nullptr && src->buffer_mb > 0.0
|
||||
? src->buffer_mb
|
||||
: std::max(1.0, c.cfg->buffer_mb));
|
||||
adw_spin_row_set_value(c.form_buffer, double(src != nullptr && src->buffer_samples > 0
|
||||
? src->buffer_samples
|
||||
: c.cfg->buffer_samples) /
|
||||
1.0e6);
|
||||
update_buffer_note(c);
|
||||
g_signal_connect(c.form_buffer, "notify::value", G_CALLBACK(+[](GObject*, GParamSpec*, gpointer user) {
|
||||
update_buffer_note(*static_cast<Chooser*>(user));
|
||||
}),
|
||||
&c);
|
||||
adw_switch_row_set_active(c.form_relative,
|
||||
(src != nullptr ? src->relative_time : c.cfg->relative_time) ? TRUE
|
||||
: FALSE);
|
||||
// The relative clock doubles the time ring, so the note under the sample budget follows it.
|
||||
g_signal_connect(c.form_relative, "notify::active", G_CALLBACK(+[](GObject*, GParamSpec*, gpointer user) {
|
||||
update_buffer_note(*static_cast<Chooser*>(user));
|
||||
}),
|
||||
&c);
|
||||
update_buffer_note(c);
|
||||
|
||||
// The dropdown lists the registered kinds. A new host starts on the kind the command line named
|
||||
// (UDPS by default); an edit starts on the configuration's own kind and cannot change it --
|
||||
@@ -701,10 +741,11 @@ void report_pick(const Chooser& c) {
|
||||
// size would look identical here otherwise, and the buffer is the one of them a user notices
|
||||
// only after a long capture has been sized wrong.
|
||||
std::fprintf(stderr,
|
||||
"[sources] picked %s:%u multicast=%s interface=%s buffer=%.6g relative=%d "
|
||||
"[sources] picked %s:%u multicast=%s interface=%s samples=%llu relative=%d "
|
||||
"capture=%u config=%s note='%s'\n",
|
||||
c.cfg->server.c_str(), c.cfg->port, c.cfg->multicast.c_str(),
|
||||
c.cfg->interface_addr.c_str(), c.cfg->buffer_mb, c.cfg->relative_time ? 1 : 0,
|
||||
c.cfg->interface_addr.c_str(),
|
||||
static_cast<unsigned long long>(c.cfg->buffer_samples), c.cfg->relative_time ? 1 : 0,
|
||||
c.cfg->capture_packets,
|
||||
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().config)
|
||||
: c.saved_config)
|
||||
@@ -748,6 +789,10 @@ void open_chooser_preferences(Chooser& c) {
|
||||
// stored configuration, and it works in both directions -- turning it on *and* off.
|
||||
c.cfg->relative_time = s.relative_time;
|
||||
c.cfg->relative_time_specified = true;
|
||||
// The memory limit is read by the model when it is built, which is after this returns, so the
|
||||
// chooser's own change is the newest word on it for the run about to start.
|
||||
c.cfg->memory_limit_mb = static_cast<std::uint64_t>(s.memory_limit_mb);
|
||||
c.cfg->memory_limit_specified = true;
|
||||
if (!written) {
|
||||
std::fprintf(stderr, "[sources] prefs: could not write %s\n", settings_path().c_str());
|
||||
std::fflush(stderr);
|
||||
@@ -1171,7 +1216,7 @@ gboolean pick_probe(gpointer user) {
|
||||
c.form_cfg.write(sc);
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name), "probe-host");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "made by the new-host probe");
|
||||
adw_spin_row_set_value(c.form_buffer, 32);
|
||||
adw_spin_row_set_value(c.form_buffer, 0.5);
|
||||
adw_switch_row_set_active(c.form_relative, TRUE);
|
||||
// 9/9 for UDPS: the form's row titles are mnemonics (`Source _type`, `_Server`, `_Port`, ...). The
|
||||
// total follows the kind's row count: five rows are the chooser's, the rest the kind's own.
|
||||
@@ -1183,7 +1228,7 @@ gboolean pick_probe(gpointer user) {
|
||||
std::fprintf(stderr, "[sources] form mnemonics=%d/%zu\n", underlines,
|
||||
5 + c.form_cfg.rows.size());
|
||||
std::fprintf(stderr,
|
||||
"[sources] form: name='%s' note='%s' buffer=%.6g relative=%d "
|
||||
"[sources] form: name='%s' note='%s' samples_m=%.6g relative=%d "
|
||||
"connect_sensitive=%d\n",
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||
@@ -1216,14 +1261,14 @@ gboolean pick_probe(gpointer user) {
|
||||
// The reading is the *pre-fill*: an edit form that opens empty is the defect this asserts
|
||||
// against, and the button's label says which mode it is in.
|
||||
std::fprintf(stderr,
|
||||
"[sources] edit form: name='%s' note='%s' buffer=%.6g relative=%d button='%s'\n",
|
||||
"[sources] edit form: name='%s' note='%s' samples_m=%.6g relative=%d button='%s'\n",
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||
adw_spin_row_get_value(c.form_buffer),
|
||||
adw_switch_row_get_active(c.form_relative) ? 1 : 0,
|
||||
gtk_button_get_label(GTK_BUTTON(c.form_connect)));
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "edited by the probe");
|
||||
adw_spin_row_set_value(c.form_buffer, 16);
|
||||
adw_spin_row_set_value(c.form_buffer, 0.25);
|
||||
adw_switch_row_set_active(c.form_relative, FALSE);
|
||||
// `UDPSCOPE_PROBE_EDITNAME=new-name` also renames it, which is a *move*: the old file must not
|
||||
// survive, or the chooser lists the same session twice.
|
||||
@@ -1263,16 +1308,17 @@ void apply_stored_settings(Config& cfg) {
|
||||
// Only where nothing newer was said: a flag the user typed, or the preferences they just applied,
|
||||
// wins -- because it is the more recent statement of intent, and the file is not a place to hide a
|
||||
// surprise from whoever just made it.
|
||||
if (!cfg.buffer_mb_specified && st.buffer_mb > 0.0) cfg.buffer_mb = st.buffer_mb;
|
||||
if (!cfg.buffer_samples_specified && st.buffer_samples > 0) cfg.buffer_samples = st.buffer_samples;
|
||||
const bool relative = !cfg.relative_time_specified && st.relative_time;
|
||||
if (relative) cfg.relative_time = true;
|
||||
if (cfg.capture_packets == 0 && st.capture_packets > 0) cfg.capture_packets = st.capture_packets;
|
||||
const bool applied = (!cfg.buffer_mb_specified && st.buffer_mb > 0.0) || relative ||
|
||||
const bool applied = (!cfg.buffer_samples_specified && st.buffer_samples > 0) || relative ||
|
||||
st.capture_packets > 0;
|
||||
if (applied) {
|
||||
std::fprintf(stderr,
|
||||
"[sources] settings from configuration '%s': buffer=%.6g relative=%d capture=%u\n",
|
||||
want.c_str(), cfg.buffer_mb, cfg.relative_time ? 1 : 0, cfg.capture_packets);
|
||||
"[sources] settings from configuration '%s': samples=%llu relative=%d capture=%u\n",
|
||||
want.c_str(), static_cast<unsigned long long>(cfg.buffer_samples),
|
||||
cfg.relative_time ? 1 : 0, cfg.capture_packets);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}
|
||||
@@ -1330,9 +1376,9 @@ void loading_show(const Config& cfg) {
|
||||
head = "Connecting to " + cfg.server + ":" + std::to_string(cfg.port);
|
||||
if (!cfg.multicast.empty()) head += " (" + cfg.multicast + ")";
|
||||
}
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "planning the %.0f MB sample buffer", cfg.buffer_mb);
|
||||
sub = buf;
|
||||
sub = "planning the buffer: " + format_samples(buffer_ticks(cfg.buffer_samples)) +
|
||||
" samples per signal, " + format_bytes(cfg.memory_limit_mb * 1024u * 1024u) +
|
||||
" memory limit";
|
||||
} else {
|
||||
const std::size_t slash = cfg.open_waveform.rfind('/');
|
||||
head = "Opening " + (slash == std::string::npos ? cfg.open_waveform
|
||||
|
||||
Reference in new issue
Block a user