fix: a recompute that renumbered its ring, and readouts frozen under a zoom
Three defects, all of them a window describing data that is not there. A math edit refilled the derived ring with `clear()`, which renumbers it from 0, leaving `produced()` short by the inputs' retained base. Since `live_window_locked()` clamps against every channel's `produced()`, that one lagging channel answered `n = 0` for the live view, every pause view and every trigger capture until a whole ring of fresh data had gone by -- the reported freeze, invisible until the ring first wraps, hence its intermittence. It now restarts with `clear_to(base)`, and `ChannelBuffer::republish_from(base)` puts the published window back: `acquire()` is incremental, so a rewrite in place published nothing and the pyramid went on describing the samples the edit replaced. The trigger's channel is an index, re-resolved by name after a renumbering -- but only in `rebuild_enabled_locked()`, so a math add or delete went through `set_math_channels()` with no restore, the index was clamped to -1, and `process()` returned at once while the panel still named the channel. The restore moves into `rebuild_channels()`, which *is* the renumbering, so no caller can forget it. And `update_measures()` suppressed its "the window moved" test while an event was held -- the one mode where a gesture is the only thing that can move it. Zooming a latched event left the pane describing the whole captured event. A pinned window needs no exemption: it lands on the range it already measured and fails the 2 % test by itself. The checks that missed all three: `UDPSCOPE_PROBE_MATHWRAP_MS` edits a graph whose ring has already wrapped, which `math_probe` structurally cannot reach; `[measure-trace]` now prints the mode, the window on screen and the window the numbers describe; and the dump's `marks_in_window` counts against the window on screen instead of against the one the marks were computed over, where it was a tautology. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
074cb611bc
commit
d91957a2fe
15 files changed
+374
-45
No files matched your search
@@ -259,13 +259,27 @@ Two lines of the self-check output are assertions in disguise:
|
||||
**The reading that must hold is `marks_in_window=K/K`**: every *time* mark inside the window
|
||||
on screen. Both ranges are on the **stream's** clock, which is the space `measure_window` and
|
||||
the time marks are stored in -- a live axis is a fixed `[-window, 0]`, so comparing display x
|
||||
would read no drift whatever the data did.
|
||||
would read no drift whatever the data did. `win` is the window as of the last **tick**
|
||||
(`App::measure_view_window`) and is deliberately not read back here: the dump runs from the
|
||||
destroy handler, by which point the exit probe has switched the trigger on, and
|
||||
`stream_at_x0()` then answers for a mode no frame was ever drawn in -- which read as a 45000 %
|
||||
drift on a run whose every frame was exact. And the count is against `win`, not against
|
||||
`measure_window`, which contains every mark by construction: read that way it was a tautology
|
||||
that printed `2/2` while the pane described a window a forty-fold zoom away from the one
|
||||
displayed. On a window **shorter than the refresh interval** the count rides the same sawtooth
|
||||
`drift` does -- the 50 ms window the zoom sweep leaves behind can lag far enough that *every*
|
||||
time mark is off screen, so `0/K` there is read beside `drift` and not alone. The strict
|
||||
reading belongs to the documented 2 s run, and to a **held** window in any run: a latched
|
||||
event does not scroll, so `[measure-trace]` must show `for=` equal to `swin=` exactly for as
|
||||
long as the mode stays `held`, zoom and pan included.
|
||||
`drift` itself is a **single sample of a sawtooth** and is not an assertion on its
|
||||
own: a live view scrolls between recomputes, so it rises to one `kMeasureRefreshUs` of scroll
|
||||
and drops to zero at each pass. The figure is `(|Δmin| + |Δmax|) / span`, so a pure scroll
|
||||
counts **twice** and the top of the sawtooth is `2 × refresh / window`: ~10 % on a 2 s window
|
||||
at the 100 ms cadence, and a few *hundred* percent on a window shorter than the interval (a
|
||||
50 ms window reads 254 % for 63.6 ms of scroll, and its ceiling is 400 %). Compute the ceiling
|
||||
counts **twice** and the top of the sawtooth is `2 × (refresh + one frame) / window` -- the
|
||||
frame term is not noise under `xvfb`, where a frame costs 30--70 ms: ~10 % on a 2 s window at
|
||||
the 100 ms cadence with instant frames, and **15.8 %** measured on the documented run, which is
|
||||
one interval plus one 58 ms frame. On a window shorter than the interval it is a few *hundred*
|
||||
percent (a 50 ms window reads 254 % for 63.6 ms of scroll, and its ceiling is 400 %). Compute the ceiling
|
||||
for the window the run actually used before calling a figure large. A `0.0000%`
|
||||
is a dump that landed just after a pass, not a better state. Read it against the recompute count:
|
||||
*more* recomputes with a *higher* drift sample is the sawtooth, while a drift that exceeds one
|
||||
@@ -1947,6 +1961,35 @@ channels and an edit does not move it, so the store survives: the count *grows*
|
||||
while the edit runs) and never falls to 0, which is what a rebuild would do. Every channel then still reads
|
||||
`N samples vs M time slots` with `N == M`, so no channel was left behind either.
|
||||
|
||||
## `[mathwrap-probe]`
|
||||
|
||||
```
|
||||
[mathwrap-probe] installed 'math wrap' on 'sine': channel=1
|
||||
[mathwrap-probe] wrapped: base=864 slots=66400 (want base > 0)
|
||||
[mathwrap-probe] after edit: produced=72400 slots=72400 (want equal) y=[-0.5000,0.5000] (want +-0.5, was +-2.0000)
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_MATHWRAP_MS`, and it must be run with a **small `--buffer-mb`** and given time to
|
||||
saturate it: `--buffer-mb 1` against `tools/mock_udpstreamer.py --rate 20000 --batch 200`, with
|
||||
`UDPSCOPE_PROBE_MATHWRAP_MS=1500 UDPSCOPE_EXIT_AFTER_MS=25000`, reads the lines above. It installs one
|
||||
`Gain` graph of 2 over the mock's unit sine, polls (100 ms) until the ring has **wrapped**, and only then
|
||||
edits the gain to 0.5 — the same channel list with one node changed, which is the in-place recompute and
|
||||
not a replan.
|
||||
|
||||
`base > 0` on the second line is the precondition and not a result: at `base == 0` both readings below are
|
||||
identities, so **`base=0` means the run tested nothing** — it was too short, or the buffer too large. This
|
||||
is also why `math_probe` cannot stand in for this one: every edit it makes follows a structural change,
|
||||
which replans the buffer and restarts every ring at absolute 0.
|
||||
|
||||
The third line is one reading per half of `recompute_math_locked()`. `produced` against `slots` is the
|
||||
derived ring's absolute count: short by exactly `base` is the renumbering `clear_to()` exists to prevent,
|
||||
and `live_window_locked()` clamps against *every* channel's `produced()`, so that one lagging channel is
|
||||
`n = 0` for the live view, every pause view and every trigger capture — the reported freeze. `y` is the
|
||||
**pyramid's**, so it describes the published window rather than the samples, and the edit *shrinks* the
|
||||
gain for that reading to exist: a pyramid left unpublished still describes the replaced ±2 samples, a
|
||||
window holding both quotes the larger, and a *growing* gain would therefore read correct either way.
|
||||
`y=[-2,2]` here is `republish_from()` missing.
|
||||
|
||||
## `[noise-probe]`
|
||||
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user