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
@@ -349,6 +349,66 @@ bool measure_accumulate_bucket(const MeasureBucket& bucket,
|
||||
const std::function<double(std::size_t)>& time_of,
|
||||
MeasureLevel& level);
|
||||
|
||||
inline void enter_side(MeasureLevel& acc, std::size_t e, double t) {
|
||||
const std::size_t other = 1 - e;
|
||||
if (acc.enters[e] == 0) acc.first_enter[e] = t;
|
||||
acc.last_enter[e] = t;
|
||||
++acc.enters[e];
|
||||
if (acc.open[other]) {
|
||||
acc.width_sum[other] += t - acc.open_at[other];
|
||||
++acc.widths[other];
|
||||
acc.open[other] = false;
|
||||
}
|
||||
acc.open_at[e] = t;
|
||||
acc.open[e] = true;
|
||||
}
|
||||
|
||||
inline bool measure_accumulate_bucket(const MeasureBucket& k,
|
||||
const std::function<double(std::size_t)>& time_of,
|
||||
MeasureLevel& acc) {
|
||||
if (k.count == 0) return true;
|
||||
// A non-finite sample in the bucket would make the edge's position a guess: the raw walk takes
|
||||
// the first *finite* sample of the new side, and an aggregate cannot say which that is.
|
||||
if (k.count != k.span) return false;
|
||||
// The *sums* need only the bare level: strictly on one side of it, so every sample decides the
|
||||
// same bin and none is *at* it, which is what makes the fold exact rather than an approximation.
|
||||
const bool above = k.lo > acc.level;
|
||||
const bool below = k.hi < acc.level;
|
||||
if (!above && !below) return false;
|
||||
const std::size_t e = above ? 1u : 0u;
|
||||
// The band is deliberately *not* folded into the test above. Tightening the whole condition to
|
||||
// `level +/- h` refuses every bucket lying inside the band -- a tenth of the amplitude either side
|
||||
// of the mid level is where a waveform moves fastest, so those are exactly the buckets a crossing
|
||||
// passes through -- which hands the raw walk a share of the window rather than a bucket or two per
|
||||
// crossing, and breaks the "`raw` stays at a fraction of a percent of `covered`" invariant.
|
||||
// Instead: a bucket on one side of the level can always be *binned*, and only a *side change*
|
||||
// whose position is inside the bucket is refused.
|
||||
const double top = acc.level + acc.hysteresis;
|
||||
const double bot = acc.level - acc.hysteresis;
|
||||
// Does the bucket reach past the band at all? One wholly inside it decides no side, exactly as a
|
||||
// raw sample inside it decides none, so the walk state is left alone.
|
||||
const bool reaches = above ? (k.hi > top) : (k.lo < bot);
|
||||
const bool change = reaches && acc.have_side && e != (acc.side ? 1u : 0u);
|
||||
// A bucket that *enters* a side while straddling its own threshold holds the crossing somewhere
|
||||
// inside itself, and only the raw walk can say where. One wholly past the threshold holds it at
|
||||
// the bucket's first sample, exactly, which is where the raw walk would have found it.
|
||||
const bool fully_outside = above ? (k.lo > top) : (k.hi < bot);
|
||||
// Nothing may be folded before this point: a refusal hands the bucket to `measure_accumulate()`,
|
||||
// which adds the same samples again. Sums added above the refusal double-counted every opened
|
||||
// bucket -- which read as `top` 0.614304 on a unit sine against the 2/pi a mean over a half then
|
||||
// gave, and which is now the sums `Top`/`Bottom` fall back to on a window with no histogram, so
|
||||
// a wrong one is a wrong answer waiting for that window.
|
||||
if (change && !fully_outside) return false;
|
||||
acc.sum[e] += k.sum;
|
||||
acc.count[e] += k.count;
|
||||
if (change) enter_side(acc, e, time_of(k.offset));
|
||||
if (reaches) {
|
||||
acc.side = e != 0;
|
||||
acc.have_side = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The levels a type reads, as shares of the window's own min..max, **lower first**, or 0 when it
|
||||
/// reads none (the pure statistics, answered from `MeasureStats` alone). `pct` must hold two: a
|
||||
/// level type fills `pct[0]` and answers 1, `Between` fills both and answers 2.
|
||||
|
||||
Reference in new issue
Block a user