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
+34
-5
@@ -39,6 +39,25 @@ double parse_double(const char* name, const std::string& v) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A count with an optional decimal suffix: `250000`, `64k`, `100M`, `1.5G`. k, M and G are 10^3, 10^6
|
||||||
|
/// and 10^9 -- a *sample* count, not a byte size -- and either case is accepted.
|
||||||
|
std::uint64_t parse_count(const char* name, const std::string& v) {
|
||||||
|
std::string digits = v;
|
||||||
|
double scale = 1.0;
|
||||||
|
if (!digits.empty()) {
|
||||||
|
switch (digits.back()) {
|
||||||
|
case 'k': case 'K': scale = 1.0e3; break;
|
||||||
|
case 'm': case 'M': scale = 1.0e6; break;
|
||||||
|
case 'g': case 'G': scale = 1.0e9; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (scale != 1.0) digits.pop_back();
|
||||||
|
}
|
||||||
|
const double d = parse_double(name, digits) * scale;
|
||||||
|
if (!(d >= 1.0) || d > 1.0e12) fail(std::string(name) + " out of range: " + v);
|
||||||
|
return static_cast<std::uint64_t>(d + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SourceConfig Config::source_config() const {
|
SourceConfig Config::source_config() const {
|
||||||
@@ -70,7 +89,11 @@ void print_usage(std::FILE* out, const char* argv0) {
|
|||||||
" --source NAME source kind (%s; default udps). --server/--port/\n"
|
" --source NAME source kind (%s; default udps). --server/--port/\n"
|
||||||
" --multicast/--interface configure udps\n"
|
" --multicast/--interface configure udps\n"
|
||||||
" --source-opt K=V a setting for the source kind; repeatable\n"
|
" --source-opt K=V a setting for the source kind; repeatable\n"
|
||||||
" --buffer-mb MB size of each of the two sample buffers (default 200)\n"
|
" --buffer-samples N samples kept per acquired signal, e.g. 100M or 250000 (k, M\n"
|
||||||
|
" and G are decimal; default 10M). Rounded down to a power\n"
|
||||||
|
" of two: 100M gives 67108864\n"
|
||||||
|
" --memory-limit-mb MB hard ceiling on the sample rings, all signals together\n"
|
||||||
|
" (default 8192); the request is halved until it fits\n"
|
||||||
" --max-window SEC largest time window the toolbar offers (default 600)\n"
|
" --max-window SEC largest time window the toolbar offers (default 600)\n"
|
||||||
" --holdoff SEC default trigger hold-off (default 0.010)\n"
|
" --holdoff SEC default trigger hold-off (default 0.010)\n"
|
||||||
" --recv-buffer BYTES SO_RCVBUF for the receiver socket (default 8388608)\n"
|
" --recv-buffer BYTES SO_RCVBUF for the receiver socket (default 8388608)\n"
|
||||||
@@ -125,10 +148,16 @@ Config parse_args(int argc, char** argv) {
|
|||||||
} else if (a == "--source-opt" || a.rfind("--source-opt=", 0) == 0) {
|
} else if (a == "--source-opt" || a.rfind("--source-opt=", 0) == 0) {
|
||||||
const std::string v = value_of(argc, argv, i, "--source-opt");
|
const std::string v = value_of(argc, argv, i, "--source-opt");
|
||||||
if (!c.source_opts.parse_assignment(v)) fail("--source-opt must be key=value: " + v);
|
if (!c.source_opts.parse_assignment(v)) fail("--source-opt must be key=value: " + v);
|
||||||
} else if (a == "--buffer-mb" || a.rfind("--buffer-mb=", 0) == 0) {
|
} else if (a == "--buffer-samples" || a.rfind("--buffer-samples=", 0) == 0) {
|
||||||
c.buffer_mb = parse_double("--buffer-mb", value_of(argc, argv, i, "--buffer-mb"));
|
c.buffer_samples = parse_count("--buffer-samples", value_of(argc, argv, i, "--buffer-samples"));
|
||||||
c.buffer_mb_specified = true;
|
c.buffer_samples_specified = true;
|
||||||
if (c.buffer_mb < 1.0) fail("--buffer-mb must be at least 1");
|
if (c.buffer_samples < 128) fail("--buffer-samples must be at least 128");
|
||||||
|
} else if (a == "--memory-limit-mb" || a.rfind("--memory-limit-mb=", 0) == 0) {
|
||||||
|
const double mb =
|
||||||
|
parse_double("--memory-limit-mb", value_of(argc, argv, i, "--memory-limit-mb"));
|
||||||
|
if (!(mb >= 1.0) || mb > 1.0e9) fail("--memory-limit-mb must be 1..1000000000 (MiB)");
|
||||||
|
c.memory_limit_mb = static_cast<std::uint64_t>(mb);
|
||||||
|
c.memory_limit_specified = true;
|
||||||
} else if (a == "--max-window" || a.rfind("--max-window=", 0) == 0) {
|
} else if (a == "--max-window" || a.rfind("--max-window=", 0) == 0) {
|
||||||
c.max_window_s = parse_double("--max-window", value_of(argc, argv, i, "--max-window"));
|
c.max_window_s = parse_double("--max-window", value_of(argc, argv, i, "--max-window"));
|
||||||
if (c.max_window_s <= 0.0) fail("--max-window must be positive");
|
if (c.max_window_s <= 0.0) fail("--max-window must be positive");
|
||||||
|
|||||||
+16
-8
@@ -11,7 +11,7 @@
|
|||||||
namespace udpscope {
|
namespace udpscope {
|
||||||
|
|
||||||
/// Everything the command line can say. Defaults are the ones the spec names
|
/// Everything the command line can say. Defaults are the ones the spec names
|
||||||
/// (200 MB per buffer, 10 ms trigger hold-off) unless the user overrides them.
|
/// (10 M samples per signal, 10 ms trigger hold-off) unless the user overrides them.
|
||||||
struct Config {
|
struct Config {
|
||||||
std::string server = "127.0.0.1"; ///< IPv4 dotted quad of the stream source.
|
std::string server = "127.0.0.1"; ///< IPv4 dotted quad of the stream source.
|
||||||
std::uint16_t port = 8000; ///< UDP port (unicast) or TCP port (multicast).
|
std::uint16_t port = 8000; ///< UDP port (unicast) or TCP port (multicast).
|
||||||
@@ -40,13 +40,21 @@ struct Config {
|
|||||||
std::string session_host() const;
|
std::string session_host() const;
|
||||||
std::uint16_t session_port() const;
|
std::uint16_t session_port() const;
|
||||||
|
|
||||||
/// Size of *each* of the two sample buffers, in megabytes. The application
|
/// The sample budget: how many samples to keep for **every acquired signal**, and for the shared
|
||||||
/// always allocates two identical buffers (spec item 1).
|
/// time ring. The rings are powers of two, so the request is rounded *down* to one (`buffer_ticks()`
|
||||||
double buffer_mb = 200.0;
|
/// in model.hpp) -- "up to" this many -- and the UI shows both figures. 10 M asks for 8 388 608.
|
||||||
/// Whether `--buffer-mb` was given. A stored configuration's buffer size fills in only when the
|
std::uint64_t buffer_samples = 10'000'000;
|
||||||
/// flag was *absent*: an explicit flag beats a saved setting, and "not asked" is not the same
|
/// Whether `--buffer-samples` was given. A stored configuration's sample budget fills in only when
|
||||||
/// question as "asked for 200".
|
/// the flag was *absent*: an explicit flag beats a saved setting, and "not asked" is not the same
|
||||||
bool buffer_mb_specified = false;
|
/// question as "asked for 10 M".
|
||||||
|
bool buffer_samples_specified = false;
|
||||||
|
|
||||||
|
/// The hard ceiling on the ring storage, in MiB, for the whole application rather than per source.
|
||||||
|
/// `plan_partition()` halves the rings until they fit under it, so it can clip `buffer_samples`.
|
||||||
|
/// Default 8 GiB; the preferences carry it between runs and `--memory-limit-mb` overrides it.
|
||||||
|
std::uint64_t memory_limit_mb = 8192;
|
||||||
|
/// Whether `--memory-limit-mb` was given, so the stored preference only fills the gap.
|
||||||
|
bool memory_limit_specified = false;
|
||||||
|
|
||||||
/// Largest time window the toolbar offers, in seconds. The buffer partition is
|
/// Largest time window the toolbar offers, in seconds. The buffer partition is
|
||||||
/// sized to hold at least this much time (spec: decimation).
|
/// sized to hold at least this much time (spec: decimation).
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ std::vector<KnownSource> known_sources() {
|
|||||||
s.port = port;
|
s.port = port;
|
||||||
s.config = name;
|
s.config = name;
|
||||||
s.comment = st.comment;
|
s.comment = st.comment;
|
||||||
s.buffer_mb = st.buffer_mb;
|
s.buffer_samples = st.buffer_samples;
|
||||||
s.relative_time = st.relative_time;
|
s.relative_time = st.relative_time;
|
||||||
s.capture_packets = st.capture_packets;
|
s.capture_packets = st.capture_packets;
|
||||||
s.multicast = st.multicast;
|
s.multicast = st.multicast;
|
||||||
@@ -347,7 +347,7 @@ std::vector<KnownSource> known_sources() {
|
|||||||
// session file per probe run and every one holds `signals = ["", "", "", ""]`. One that
|
// session file per probe run and every one holds `signals = ["", "", "", ""]`. One that
|
||||||
// carries a *setting* is worth a row even before it has ever streamed -- that is how a host is
|
// carries a *setting* is worth a row even before it has ever streamed -- that is how a host is
|
||||||
// set up from the chooser's form, and the settings are the whole point of editing one.
|
// set up from the chooser's form, and the settings are the whole point of editing one.
|
||||||
const bool configured = s.signals > 0 || s.buffer_mb > 0.0 || !st.comment.empty() ||
|
const bool configured = s.signals > 0 || s.buffer_samples > 0 || !st.comment.empty() ||
|
||||||
st.relative_time || st.capture_packets > 0;
|
st.relative_time || st.capture_packets > 0;
|
||||||
if (!configured) continue;
|
if (!configured) continue;
|
||||||
struct stat fb {};
|
struct stat fb {};
|
||||||
@@ -583,9 +583,11 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
if (SourceConfig::valid_key(k)) st.source_opts.set(k, strip_quote(value));
|
if (SourceConfig::valid_key(k)) st.source_opts.set(k, strip_quote(value));
|
||||||
} else if (key == "comment") {
|
} else if (key == "comment") {
|
||||||
st.comment = strip_quote(value);
|
st.comment = strip_quote(value);
|
||||||
} else if (key == "buffer_mb") {
|
} else if (key == "buffer_samples") {
|
||||||
try {
|
try {
|
||||||
st.buffer_mb = std::stod(value);
|
// A negative or absurd count is "not stored", not a request: `stoull` wraps a minus sign.
|
||||||
|
const unsigned long long n = value.find('-') == std::string::npos ? std::stoull(value) : 0;
|
||||||
|
st.buffer_samples = n >= 128 && n <= 1000000000000ull ? n : 0;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
}
|
}
|
||||||
} else if (key == "relative_time") {
|
} else if (key == "relative_time") {
|
||||||
@@ -768,7 +770,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
|||||||
o << "trigger_density_tau = " << state.trigger.density_tau_s << "\n";
|
o << "trigger_density_tau = " << state.trigger.density_tau_s << "\n";
|
||||||
o << "trigger_density_factor = " << state.trigger.density_factor << "\n";
|
o << "trigger_density_factor = " << state.trigger.density_factor << "\n";
|
||||||
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
||||||
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
|
if (state.buffer_samples > 0) o << "buffer_samples = " << state.buffer_samples << "\n";
|
||||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||||
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
|
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
|
||||||
for (const auto& row : state.rows) {
|
for (const auto& row : state.rows) {
|
||||||
|
|||||||
@@ -139,9 +139,12 @@ struct LayoutState {
|
|||||||
/// The startup settings the configuration carries, applied when the chooser opens it -- which is
|
/// The startup settings the configuration carries, applied when the chooser opens it -- which is
|
||||||
/// why they live here and not only on the command line: two sources on one machine want different
|
/// why they live here and not only on the command line: two sources on one machine want different
|
||||||
/// buffers and different clocks, and retyping the flags to look at the second one is how a
|
/// buffers and different clocks, and retyping the flags to look at the second one is how a
|
||||||
/// setting gets forgotten. A zero `buffer_mb` and an empty `capture_packets` mean "not stored":
|
/// setting gets forgotten. A zero `buffer_samples` and an empty `capture_packets` mean "not stored":
|
||||||
/// the command line's value stands, so an old file cannot silently shrink a buffer.
|
/// the command line's value stands, so an old file cannot silently shrink a buffer. The budget is
|
||||||
double buffer_mb = 0.0;
|
/// samples per acquired signal (the memory limit is the application's, not a source's). A file from
|
||||||
|
/// before that unit carries `buffer_mb`, which is not read: it reads as "not stored" and the
|
||||||
|
/// default stands, rather than a megabyte count being taken for a sample count.
|
||||||
|
std::uint64_t buffer_samples = 0;
|
||||||
bool relative_time = false;
|
bool relative_time = false;
|
||||||
std::uint32_t capture_packets = 0;
|
std::uint32_t capture_packets = 0;
|
||||||
};
|
};
|
||||||
@@ -162,7 +165,7 @@ struct KnownSource {
|
|||||||
std::string config;
|
std::string config;
|
||||||
std::string comment; ///< the configuration's note, shown beside its name
|
std::string comment; ///< the configuration's note, shown beside its name
|
||||||
/// The startup settings this configuration carries, so the chooser can apply them.
|
/// The startup settings this configuration carries, so the chooser can apply them.
|
||||||
double buffer_mb = 0.0;
|
std::uint64_t buffer_samples = 0;
|
||||||
bool relative_time = false;
|
bool relative_time = false;
|
||||||
std::uint32_t capture_packets = 0;
|
std::uint32_t capture_packets = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-1
@@ -24,7 +24,11 @@ int main(int argc, char** argv) {
|
|||||||
// The preferences fill in what the command line did not decide. `--relative-time` is an explicit
|
// The preferences fill in what the command line did not decide. `--relative-time` is an explicit
|
||||||
// instruction for this run, so a preference can only turn the mode on, never off -- which is also
|
// instruction for this run, so a preference can only turn the mode on, never off -- which is also
|
||||||
// why this is read here, before the buffers are planned: the mode changes how they are filled.
|
// why this is read here, before the buffers are planned: the mode changes how they are filled.
|
||||||
if (udpscope::load_settings().relative_time) cfg.relative_time = true;
|
const udpscope::Settings prefs = udpscope::load_settings();
|
||||||
|
if (prefs.relative_time) cfg.relative_time = true;
|
||||||
|
// The memory limit is the same kind of statement, the other way round: the flag is the explicit
|
||||||
|
// instruction for this run and the preference only fills in when it was absent.
|
||||||
|
if (!cfg.memory_limit_specified) cfg.memory_limit_mb = static_cast<std::uint64_t>(prefs.memory_limit_mb);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::fprintf(stderr, "udpscope: %s\n", e.what());
|
std::fprintf(stderr, "udpscope: %s\n", e.what());
|
||||||
return 2;
|
return 2;
|
||||||
|
|||||||
+92
-41
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
|
|
||||||
|
#include "status_text.hpp" // `format_samples()` / `format_bytes()`, for the one line a clipped plan prints
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -40,12 +42,24 @@ constexpr std::uint64_t kMathCostSample = 1024;
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
|
u64 buffer_ticks(u64 requested_samples) {
|
||||||
double target_span_s, bool with_abs_time) {
|
// 2^40 slots is already 8 TiB of clock alone: a request past it is nonsense and the cap keeps
|
||||||
|
// `bytes_for()` far from overflowing.
|
||||||
|
constexpr u64 kMaxTicks = u64(1) << 40;
|
||||||
|
return std::min(pow2_floor(std::max<u64>(requested_samples, 128)), kMaxTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 planned_bytes(u64 ticks, std::size_t channels, bool with_abs_time) {
|
||||||
|
return ticks * sizeof(f64) * (with_abs_time ? 2u : 1u) + ticks * sizeof(f32) * channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
Partition plan_partition(const std::vector<double>& channel_dt, u64 samples_per_signal,
|
||||||
|
u64 max_bytes, double target_span_s, bool with_abs_time) {
|
||||||
Partition p;
|
Partition p;
|
||||||
const std::size_t n = channel_dt.size();
|
const std::size_t n = channel_dt.size();
|
||||||
p.stride.assign(n, 1);
|
p.stride.assign(n, 1);
|
||||||
p.value_capacity.assign(n, 0);
|
p.value_capacity.assign(n, 0);
|
||||||
|
p.requested_samples = samples_per_signal;
|
||||||
if (n == 0) return p;
|
if (n == 0) return p;
|
||||||
|
|
||||||
// The fastest channel drives the shared time base: every other channel is
|
// The fastest channel drives the shared time base: every other channel is
|
||||||
@@ -69,25 +83,13 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
|||||||
|
|
||||||
p.abs_time = with_abs_time;
|
p.abs_time = with_abs_time;
|
||||||
|
|
||||||
const u64 budget_bytes = static_cast<u64>(std::max(1.0, buffer_bytes));
|
// The ring is a power of two, so the user's count is a ceiling and the plan takes the largest
|
||||||
// Weighted in *bytes*, not in slots: the shared clock is f64 and the values are f32
|
// power of two under it. The budget is in *samples* now: it is the memory limit, not the request,
|
||||||
// (see `BufferStore::time()`), so a time slot costs twice a value sample and the two
|
// that is in bytes -- and a time slot (f64) and a value sample (f32) are not the same size, which
|
||||||
// slices are not interchangeable. Counting both as one f32 is what would make
|
// is why the limit below is weighed with `bytes_for()` and not as a count of slots.
|
||||||
// `--buffer-mb` quietly understate the time ring by its own size.
|
//
|
||||||
double weight = static_cast<double>(sizeof(f64)) * (with_abs_time ? 2.0 : 1.0); // the time slice(s)
|
// A channel's slots are rounded *down* on the power of two `RingBuffer` will round them to
|
||||||
for (u64 s : p.stride) weight += static_cast<double>(sizeof(f32)) / static_cast<double>(s);
|
// anyway, so every channel covers at most the time ring's span.
|
||||||
|
|
||||||
const u64 slots = static_cast<u64>(static_cast<double>(budget_bytes) / weight);
|
|
||||||
u64 time_capacity = pow2_floor(std::max<u64>(slots, 128));
|
|
||||||
if (time_capacity == 0) {
|
|
||||||
throw std::runtime_error("buffer too small for one time-base sample per channel");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The target span sets a floor, not a ceiling: the rest of the budget buys a
|
|
||||||
// longer ring, which is strictly more history to scroll back through. Growth
|
|
||||||
// stops before the total would exceed the budget. A channel's slots are rounded
|
|
||||||
// *up* on the power of two `RingBuffer` will round them to anyway, so every
|
|
||||||
// channel covers at least the time ring's span.
|
|
||||||
// Floor, not ceiling: a channel's window must not be *longer* than the time ring
|
// Floor, not ceiling: a channel's window must not be *longer* than the time ring
|
||||||
// it reads its stamps from. Rounding up (with a stride that is not a power of two)
|
// it reads its stamps from. Rounding up (with a stride that is not a power of two)
|
||||||
// gave a channel more samples than the shared timeline covers, and the oldest of
|
// gave a channel more samples than the shared timeline covers, and the oldest of
|
||||||
@@ -103,19 +105,24 @@ Partition plan_partition(const std::vector<double>& channel_dt, double buffer_by
|
|||||||
for (u64 s : p.stride) total += capacity_for(ticks, s) * sizeof(f32);
|
for (u64 s : p.stride) total += capacity_for(ticks, s) * sizeof(f32);
|
||||||
return total;
|
return total;
|
||||||
};
|
};
|
||||||
// `bytes_for` bounds the doubling on its own: it is at least `2 * ticks * sizeof(f64)`,
|
u64 time_capacity = buffer_ticks(samples_per_signal);
|
||||||
// so a pass means `2 * ticks` is well inside the budget and cannot overflow.
|
const u64 wanted_ticks = time_capacity;
|
||||||
while (bytes_for(time_capacity * 2) <= budget_bytes) {
|
// The memory limit halves the request until the rings fit, never the other way: a bigger plan than
|
||||||
time_capacity *= 2;
|
// asked for is not "up to".
|
||||||
|
while (time_capacity > 128 && bytes_for(time_capacity) > max_bytes) time_capacity /= 2;
|
||||||
|
if (bytes_for(time_capacity) > max_bytes) {
|
||||||
|
throw std::runtime_error("memory limit too small for the sample rings of " +
|
||||||
|
std::to_string(n) + " channels");
|
||||||
}
|
}
|
||||||
|
p.memory_limited = time_capacity < wanted_ticks;
|
||||||
p.time_capacity = time_capacity;
|
p.time_capacity = time_capacity;
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
p.value_capacity[i] = capacity_for(p.time_capacity, p.stride[i]);
|
p.value_capacity[i] = capacity_for(p.time_capacity, p.stride[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only now, with the ring as long as the budget can buy, decimate the shared clock
|
// Only now, with the ring as long as the request and the memory limit allow, decimate the shared clock
|
||||||
// enough for the requested window to fit in it: deciding this earlier would over-
|
// enough for the requested window to fit in it: deciding this earlier would over-
|
||||||
// decimate by whatever factor the growth loop was about to add. A power of two keeps
|
// decimate by whatever the ring's final length would have made unnecessary. A power of two keeps
|
||||||
// the ring capacities (themselves powers of two) in step and lets the producer's group
|
// the ring capacities (themselves powers of two) in step and lets the producer's group
|
||||||
// test be a mask (`Acquisition::acc_mask_`) rather than a modulo -- a modulo on a stride
|
// test be a mask (`Acquisition::acc_mask_`) rather than a modulo -- a modulo on a stride
|
||||||
// this function chose at runtime is a real 64-bit division, which the test *was* paying
|
// this function chose at runtime is a real 64-bit division, which the test *was* paying
|
||||||
@@ -178,6 +185,7 @@ void BufferStore::acquire_all() {
|
|||||||
// --- ScopeModel -------------------------------------------------------------
|
// --- ScopeModel -------------------------------------------------------------
|
||||||
|
|
||||||
ScopeModel::ScopeModel(const Config& cfg) : cfg_(cfg) {
|
ScopeModel::ScopeModel(const Config& cfg) : cfg_(cfg) {
|
||||||
|
memory_limit_bytes_.store(cfg.memory_limit_mb * 1024u * 1024u, std::memory_order_relaxed);
|
||||||
trigger_.holdoff_s = cfg.trigger_holdoff_s;
|
trigger_.holdoff_s = cfg.trigger_holdoff_s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,22 +575,16 @@ void ScopeModel::rebuild_locked() {
|
|||||||
holes_.clear();
|
holes_.clear();
|
||||||
++plan_generation_;
|
++plan_generation_;
|
||||||
|
|
||||||
std::vector<double> dts;
|
if (channels_.empty()) {
|
||||||
dts.reserve(channels_.size());
|
|
||||||
for (const ChannelInfo& c : channels_) dts.push_back(c.dt);
|
|
||||||
|
|
||||||
if (dts.empty()) {
|
|
||||||
part_ = Partition{};
|
part_ = Partition{};
|
||||||
store_.reset();
|
store_.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Partition want = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0,
|
const Partition want = plan_locked(target_span_.load(std::memory_order_relaxed));
|
||||||
target_span_.load(std::memory_order_relaxed),
|
|
||||||
cfg_.relative_time);
|
|
||||||
// A plan that describes the storage already allocated is cleared, not rebuilt. Allocating
|
// A plan that describes the storage already allocated is cleared, not rebuilt. Allocating
|
||||||
// a store is what costs -- its rings are value-initialised, so it touches the whole
|
// a store is what costs -- its rings are value-initialised, so it touches all of the
|
||||||
// `--buffer-mb` -- and most structural changes do not move the plan at all: a calibration
|
// planned rings -- and most structural changes do not move the plan at all: a calibration
|
||||||
// keeps the channel set, and so does leaving an event for the live view. Startup asked for
|
// keeps the channel set, and so does leaving an event for the live view. Startup asked for
|
||||||
// sixteen of these and eight of them planned the same buffer twice. Clearing has the same
|
// sixteen of these and eight of them planned the same buffer twice. Clearing has the same
|
||||||
// meaning (`BufferStore::reset()`): the history is gone either way, which is why
|
// meaning (`BufferStore::reset()`): the history is gone either way, which is why
|
||||||
@@ -601,6 +603,17 @@ void ScopeModel::rebuild_locked() {
|
|||||||
// the defect the line exists to name (it was fifteen of them at startup).
|
// the defect the line exists to name (it was fifteen of them at startup).
|
||||||
std::fprintf(stderr, "[udpscope] buffer allocated: %zu channels, %.3g s span, %.0f ms\n",
|
std::fprintf(stderr, "[udpscope] buffer allocated: %zu channels, %.3g s span, %.0f ms\n",
|
||||||
channels_.size(), part_.span_s, double(now_ns() - t0) / 1.0e6);
|
channels_.size(), part_.span_s, double(now_ns() - t0) / 1.0e6);
|
||||||
|
if (part_.memory_limited) {
|
||||||
|
// The one line that says the request was not honoured, and why: a smaller ring than the source
|
||||||
|
// form promised is otherwise explained nowhere.
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[udpscope] buffer limited to %s samples per signal (%s requested) by the %s "
|
||||||
|
"memory limit; the rings take %s\n",
|
||||||
|
format_samples(part_.time_capacity).c_str(),
|
||||||
|
format_samples(part_.requested_samples).c_str(),
|
||||||
|
format_bytes(memory_limit_bytes()).c_str(),
|
||||||
|
format_bytes(part_.bytes()).c_str());
|
||||||
|
}
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +621,47 @@ void ScopeModel::rebuild_locked() {
|
|||||||
prepare_math_locked();
|
prepare_math_locked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Partition ScopeModel::plan_locked(double seconds) const {
|
||||||
|
std::vector<double> dts;
|
||||||
|
dts.reserve(channels_.size());
|
||||||
|
for (const ChannelInfo& c : channels_) dts.push_back(c.dt);
|
||||||
|
return plan_partition(dts, cfg_.buffer_samples, memory_limit_bytes(), seconds, cfg_.relative_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScopeModel::set_memory_limit_bytes(std::uint64_t bytes, bool replan) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
const std::uint64_t previous = memory_limit_bytes_.load(std::memory_order_relaxed);
|
||||||
|
if (bytes == previous) return false;
|
||||||
|
if (channels_.empty() || !replan) {
|
||||||
|
memory_limit_bytes_.store(bytes, std::memory_order_relaxed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memory_limit_bytes_.store(bytes, std::memory_order_relaxed);
|
||||||
|
Partition want;
|
||||||
|
try {
|
||||||
|
want = plan_locked(target_span_.load(std::memory_order_relaxed));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
memory_limit_bytes_.store(previous, std::memory_order_relaxed);
|
||||||
|
std::fprintf(stderr, "[udpscope] memory limit %s refused: %s\n", format_bytes(bytes).c_str(),
|
||||||
|
e.what());
|
||||||
|
std::fflush(stderr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (store_ && part_.same_storage_as(want)) {
|
||||||
|
// The same rings: only what the plan *says* about itself moved. Written in place, because the
|
||||||
|
// producer reads `part_` under the lock we hold and nothing here reallocates it.
|
||||||
|
part_.requested_samples = want.requested_samples;
|
||||||
|
part_.memory_limited = want.memory_limited;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "[udpscope] memory limit %s -> %s: buffer re-planned\n",
|
||||||
|
format_bytes(previous).c_str(), format_bytes(bytes).c_str());
|
||||||
|
std::fflush(stderr);
|
||||||
|
rebuild_locked();
|
||||||
|
generation_.fetch_add(1, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ScopeModel::set_target_span(double seconds) {
|
bool ScopeModel::set_target_span(double seconds) {
|
||||||
if (!(seconds > 0.0)) return false;
|
if (!(seconds > 0.0)) return false;
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
@@ -616,10 +670,7 @@ bool ScopeModel::set_target_span(double seconds) {
|
|||||||
// Re-plan only if the factor moves. The planner is cheap, so ask it rather than
|
// Re-plan only if the factor moves. The planner is cheap, so ask it rather than
|
||||||
// reimplementing its arithmetic here -- two expressions for the same decision is how
|
// reimplementing its arithmetic here -- two expressions for the same decision is how
|
||||||
// the buffer and the view end up disagreeing about the stored rate.
|
// the buffer and the view end up disagreeing about the stored rate.
|
||||||
std::vector<double> dts;
|
const Partition want = plan_locked(seconds);
|
||||||
dts.reserve(channels_.size());
|
|
||||||
for (const ChannelInfo& c : channels_) dts.push_back(c.dt);
|
|
||||||
const Partition want = plan_partition(dts, cfg_.buffer_mb * 1024.0 * 1024.0, seconds, cfg_.relative_time);
|
|
||||||
if (want.time_stride == part_.time_stride) return false;
|
if (want.time_stride == part_.time_stride) return false;
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[udpscope] window %.4f s -> %.4f s: decimation %llu -> %llu, stored rate "
|
"[udpscope] window %.4f s -> %.4f s: decimation %llu -> %llu, stored rate "
|
||||||
|
|||||||
+67
-15
@@ -104,9 +104,27 @@ struct Partition {
|
|||||||
|
|
||||||
/// True when a second f64 ring parallel to the clock was planned and paid for in the
|
/// True when a second f64 ring parallel to the clock was planned and paid for in the
|
||||||
/// byte budget. Relative mode only -- in stream mode `time()` already *is* the absolute
|
/// byte budget. Relative mode only -- in stream mode `time()` already *is* the absolute
|
||||||
/// clock and a copy would cost ~22% of the user's `--buffer-mb` for nothing.
|
/// clock and a copy would cost ~22% of the user's sample budget for nothing.
|
||||||
bool abs_time = false;
|
bool abs_time = false;
|
||||||
|
|
||||||
|
/// The samples-per-signal the user asked for, as given (not rounded). `time_capacity` is what
|
||||||
|
/// the plan actually holds: the request rounded down to a power of two, then halved further
|
||||||
|
/// while the rings did not fit the memory limit. These two are readings, not storage -- they are
|
||||||
|
/// not in `same_storage_as()` -- so a re-plan that only moves them does not drop history.
|
||||||
|
u64 requested_samples = 0;
|
||||||
|
/// True when the memory limit, and not the power-of-two rounding, made `time_capacity` smaller than
|
||||||
|
/// the request: the one case the UI has to explain.
|
||||||
|
bool memory_limited = false;
|
||||||
|
|
||||||
|
/// The bytes the rings of this plan occupy: the time ring (twice in relative mode) plus every
|
||||||
|
/// value ring. This is the figure to quote and to budget against; a count of the user's
|
||||||
|
/// request is not, because the rings round and the limit can clip.
|
||||||
|
u64 bytes() const noexcept {
|
||||||
|
u64 total = time_capacity * sizeof(f64) * (abs_time ? 2u : 1u);
|
||||||
|
for (u64 c : value_capacity) total += c * sizeof(f32);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
/// Seconds between two time-base slots (the sample interval of every channel).
|
/// Seconds between two time-base slots (the sample interval of every channel).
|
||||||
double slot_dt() const noexcept { return base_dt * static_cast<double>(time_stride); }
|
double slot_dt() const noexcept { return base_dt * static_cast<double>(time_stride); }
|
||||||
/// The rate samples are actually stored at, in samples per second.
|
/// The rate samples are actually stored at, in samples per second.
|
||||||
@@ -137,13 +155,24 @@ struct Partition {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Plans a partition for `channel_dt` cadences within `buffer_bytes`, covering at least
|
/// The ring length a request of `requested_samples` per signal buys: the largest power of two not
|
||||||
/// `target_span_s` seconds of stream -- decimating (with the ingest filter) by whatever
|
/// above it (at least 128). The rings are powers of two, so the user's number is a ceiling.
|
||||||
/// power of two that takes. Throws std::runtime_error when the budget cannot hold even
|
u64 buffer_ticks(u64 requested_samples);
|
||||||
/// one sample per channel. `with_abs_time` adds a second f64 per time slot to the budget
|
|
||||||
/// and sets `Partition::abs_time`.
|
/// What `ticks` slots cost for `channels` unit-stride channels: the f64 time ring (twice with the
|
||||||
Partition plan_partition(const std::vector<double>& channel_dt, double buffer_bytes,
|
/// absolute clock) plus one f32 ring per channel. The planner's own byte formula, for a caller
|
||||||
double target_span_s, bool with_abs_time = false);
|
/// that has no `Partition` yet (the source form, the probes).
|
||||||
|
u64 planned_bytes(u64 ticks, std::size_t channels, bool with_abs_time);
|
||||||
|
|
||||||
|
/// Plans a partition for `channel_dt` cadences: `samples_per_signal` slots for the time ring and for
|
||||||
|
/// every channel, rounded down to a power of two, then halved while the rings (`planned_bytes()`)
|
||||||
|
/// exceed `max_bytes` -- the memory limit. Covers at least `target_span_s` seconds of stream,
|
||||||
|
/// decimating (with the ingest filter) by whatever power of two that takes. Throws
|
||||||
|
/// std::runtime_error when even 128 slots do not fit `max_bytes`. `with_abs_time` adds a second f64
|
||||||
|
/// per time slot to the budget and sets `Partition::abs_time`. `Partition::memory_limited` says
|
||||||
|
/// whether the limit clipped the request.
|
||||||
|
Partition plan_partition(const std::vector<double>& channel_dt, u64 samples_per_signal,
|
||||||
|
u64 max_bytes, double target_span_s, bool with_abs_time = false);
|
||||||
|
|
||||||
/// One of the two identical sample buffers: a shared time-base ring plus one
|
/// One of the two identical sample buffers: a shared time-base ring plus one
|
||||||
/// value ring and one `ChannelBuffer` per channel.
|
/// value ring and one `ChannelBuffer` per channel.
|
||||||
@@ -187,7 +216,7 @@ class BufferStore {
|
|||||||
///
|
///
|
||||||
/// Why it exists: constructing a store *is* its cost. A ring is a
|
/// Why it exists: constructing a store *is* its cost. A ring is a
|
||||||
/// `vector<std::atomic<T>>`, which is value-initialised, so building one touches every
|
/// `vector<std::atomic<T>>`, which is value-initialised, so building one touches every
|
||||||
/// byte of the user's `--buffer-mb` -- about 300 ms per GB, nearly all of it first-touch
|
/// byte of the planned buffer (`--buffer-samples`) -- about 300 ms per GB, nearly all of it first-touch
|
||||||
/// page faults. Clearing is a count store per ring. Most structural changes (a
|
/// page faults. Clearing is a count store per ring. Most structural changes (a
|
||||||
/// calibration, a re-enable of what was just disabled, going back to live) re-plan to the
|
/// calibration, a re-enable of what was just disabled, going back to live) re-plan to the
|
||||||
/// *same* partition, and paying a second full allocation for them is what made a large
|
/// *same* partition, and paying a second full allocation for them is what made a large
|
||||||
@@ -546,13 +575,13 @@ class ScopeModel {
|
|||||||
std::uint64_t persist_dropped() const noexcept {
|
std::uint64_t persist_dropped() const noexcept {
|
||||||
return persist_dropped_.load(std::memory_order_relaxed);
|
return persist_dropped_.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
/// The bytes persistence may retain, which is the ring's own byte budget (`--buffer-mb`): the
|
/// The bytes persistence may retain, which is the rings' own planned byte total
|
||||||
/// snapshots are copies *of* what the rings hold, so holding as much again as the buffer does is
|
/// (`Partition::bytes()`): the snapshots are copies *of* what the rings hold, so holding as much
|
||||||
/// the honest ceiling for a display of it -- and it is the bound that cannot be driven into an
|
/// again as the buffer does is the honest ceiling for a display of it -- and it is the bound that
|
||||||
/// OOM by a long window on a fast stream, whatever depth the panel asks for.
|
/// cannot be driven into an OOM by a long window on a fast stream, whatever depth the panel asks
|
||||||
|
/// for. Zero until a stream has planned a buffer. Read under `mutex()`, like `partition()`.
|
||||||
std::size_t persist_budget_bytes() const noexcept {
|
std::size_t persist_budget_bytes() const noexcept {
|
||||||
const int mb = cfg_.buffer_mb > 0 ? cfg_.buffer_mb : 0;
|
return static_cast<std::size_t>(part_.bytes());
|
||||||
return static_cast<std::size_t>(mb) * 1024u * 1024u;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets how many acquisitions the persistence display keeps, clamped to `kMaxPersistDepth`, and
|
/// Sets how many acquisitions the persistence display keeps, clamped to `kMaxPersistDepth`, and
|
||||||
@@ -671,6 +700,22 @@ class ScopeModel {
|
|||||||
/// `mutex()` itself; do not call it holding the lock.
|
/// `mutex()` itself; do not call it holding the lock.
|
||||||
/// Returns true when the buffer was rebuilt.
|
/// Returns true when the buffer was rebuilt.
|
||||||
bool set_target_span(double seconds);
|
bool set_target_span(double seconds);
|
||||||
|
|
||||||
|
/// The memory limit the planner honours, in bytes. Starts as `Config::memory_limit_mb` and is the
|
||||||
|
/// one planner input that changes while the application runs (the preferences). Atomic so the
|
||||||
|
/// status and the probes can read it without the lock.
|
||||||
|
std::uint64_t memory_limit_bytes() const noexcept {
|
||||||
|
return memory_limit_bytes_.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
/// Changes the memory limit and re-plans. The buffer is rebuilt -- and the history dropped -- only
|
||||||
|
/// when the new limit changes the *storage* (`Partition::same_storage_as()`): a limit that still
|
||||||
|
/// leaves room for the plan updates `Partition::memory_limited` in place and costs nothing.
|
||||||
|
/// Returns true when the buffer was rebuilt. Takes `mutex()` itself; do not call it holding the
|
||||||
|
/// lock. A limit too small for even 128 slots per channel is refused (returns false, keeps the old
|
||||||
|
/// limit) and said on stderr.
|
||||||
|
/// With `replan` false the limit is only stored, for the next plan: a file or a replayed event
|
||||||
|
/// lives in the buffer, and re-planning it would clear what the user is looking at.
|
||||||
|
bool set_memory_limit_bytes(std::uint64_t bytes, bool replan = true);
|
||||||
double target_span() const noexcept { return target_span_.load(std::memory_order_relaxed); }
|
double target_span() const noexcept { return target_span_.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
/// Trigger state for the UI, published by the acquisition thread. Atomics
|
/// Trigger state for the UI, published by the acquisition thread. Atomics
|
||||||
@@ -734,7 +779,14 @@ class ScopeModel {
|
|||||||
/// so a window change that moves the decimation re-prepares the filters exactly as an edit does.
|
/// so a window change that moves the decimation re-prepares the filters exactly as an edit does.
|
||||||
void prepare_math_locked();
|
void prepare_math_locked();
|
||||||
|
|
||||||
|
/// Plans the partition for the current channels at `seconds` of window with the current sample
|
||||||
|
/// budget and memory limit. The one place the planner's inputs are assembled, so the re-plan on a
|
||||||
|
/// window change, a structural change and a memory-limit change cannot disagree. Throws what
|
||||||
|
/// `plan_partition()` throws. Caller holds `mutex()`.
|
||||||
|
Partition plan_locked(double seconds) const;
|
||||||
|
|
||||||
Config cfg_;
|
Config cfg_;
|
||||||
|
std::atomic<std::uint64_t> memory_limit_bytes_{0};
|
||||||
std::vector<SignalInfo> signals_;
|
std::vector<SignalInfo> signals_;
|
||||||
std::vector<ChannelInfo> channels_;
|
std::vector<ChannelInfo> channels_;
|
||||||
// --- math channels --------------------------------------------------------
|
// --- math channels --------------------------------------------------------
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "settings.hpp"
|
#include "settings.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -64,6 +65,9 @@ Settings load_settings() {
|
|||||||
s.event_cache_max = std::max(0, std::atoi(value.c_str()));
|
s.event_cache_max = std::max(0, std::atoi(value.c_str()));
|
||||||
} else if (key == "relative_time") {
|
} else if (key == "relative_time") {
|
||||||
s.relative_time = parse_bool(value, s.relative_time);
|
s.relative_time = parse_bool(value, s.relative_time);
|
||||||
|
} else if (key == "memory_limit_mb") {
|
||||||
|
const long mb = std::strtol(value.c_str(), nullptr, 10);
|
||||||
|
if (mb >= 16) s.memory_limit_mb = static_cast<int>(std::min<long>(mb, 1000000000L));
|
||||||
} else if (key == "theme") {
|
} else if (key == "theme") {
|
||||||
if (value == "auto" || value == "light" || value == "dark") s.theme = value;
|
if (value == "auto" || value == "light" || value == "dark") s.theme = value;
|
||||||
} else if (key == "contextual_help") {
|
} else if (key == "contextual_help") {
|
||||||
@@ -83,6 +87,7 @@ bool save_settings(const Settings& settings) {
|
|||||||
out << "event_cache_dir = \"" << settings.event_cache_dir << "\"\n";
|
out << "event_cache_dir = \"" << settings.event_cache_dir << "\"\n";
|
||||||
out << "event_cache_max = " << settings.event_cache_max << "\n";
|
out << "event_cache_max = " << settings.event_cache_max << "\n";
|
||||||
out << "relative_time = " << (settings.relative_time ? "true" : "false") << "\n";
|
out << "relative_time = " << (settings.relative_time ? "true" : "false") << "\n";
|
||||||
|
out << "memory_limit_mb = " << settings.memory_limit_mb << "\n";
|
||||||
out << "theme = \"" << settings.theme << "\"\n";
|
out << "theme = \"" << settings.theme << "\"\n";
|
||||||
// The two help keys are the *app's* memory rather than the dialog's: the contextual-help rows --
|
// The two help keys are the *app's* memory rather than the dialog's: the contextual-help rows --
|
||||||
// the Help menu's and the source chooser's own -- write the first, and the first-run tour writes the
|
// the Help menu's and the source chooser's own -- write the first, and the first-run tour writes the
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ struct Settings {
|
|||||||
/// buffers are planned before any stream is known, so it takes effect on the next start (the
|
/// buffers are planned before any stream is known, so it takes effect on the next start (the
|
||||||
/// preferences dialog says so, like the configurations dialog does).
|
/// preferences dialog says so, like the configurations dialog does).
|
||||||
bool relative_time = false;
|
bool relative_time = false;
|
||||||
|
/// The hard ceiling on the sample rings' memory, in MiB, for the whole application rather than per
|
||||||
|
/// source (`--memory-limit-mb` overrides it for one run). The planner halves a source's sample
|
||||||
|
/// budget until the rings fit under it. Unlike the cache directory and relative time it applies as
|
||||||
|
/// it is changed: a running scope re-plans its buffer (and so drops its history) when the new limit
|
||||||
|
/// changes the rings.
|
||||||
|
int memory_limit_mb = 8192;
|
||||||
/// "auto", "light" or "dark": what the interface should look like. Kept as the three names libadwaita
|
/// "auto", "light" or "dark": what the interface should look like. Kept as the three names libadwaita
|
||||||
/// uses, so the file says what the setting is rather than what an enum happens to be numbered.
|
/// uses, so the file says what the setting is rather than what an enum happens to be numbered.
|
||||||
std::string theme = "auto";
|
std::string theme = "auto";
|
||||||
|
|||||||
+73
-27
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
#include "help_window.hpp"
|
#include "help_window.hpp"
|
||||||
#include "layout_store.hpp"
|
#include "layout_store.hpp"
|
||||||
|
#include "model.hpp" // `buffer_ticks()` / `planned_bytes()`: what a request becomes
|
||||||
#include "settings.hpp"
|
#include "settings.hpp"
|
||||||
|
#include "status_text.hpp" // `format_samples()` and friends
|
||||||
#include "source/registry.hpp"
|
#include "source/registry.hpp"
|
||||||
// The app's shared chrome -- `ui_impl::menu_button()`/`menu_toggle()` and `install_css()` -- so the
|
// 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
|
// 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 <unistd.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#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
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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.
|
// 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) {
|
void report_sources(const std::vector<KnownSource>& known) {
|
||||||
std::fprintf(stderr, "[sources] %zu known:", known.size());
|
std::fprintf(stderr, "[sources] %zu known:", known.size());
|
||||||
for (const KnownSource& s : known) {
|
for (const KnownSource& s : known) {
|
||||||
std::fprintf(stderr, " %s:%u/%s plots=%d signals=%d buffer=%.6g relative=%d capture=%u",
|
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, s.buffer_mb,
|
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);
|
s.relative_time ? 1 : 0, s.capture_packets);
|
||||||
if (!s.comment.empty()) std::fprintf(stderr, " note='%s'", s.comment.c_str());
|
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());
|
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::string sub = std::to_string(s.rows) + (s.rows == 1 ? " plot, " : " plots, ") +
|
||||||
std::to_string(s.signals) + (s.signals == 1 ? " signal" : " signals");
|
std::to_string(s.signals) + (s.signals == 1 ? " signal" : " signals");
|
||||||
if (s.buffer_mb > 0.0) {
|
if (s.buffer_samples > 0) sub += ", buffer " + format_samples(s.buffer_samples) + " samples";
|
||||||
sub += ", buffer " + std::to_string(static_cast<int>(s.buffer_mb)) + " MB";
|
|
||||||
}
|
|
||||||
if (s.relative_time) sub += ", relative time";
|
if (s.relative_time) sub += ", relative time";
|
||||||
if (s.capture_packets > 0) {
|
if (s.capture_packets > 0) {
|
||||||
sub += ", capture " + std::to_string(s.capture_packets) + " packets";
|
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) {
|
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
|
// The form's own `closed` handler clears the pointer, so this is the only place that has to know how
|
||||||
// an `AdwDialog` closes.
|
// an `AdwDialog` closes.
|
||||||
@@ -497,7 +525,7 @@ void on_form_connect(GtkButton*, gpointer user) {
|
|||||||
const std::string iface = udps ? scfg.get("interface") : std::string();
|
const std::string iface = udps ? scfg.get("interface") : std::string();
|
||||||
std::string name = gtk_editable_get_text(GTK_EDITABLE(c.form_name));
|
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 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;
|
const bool relative = adw_switch_row_get_active(c.form_relative) != FALSE;
|
||||||
if (name.empty()) name = "default";
|
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
|
// 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.
|
// 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;
|
c.cfg->relative_time = relative;
|
||||||
|
|
||||||
LayoutState st;
|
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);
|
load_layout(configuration_path(c.edit_host, c.edit_port, c.edit_config), st);
|
||||||
}
|
}
|
||||||
st.comment = note;
|
st.comment = note;
|
||||||
st.buffer_mb = buffer_mb;
|
st.buffer_samples = buffer_samples;
|
||||||
st.relative_time = relative;
|
st.relative_time = relative;
|
||||||
st.multicast = group;
|
st.multicast = group;
|
||||||
st.interface_addr = iface;
|
st.interface_addr = iface;
|
||||||
@@ -541,9 +569,9 @@ void on_form_connect(GtkButton*, gpointer user) {
|
|||||||
c.saved_config = name;
|
c.saved_config = name;
|
||||||
c.saved_note = note;
|
c.saved_note = note;
|
||||||
std::fprintf(stderr,
|
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(),
|
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);
|
std::fflush(stderr);
|
||||||
} else {
|
} else {
|
||||||
std::fprintf(stderr, "[sources] configuration write failed: %s:%u/%s\n", host.c_str(), port,
|
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.
|
// 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_name), src != nullptr ? src->config.c_str() : "default");
|
||||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), src != nullptr ? src->comment.c_str() : "");
|
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
|
adw_spin_row_set_value(c.form_buffer, double(src != nullptr && src->buffer_samples > 0
|
||||||
? src->buffer_mb
|
? src->buffer_samples
|
||||||
: std::max(1.0, c.cfg->buffer_mb));
|
: 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,
|
adw_switch_row_set_active(c.form_relative,
|
||||||
(src != nullptr ? src->relative_time : c.cfg->relative_time) ? TRUE
|
(src != nullptr ? src->relative_time : c.cfg->relative_time) ? TRUE
|
||||||
: FALSE);
|
: 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
|
// 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 --
|
// (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
|
// 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.
|
// only after a long capture has been sized wrong.
|
||||||
std::fprintf(stderr,
|
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",
|
"capture=%u config=%s note='%s'\n",
|
||||||
c.cfg->server.c_str(), c.cfg->port, c.cfg->multicast.c_str(),
|
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.cfg->capture_packets,
|
||||||
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().config)
|
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().config)
|
||||||
: c.saved_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.
|
// stored configuration, and it works in both directions -- turning it on *and* off.
|
||||||
c.cfg->relative_time = s.relative_time;
|
c.cfg->relative_time = s.relative_time;
|
||||||
c.cfg->relative_time_specified = true;
|
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) {
|
if (!written) {
|
||||||
std::fprintf(stderr, "[sources] prefs: could not write %s\n", settings_path().c_str());
|
std::fprintf(stderr, "[sources] prefs: could not write %s\n", settings_path().c_str());
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
@@ -1171,7 +1216,7 @@ gboolean pick_probe(gpointer user) {
|
|||||||
c.form_cfg.write(sc);
|
c.form_cfg.write(sc);
|
||||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name), "probe-host");
|
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");
|
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);
|
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
|
// 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.
|
// 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,
|
std::fprintf(stderr, "[sources] form mnemonics=%d/%zu\n", underlines,
|
||||||
5 + c.form_cfg.rows.size());
|
5 + c.form_cfg.rows.size());
|
||||||
std::fprintf(stderr,
|
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",
|
"connect_sensitive=%d\n",
|
||||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
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
|
// 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.
|
// against, and the button's label says which mode it is in.
|
||||||
std::fprintf(stderr,
|
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_name)),
|
||||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||||
adw_spin_row_get_value(c.form_buffer),
|
adw_spin_row_get_value(c.form_buffer),
|
||||||
adw_switch_row_get_active(c.form_relative) ? 1 : 0,
|
adw_switch_row_get_active(c.form_relative) ? 1 : 0,
|
||||||
gtk_button_get_label(GTK_BUTTON(c.form_connect)));
|
gtk_button_get_label(GTK_BUTTON(c.form_connect)));
|
||||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "edited by the probe");
|
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);
|
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
|
// `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.
|
// 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,
|
// 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
|
// 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.
|
// 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;
|
const bool relative = !cfg.relative_time_specified && st.relative_time;
|
||||||
if (relative) cfg.relative_time = true;
|
if (relative) cfg.relative_time = true;
|
||||||
if (cfg.capture_packets == 0 && st.capture_packets > 0) cfg.capture_packets = st.capture_packets;
|
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;
|
st.capture_packets > 0;
|
||||||
if (applied) {
|
if (applied) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[sources] settings from configuration '%s': buffer=%.6g relative=%d capture=%u\n",
|
"[sources] settings from configuration '%s': samples=%llu relative=%d capture=%u\n",
|
||||||
want.c_str(), cfg.buffer_mb, cfg.relative_time ? 1 : 0, cfg.capture_packets);
|
want.c_str(), static_cast<unsigned long long>(cfg.buffer_samples),
|
||||||
|
cfg.relative_time ? 1 : 0, cfg.capture_packets);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1330,9 +1376,9 @@ void loading_show(const Config& cfg) {
|
|||||||
head = "Connecting to " + cfg.server + ":" + std::to_string(cfg.port);
|
head = "Connecting to " + cfg.server + ":" + std::to_string(cfg.port);
|
||||||
if (!cfg.multicast.empty()) head += " (" + cfg.multicast + ")";
|
if (!cfg.multicast.empty()) head += " (" + cfg.multicast + ")";
|
||||||
}
|
}
|
||||||
char buf[128];
|
sub = "planning the buffer: " + format_samples(buffer_ticks(cfg.buffer_samples)) +
|
||||||
std::snprintf(buf, sizeof(buf), "planning the %.0f MB sample buffer", cfg.buffer_mb);
|
" samples per signal, " + format_bytes(cfg.memory_limit_mb * 1024u * 1024u) +
|
||||||
sub = buf;
|
" memory limit";
|
||||||
} else {
|
} else {
|
||||||
const std::size_t slash = cfg.open_waveform.rfind('/');
|
const std::size_t slash = cfg.open_waveform.rfind('/');
|
||||||
head = "Opening " + (slash == std::string::npos ? cfg.open_waveform
|
head = "Opening " + (slash == std::string::npos ? cfg.open_waveform
|
||||||
|
|||||||
+49
-4
@@ -41,6 +41,43 @@ std::string format_rate(double per_s) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string format_samples(std::uint64_t n) {
|
||||||
|
char buf[32];
|
||||||
|
const double d = static_cast<double>(n);
|
||||||
|
if (n >= 1000000000ull) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g G", d / 1.0e9);
|
||||||
|
} else if (n >= 1000000ull) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g M", d / 1.0e6);
|
||||||
|
} else if (n >= 1000ull) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g k", d / 1.0e3);
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%llu", static_cast<unsigned long long>(n));
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string format_bytes(std::uint64_t bytes) {
|
||||||
|
char buf[32];
|
||||||
|
const double d = static_cast<double>(bytes);
|
||||||
|
if (bytes >= (1ull << 30)) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g GiB", d / double(1ull << 30));
|
||||||
|
} else if (bytes >= (1ull << 20)) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g MiB", d / double(1ull << 20));
|
||||||
|
} else if (bytes >= (1ull << 10)) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g KiB", d / double(1ull << 10));
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%llu B", static_cast<unsigned long long>(bytes));
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string describe_buffer(std::uint64_t requested, std::uint64_t used, bool memory_limited,
|
||||||
|
std::uint64_t memory_limit_bytes) {
|
||||||
|
std::string out = format_samples(requested) + " requested, " + format_samples(used) + " used";
|
||||||
|
if (memory_limited) out += " (limited by the " + format_bytes(memory_limit_bytes) + " memory limit)";
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
std::string format_glance(const StatusSnapshot& s) {
|
std::string format_glance(const StatusSnapshot& s) {
|
||||||
std::string out = s.state_word;
|
std::string out = s.state_word;
|
||||||
out += " · ";
|
out += " · ";
|
||||||
@@ -68,17 +105,25 @@ std::string format_detail(const StatusSnapshot& s) {
|
|||||||
char buf[1024];
|
char buf[1024];
|
||||||
std::snprintf(buf, sizeof(buf),
|
std::snprintf(buf, sizeof(buf),
|
||||||
"[%s] %s %s | rate %s | packets %llu frames %llu gaps %llu | %zu signals, "
|
"[%s] %s %s | rate %s | packets %llu frames %llu gaps %llu | %zu signals, "
|
||||||
"%zu channels | plot 1: %d trace(s), %zu samples | buffer %.1f MB, %.2f s "
|
"%zu channels | plot 1: %d trace(s), %zu samples | buffer %s samples/signal "
|
||||||
"span, time x%llu",
|
"(%s), %.2f s span, time x%llu",
|
||||||
s.state_detail.c_str(), s.connected ? "connected" : "waiting",
|
s.state_detail.c_str(), s.connected ? "connected" : "waiting",
|
||||||
s.have_config ? "configured" : "-",
|
s.have_config ? "configured" : "-",
|
||||||
s.rate_text.empty() ? "--" : s.rate_text.c_str(),
|
s.rate_text.empty() ? "--" : s.rate_text.c_str(),
|
||||||
static_cast<unsigned long long>(s.packets),
|
static_cast<unsigned long long>(s.packets),
|
||||||
static_cast<unsigned long long>(s.frames),
|
static_cast<unsigned long long>(s.frames),
|
||||||
static_cast<unsigned long long>(s.gaps), s.signals, s.channels,
|
static_cast<unsigned long long>(s.gaps), s.signals, s.channels,
|
||||||
s.plot1_traces, s.plot1_samples, s.buffer_mb, s.span_s,
|
s.plot1_traces, s.plot1_samples, format_samples(s.buffer_samples).c_str(),
|
||||||
static_cast<unsigned long long>(s.time_stride));
|
s.buffer_bytes > 0 ? format_bytes(s.buffer_bytes).c_str() : "not planned",
|
||||||
|
s.span_s, static_cast<unsigned long long>(s.time_stride));
|
||||||
std::string out = buf;
|
std::string out = buf;
|
||||||
|
// The request is quoted only where it differs from what was planned, and the limit only where it
|
||||||
|
// is the reason: a clean run says the figure once, and a clipped one says why in the same breath.
|
||||||
|
if (s.memory_limited) {
|
||||||
|
out += " | limited to " + format_samples(s.buffer_samples) + " samples by the " +
|
||||||
|
format_bytes(s.memory_limit_bytes) + " memory limit (" +
|
||||||
|
format_samples(s.buffer_requested) + " requested)";
|
||||||
|
}
|
||||||
// Nothing about resequencing is said at all while all six read zero, which is a stream that
|
// Nothing about resequencing is said at all while all six read zero, which is a stream that
|
||||||
// never lost ordering: the field would otherwise be six zeros on every clean run, and this
|
// never lost ordering: the field would otherwise be six zeros on every clean run, and this
|
||||||
// string is read by eye out of the exit dump.
|
// string is read by eye out of the exit dump.
|
||||||
|
|||||||
+22
-1
@@ -41,7 +41,15 @@ struct StatusSnapshot {
|
|||||||
std::size_t channels = 0;
|
std::size_t channels = 0;
|
||||||
int plot1_traces = 0;
|
int plot1_traces = 0;
|
||||||
std::size_t plot1_samples = 0;
|
std::size_t plot1_samples = 0;
|
||||||
double buffer_mb = 0.0;
|
/// The sample budget as planned: slots per signal the rings hold (the request rounded down to a
|
||||||
|
/// power of two and clipped by the memory limit), what was asked for, and the bytes those rings
|
||||||
|
/// occupy -- 0 until a stream has planned a buffer. `memory_limited` says the limit, and not the
|
||||||
|
/// rounding, made `buffer_samples` smaller than the request.
|
||||||
|
std::uint64_t buffer_samples = 0;
|
||||||
|
std::uint64_t buffer_requested = 0;
|
||||||
|
std::uint64_t buffer_bytes = 0;
|
||||||
|
std::uint64_t memory_limit_bytes = 0;
|
||||||
|
bool memory_limited = false;
|
||||||
double span_s = 0.0;
|
double span_s = 0.0;
|
||||||
std::uint64_t time_stride = 1;
|
std::uint64_t time_stride = 1;
|
||||||
bool have_event = false;
|
bool have_event = false;
|
||||||
@@ -82,6 +90,19 @@ std::string format_detail(const StatusSnapshot& s);
|
|||||||
/// cannot track the stream. Moved here from ui.cpp, which still calls it.
|
/// cannot track the stream. Moved here from ui.cpp, which still calls it.
|
||||||
std::string format_rate(double hz);
|
std::string format_rate(double hz);
|
||||||
|
|
||||||
|
/// A sample count as "67.1 M": `%.3g` of the count in the unit it reads shortest in (k, M, G; decimal,
|
||||||
|
/// like `--buffer-samples`). Under a thousand it is the bare number.
|
||||||
|
std::string format_samples(std::uint64_t n);
|
||||||
|
|
||||||
|
/// A byte count in binary units, "168 MiB" / "8 GiB", `%.3g` so the width does not track the figure.
|
||||||
|
std::string format_bytes(std::uint64_t bytes);
|
||||||
|
|
||||||
|
/// The sentence the source form, the status popover and the exit dump share for a sample budget:
|
||||||
|
/// "100 M requested, 67.1 M used", and " (limited by the 1 GiB memory limit)" when the limit clipped it.
|
||||||
|
/// `requested` is the user's number, `used` the planned slots per signal.
|
||||||
|
std::string describe_buffer(std::uint64_t requested, std::uint64_t used, bool memory_limited,
|
||||||
|
std::uint64_t memory_limit_bytes);
|
||||||
|
|
||||||
/// An elapsed time, stepping to a coarser unit rather than growing digits, so its width
|
/// An elapsed time, stepping to a coarser unit rather than growing digits, so its width
|
||||||
/// cannot track the stream. Moved here from ui.cpp.
|
/// cannot track the stream. Moved here from ui.cpp.
|
||||||
std::string format_elapsed(double seconds);
|
std::string format_elapsed(double seconds);
|
||||||
|
|||||||
+50
-3
@@ -4397,7 +4397,16 @@ gboolean tick(gpointer user) {
|
|||||||
snap.channels = channel_count;
|
snap.channels = channel_count;
|
||||||
snap.plot1_traces = bound;
|
snap.plot1_traces = bound;
|
||||||
snap.plot1_samples = samples;
|
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.span_s = span_s;
|
||||||
snap.time_stride = time_stride;
|
snap.time_stride = time_stride;
|
||||||
snap.have_event = app->last_latch_us != 0;
|
snap.have_event = app->last_latch_us != 0;
|
||||||
@@ -5281,8 +5290,9 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
if (app->waveform_name.empty()) {
|
if (app->waveform_name.empty()) {
|
||||||
const Config& cfg = app->model->config();
|
const Config& cfg = app->model->config();
|
||||||
char buf[192];
|
char buf[192];
|
||||||
std::snprintf(buf, sizeof(buf), "connecting to %s:%u -- planning the %.0f MB buffer",
|
std::snprintf(buf, sizeof(buf), "connecting to %s:%u -- planning the buffer: %s samples per signal",
|
||||||
cfg.server.c_str(), unsigned(cfg.port), cfg.buffer_mb);
|
cfg.server.c_str(), unsigned(cfg.port),
|
||||||
|
format_samples(buffer_ticks(cfg.buffer_samples)).c_str());
|
||||||
set_notice(*app, buf, 6);
|
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().values(i).produced()),
|
||||||
static_cast<unsigned long long>(model.live().channel(i).size()));
|
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
|
// `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
|
// destroyed (the PlotArea wrapper dies with its widget): one figure, one render per
|
||||||
// frame, so it is the same count for every row.
|
// frame, so it is the same count for every row.
|
||||||
|
|||||||
+22
-1
@@ -13,7 +13,7 @@
|
|||||||
show-apply-button is off here: the write is a few bytes to one file, so a keystroke is not worth
|
show-apply-button is off here: the write is a few bytes to one file, so a keystroke is not worth
|
||||||
batching). The two settings that cannot take effect immediately — the cache directory and relative
|
batching). The two settings that cannot take effect immediately — the cache directory and relative
|
||||||
time, both of which are planned before any source is known — say so in their subtitles instead of
|
time, both of which are planned before any source is known — say so in their subtitles instead of
|
||||||
being held back by a button.
|
being held back by a button. The memory limit does take effect at once: a running buffer re-plans.
|
||||||
|
|
||||||
Nothing here carries a value: `ui_prefs.cpp` fills the rows from `Settings` when the dialog opens,
|
Nothing here carries a value: `ui_prefs.cpp` fills the rows from `Settings` when the dialog opens,
|
||||||
and writes it back on every change. This file is the shape, that is the state.
|
and writes it back on every change. This file is the shape, that is the state.
|
||||||
@@ -77,6 +77,27 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="AdwPreferencesGroup">
|
||||||
|
<property name="title" translatable="yes">Sample buffer</property>
|
||||||
|
<child>
|
||||||
|
<object class="AdwSpinRow" id="memory_row">
|
||||||
|
<property name="title" translatable="yes">_Memory limit (MB)</property>
|
||||||
|
<property name="use-underline">True</property>
|
||||||
|
<property name="subtitle" translatable="yes">Hard ceiling on the memory of the sample rings, all signals together. A source asks for a number of samples per signal; if that does not fit under this limit it is halved until it does, and the status line says so. Applies at once: a running buffer is re-planned, which clears its history.</property>
|
||||||
|
<property name="adjustment">
|
||||||
|
<object class="GtkAdjustment">
|
||||||
|
<property name="lower">16</property>
|
||||||
|
<property name="upper">1048576</property>
|
||||||
|
<property name="step-increment">256</property>
|
||||||
|
<property name="page-increment">1024</property>
|
||||||
|
<property name="value">8192</property>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
|||||||
@@ -85,15 +85,17 @@
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="AdwSpinRow" id="buffer_row">
|
<object class="AdwSpinRow" id="buffer_row">
|
||||||
<property name="title" translatable="yes">_Buffer MB</property>
|
<property name="title" translatable="yes">_Samples per signal (millions)</property>
|
||||||
<property name="use-underline">True</property>
|
<property name="use-underline">True</property>
|
||||||
<property name="subtitle" translatable="yes">sample buffer per source, applied when this source is opened</property>
|
<property name="subtitle" translatable="yes">samples kept for every signal, rounded down to a power of two; applied when this source is opened</property>
|
||||||
|
<property name="digits">3</property>
|
||||||
<property name="adjustment">
|
<property name="adjustment">
|
||||||
<object class="GtkAdjustment">
|
<object class="GtkAdjustment">
|
||||||
<property name="lower">1</property>
|
<property name="lower">0.001</property>
|
||||||
<property name="upper">65536</property>
|
<property name="upper">4096</property>
|
||||||
<property name="step-increment">1</property>
|
<property name="step-increment">1</property>
|
||||||
<property name="value">64</property>
|
<property name="page-increment">10</property>
|
||||||
|
<property name="value">10</property>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
|
|||||||
+5
-7
@@ -103,7 +103,7 @@ LayoutState collect_layout(App& app) {
|
|||||||
// *startup* parameter -- the rings are allocated before any stream is known -- so a configuration
|
// *startup* parameter -- the rings are allocated before any stream is known -- so a configuration
|
||||||
// is the only place it can be remembered per source, and the chooser applies it on open.
|
// is the only place it can be remembered per source, and the chooser applies it on open.
|
||||||
st.comment = app.config_comment;
|
st.comment = app.config_comment;
|
||||||
st.buffer_mb = app.model->config().buffer_mb;
|
st.buffer_samples = app.model->config().buffer_samples;
|
||||||
st.relative_time = app.model->config().relative_time;
|
st.relative_time = app.model->config().relative_time;
|
||||||
st.capture_packets = app.model->config().capture_packets;
|
st.capture_packets = app.model->config().capture_packets;
|
||||||
return st;
|
return st;
|
||||||
@@ -135,8 +135,8 @@ void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
|||||||
// a configuration that appears to half-apply.
|
// a configuration that appears to half-apply.
|
||||||
const Config& cfg = app.model->config();
|
const Config& cfg = app.model->config();
|
||||||
std::string later;
|
std::string later;
|
||||||
if (st.buffer_mb > 0.0 && st.buffer_mb != cfg.buffer_mb) {
|
if (st.buffer_samples > 0 && st.buffer_samples != cfg.buffer_samples) {
|
||||||
later += "buffer " + std::to_string(static_cast<int>(st.buffer_mb)) + " MB";
|
later += "buffer " + format_samples(st.buffer_samples) + " samples";
|
||||||
}
|
}
|
||||||
if (st.relative_time != cfg.relative_time) {
|
if (st.relative_time != cfg.relative_time) {
|
||||||
later += later.empty() ? "" : ", ";
|
later += later.empty() ? "" : ", ";
|
||||||
@@ -237,9 +237,7 @@ void config_dialog_refresh(ConfigDialog& d) {
|
|||||||
}
|
}
|
||||||
std::string sub = std::to_string(st.rows.size()) + " plots, " + std::to_string(signals) +
|
std::string sub = std::to_string(st.rows.size()) + " plots, " + std::to_string(signals) +
|
||||||
" signals";
|
" signals";
|
||||||
if (st.buffer_mb > 0.0) {
|
if (st.buffer_samples > 0) sub += ", buffer " + format_samples(st.buffer_samples) + " samples";
|
||||||
sub += ", buffer " + std::to_string(static_cast<int>(st.buffer_mb)) + " MB";
|
|
||||||
}
|
|
||||||
if (st.relative_time) sub += ", relative time";
|
if (st.relative_time) sub += ", relative time";
|
||||||
GtkWidget* counts = gtk_label_new(sub.c_str());
|
GtkWidget* counts = gtk_label_new(sub.c_str());
|
||||||
gtk_widget_add_css_class(counts, "dim-label");
|
gtk_widget_add_css_class(counts, "dim-label");
|
||||||
@@ -410,7 +408,7 @@ void apply_layout(App& app, const LayoutState& st) {
|
|||||||
|
|
||||||
// What the session had on, collected and enabled **once**, at the end of this function.
|
// What the session had on, collected and enabled **once**, at the end of this function.
|
||||||
// Each enable re-plans the partition, and a re-plan allocates -- and so touches -- the whole
|
// Each enable re-plans the partition, and a re-plan allocates -- and so touches -- the whole
|
||||||
// of `--buffer-mb`: seven signals one at a time is seven allocations before a single sample
|
// of the planned rings: seven signals one at a time is seven allocations before a single sample
|
||||||
// is kept, which is the ten seconds of blank window a large buffer used to start with. The
|
// is kept, which is the ten seconds of blank window a large buffer used to start with. The
|
||||||
// math channels are not collected here: their enable state is the session's `math` and
|
// math channels are not collected here: their enable state is the session's `math` and
|
||||||
// `set_math_channels()` has already restored it, so listing them again could turn on one
|
// `set_math_channels()` has already restored it, so listing them again could turn on one
|
||||||
|
|||||||
+29
-1
@@ -4,6 +4,7 @@
|
|||||||
#include "ui_prefs.hpp"
|
#include "ui_prefs.hpp"
|
||||||
|
|
||||||
#include "event_store.hpp" // `default_events_dir()`, so the row shows the path the cache uses
|
#include "event_store.hpp" // `default_events_dir()`, so the row shows the path the cache uses
|
||||||
|
#include "status_text.hpp" // `format_bytes()`
|
||||||
#include "ui_app.hpp" // the application's half of the contract, at the bottom of this file
|
#include "ui_app.hpp" // the application's half of the contract, at the bottom of this file
|
||||||
|
|
||||||
#include <adwaita.h>
|
#include <adwaita.h>
|
||||||
@@ -36,6 +37,7 @@ Settings read_rows(const PrefsForm& f) {
|
|||||||
s.event_cache_dir = gtk_editable_get_text(GTK_EDITABLE(f.dir_row));
|
s.event_cache_dir = gtk_editable_get_text(GTK_EDITABLE(f.dir_row));
|
||||||
s.event_cache_max = static_cast<int>(adw_spin_row_get_value(f.keep_row));
|
s.event_cache_max = static_cast<int>(adw_spin_row_get_value(f.keep_row));
|
||||||
s.relative_time = adw_switch_row_get_active(f.relative_row) != FALSE;
|
s.relative_time = adw_switch_row_get_active(f.relative_row) != FALSE;
|
||||||
|
s.memory_limit_mb = static_cast<int>(adw_spin_row_get_value(f.memory_row));
|
||||||
s.theme = theme_name(adw_combo_row_get_selected(f.theme_row));
|
s.theme = theme_name(adw_combo_row_get_selected(f.theme_row));
|
||||||
// The row shows the *effective* directory, so the default is written out in full rather than
|
// The row shows the *effective* directory, so the default is written out in full rather than
|
||||||
// described. Leaving it at that default is not a choice to pin it: the file keeps "unset", and the
|
// described. Leaving it at that default is not a choice to pin it: the file keeps "unset", and the
|
||||||
@@ -63,6 +65,7 @@ void fill_rows(PrefsForm& f, const Settings& s) {
|
|||||||
(s.event_cache_dir.empty() ? default_events_dir() : s.event_cache_dir).c_str());
|
(s.event_cache_dir.empty() ? default_events_dir() : s.event_cache_dir).c_str());
|
||||||
adw_spin_row_set_value(f.keep_row, s.event_cache_max < 0 ? 0.0 : s.event_cache_max);
|
adw_spin_row_set_value(f.keep_row, s.event_cache_max < 0 ? 0.0 : s.event_cache_max);
|
||||||
adw_switch_row_set_active(f.relative_row, s.relative_time ? TRUE : FALSE);
|
adw_switch_row_set_active(f.relative_row, s.relative_time ? TRUE : FALSE);
|
||||||
|
adw_spin_row_set_value(f.memory_row, s.memory_limit_mb);
|
||||||
adw_combo_row_set_selected(f.theme_row, s.theme == "light" ? 1 : (s.theme == "dark" ? 2 : 0));
|
adw_combo_row_set_selected(f.theme_row, s.theme == "light" ? 1 : (s.theme == "dark" ? 2 : 0));
|
||||||
f.filling = false;
|
f.filling = false;
|
||||||
}
|
}
|
||||||
@@ -141,6 +144,7 @@ void on_closed(AdwDialog*, gpointer user) {
|
|||||||
f.dir_row = nullptr;
|
f.dir_row = nullptr;
|
||||||
f.keep_row = nullptr;
|
f.keep_row = nullptr;
|
||||||
f.relative_row = nullptr;
|
f.relative_row = nullptr;
|
||||||
|
f.memory_row = nullptr;
|
||||||
f.theme_row = nullptr;
|
f.theme_row = nullptr;
|
||||||
f.browse = nullptr;
|
f.browse = nullptr;
|
||||||
f.parent = nullptr;
|
f.parent = nullptr;
|
||||||
@@ -155,12 +159,14 @@ void resolve_rows(PrefsForm& f) {
|
|||||||
f.keep_row = ADW_SPIN_ROW(gtk_builder_get_object(f.builder, "keep_row"));
|
f.keep_row = ADW_SPIN_ROW(gtk_builder_get_object(f.builder, "keep_row"));
|
||||||
f.relative_row = ADW_SWITCH_ROW(gtk_builder_get_object(f.builder, "relative_row"));
|
f.relative_row = ADW_SWITCH_ROW(gtk_builder_get_object(f.builder, "relative_row"));
|
||||||
f.theme_row = ADW_COMBO_ROW(gtk_builder_get_object(f.builder, "theme_row"));
|
f.theme_row = ADW_COMBO_ROW(gtk_builder_get_object(f.builder, "theme_row"));
|
||||||
|
f.memory_row = ADW_SPIN_ROW(gtk_builder_get_object(f.builder, "memory_row"));
|
||||||
if (f.dir_row == nullptr || f.keep_row == nullptr || f.relative_row == nullptr ||
|
if (f.dir_row == nullptr || f.keep_row == nullptr || f.relative_row == nullptr ||
|
||||||
f.theme_row == nullptr) {
|
f.theme_row == nullptr || f.memory_row == nullptr) {
|
||||||
f.dir_row = nullptr;
|
f.dir_row = nullptr;
|
||||||
f.keep_row = nullptr;
|
f.keep_row = nullptr;
|
||||||
f.relative_row = nullptr;
|
f.relative_row = nullptr;
|
||||||
f.theme_row = nullptr;
|
f.theme_row = nullptr;
|
||||||
|
f.memory_row = nullptr;
|
||||||
g_clear_object(&f.builder);
|
g_clear_object(&f.builder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,6 +220,7 @@ void prefs_open(PrefsForm& form, GtkWidget* parent, const Settings& current,
|
|||||||
g_signal_connect(form.dir_row, "changed", G_CALLBACK(on_entry_changed), &form);
|
g_signal_connect(form.dir_row, "changed", G_CALLBACK(on_entry_changed), &form);
|
||||||
g_signal_connect(form.keep_row, "notify::value", G_CALLBACK(on_row_notify), &form);
|
g_signal_connect(form.keep_row, "notify::value", G_CALLBACK(on_row_notify), &form);
|
||||||
g_signal_connect(form.relative_row, "notify::active", G_CALLBACK(on_row_notify), &form);
|
g_signal_connect(form.relative_row, "notify::active", G_CALLBACK(on_row_notify), &form);
|
||||||
|
g_signal_connect(form.memory_row, "notify::value", G_CALLBACK(on_row_notify), &form);
|
||||||
g_signal_connect(form.theme_row, "notify::selected", G_CALLBACK(on_row_notify), &form);
|
g_signal_connect(form.theme_row, "notify::selected", G_CALLBACK(on_row_notify), &form);
|
||||||
if (GObject* reset = gtk_builder_get_object(form.builder, "reset_row"); reset != nullptr) {
|
if (GObject* reset = gtk_builder_get_object(form.builder, "reset_row"); reset != nullptr) {
|
||||||
g_signal_connect(reset, "activated", G_CALLBACK(on_reset), &form);
|
g_signal_connect(reset, "activated", G_CALLBACK(on_reset), &form);
|
||||||
@@ -239,9 +246,29 @@ void prefs_probe_change(PrefsForm& form) {
|
|||||||
gtk_editable_set_text(GTK_EDITABLE(form.dir_row), "/tmp/udpscope-prefs-events");
|
gtk_editable_set_text(GTK_EDITABLE(form.dir_row), "/tmp/udpscope-prefs-events");
|
||||||
adw_spin_row_set_value(form.keep_row, 7.0);
|
adw_spin_row_set_value(form.keep_row, 7.0);
|
||||||
adw_switch_row_set_active(form.relative_row, TRUE);
|
adw_switch_row_set_active(form.relative_row, TRUE);
|
||||||
|
adw_spin_row_set_value(form.memory_row, 4096.0);
|
||||||
adw_combo_row_set_selected(form.theme_row, 2);
|
adw_combo_row_set_selected(form.theme_row, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A changed memory limit reaches the running model. Only when it differs from the model's own (every
|
||||||
|
/// preference arrives through the same callback, and a theme change is not a re-plan), and without
|
||||||
|
/// re-planning while a file or a replayed event is what the buffer holds -- clearing those would
|
||||||
|
/// throw away what the user opened, so the limit is stored and the next plan honours it.
|
||||||
|
static void apply_memory_limit(App& app, int mb) {
|
||||||
|
if (app.model == nullptr || mb <= 0) return;
|
||||||
|
const std::uint64_t bytes = static_cast<std::uint64_t>(mb) * 1024u * 1024u;
|
||||||
|
if (bytes == app.model->memory_limit_bytes()) return;
|
||||||
|
const bool holds_a_view = !app.waveform_name.empty() || app.replaying;
|
||||||
|
const bool rebuilt = app.model->set_memory_limit_bytes(bytes, !holds_a_view);
|
||||||
|
std::fprintf(stderr, "[udpscope] memory limit now %s (rebuilt=%d%s)\n",
|
||||||
|
format_bytes(bytes).c_str(), rebuilt ? 1 : 0,
|
||||||
|
holds_a_view ? ", applies to the next plan" : "");
|
||||||
|
std::fflush(stderr);
|
||||||
|
if (rebuilt) {
|
||||||
|
set_notice(app, "memory limit " + format_bytes(bytes) + ": buffer re-planned, history cleared", 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The application's entry point: **Preferences…** in the overflow menu. Kept here rather than in
|
/// The application's entry point: **Preferences…** in the overflow menu. Kept here rather than in
|
||||||
/// `ui.cpp` with the rest of the chrome, because it is the application's half of *this* form's
|
/// `ui.cpp` with the rest of the chrome, because it is the application's half of *this* form's
|
||||||
/// contract -- what a preference changing means to a running scope -- and reading it beside the form is
|
/// contract -- what a preference changing means to a running scope -- and reading it beside the form is
|
||||||
@@ -249,6 +276,7 @@ void prefs_probe_change(PrefsForm& form) {
|
|||||||
void open_preferences(App& app) {
|
void open_preferences(App& app) {
|
||||||
prefs_open(app.prefs, app.window, app.settings, [&app](const Settings& s, bool written) {
|
prefs_open(app.prefs, app.window, app.settings, [&app](const Settings& s, bool written) {
|
||||||
app.settings = s;
|
app.settings = s;
|
||||||
|
apply_memory_limit(app, s.memory_limit_mb);
|
||||||
// Only a failure is announced. A change arrives once per keystroke in the directory row, and a
|
// Only a failure is announced. A change arrives once per keystroke in the directory row, and a
|
||||||
// status line that flashed on each one would be the app shouting about its own bookkeeping.
|
// status line that flashed on each one would be the app shouting about its own bookkeeping.
|
||||||
if (!written) set_notice(app, "could not write the preferences file", 4);
|
if (!written) set_notice(app, "could not write the preferences file", 4);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ struct PrefsForm {
|
|||||||
AdwEntryRow* dir_row = nullptr;
|
AdwEntryRow* dir_row = nullptr;
|
||||||
AdwSpinRow* keep_row = nullptr;
|
AdwSpinRow* keep_row = nullptr;
|
||||||
AdwSwitchRow* relative_row = nullptr;
|
AdwSwitchRow* relative_row = nullptr;
|
||||||
|
AdwSpinRow* memory_row = nullptr;
|
||||||
AdwComboRow* theme_row = nullptr;
|
AdwComboRow* theme_row = nullptr;
|
||||||
/// The folder chooser beside the directory row. A text field alone asks a user to know a path and
|
/// The folder chooser beside the directory row. A text field alone asks a user to know a path and
|
||||||
/// type it correctly; the chooser is how they find one, and what it returns goes into that row -- so
|
/// type it correctly; the chooser is how they find one, and what it returns goes into that row -- so
|
||||||
|
|||||||
+5
-4
@@ -1182,7 +1182,7 @@ gboolean math_probe(gpointer user) {
|
|||||||
// The one state `math_probe` structurally cannot reach: an edit of a graph whose ring has already
|
// The one state `math_probe` structurally cannot reach: an edit of a graph whose ring has already
|
||||||
// *wrapped*. Every edit that probe makes follows a structural change, which replans the buffer and
|
// *wrapped*. Every edit that probe makes follows a structural change, which replans the buffer and
|
||||||
// restarts every ring at absolute 0 -- and at `base == 0` the recompute's two renumbering bugs are
|
// restarts every ring at absolute 0 -- and at `base == 0` the recompute's two renumbering bugs are
|
||||||
// both identities. Run it with a small `--buffer-mb` and give it time to saturate; `base=0` in its
|
// both identities. Run it with a small `--buffer-samples` and give it time to saturate; `base=0` in its
|
||||||
// second line means the run was too short or the buffer too large to have read anything.
|
// second line means the run was too short or the buffer too large to have read anything.
|
||||||
int g_mathwrap_index = -1;
|
int g_mathwrap_index = -1;
|
||||||
double g_mathwrap_y_before = 0.0;
|
double g_mathwrap_y_before = 0.0;
|
||||||
@@ -1231,7 +1231,7 @@ gboolean mathwrap_probe_wait(gpointer user) {
|
|||||||
g_mathwrap_y_before = a->model->live().channel(idx).y_bounds().max;
|
g_mathwrap_y_before = a->model->live().channel(idx).y_bounds().max;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Polled rather than timed: the wrap is a function of the rate and of `--buffer-mb`, and a fixed
|
// Polled rather than timed: the wrap is a function of the rate and of `--buffer-samples`, and a fixed
|
||||||
// delay that fell short would print a passing line having tested nothing.
|
// delay that fell short would print a passing line having tested nothing.
|
||||||
if (base == 0 && ++g_mathwrap_polls < 200) return G_SOURCE_CONTINUE;
|
if (base == 0 && ++g_mathwrap_polls < 200) return G_SOURCE_CONTINUE;
|
||||||
std::fprintf(stderr, "[mathwrap-probe] wrapped: base=%llu slots=%llu (want base > 0)\n",
|
std::fprintf(stderr, "[mathwrap-probe] wrapped: base=%llu slots=%llu (want base > 0)\n",
|
||||||
@@ -5384,9 +5384,10 @@ gboolean configs_probe(gpointer user) {
|
|||||||
const double noise_back = st.measures.empty() ? -1.0 : st.measures.front().options.noise_pct;
|
const double noise_back = st.measures.empty() ? -1.0 : st.measures.front().options.noise_pct;
|
||||||
const double hf_back = st.measures.empty() ? -1.0 : st.measures.front().options.hf_fc;
|
const double hf_back = st.measures.empty() ? -1.0 : st.measures.front().options.hf_fc;
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[configs] round trip read=%d comment='%s' buffer_mb=%.6g relative=%d "
|
"[configs] round trip read=%d comment='%s' buffer_samples=%llu relative=%d "
|
||||||
"capture=%u plots=%zu signals=%d measures=%zu math=%zu noise=%.6g hf=%.6g\n",
|
"capture=%u plots=%zu signals=%d measures=%zu math=%zu noise=%.6g hf=%.6g\n",
|
||||||
read ? 1 : 0, st.comment.c_str(), st.buffer_mb, st.relative_time ? 1 : 0,
|
read ? 1 : 0, st.comment.c_str(),
|
||||||
|
static_cast<unsigned long long>(st.buffer_samples), st.relative_time ? 1 : 0,
|
||||||
st.capture_packets, st.rows.size(), signals, st.measures.size(), st.math.size(),
|
st.capture_packets, st.rows.size(), signals, st.measures.size(), st.math.size(),
|
||||||
noise_back, hf_back);
|
noise_back, hf_back);
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -106,9 +106,21 @@ void update_status_popover(App& app, const udpscope::StatusSnapshot& s) {
|
|||||||
std::snprintf(buf, sizeof(buf), "%zu signals · %zu channels", s.signals, s.channels);
|
std::snprintf(buf, sizeof(buf), "%zu signals · %zu channels", s.signals, s.channels);
|
||||||
gtk_label_set_text(GTK_LABEL(app.popover_label[6]), buf);
|
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,
|
std::snprintf(buf, sizeof(buf), "%s samples/signal · %s · %.0f s span · time x%llu",
|
||||||
static_cast<unsigned long long>(s.time_stride));
|
udpscope::format_samples(s.buffer_samples).c_str(),
|
||||||
|
s.buffer_bytes > 0 ? udpscope::format_bytes(s.buffer_bytes).c_str() : "not planned",
|
||||||
|
s.span_s, static_cast<unsigned long long>(s.time_stride));
|
||||||
gtk_label_set_text(GTK_LABEL(app.popover_label[7]), buf);
|
gtk_label_set_text(GTK_LABEL(app.popover_label[7]), buf);
|
||||||
|
// The reason the figure is not the one asked for, where there is one: a smaller ring than the
|
||||||
|
// source form promised, with the limit that did it, as the label's tooltip.
|
||||||
|
gtk_widget_set_tooltip_text(
|
||||||
|
app.popover_label[7],
|
||||||
|
s.memory_limited
|
||||||
|
? ("limited to " + udpscope::format_samples(s.buffer_samples) + " samples by the " +
|
||||||
|
udpscope::format_bytes(s.memory_limit_bytes) + " memory limit (" +
|
||||||
|
udpscope::format_samples(s.buffer_requested) + " requested)")
|
||||||
|
.c_str()
|
||||||
|
: nullptr);
|
||||||
|
|
||||||
const std::string stored = udpscope::format_rate(s.stored_rate_hz);
|
const std::string stored = udpscope::format_rate(s.stored_rate_hz);
|
||||||
const std::string wire = udpscope::format_rate(s.declared_rate_hz);
|
const std::string wire = udpscope::format_rate(s.declared_rate_hz);
|
||||||
|
|||||||
Reference in new issue
Block a user