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
@@ -1638,6 +1638,35 @@ Measured on the same stream and window: tick mean **1.39 ms** with the pane hold
|
||||
measurements against **0.73 ms** without (595 frames against 610), the pass itself 11 ms every
|
||||
400 ms — and the 82 ms worst tick is the *control's* too, i.e. the publish stall, not this.
|
||||
|
||||
**...and the same pane costs what the *window* costs, which a big time scale buys.** The three
|
||||
fixes above bound *how often* and *how much per sample*; what is left is the arithmetic of the
|
||||
scale: a pass is priced per measured **channel** (one fetch of the window's level-0 buckets, one
|
||||
statistics scan) and per **distinct share** in the pane (one level accumulator, walked over those
|
||||
buckets), and the window's own sample count multiplies both. Measured with the new
|
||||
`[levels-probe]` sweep on the mock at a **15 s** window and `--buffer-mb 4000` (so `time_stride`
|
||||
stays 1 and the window holds 11.8 M samples, 184 k buckets), four signals, one row, on llvmpipe
|
||||
— `measure cost:`'s `each` per recompute, and the tick's mean:
|
||||
|
||||
| pane | `each` | `raw` | tick mean |
|
||||
|---|---|---|---|
|
||||
| empty | — | — | 1.34 ms |
|
||||
| 1 measurement, 1 level, 1 channel | 6.58 ms | 0.1 % | 1.99 ms |
|
||||
| 4 measurements, 4 shares, 1 channel | 10.28 ms | 0.4 % | 2.38 ms |
|
||||
| 16 measurements, 16 shares, 1 channel | 24.55 ms | 1.2 % | 3.86 ms |
|
||||
| 4 measurements, 1 share each, **4 channels** | 35.69 ms | 2.6 % | 4.58 ms |
|
||||
|
||||
So a machine's ten measured signals at a 15 s window is tens of milliseconds a pass, several
|
||||
times a second, on the **tick** — the pane is a *frame* cost and not an ingest one (`gaps` and
|
||||
`packets` stay clean even though each reader holds the model mutex, which is what "the plot is
|
||||
slow with measurements on" actually is). One of those terms was a pure call-overhead defect:
|
||||
pass two folds each bucket into each level through `measure_accumulate_bucket()`, the innermost
|
||||
loop of a walk whose iteration count is the window's buckets *times* the pane's levels, and as an
|
||||
out-of-line function in its own translation unit it cost **30 % of the pass** — sixteen shares on
|
||||
one channel read `24.55 ms` a recompute with every value in `measurements:` unchanged at
|
||||
**17.25 ms** once it was moved into `measure.hpp` as `inline`. The fetch's three out-of-line
|
||||
pyramid accessors per bucket (3.3 ms of an 11.8 M-sample window's 11.6 ms pass) are the next
|
||||
lever, and `raw` is the last one and belongs to the signal rather than to the pane.
|
||||
|
||||
**"Show on plot" draws full-width rulers, and they must not blink.** A level measurement draws an
|
||||
`h_ruler` across the whole plot and a time one a full-height cursor, so a pane holding four
|
||||
measurements can put six lines over the data — that is the feature, and the lines move with the
|
||||
|
||||
Reference in new issue
Block a user