fix: a still frame's measurements are the frame's, not the viewport's
A measurement is a statistic of the window the pane is *about*, and on a pinned view -- a held event, a paused stream, an opened file -- that is the frame, not the part of it on screen. The pane followed the viewport instead, so a wheel on a latched event moved `average` and `top`: the reported "when measuring a still frame, moving the zoom makes measurements such as average or top change". Nothing about the data had changed; the user was only looking closer. The window is `RowSet::pinned_range()` while the pin frames the view and the viewport otherwise, and the choice is made in `update_measures()` -- the call site -- rather than inside `MeasureWindowReader`, because it is *booked* as well as read: that window is what the "it moved" test compares against `App::measure_window`, so a gesture inside the pin spends no recompute either, and a reader that chose its own would leave a pinned view recomputing every gesture frame for a number that cannot change. `pinned_range()` is now the one expression of the pin's extent, shared with the framing (`hold_pinned_window()`, `fit_x()`), and the reader is handed the window instead of reading the plot. Live is unchanged: there the window really moves and the statistic follows it, and the accumulator's rule about what resets the history is about neither. `UDPSCOPE_PROBE_MEASUREPIN_MS` is the guard, three legs of before/after a gesture on the row's own axis. On a latched event zoomed to a quarter (`view=[-1..1]` -> `[-0.25..0.25]`, `pin` unchanged): `same=4/4 for_same=1 pass_delta=0`, every value bit-identical -- `average=0.000000 top=0.957975 peak to peak=2.000000 amplitude=1.915950` both sides. The same on a pause taken from it. Since the key `for=` follows `pin=` now, `[measure-trace]` gained the field, so a held run reads `for==pin` with `swin` free to sit inside it. On a live view a zoom to an eighth reads `same=0/4 for_same=0 pass_delta=3 span_after=0.125000 follows=1` with the values moving -- the case the rule must not swallow. Before the fix the same probe reads `same=1/4 for_same=0 pass_delta=1 span_after=0.500000` on the held event (`top` `0.957975` -> `0.957966`) and `same=0/4 span_after=0.125000` on the pause (`top` `0.957944` -> `0.358614`). Live reads the same either way. Also fixed, found by the pause leg: pausing while an event is *held* froze an **empty** picture. `on_pause_toggled()` read `stream_at_x0()` after setting the pause state, and that accessor follows the display -- `paused` with no pause view published yet answers `-display_epoch()`, which on a held event is 0, so the cut searched the ring at edge-relative x, found nothing, and the frame bound an empty snapshot: every measurement `--`, every ruler gone (`ev=650001` on the probe's line now, `ev=0` before). The offset and the window are read first, which is the offset the axis is actually displaying. The live case is unaffected, which is why the pause probe, which pauses a live view, never saw it. Docs corrected where they asserted the old half-truth: the invariants now separate the accumulator's rule (unchanged) from the window a value is computed over (this change), the trigger/pinning file names the pin as load-bearing for both the framing and the numbers and records the pause ordering rule, `verification.md`/`probe-readings.md` describe the new probe and `pin=`, and the README and the manual say what a still frame measures. Checked: `cmake --build build --clean-first -j4` 0 errors and the documented three warnings; `make -C build test-ui` 13 ok; `make -C build test-help` green; an exit-dump run 0 Gtk-WARNING/Gtk-CRITICAL and `gtk warnings: 0`; `[measurepin-probe]`, `[pan-probe]` (`held=1`, `allowed=1`, `limited=1`), `[pause-probe]` (`event_size` non-zero exactly while paused, `axis` inside `frozen_x`, `skips` 0 live / 30 paused), `[stats-probe]` (`dn=1` across the pause), `[zoom-probe]` (`gap_px=0.0` at every step) and `[measure-probe]` + the dump's `measures window:` (all-types run `marks_in_window=4/4`, `pin=off` live) on the fast mock. `measure cost:` is unchanged in kind: `raw=37298 (0.0%)` of `covered=121704537`, 1.04 ms a pass -- the runs that read higher are the probe's own zoomed-to-a-slice windows, where the window is legitimately shorter than a bucket.
This commit is contained in:
1 parent
d91957a2fe
commit
0a209becb1
13 files changed
+719
-114
No files matched your search
@@ -253,9 +253,10 @@ Two lines of the self-check output are assertions in disguise:
|
||||
seconds, and the rest the channel's unit. `switch_on` and `reset_sensitive` are the panel's *frame* read back: the switch widget against
|
||||
`measure_stats_on`, which the code drives, and the Reset button that follows it. Either disagreeing with
|
||||
`[stats-probe] on=` is a `.ui` that lost the widget, or lost the wiring to it; `pane_rows=R` is what
|
||||
proves the readout rows landed in the frame's `measures_list` rather than nowhere. `marks_shown` counts two per shown measurement
|
||||
(a min/max showing both its marks counts two of its own). `measures window:` is the
|
||||
timeliness assertion -- the window the numbers were computed over against the one on screen.
|
||||
`marks_shown` counts two per shown measurement
|
||||
(a min/max showing both marks counts two of its own). `measures window:` is the
|
||||
timeliness assertion -- the window the numbers were computed over against the one that *should*
|
||||
be on screen.
|
||||
**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
|
||||
@@ -263,15 +264,22 @@ Two lines of the self-check output are assertions in disguise:
|
||||
(`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
|
||||
drift on a run whose every frame was exact. `pin` is the same tick's pin (`App::measure_pin_window`,
|
||||
`off` when nothing was pinned) and it is the **target** while the view is pinned: a measurement
|
||||
on a still frame describes the frame, so `drift` there is `results_for` against the *pin*, and a
|
||||
zoom that leaves the screen showing a slice of that frame is not drift. Read against `win` -- the
|
||||
old rule -- a zoomed held event would report the intended behaviour as the defect it was once
|
||||
reported as. The count is still against `win`, the window **on screen**, not against
|
||||
`measure_window`/`pin`, which contain 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
|
||||
displayed. A *time* mark may legitimately sit outside `win` on a pinned view that has been zoomed
|
||||
into the pin (the mark is a position in the window the result describes, and that window is the
|
||||
frame), so the strict `K/K` reading belongs to a live view and to a pinned view left at its pin.
|
||||
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.
|
||||
time mark is off screen, so `0/K` there is read beside `drift` and not alone. `0/0` is a pane
|
||||
whose types carry no time marks at all (`average`, `top`, `amplitude` and `peak to peak` draw
|
||||
rulers only), and is not a reading about marks.
|
||||
`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
|
||||
@@ -284,9 +292,11 @@ Two lines of the self-check output are assertions in disguise:
|
||||
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
|
||||
refresh interval of scroll, or marks outside the window, is the defect. A measurement is
|
||||
recomputed when the window moves (2 % of its span) or the held/live state changes, never on a
|
||||
wall-clock timer: a timer is what let a marker sit outside the viewport and a dragged one
|
||||
survive a frame. **`marks_selectable` must be 0**: the
|
||||
recomputed when the window **it describes** moves (2 % of its span) or the held/live state
|
||||
changes, never on a wall-clock timer: a timer is what let a marker sit outside the viewport and a
|
||||
dragged one survive a frame, and it is the window it describes -- the viewport live, the pin while
|
||||
pinned -- so that a gesture on a still frame spends no recompute at all. **`marks_selectable`
|
||||
must be 0**: the
|
||||
measurement marks are created unselectable (`Plot::set_cursor_selectable()`, which the
|
||||
library gained for this), read back out of the plot here -- re-placing a marker every frame
|
||||
does **not** stop a drag, because the controller holds it for the gesture's life and the
|
||||
@@ -1152,6 +1162,58 @@ Four line kinds, and the reading is the third:
|
||||
most and `0.986495` on the rest. **A line's `--` on a *lagging* trace is the trace's, not the
|
||||
reading's** — the pane is refreshed on a cadence, so quote an earlier line beside its own `view=`.
|
||||
|
||||
## `[measurepin-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_MEASUREPIN_MS`. The reading for **what window a measurement describes**: on a still
|
||||
frame the pane is about the *frame*, so a pan or a zoom changes no number, and on a live view the
|
||||
numbers still follow the window. It installs `measure_probe`'s own four types on the first *channel*
|
||||
(`average`, `top`, `peak to peak`, `amplitude`), arms the trigger in **single** mode, and runs three
|
||||
legs — each one reading the pane before and after a **gesture** written on the row's own axis, which
|
||||
is what a drag, a wheel or a box zoom writes. Nothing here takes the window-control path: that one
|
||||
*calls off* the pin (`set_time_window()` clears `pinned_`), so measuring it would test a different
|
||||
question.
|
||||
|
||||
Run it against the fast mock, and mind the datagram limit: `--batch 2000` is 80 kB of payload for the
|
||||
mock's five signals and dies with `OSError: [Errno 90] Message too long`, so the command here is
|
||||
`--rate 1300000 --batch 1500 --freq 1`. Its sequence is about 12 s: it waits for a latch, then
|
||||
~1.5 s per leg, so `UDPSCOPE_EXIT_AFTER_MS=20000` covers it with room.
|
||||
|
||||
```
|
||||
[measurepin-probe] 4 measurements on 'sine', trigger single
|
||||
[measurepin-probe] wait mode=waiting pinned=0 at_pin=0 for=[-1.419616..0.580384] pin=[0.000000..1.000000] passes=14
|
||||
[measurepin-probe] wait mode=capturing pinned=1 at_pin=0 for=[-1.419616..0.580384] pin=[0.000000..1.000000] passes=14
|
||||
[measurepin-probe] wait mode=capturing pinned=1 at_pin=1 for=[0.881155..2.881155] pin=[0.881155..2.881155] passes=15
|
||||
[measurepin-probe] wait mode=held pinned=1 at_pin=1 for=[0.881155..2.881155] pin=[0.881155..2.881155] passes=15
|
||||
[measurepin-probe] held/before pinned=1 mode=held view=[-1.000000..1.000000] pin=[-1.000000..1.000000] for=[0.881155..2.881155] passes=16 ev=2600001 evx=[-1.000000..1.000000] average=0.000000 top=0.957975 peak to peak=2.000000 amplitude=1.915950
|
||||
[measurepin-probe] held/after pinned=1 mode=held view=[-0.250000..0.250000] pin=[-1.000000..1.000000] for=[0.881155..2.881155] passes=16 ev=2600001 evx=[-1.000000..1.000000] average=0.000000 top=0.957975 peak to peak=2.000000 amplitude=1.915950 same=4/4 for_same=1 pass_delta=0 span_before=2.000000 span_after=2.000000 follows=0
|
||||
[measurepin-probe] paused/before pinned=1 mode=paused view=[-0.250000..0.250000] pin=[-0.250000..0.250000] for=[1.631155..2.131155] passes=17 ev=650001 evx=[-0.250000..0.250000] average=0.000003 top=0.957944 peak to peak=2.000000 amplitude=1.915906
|
||||
[measurepin-probe] paused/after pinned=1 mode=paused view=[-0.062500..0.062500] pin=[-0.250000..0.250000] for=[1.631155..2.131155] passes=17 ev=650001 evx=[-0.250000..0.250000] average=0.000003 top=0.957944 peak to peak=2.000000 amplitude=1.915906 same=4/4 for_same=1 pass_delta=0 span_before=0.500000 span_after=0.500000 follows=0
|
||||
[measurepin-probe] live/before pinned=0 mode=live view=[-2.000000..0.000000] pin=[off] for=[1.666922..3.666922] passes=22 ev=0 evx=[0.000000..0.000000] average=-0.000000 top=0.957975 peak to peak=2.000000 amplitude=1.915948
|
||||
[measurepin-probe] live/after pinned=0 mode=live view=[-1.062500..-0.937500] pin=[off] for=[2.746345..2.871345] passes=24 ev=0 evx=[0.000000..0.000000] average=-0.427665 top=-0.083224 peak to peak=0.687725 amplitude=0.644726 same=0/4 for_same=0 pass_delta=2 span_before=2.000000 span_after=0.125000 follows=1
|
||||
```
|
||||
|
||||
- `view`/`pin`/`for`/`passes`/`ev`/`evx` are the state: the axis, the pin's own extent, the window
|
||||
the printed values were computed over, the pane's pass count, and the **bound event series'** own
|
||||
sample count and extent — the frame the numbers are about. `ev=0` while a view is pinned is an
|
||||
*empty* frozen frame (every value `--`), which is the reading that named the pause ordering defect
|
||||
(`on_pause_toggled()` read its display offset after setting the pause state).
|
||||
- `same=K/4` — the values that are **bit-identical** before and after the gesture. On a pinned view
|
||||
it must be `4/4` with `for_same=1` and `pass_delta=0`: the frame's samples did not change, so
|
||||
neither may a number, and the pane may not even spend a recompute on the gesture. `for_same` is
|
||||
the app's own 2 %-of-span criterion (the rule that decides a recompute is not owed), not equality:
|
||||
the pin is re-derived from the snapshot's stamps every tick and the pane keeps the window its last
|
||||
pass used, so the two differ in the last digits without anything being owed.
|
||||
- `follows` — the live leg's assertion, and the case the still-frame rule must *not* swallow:
|
||||
`for_same=0`, a positive `pass_delta` and the results window's span narrowed from the axis'
|
||||
2.000000 to 0.125000, with `average`, `top` and `peak to peak` all moving. `follows=1` with `same=4/4` on a
|
||||
pinned leg, or `follows=1` while pinned at all, is the defect in one direction or the other.
|
||||
- The `before`/`after` pair is taken only once the pinned frame has **settled** (a `wait` line per
|
||||
*change* of the state being waited on until then, then nothing): the frames that bind a snapshot or freeze a pause re-state the
|
||||
pin from the request and then from the snapshot's own stamps, and a gesture made in one of those
|
||||
frames is reverted by that re-statement — the `[zoom-probe]`'s own gesture sweep loses its first
|
||||
two steps to it, which is visible in its `ges/x1`, `ges/x2` lines. A `gave up waiting after N
|
||||
polls` line means no latch (or no pin) was ever reached, and is a reading about the stream.
|
||||
|
||||
## `[search-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is
|
||||
|
||||
Reference in new issue
Block a user