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
@@ -714,6 +714,33 @@ Two lines of the self-check output are assertions in disguise:
|
||||
reported `gaps=73` with a pause against 0 without, which is what made the stall worth measuring;
|
||||
it has not reproduced and did not scale with the publish length — 79 gaps with the pyramid build
|
||||
removed against 73 with it — so it was incidental load on one run. Do not quote it as a cost.)
|
||||
- `[levels-probe] N measurement(s) over K channel(s), shares='...' -> W window(s), L level
|
||||
walk(s)` — then the exit dump's `measure cost:` and `frame cost:` beside it. This is how a
|
||||
pane's cost is read, and the three fields are the three factors: **`W` is the pane's price
|
||||
for the window**, one fetch and one scan per *measured channel*; **`L` is its price for the
|
||||
pane's shares**, one accumulator per distinct one, walked over the window's buckets; and the
|
||||
window's samples are the multiplier on both, so a sweep at one scale says nothing about
|
||||
another. Measured at the user's scale (15 s window, `--buffer-mb 4000` -> `stride=1`, 11.8 M
|
||||
samples in the window = 184 k buckets, four signals, mock at ~0.5 MS/s per signal, llvmpipe),
|
||||
with `UDPSCOPE_PROBE_LEVELS_CHANNELS` set:
|
||||
* `50`, 1 channel -> `1 window, 1 level`, `measure cost: 226 recomputes, 6.58 ms each`, `raw=0.1%`,
|
||||
`tick 1.99 ms` against an empty pane's **1.34**;
|
||||
* `5,10,15,20`, 1 channel -> `4 levels`, **10.28 ms** each, `raw=0.4%`, `tick 2.38 ms`;
|
||||
* the sixteen shares `5..80`, 1 channel -> `16 levels`, **24.55 ms** each, `raw=1.2%`, `tick 3.86 ms`;
|
||||
* `5,10,15,20` over **4 channels** -> 4 windows and 4 levels, **35.69 ms** each, `raw=2.6%`,
|
||||
`tick 4.58 ms`, and still `gaps=0 packets=30580` — the pane is a *tick* cost and not an
|
||||
ingest one, which is the thing to say before blaming the producer.
|
||||
`measure cost:`'s `each` is the pass and `covered`/`recomputes` is the mean window it ran
|
||||
over (11.8 M samples here), so `each` is only comparable between runs at the same window and
|
||||
the same pane; `raw` is the *signal's* share (the mock's `noise` channel reads `4.6%` against
|
||||
its `sine`'s `0.1%` at the same window, because a level inside a bucket's own range has to be
|
||||
walked sample by sample). A pass that reads **tens of ms** on a 15 s window with a handful of
|
||||
measurements is the curve in
|
||||
[invariants-measurements.md](invariants-measurements.md) and not a defect, and the same pane
|
||||
at a 2 s window reads the 1.2--1.6 ms the pane's budget was fixed at. The guard for the walk
|
||||
itself is the exit dump's `frame cost:`: sixteen shares on one channel read `24.55 ms` a pass
|
||||
and `3.86 ms` of tick mean before `measure_accumulate_bucket()` was made `inline`, and
|
||||
**17.25 ms** a pass after it, with every value in `measurements:` unchanged.
|
||||
- `trace ink: N paints checked, C collapsed, S staircase, H short, worst cover P%, worst
|
||||
drawn-vs-quoted D px over R raw paints` plus a
|
||||
`[ink] paint N <flags> mode=.. axis=[..] n=.. idx=[..) spp=.. level=.. pts=.. kept=..
|
||||
|
||||
Reference in new issue
Block a user