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:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 02:47:04 +01:00
1 parent 616cde5b13
commit 034c82f7d7
20 files changed
+516 -134

No files matched your search

+50 -3
View File
@@ -4397,7 +4397,16 @@ gboolean tick(gpointer user) {
snap.channels = channel_count;
snap.plot1_traces = bound;
snap.plot1_samples = samples;
snap.buffer_mb = double(model.config().buffer_mb);
{
// The planned buffer when there is one; before the first CONFIG there is no plan, and the
// request (rounded as the ring will round it) is the honest figure, with no bytes to quote.
const Partition& part = model.partition();
snap.buffer_requested = model.config().buffer_samples;
snap.buffer_samples = part.time_capacity > 0 ? part.time_capacity : buffer_ticks(snap.buffer_requested);
snap.buffer_bytes = part.bytes();
snap.memory_limit_bytes = model.memory_limit_bytes();
snap.memory_limited = part.memory_limited;
}
snap.span_s = span_s;
snap.time_stride = time_stride;
snap.have_event = app->last_latch_us != 0;
@@ -5281,8 +5290,9 @@ void on_activate(GtkApplication* application, gpointer user) {
if (app->waveform_name.empty()) {
const Config& cfg = app->model->config();
char buf[192];
std::snprintf(buf, sizeof(buf), "connecting to %s:%u -- planning the %.0f MB buffer",
cfg.server.c_str(), unsigned(cfg.port), cfg.buffer_mb);
std::snprintf(buf, sizeof(buf), "connecting to %s:%u -- planning the buffer: %s samples per signal",
cfg.server.c_str(), unsigned(cfg.port),
format_samples(buffer_ticks(cfg.buffer_samples)).c_str());
set_notice(*app, buf, 6);
}
}
@@ -6081,6 +6091,43 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
static_cast<unsigned long long>(model.live().values(i).produced()),
static_cast<unsigned long long>(model.live().channel(i).size()));
}
// The sample budget as this run planned it, and the planner on fixed inputs. The first line is
// the live buffer: `used` is the request rounded down to a power of two and clipped by the memory
// limit, `bytes` the rings' real size. The second feeds `plan_partition()` three channels and a
// 100 M request three ways -- room to spare, a 1 GiB limit that must clip, a limit too small for
// even 128 slots -- so a change to the rounding, the halving or the byte formula reads here even
// when the run's own plan happens not to be clipped.
std::printf("[udpscope] buffer: requested=%llu used=%llu bytes=%llu limit=%llu limited=%d\n",
static_cast<unsigned long long>(model.config().buffer_samples),
static_cast<unsigned long long>(part.time_capacity),
static_cast<unsigned long long>(part.bytes()),
static_cast<unsigned long long>(model.memory_limit_bytes()),
part.memory_limited ? 1 : 0);
{
const std::vector<double> dts(3, 1.0e-4);
const auto pow2 = [](unsigned long long v) { return v != 0 && (v & (v - 1)) == 0; };
const Partition open = plan_partition(dts, 100000000ull, 16ull << 30, 0.0);
constexpr unsigned long long kGiB = 1ull << 30;
const Partition clip = plan_partition(dts, 100000000ull, kGiB, 0.0);
const Partition clip_rel = plan_partition(dts, 100000000ull, kGiB, 0.0, true);
bool thrown = false;
try {
(void)plan_partition(dts, 100000000ull, 1000, 0.0);
} catch (const std::exception&) {
thrown = true;
}
std::printf("[udpscope] planner: unclipped got=%llu pow2=%d limited=%d | clipped(1GiB) "
"got=%llu bytes=%llu fits=%d pow2=%d limited=%d | clipped_rel got=%llu "
"bytes=%llu fits=%d | too_small_throws=%d\n",
static_cast<unsigned long long>(open.time_capacity),
pow2(open.time_capacity) ? 1 : 0, open.memory_limited ? 1 : 0,
static_cast<unsigned long long>(clip.time_capacity),
static_cast<unsigned long long>(clip.bytes()), clip.bytes() <= kGiB ? 1 : 0,
pow2(clip.time_capacity) ? 1 : 0, clip.memory_limited ? 1 : 0,
static_cast<unsigned long long>(clip_rel.time_capacity),
static_cast<unsigned long long>(clip_rel.bytes()),
clip_rel.bytes() <= kGiB ? 1 : 0, thrown ? 1 : 0);
}
// `frames_painted` is the area's, read from the snapshot taken before the window was
// destroyed (the PlotArea wrapper dies with its widget): one figure, one render per
// frame, so it is the same count for every row.