perf: a measurement pane is priced by its window, and its fold is inline
A pane is recomputed over the whole visible window on a cadence, and at a 15 s window (11.8 M samples, 184 k level-0 level buckets, `--buffer-mb 4000` so the stride stays 1) that is priced per *measured channel* and per *distinct share*. Measured with the new `[levels-probe]` sweep, per recompute: one window with one level 6.58 ms, four shares 10.28 ms, sixteen shares 24.55 ms, and four measurements one share each over four channels **35.69 ms** -- against a tick mean of 1.34 ms with an empty pane and 4.58 ms with that one. That is the reported "very slow with multiple measurements": the pass runs inside `tick()`, so it is a *frame* cost, and not an ingest one -- `gaps` and `packets` stay clean even though each `MeasureWindowReader` holds the model mutex for its whole life, because a socket buffer absorbs a few tens of milliseconds of producer. The largest single term inside a pass was a call. `measure_accumulate_bucket()` folds one bucket into one level, and it is the innermost loop of a walk whose iteration count is the window's buckets *times* the pane's levels; it lived out of line in `measure.cpp`, so every one of those iterations was a cross-translation-unit call. Moving it, and the crossing state it updates, into `measure.hpp` as `inline` takes sixteen shares on one channel from 24.55 to **17.25 ms** a recompute -- with every value in the dump's `measurements:` unchanged. Two levers remain and both are measured: the fetch's three out-of-line pyramid accessors plus a 48-byte copy per bucket (3.3 ms of an 11.8 M-sample window's 11.6 ms pass) needs a combined accessor upstream, and `raw` is the *signal's* share (4.6 % on the mock's noisy channel against 0.1 % on its sine), because a level inside a bucket's own `[lo, hi]` has to be walked sample by sample. Bisected with the pane populated (the earlier bisect had it empty, which cannot see this): the parent of the recent range and HEAD read **8.29** and **7.92 ms** a recompute for the same four-type pane at the same window, and neither of the two commits between them touches `ui_measure.cpp` or `measure.cpp` at all. So the cost is the scale and not a regression, and the fix makes a pass cheaper rather than removing one -- the plot's own per-frame cost was checked the same way (four commits, equal within 3 %, and unchanged by the window, the ring size and the visible sample count). `UDPSCOPE_PROBE_LEVELS_MS` with `UDPSCOPE_PROBE_LEVELS` and `UDPSCOPE_PROBE_LEVELS_CHANNELS` is the sweep; `docs/claude/invariants-measurements.md` records the curve and the inline rule, `verification.md` and `probe-readings.md` the probe and what its numbers must read, and the README the pane's cost at this scale.
This commit is contained in:
1 parent
0a209becb1
commit
fc4700d1c9
9 files changed
+242
-61
No files matched your search
@@ -1326,6 +1326,71 @@ gboolean measure_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_LEVELS_MS`: puts a pane of **N** measurements on screen -- one `above` per
|
||||
/// share in `UDPSCOPE_PROBE_LEVELS` (comma separated, default `50`) -- spread in turn over the
|
||||
/// first `UDPSCOPE_PROBE_LEVELS_CHANNELS` channels (default 1), and says how many *windows* and
|
||||
/// *levels* the pass then had to walk.
|
||||
///
|
||||
/// The pane's cost has three factors and a count alone names none of them: the window's samples
|
||||
/// (one fetch and one scan per **measured channel**, whatever the pane holds), the number of
|
||||
/// *distinct levels* in it (measurements that share a share share the walk), and the rows that
|
||||
/// are only labels over the two. `50,50,50,50` is four rows and one walk; `5,10,15,20` is four
|
||||
/// rows and four; and the same four shares on four signals is four windows *and* four walks.
|
||||
/// This probe is the only way to vary the three apart, which is what a report of "it is slow
|
||||
/// with many measurements" needs before it is a number.
|
||||
gboolean levels_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
std::vector<std::string> names;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
for (const ChannelInfo& c : a->model->channels()) names.push_back(c.name);
|
||||
}
|
||||
if (names.empty()) return G_SOURCE_REMOVE;
|
||||
const char* chan_env = g_getenv("UDPSCOPE_PROBE_LEVELS_CHANNELS");
|
||||
const long want_channels = chan_env != nullptr ? std::atol(chan_env) : 1;
|
||||
const std::size_t channels =
|
||||
want_channels > 0 ? std::min<std::size_t>(static_cast<std::size_t>(want_channels),
|
||||
names.size())
|
||||
: 1u;
|
||||
const char* list = g_getenv("UDPSCOPE_PROBE_LEVELS");
|
||||
const std::string shares = list != nullptr && list[0] != '\0' ? list : "50";
|
||||
std::size_t rows = 0;
|
||||
for (std::size_t at = 0; at <= shares.size();) {
|
||||
const std::size_t comma = shares.find(',', at);
|
||||
const std::string field =
|
||||
shares.substr(at, comma == std::string::npos ? std::string::npos : comma - at);
|
||||
at = comma == std::string::npos ? shares.size() + 1u : comma + 1u;
|
||||
char* end = nullptr;
|
||||
const double pct = std::strtod(field.c_str(), &end);
|
||||
if (end == field.c_str() || !(pct >= 0.0)) continue;
|
||||
Measure m;
|
||||
m.signal = names[rows % channels];
|
||||
m.type = MeasureType::Above;
|
||||
m.options.threshold_pct = pct;
|
||||
a->measures.push_back(std::move(m));
|
||||
++rows;
|
||||
}
|
||||
rebuild_measures_pane(*a);
|
||||
update_measures(*a, /*force=*/true, /*accumulate=*/true);
|
||||
// What that pass walked: the level accumulators the pane deduplicated to, and the windows they
|
||||
// hang off. Read without the model lock -- `measure_samples` is the *tick*'s own state, not the
|
||||
// producer's.
|
||||
std::size_t levels = 0;
|
||||
std::size_t windows = 0;
|
||||
for (const App::MeasureWindow& w : a->measure_samples) {
|
||||
++windows;
|
||||
levels += w.pct.size();
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[levels-probe] %zu measurement(s) over %zu channel(s), shares='%s' -> %zu "
|
||||
"window(s), %zu level walk(s)\n",
|
||||
rows, channels, shares.c_str(), windows, levels);
|
||||
std::fflush(stderr);
|
||||
g_timeout_add(250, measure_trace, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_MEASUREPIN_MS`: on a **still frame** the pane describes the frame and not the
|
||||
/// viewport, so a pan or a zoom changes no number -- and on a live view the numbers still follow
|
||||
/// their window.
|
||||
|
||||
Reference in new issue
Block a user