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:
Martino Ferrari committed 2026-10-16 06:10:24 +02:00
1 parent d91957a2fe
commit 0a209becb1
13 files changed
+719 -114

No files matched your search

+40 -13
View File
@@ -377,23 +377,37 @@ range") only for `between`, which is the one type whose two edges are both param
words on the split row name its side ("above % of the range" for `above`) — and the "show on plot" words on the split row name its side ("above % of the range" for `above`) — and the "show on plot"
line says what would be drawn. line says what would be drawn.
The values are **window statistics over exactly the visible range, at the stored rate**: the The values are **window statistics over the window the pane is about, at the stored rate**: the
same cut the export makes, so a measurement describes the data on screen rather than the ring same cut the export makes, so a measurement describes data rather than the ring behind it — and
behind it — and the marks it draws belong to that window, so they stay in the viewport. the marks it draws belong to that window.
They are recomputed when the **window moves**, not on a timer: a timer decouples the two, and **Which** window, when the view is pinned, is the frame and not the viewport. A held event or a
paused stream is a *still frame*: the data behind it cannot change, so a pan or a zoom there is
only looking closer, and the pane goes on describing the **pin's own extent**
(`RowSet::pinned_range()` — the same statement the framing itself is clamped into, so "what this
view is a view of" has one expression). Zoom into a latched event and the numbers do not move:
that is what a still picture means, and it is the reported *"when measuring a still frame, moving
the zoom makes measurements such as average or top change"*. A live view is the other case and is
unchanged: there the window genuinely moves and the statistic follows it.
They are recomputed when that **window moves**, not on a timer: a timer decouples the two, and
with a scrolling window the stored marks then describe a range the plot has already left (the with a scrolling window the stored marks then describe a range the plot has already left (the
min/max marker ends up outside the viewport, and a dragged marker survives until the next min/max marker ends up outside the viewport, and a dragged marker survives until the next
tick). The trigger is 2 % of the span, so the numbers track the window within a few pixels tick). The trigger is 2 % of the span, so the numbers track the window within a few pixels
while the cost stays bounded by the window's motion rather than by the frame rate; one pass while the cost stays bounded by the window's motion rather than by the frame rate; one pass
copies the visible samples of each *channel* once, however many measurements read it. copies the visible samples of each *channel* once, however many measurements read it.
"The window moved" is asked in **every** mode, including a held trigger event — which is the "The window moved" is asked in **every** mode, but it is asked about the window the pane
one mode where a zoom or a pan is the only thing that can move it. Held views were once *describes*. Suppressing it while a view is held left the pane describing the whole captured
exempted, on the grounds that a pinned window must not recompute every frame, and the result event while you zoomed into a slice of it; dropping the exemption made the opposite mistake — the
was a pane that kept describing the whole captured event while you zoomed into a slice of it. numbers followed the zoom, so a still frame's `average` moved whenever the wheel did. On a pinned
A pinned window needs no exemption: it lands on exactly the range it already measured and view neither can happen: the pin is what the pane is about, so a gesture moves no number and
fails the 2 % test by itself. spends no recompute, while a fresh latch, a re-stated pin or a new snapshot still recomputes it.
The one path that is *not* the pin is the window control, which unpins as it re-frames
(`apply_time_window()`), so for that frame the view really is live-framed and measuring the
viewport is right. `UDPSCOPE_PROBE_MEASUREPIN_MS` is the guard: a gesture on a held event and on a
pause must leave every value bit-identical (`same=4/4`, `pass_delta=0`), and on a live view the
results must still narrow with the axis (`follows=1`).
**Show on plot** marks the result on the row that plots the signal: a ruler at the level **Show on plot** marks the result on the row that plots the signal: a ruler at the level
(`top`/`bottom`), both rulers at the extremes (`min`/`max`), the pulse's or the cycle's two (`top`/`bottom`), both rulers at the extremes (`min`/`max`), the pulse's or the cycle's two
@@ -534,9 +548,11 @@ loses that spread entirely.
**Reset** clears every accumulator. So does editing, adding or deleting a measurement, a CONFIG **Reset** clears every accumulator. So does editing, adding or deleting a measurement, a CONFIG
that renames the signals, and a change of the display window — each of those changes *what is that renames the signals, and a change of the display window — each of those changes *what is
being measured*, and a mean across the change describes nothing. A pause, a resume, a trigger being measured*, and a mean across the change describes nothing. A pause, a resume, a trigger
latch, a pan and a zoom do **not** reset: they change which samples are in view, which is latch, a pan and a zoom do **not** reset: on a live view they change which samples are in view,
exactly what a statistic over many windows is meant to average. The toggle itself does not which is exactly what a statistic over many windows is meant to average — and on a pinned one they
reset either, so turning the line off to read the plot and back on again keeps the history. change nothing at all, values included (above), so the history has nothing to see either way. The
toggle itself does not reset either, so turning the line off to read the plot and back on again
keeps the history.
## Resequencing and relative timing ## Resequencing and relative timing
@@ -1556,6 +1572,17 @@ event pin already worked this way, and the pause agrees with it. `[pause-probe]`
beside `frozen_x=`, and while paused the axis must lie *inside* `frozen_x` — equal at the pause, a beside `frozen_x=`, and while paused the axis must lie *inside* `frozen_x` — equal at the pause, a
subset after a zoom-in. subset after a zoom-in.
**The pause reads its display offset *before* it sets the pause state**, and pausing while an event
is held is the case that depends on the order. `stream_at_x0()` follows the *display*: the moment
`paused` is true with no pause view published yet it answers `-display_epoch()`, which on a held
event is `0` — so the cut searched the ring at the event's edge-relative x, found no samples, and
froze an **empty** picture: no trace, every measurement `--`, every ruler gone. Read first, the
offset is the one the axis is actually displaying, which is what hands the snapshot stamps back on
the coordinates the axis is already showing. The live case is unaffected (its epoch does not depend
on `paused()`), which is why the pause probe, which pauses a live view, never caught it;
`[measurepin-probe]`'s `paused/before` leg pauses a *held* event and prints the bound frame's own
sample count (`ev=650001` for a quarter-of-the-event zoom), which is `ev=0` when this regresses.
**Zooming into a held event clipped it further and further from the left**, and the cause was the **Zooming into a held event clipped it further and further from the left**, and the cause was the
one the library's own margin fix is about, in the half of the code the fix could not reach. one the library's own margin fix is about, in the half of the code the fix could not reach.
`EventSeries::neighbours()` — the call the renderer maps the visible range onto sample indices with `EventSeries::neighbours()` — the call the renderer maps the visible range onto sample indices with
+39 -14
View File
@@ -20,17 +20,38 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
own -- `top`/`bottom`/`amplitude` take a second read of that same **bucket** list and never of own -- `top`/`bottom`/`amplitude` take a second read of that same **bucket** list and never of
the window (the flat-level invariant below). **Adding a type must not add a pass** -- that is the window (the flat-level invariant below). **Adding a type must not add a pass** -- that is
what `compute_measure()`'s `stats` argument is for. what `compute_measure()`'s `stats` argument is for.
**The "it moved" test is asked in every mode, a held event included.** Suppressing it while **The window a value is computed over is the *pin* while the view is pinned, and the viewport
held (`moved = ... && !held`) is the shape the pinned-window fix first took, and it is wrong otherwise.** A pinned view -- a held event, a paused stream, an opened file -- is a *still
for the one mode where a *gesture* is the only thing that can move the window: a latched event frame*: the data behind it does not change, so a pan or a zoom only looks closer. Measuring the
zoomed forty times over left the pane describing the whole 2 s event with `passes` frozen, pin (`RowSet::pinned_range()`, the same accessor `hold_pinned_window()` clamps into, so "what
which is the reported "measurements move when zooming or panning on data" -- the numbers stand frames this view" has one expression) is what makes a gesture change **no value at all** --
still while the data under them moves. Nothing is needed for the pinned case anyway, because a and it is decided in `update_measures()`, at the call site, rather than inside
window that is merely re-pinned satisfies `swin == measure_window` exactly and fails the move `MeasureWindowReader`, because the decision is also *booked*: the window
test on its own; a pause is the standing proof, since `display_mode()` reports `Paused` rather `MeasureWindowReader` is handed is the same one the "it moved" test compares against
than `Held` for one and the statistics probe already requires `n` to advance by at most one `App::measure_window`, so a gesture inside the pin spends no recompute either. A reader that
across a two-second pause. The held/live state is still a recompute of its own, so a mode chose its own window would leave a pinned view recomputing on every gesture frame for a number
change is not missed. that cannot change (457 passes in one run is the shape that mistake took).
This test has been wrong in both directions and the reasoning is worth keeping. Suppressing it
while held (`moved = ... && !held`) left the pane describing the whole 2 s event
(`for=[0.47..2.47]`) while a 50 ms slice of it was on screen; *removing* the exemption was the
opposite mistake, and it is the reported "when measuring a still frame (a triggered event, or a
paused stream) moving the zoom makes measurements such as average or top change, which should
never happen". The two are one question asked of two different windows: **which window is this
pane about** (the pin, while pinned) and **has it moved** (asked of that window in every mode).
Answers: a pinned view still recomputes when it should -- a fresh latch, a re-stated pin, a new
snapshot, the held/live change -- and a settled one satisfies `swin == measure_window` exactly
and so reads `moved == false` on its own.
`[measurepin-probe]` is the reading: on a latched event zoomed to a quarter of itself,
`same=4/4 for_same=1 pass_delta=0` with the values bit-identical and `for=` still at the pin;
the same on a pause taken from a held event. Live is the case that must **not** be swallowed,
and it reads `same=0/4 for_same=0` with a positive `pass_delta`, `span_before=2.000000`,
`span_after=0.125000` and `follows=1` -- the results window narrowed with the viewport and the
values moved with it. The
one path that is *not* this rule is the window control: `apply_time_window()` unpins
(`set_time_window()` clears `pinned_`), so for the frames before the held branch re-states the
pin the view really is live-framed and the pane measures the viewport -- `[measure-trace]`
prints `pin=off` for exactly those frames, which is what makes them legible rather than looking
like the defect again.
The marks' *ink* is set where they are created (`install()` in `plot_host.cpp`): 0.75 px and The marks' *ink* is set where they are created (`install()` in `plot_host.cpp`): 0.75 px and
dashed 4/4 through `Plot::set_cursor_stroke()`, which is per cursor because the trigger marker dashed 4/4 through `Plot::set_cursor_stroke()`, which is per cursor because the trigger marker
(also a cursor) keeps the solid `line_width_px` stroke. A new measure mark must take that (also a cursor) keeps the solid `line_width_px` stroke. A new measure mark must take that
@@ -268,6 +289,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
changed".** An explicit Reset, an add/edit/delete (all three funnel through changed".** An explicit Reset, an add/edit/delete (all three funnel through
`rebuild_measures_pane()`), a CONFIG, and a change of display window each change the `rebuild_measures_pane()`), a CONFIG, and a change of display window each change the
*question*, so the history is cleared. A pause, a resume, a trigger latch, a pan and a zoom do *question*, so the history is cleared. A pause, a resume, a trigger latch, a pan and a zoom do
not: they change which samples answer it, which is precisely what averaging over many windows not: on a **live** view they change which samples answer it, which is precisely what averaging
is for. The pane-wide toggle does not reset either — it only shows and hides labels that are over many windows is for -- and on a **pinned** one they change *nothing*, not even a value
always created and always fed, so reading the plot for a moment does not cost the history. (the window invariant above), so there is nothing for the history to see either way. The rule
is about the *accumulator* only; it is not a claim that a gesture leaves a value alone, which is
a property of the window a value is computed over and is the other half of this change. The
pane-wide toggle does not reset either — it only shows and hides labels that are always created
and always fed, so reading the plot for a moment does not cost the history.
+20 -1
View File
@@ -81,13 +81,20 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
the far threshold of the side it is leaving. Seeding the previous side from `0` -- the undecided code -- the far threshold of the side it is leaving. Seeding the previous side from `0` -- the undecided code --
fires on a signal that starts *inside* the band and leaves it without ever reaching the level, an event the fires on a signal that starts *inside* the band and leaves it without ever reaching the level, an event the
trigger is not supposed to see. trigger is not supposed to see.
- **A pinned window is *clamped* into the event every frame, and a zoom in is allowed.** **A pinned window is *clamped* into the event every frame, and a zoom in is allowed.**
While an event is held, a capture is filling, or the view is paused, `tick()` calls While an event is held, a capture is filling, or the view is paused, `tick()` calls
`RowSet::hold_pinned_window()`, which caps the span at `[-pinned_pre_, +pinned_post_]`'s and `RowSet::hold_pinned_window()`, which caps the span at `[-pinned_pre_, +pinned_post_]`'s and
clamps the position into it (and states `Off`, since a following group would re-derive from a clamps the position into it (and states `Off`, since a following group would re-derive from a
live edge). **A zoom in must keep working** -- "not outside the event" is not "not readable", live edge). **A zoom in must keep working** -- "not outside the event" is not "not readable",
and this function's first version *re-stated* the pin every frame, which made the wheel a no-op and this function's first version *re-stated* the pin every frame, which made the wheel a no-op
in single trigger mode: an acquired event could not be examined at all. in single trigger mode: an acquired event could not be examined at all.
**What a gesture inside the pin may *not* move is a measurement**, and it is not this function
that holds that: `pinned_range()` is the one expression of the pin's extent, and
`update_measures()` measures *it* rather than the viewport while the view is pinned, so a pan
or a zoom on a still frame changes no value and spends no recompute (see
[invariants-measurements.md](invariants-measurements.md); `[measurepin-probe]` is the reading,
`pin=`/`for=` in `[measure-trace]` the field to watch). The pin is therefore load-bearing twice
over, and a change to what `pinned_range()` says changes both the framing and the numbers.
**The request is read from the row's own axis, not from the group.** A member's `set_x_range` **The request is read from the row's own axis, not from the group.** A member's `set_x_range`
-- what the controller writes for a gesture -- is *not* adopted into the group while the follow -- what the controller writes for a gesture -- is *not* adopted into the group while the follow
is off, so a clamp that read the group only ever saw its own previous answer (the probe showed is off, so a clamp that read the group only ever saw its own previous answer (the probe showed
@@ -148,6 +155,18 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
afterwards if the probe zoomed in. Before the fix the two disagreed outright: afterwards if the probe zoomed in. Before the fix the two disagreed outright:
`axis=[-1.44..0.56]` over data that started at `0.0`. (The event pin already worked this way; the `axis=[-1.44..0.56]` over data that started at `0.0`. (The event pin already worked this way; the
pause now agrees.) pause now agrees.)
- **The pause reads the display offset *before* it sets the pause state, and a pause taken on a
held event depends on that order.** `on_pause_toggled()` reads the row's axis and
`stream_at_x0()`, cuts `view + offset` out of the ring, and hands the same offset to the
snapshot as its `x_offset`. `stream_at_x0()` follows the *display*: the moment `paused` is true
with no pause view published yet it answers `-display_epoch()`, which on a `Held` view is **0**
-- so the cut searched the ring at the event's edge-relative x, found nothing, and froze an
**empty** picture: `ev=0` on the probe's line, every measurement `--` and every ruler gone,
which is a much louder symptom than the empty margin above and reaches a user the same way
(pause while an event is on screen). Read first, it is the offset the axis is actually
displaying, which is what the whole scheme requires. The live case is unchanged -- there the
epoch does not depend on `paused()`, which is why the pause probe, which pauses a *live* view,
never saw it.
- **The event axis is pinned from the snapshot's own stamps, not from the request.** A - **The event axis is pinned from the snapshot's own stamps, not from the request.** A
capture may legitimately be shorter than asked for — the ring need not reach back a capture may legitimately be shorter than asked for — the ring need not reach back a
full pre-trigger share on the first latch after arming, so `snap=[-0.339..1.000]` full pre-trigger share on the first latch after arming, so `snap=[-0.339..1.000]`
+75 -13
View File
@@ -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 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 `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 `[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 `marks_shown` counts two per shown measurement
(a min/max showing both its marks counts two of its own). `measures window:` is the (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 on screen. 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 **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 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 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 (`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 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 % `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 drift on a run whose every frame was exact. `pin` is the same tick's pin (`App::measure_pin_window`,
`measure_window`, which contains every mark by construction: read that way it was a tautology `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 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* `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 time mark is off screen, so `0/K` there is read beside `drift` and not alone. `0/0` is a pane
reading belongs to the documented 2 s run, and to a **held** window in any run: a latched whose types carry no time marks at all (`average`, `top`, `amplitude` and `peak to peak` draw
event does not scroll, so `[measure-trace]` must show `for=` equal to `swin=` exactly for as rulers only), and is not a reading about marks.
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 `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 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 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: 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 *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 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 recomputed when the window **it describes** moves (2 % of its span) or the held/live state
wall-clock timer: a timer is what let a marker sit outside the viewport and a dragged one changes, never on a wall-clock timer: a timer is what let a marker sit outside the viewport and a
survive a frame. **`marks_selectable` must be 0**: the 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 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 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 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 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=`. 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]` ## `[search-probe]`
`UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is `UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is
+34 -14
View File
@@ -96,20 +96,25 @@ precedent, not an exception.
overshoot, kept by the extremes and excluded by the levels. The per-type values, the reason for overshoot, kept by the extremes and excluded by the levels. The per-type values, the reason for
each, and the cost of the histogram are in each, and the cost of the histogram are in
[invariants-measurements.md](invariants-measurements.md). [invariants-measurements.md](invariants-measurements.md).
`[measure-trace] mode=M view=[..] s0=S swin=[..] for=[..] passes=N <type>=<value>... marks=N `[measure-trace] mode=M view=[..] s0=S swin=[..] pin=[..|off] for=[..] passes=N
level:V/time:V...` is a repeating line (250 ms) from the probe: the pane's numbers beside the <type>=<value>... marks=N level:V/time:V...` is a repeating line (250 ms) from the probe: the
*current* window and the marks actually placed, which is the only way a stale result or a mark pane's numbers beside the *current* window and the marks actually placed, which is the only way a
that should not be drawn shows up as a number. `swin` is `view` moved onto the stream's clock by stale result or a mark that should not be drawn shows up as a number. `swin` is `view` moved onto
`s0` (`stream_at_x0()`), and `for` is the window the printed numbers were computed over, in the the stream's clock by `s0` (`stream_at_x0()`), `pin` is the pin's own extent in that clock (`off`
same clock -- so a reading that moved while `view` stood still is a change of `s0` (a new when nothing is pinned) and `for` is the window the printed numbers were computed over -- so a
snapshot under the same mode) and not a stale pass, and only these three fields tell the two reading that moved while `view` stood still is a change of `s0` (a new snapshot under the same
apart. `passes` beside them says whether a recompute answered the move. **While `mode=held`, mode) and not a stale pass, and only these fields tell those apart. `passes` beside them says
`for` must equal `swin` exactly**, through any number of zooms and pans: a latched event does whether a recompute answered the move. **While the view is pinned, `for` must equal `pin` -- and
not scroll, so there is nothing for a recompute to be late for. That reading is where `swin` is free to sit anywhere inside it** -- through any number of zooms and pans: a latched
the measurements standing still under a gesture was found -- `for=[0.47..2.47]` against a event's numbers are the *frame's*, so a gesture inside the pin changes neither them nor `passes`.
`swin=[1.417..1.467]` with `passes` frozen, the pane describing the whole 2 s event while a 50 ms Read the other way round it is the defect: `for` following `swin` into the zoom while the values
slice of it was on screen. The line must also never print `marks=0` while measurements with move is the reported "on a still frame the measurements change when I zoom". Neither reading is
`show_on_plot` are on and the data is there -- that was the rulers blinking off at a mode change. the old "measurements stand still while the data moves", which was `for=[0.47..2.47]` against a
`swin=[1.417..1.467]` with `passes` frozen -- the pane describing a window the plot had left.
The line must also never print `marks=0` while measurements with `show_on_plot` are on and the
data is there -- that was the rulers blinking off at a mode change. `pin=off` while the mode is
`held` is not a stale reading but the window control: `apply_time_window()` unpins, and for those
frames the view is live-framed and measuring the viewport is right.
`measure cost: N recomputes, X ms total, Y ms each, data D ms (P%), covered=C buckets=B `measure cost: N recomputes, X ms total, Y ms each, data D ms (P%), covered=C buckets=B
raw=R (F%), N ns/covered sample` follows it: **read it against a run with raw=R (F%), N ns/covered sample` follows it: **read it against a run with
`UDPSCOPE_PROBE_MEASURE_MS` unset**, because a frame mean cannot attribute a cost. The last `UDPSCOPE_PROBE_MEASURE_MS` unset**, because a frame mean cannot attribute a cost. The last
@@ -168,6 +173,21 @@ precedent, not an exception.
`--square-harmonics 30` for the `0.986496` and the `0.302744`, `--square-harmonics 0` for the `--square-harmonics 30` for the `0.986496` and the `0.302744`, `--square-harmonics 0` for the
`1.000000` and the empty band. `1.000000` and the empty band.
- `[measurepin-probe]` (stderr, `UDPSCOPE_PROBE_MEASUREPIN_MS`) — **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. Three legs — a latched event, a
pause taken from it, and a live view after the trigger is switched off — each read the pane
before and after a *gesture* written on the row's own axis (never the window control, which
calls off the pin). The assertions, in order: the pinned legs read **`same=4/4 for_same=1
pass_delta=0`** with `for=` still at the pin (the values bit-identical and no recompute spent on
the gesture), and the live leg reads **`follows=1`** with `for_same=0`, `pass_delta>0` and the
results window's span narrowed to the axis'. Its sequence is about 12 s, so
`UDPSCOPE_EXIT_AFTER_MS=20000`; run it on the fast mock with a batch the datagram limit allows
(`--rate 1300000 --batch 1500 --freq 1`, **not** `--batch 2000`, which is 80 kB of payload and
dies with `Message too long`). The lines, the fields and the settle wait are in
[probe-readings.md](probe-readings.md); the window rule itself is in
[invariants-measurements.md](invariants-measurements.md).
- `[pan-probe] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then - `[pan-probe] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then
`[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L` `[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L`
(stderr, `UDPSCOPE_PROBE_PAN_MS`) — **`held` must be 1**: a gesture must not move a window (stderr, `UDPSCOPE_PROBE_PAN_MS`) — **`held` must be 1**: a gesture must not move a window
+11 -3
View File
@@ -5,7 +5,8 @@
<info> <info>
<link type="guide" xref="index#analysis"/> <link type="guide" xref="index#analysis"/>
<link type="seealso" xref="cursors"/> <link type="seealso" xref="cursors"/>
<desc>A measurement is a statistic of the visible window of one signal, with optional <desc>A measurement is a statistic of one signal over the window the pane is about — the
visible range, or the frozen frame while a view is pinned — with optional
on-plot marks and a per-measurement statistics line.</desc> on-plot marks and a per-measurement statistics line.</desc>
<credit type="author copyright"> <credit type="author copyright">
<name>Martino Ferrari</name> <name>Martino Ferrari</name>
@@ -14,10 +15,15 @@
<title>Measurements</title> <title>Measurements</title>
<p>A measurement is a statistic of the <em>visible window</em> of one signal, recomputed as <p>A measurement is a statistic of one signal over the window the pane is about, recomputed as
that window moves. The pane lists them with their values; each can also draw its result on that window moves. The pane lists them with their values; each can also draw its result on
the plot.</p> the plot.</p>
<p>That window is the <em>visible range</em> — so on a live view the numbers follow what you
pan and zoom to. On a <em>still frame</em> — a held event, a paused stream, an opened file — it
is the frame's own extent instead, so panning or zooming inside it changes nothing: the data
being measured has not changed and you are only looking closer.</p>
<section id="reach"> <section id="reach">
<title>How to reach it</title> <title>How to reach it</title>
<steps> <steps>
@@ -122,7 +128,9 @@
a mean frequency over many windows is what that claims to be. <gui>Reset</gui> clears a mean frequency over many windows is what that claims to be. <gui>Reset</gui> clears
every accumulator and starts fresh. A change of display window, a CONFIG or a every accumulator and starts fresh. A change of display window, a CONFIG or a
measurement edit also resets; a pause, a resume, a zoom, a pan and the toggle itself do measurement edit also resets; a pause, a resume, a zoom, a pan and the toggle itself do
not, so turning the line off to read the plot and back on again keeps the history.</p></item> not, so turning the line off to read the plot and back on again keeps the history. On a
still frame a pan or a zoom changes nothing for the history to record in the first place —
the values do not move there at all.</p></item>
</list> </list>
</section> </section>
+3 -1
View File
@@ -41,7 +41,9 @@
snapshot, and the pinned axis is that shorter span — so you see a narrower view rather snapshot, and the pinned axis is that shorter span — so you see a narrower view rather
than an empty margin.</p></item> than an empty margin.</p></item>
<item><p>A paused view is pinned exactly as a held trigger event is: zoom in freely, but <item><p>A paused view is pinned exactly as a held trigger event is: zoom in freely, but
a pan or a zoom out is clamped to the snapshot's data.</p></item> a pan or a zoom out is clamped to the snapshot's data. A measurement goes on describing
that frozen frame rather than the part of it you are looking at, so a zoom on a pause
changes no reading — see <link xref="measurements"/>.</p></item>
<item><p>Pausing is the easiest way to measure a layout or read a cursor precisely, <item><p>Pausing is the easiest way to measure a layout or read a cursor precisely,
because nothing under the pointer moves between frames.</p></item> because nothing under the pointer moves between frames.</p></item>
<item><p>The status bar says how long you have been paused — wall time, since the stream <item><p>The status bar says how long you have been paused — wall time, since the stream
+2 -2
View File
@@ -745,7 +745,7 @@ void RowSet::hold_pinned_window() {
// and a following group would re-derive the window from a live edge on the next layout -- // and a following group would re-derive the window from a live edge on the next layout --
// ignoring the pin altogether. // ignoring the pin altogether.
figure_->set_x_link_follow(x_group_, plot::XFollow::Off); figure_->set_x_link_follow(x_group_, plot::XFollow::Off);
const plot::Range pin{-pinned_pre_, pinned_post_}; const plot::Range pin = pinned_range();
// The *request*: what the user's gesture left on the row's own axis. A member's `set_x_range` // The *request*: what the user's gesture left on the row's own axis. A member's `set_x_range`
// -- what the controller writes -- is not adopted into the group while the group is not // -- what the controller writes -- is not adopted into the group while the group is not
// following, so reading the group here only ever saw the previous clamp's answer and the wheel // following, so reading the group here only ever saw the previous clamp's answer and the wheel
@@ -857,7 +857,7 @@ void RowSet::fit_x() {
// *members* have to be written as well as the group, because `hold_pinned_window()` prefers a // *members* have to be written as well as the group, because `hold_pinned_window()` prefers a
// member that has moved since our own last statement -- that is how it tells a gesture from its // member that has moved since our own last statement -- that is how it tells a gesture from its
// own echo -- so writing the group alone would be reverted to the zoomed-in member next frame. // own echo -- so writing the group alone would be reverted to the zoomed-in member next frame.
const plot::Range pin{-pinned_pre_, pinned_post_}; const plot::Range pin = pinned_range();
if (!(pin.span() > 0.0) || !std::isfinite(pin.min) || !std::isfinite(pin.max)) return; if (!(pin.span() > 0.0) || !std::isfinite(pin.min) || !std::isfinite(pin.max)) return;
for (int i = 0; i < rows_; ++i) { for (int i = 0; i < rows_; ++i) {
Row& r = row(i); Row& r = row(i);
+6
View File
@@ -385,6 +385,12 @@ class RowSet {
/// Whether a window is pinned: a held event, a filling capture or a paused view. The condition /// Whether a window is pinned: a held event, a filling capture or a paused view. The condition
/// `fit_x()` branches on, which is why the fit probe reports it. /// `fit_x()` branches on, which is why the fit probe reports it.
bool window_pinned() const noexcept { return pinned_; } bool window_pinned() const noexcept { return pinned_; }
/// The pin's own extent, in the x the rows' axes read -- what a pinned view is a view *of*.
/// The one expression of it, shared by the framing (`hold_pinned_window()`, `fit_x()`) and by
/// the measurement pane, which measures *this* while the view is pinned so that a gesture
/// changes no number (see `update_measures()`); meaningless when `window_pinned()` is false.
plot::Range pinned_range() const noexcept { return {-pinned_pre_, pinned_post_}; }
/// True when the pin already *is* this window. A caller that re-states a pin every frame -- the file /// True when the pin already *is* this window. A caller that re-states a pin every frame -- the file
/// view does, from the ring's own extent -- uses this to update the pin's numbers with /// view does, from the ring's own extent -- uses this to update the pin's numbers with
/// `set_data_pin()` instead of writing the group's range again with `pin_event_window()`, which would /// `set_data_pin()` instead of writing the group's range again with `pin_event_window()`, which would
+65 -31
View File
@@ -1136,22 +1136,38 @@ void on_window_step(GtkButton* button, gpointer user) {
void on_pause_toggled(GtkToggleButton* button, gpointer user) { void on_pause_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
const bool paused = gtk_toggle_button_get_active(button); const bool paused = gtk_toggle_button_get_active(button);
// Freeze the *view*: snapshot exactly the window on screen, and pin the axes there. The
// acquisition thread keeps writing the live ring behind it, so resuming shows the data that
// arrived while paused instead of a hole.
//
// The window and its offset are read **before** the pause is set, and that order is the whole
// of what makes a pause taken while an event is *held* work. `stream_at_x0()` follows the
// *display*: the moment `paused` is true with no pause view published yet, it answers
// `-display_epoch()`, which on a held event is 0 -- so the cut searched the ring at
// edge-relative x, found no samples, and froze an *empty* picture (`ev=0` on the probe's own
// line, every measurement `--`). What it has to hand the snapshot is the offset the axis is
// reading at the instant of the click, which is exactly what it says below.
plot::Range view{0.0, 0.0};
double s0 = 0.0;
bool have_view = false;
if (RowSet::Row& r0 = app->rows->row(0); r0.plot != nullptr) {
view = r0.plot->x_range(r0.axis_x);
have_view = true;
}
if (have_view) {
// The axis is in display x and the ring is on the stream's clock, so the window has to be
// put back on that clock to be cut -- `stream_at_x0()` is the one accessor that is right in
// every mode, including a pause taken while an event is *held*, where the axis is
// edge-relative and the old `- display_epoch()` form searched the ring with event
// coordinates. The same offset is handed to the snapshot as its `x_offset`, so its stamps
// come back rebased onto the x the axis is already showing and the frozen picture does not
// move at the instant of the pause.
s0 = app->model->stream_at_x0();
}
app->acq->set_paused(paused); app->acq->set_paused(paused);
app->model->set_paused(paused); app->model->set_paused(paused);
if (paused) { if (paused) {
// Freeze the *view*: snapshot exactly the window on screen, and pin the axes there. The if (have_view) {
// acquisition thread keeps writing the live ring behind it, so resuming shows the data that
// arrived while paused instead of a hole.
if (RowSet::Row& r0 = app->rows->row(0); r0.plot != nullptr) {
const plot::Range view = r0.plot->x_range(r0.axis_x);
// The axis is in display x and the ring is on the stream's clock, so the window has to be
// put back on that clock to be cut -- `stream_at_x0()` is the one accessor that is right in
// every mode, including a pause taken while an event is *held*, where the axis is
// edge-relative and the old `- display_epoch()` form searched the ring with event
// coordinates. The same offset is handed to the snapshot as its `x_offset`, so its stamps
// come back rebased onto the x the axis is already showing and the frozen picture does not
// move at the instant of the pause.
const double s0 = app->model->stream_at_x0();
app->model->publish_pause_view(view.min + s0, view.max + s0, s0); app->model->publish_pause_view(view.min + s0, view.max + s0, s0);
app->pause_started_us = g_get_monotonic_time(); app->pause_started_us = g_get_monotonic_time();
// Pin on what the snapshot *got*: the cut is clamped to the ring, so it can be narrower than // Pin on what the snapshot *got*: the cut is clamped to the ring, so it can be narrower than
@@ -5815,6 +5831,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURE_MS")) { if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURE_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), measure_probe, &app); g_timeout_add(static_cast<guint>(std::atoi(env)), measure_probe, &app);
} }
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASUREPIN_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), measurepin_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_EVENTZOOM_MS")) { if (const char* env = g_getenv("UDPSCOPE_PROBE_EVENTZOOM_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), eventzoom_probe, &app); g_timeout_add(static_cast<guint>(std::atoi(env)), eventzoom_probe, &app);
} }
@@ -6474,24 +6493,34 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
{ {
std::lock_guard<std::mutex> lock(model.mutex()); std::lock_guard<std::mutex> lock(model.mutex());
// `results_for` is the window the numbers describe, `win` the one on screen now: the // `results_for` is the window the numbers describe, `win` the one on screen now: the
// drift between them is the assertion that a measurement is a statistic of the *current* // drift between them is the assertion that a measurement is a statistic of the window it
// window -- a timer-decoupled recomputation is what used to leave a marker outside it. // *should* describe -- a timer-decoupled recomputation is what used to leave a marker
// Both on the **stream's** clock, which is the space `measure_window` and the time marks // outside it. Both on the **stream's** clock, which is the space `measure_window` and
// are stored in: a live axis is a fixed `[-window, 0]`, so comparing display x would read // the time marks are stored in: a live axis is a fixed `[-window, 0]`, so comparing display x
// no drift whatever the data did (see `App::measure_window`). // would read no drift whatever the data did (see `App::measure_window`).
// //
// `win` comes from the last **tick** (`App::measure_view_window`) and is not read back here: // Which window that should be depends on the view, so `drift` is measured against the
// this runs from the destroy handler, where the exit probe has already switched the trigger // **target**: `win` live, and the **pin** (`pin=`, the same stream seconds) while the view
// on, and `stream_at_x0()` then answers for a mode no frame was drawn in. // is pinned. On a pinned view the pane is about the frame, and a zoom inside it is allowed
// to leave the screen showing a slice of that frame -- reading that as drift would report
// the intended behaviour as the defect it was already reported as once.
//
// `win` and `pin` come from the last **tick** (`App::measure_view_window`,
// `App::measure_pin_window`) and are not read back here: this runs from the destroy
// handler, where the exit probe has already switched the trigger on, and `stream_at_x0()`
// then answers for a mode no frame was drawn in.
const plot::Range win = app.measure_view_window; const plot::Range win = app.measure_view_window;
const double span = app.measure_window.span(); const plot::Range target = app.measure_pin_valid ? app.measure_pin_window : win;
const double span = target.span();
int in_window = 0; int in_window = 0;
int marks = 0; int marks = 0;
// Against `win`, the window **on screen** -- not against `measure_window`, which is the // Against `win`, the window **on screen** -- not against `measure_window`/`target`, which
// window the mark was computed over and so contains it by construction. Read that way the // contain every mark by construction: read that way the count was a tautology, and it
// count was a tautology: it printed `2/2` while the pane's numbers described a window 11 s // printed `2/2` while the pane's numbers described a window 11 s and a forty-fold zoom
// and a forty-fold zoom away from the one displayed, which is how a held event that never // away from the one displayed. A *time* mark may legitimately sit outside `win` on a
// recomputed under a gesture stayed invisible to every run of this check. // 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, not the slice -- so the strict `K/K`
// reading belongs to a live view and to a pinned view left at its pin.
for (const RowSet::MeasureMark& mk : app.measure_marks) { for (const RowSet::MeasureMark& mk : app.measure_marks) {
if (mk.level) continue; if (mk.level) continue;
++marks; ++marks;
@@ -6510,11 +6539,16 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (r.plot->cursor(r.measure_v[mi]).selectable) ++selectable; if (r.plot->cursor(r.measure_v[mi]).selectable) ++selectable;
} }
} }
std::printf("[udpscope] measures window: win=[%.6f..%.6f] results_for=[%.6f..%.6f] " const std::string pin_text =
app.measure_pin_valid ? std::to_string(app.measure_pin_window.min) + ".." +
std::to_string(app.measure_pin_window.max)
: std::string("off");
std::printf("[udpscope] measures window: win=[%.6f..%.6f] pin=[%s] results_for=[%.6f..%.6f] "
"drift=%.4f%% marks_in_window=%d/%d marks_selectable=%d\n", "drift=%.4f%% marks_in_window=%d/%d marks_selectable=%d\n",
win.min, win.max, app.measure_window.min, app.measure_window.max, win.min, win.max, pin_text.c_str(), app.measure_window.min,
span > 0.0 ? 100.0 * (std::fabs(win.min - app.measure_window.min) + app.measure_window.max,
std::fabs(win.max - app.measure_window.max)) / span > 0.0 ? 100.0 * (std::fabs(target.min - app.measure_window.min) +
std::fabs(target.max - app.measure_window.max)) /
span span
: 0.0, : 0.0,
in_window, marks, selectable); in_window, marks, selectable);
+29 -5
View File
@@ -484,6 +484,11 @@ struct App {
/// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates /// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates
/// and the pane would compute once and then never again -- the readout frozen on the samples /// and the pane would compute once and then never again -- the readout frozen on the samples
/// that happened to be on screen when the pane opened. /// that happened to be on screen when the pane opened.
///
/// **Which** window that is depends on the view: the viewport while the view is live, and the
/// **pin** (`RowSet::pinned_range()`, captured in `measure_pin_window` below) while it is
/// pinned. A still frame is measured as a frame, so a gesture inside the pin moves neither this
/// nor any number.
plot::Range measure_window{}; plot::Range measure_window{};
bool measure_window_valid = false; bool measure_window_valid = false;
bool measure_was_held = false; bool measure_was_held = false;
@@ -493,7 +498,16 @@ struct App {
/// destroy handler, after the exit probe has switched the trigger on, and `stream_at_x0()` by /// destroy handler, after the exit probe has switched the trigger on, and `stream_at_x0()` by
/// then answers for a mode no frame was ever drawn in -- which read as a 45000 % drift on a run /// 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. /// whose every frame was exact.
///
/// While the view is pinned this and `measure_window` are *allowed* to differ: the pane
/// describes the pin, and a gesture may have narrowed what is on screen to a slice of it.
plot::Range measure_view_window{}; plot::Range measure_view_window{};
/// The pin's own extent in the same stream seconds, and whether the view was pinned when the
/// tick read it. This is the window a pinned view's results describe, so the dump measures its
/// drift against it -- otherwise a zoomed held event would read as a pane describing a window
/// the plot has left, which is precisely what it must do there.
plot::Range measure_pin_window{};
bool measure_pin_valid = false;
/// What the two walks over one channel's visible window produced, shared by every measurement /// What the two walks over one channel's visible window produced, shared by every measurement
/// on that signal: the window's statistics, and one accumulator per *distinct level* any of /// on that signal: the window's statistics, and one accumulator per *distinct level* any of
/// them asked for. Several measurements on one signal must cost two walks, not two each -- and /// them asked for. Several measurements on one signal must cost two walks, not two each -- and
@@ -1076,16 +1090,22 @@ struct ColorPick {
}; };
class MeasureWindowReader { class MeasureWindowReader {
public: public:
MeasureWindowReader(App& app, std::size_t channel) { /// `window` is the x range to read, **in the coordinates the axes read** -- the viewport for a
/// live view, the pin for a pinned one -- and it is handed in rather than read off the plot
/// here, because *which* window a measurement describes is a decision (`update_measures()`)
/// that also has to be booked: the pane compares the window it last measured against this one
/// to decide whether a recompute is owed, and a reader that chose its own would leave the two
/// disagreeing -- a pinned view recomputing on every gesture frame for a number that cannot
/// change.
MeasureWindowReader(App& app, std::size_t channel, plot::Range window) {
const RowSet::Row& r0 = app.rows->row(0); const RowSet::Row& r0 = app.rows->row(0);
if (r0.plot == nullptr) return; if (r0.plot == nullptr) return;
const plot::Range view = r0.plot->x_range(r0.axis_x);
if ((snap_ = app.model->display_snapshot()) != nullptr) { if ((snap_ = app.model->display_snapshot()) != nullptr) {
if (channel >= snap_->values.size()) return; if (channel >= snap_->values.size()) return;
const std::vector<f64>& st = snap_->stamps; const std::vector<f64>& st = snap_->stamps;
if (snap_->values[channel].size() != st.size()) return; if (snap_->values[channel].size() != st.size()) return;
const auto lo = std::lower_bound(st.begin(), st.end(), view.min); const auto lo = std::lower_bound(st.begin(), st.end(), window.min);
const auto hi = std::upper_bound(st.begin(), st.end(), view.max); const auto hi = std::upper_bound(st.begin(), st.end(), window.max);
if (hi <= lo) return; if (hi <= lo) return;
off_ = static_cast<std::size_t>(lo - st.begin()); off_ = static_cast<std::size_t>(lo - st.begin());
count_ = static_cast<std::size_t>(hi - lo); count_ = static_cast<std::size_t>(hi - lo);
@@ -1101,7 +1121,7 @@ class MeasureWindowReader {
epoch_ = app.model->display_epoch(); epoch_ = app.model->display_epoch();
u64 first = 0; u64 first = 0;
u64 last = 0; u64 last = 0;
app.model->live_window_locked(view.min - epoch_, view.max - epoch_, first, last); app.model->live_window_locked(window.min - epoch_, window.max - epoch_, first, last);
if (last <= first) return; if (last <= first) return;
store_ = &app.model->live(); store_ = &app.model->live();
channel_ = channel; channel_ = channel;
@@ -1710,6 +1730,10 @@ gboolean mathwrap_probe(gpointer user);
gboolean mathwrap_probe_wait(gpointer user); gboolean mathwrap_probe_wait(gpointer user);
gboolean mathwrap_probe_report(gpointer user); gboolean mathwrap_probe_report(gpointer user);
gboolean measure_probe(gpointer user); gboolean measure_probe(gpointer user);
gboolean measurepin_probe(gpointer user);
gboolean measurepin_held_wait(gpointer user);
gboolean measurepin_pause_wait(gpointer user);
gboolean measurepin_live_wait(gpointer user);
gboolean measure_trace(gpointer user); gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user); gboolean stats_probe_report(gpointer user);
gboolean stats_probe(gpointer user); gboolean stats_probe(gpointer user);
+56 -17
View File
@@ -264,10 +264,13 @@ void place_measure_marks(App& app) {
app.rows->set_measure_marks(app.measure_marks_display); app.rows->set_measure_marks(app.measure_marks_display);
} }
/// Recomputes every measurement over the visible window and writes the results into the pane, /// Recomputes every measurement over the window it describes and writes the results into the
/// collecting the marks the ones asking to be shown need. /// pane, collecting the marks the ones asking to be shown need.
/// ///
/// A measurement is a statistic of the window, so it is recomputed when the *window* moves -- /// **The window is the viewport live and the pin while the view is pinned** (see below): a still
/// frame is measured as a frame, so on a held event or a pause a pan or a zoom changes no value.
///
/// A measurement is a statistic of that window, so it is recomputed when the *window* moves --
/// not on a timer. A timer decouples the two: with the window scrolling under a 5 Hz cadence /// not on a timer. A timer decouples the two: with the window scrolling under a 5 Hz cadence
/// the stored marks describe a range the plot has already left, which is how a min/max marker /// the stored marks describe a range the plot has already left, which is how a min/max marker
/// ended up outside the viewport, and how a dragged marker survived until the next tick. The /// ended up outside the viewport, and how a dragged marker survived until the next tick. The
@@ -283,19 +286,46 @@ void update_measures(App& app, bool force, bool accumulate) {
// `[-window, 0]` that never changes while the data under it does. // `[-window, 0]` that never changes while the data under it does.
const double s0 = app.model->stream_at_x0(); const double s0 = app.model->stream_at_x0();
const plot::Range view = r0.plot->x_range(r0.axis_x); const plot::Range view = r0.plot->x_range(r0.axis_x);
const plot::Range swin{view.min + s0, view.max + s0}; // The window a statistic describes. A **pinned** view -- a held event, a paused view, an opened
app.measure_view_window = swin; // file -- is a *still frame*: the data behind it does not change, so a pan or a zoom only looks
// closer and no number may move because of one. What the pane describes there is the **pin's own
// extent**, which is the same accessor the framing itself uses (`hold_pinned_window()` clamps a
// pinned view into exactly this, so the pin *is* what frames it), and it is decided here rather
// than inside `MeasureWindowReader` because the decision is booked as well as read: `swin` below
// is what the "it moved" test compares against the last result's window, and a reader that chose
// its own window would leave a pinned view recomputing on every gesture frame for a number that
// cannot change -- the 457-pass run this pane's cadence was fixed for.
//
// A live view is the other case and is unchanged: there the window genuinely moves and the
// statistic follows it, and the accumulator's rule about what resets the history (`measure_*`
// stats) is about neither -- a pan or a zoom still changes *which samples* answer a live
// measurement, which is what averaging over many windows is for.
const plot::Range pin = app.rows->pinned_range();
const bool pinned_view = app.rows->window_pinned() && pin.span() > 0.0 &&
std::isfinite(pin.min) && std::isfinite(pin.max);
const plot::Range window = pinned_view ? pin : view;
const plot::Range swin{window.min + s0, window.max + s0};
// Both windows of the tick, on the stream's clock: what is **on screen**, and (while pinned)
// what the pin *is*. The dump reads them instead of calling `stream_at_x0()` itself, which by
// the time it runs answers for a mode no frame was drawn in.
app.measure_view_window = plot::Range{view.min + s0, view.max + s0};
app.measure_pin_valid = pinned_view;
if (pinned_view) app.measure_pin_window = swin;
const bool held = app.model->display_mode() == DisplayMode::Held; const bool held = app.model->display_mode() == DisplayMode::Held;
// "It moved" is asked in **every** mode, a held event included. This test used to carry a // "It moved" is asked in **every** mode, a held event included -- but it is asked about the
// `&& !held`, and a held event is precisely the mode where a *gesture* is the only thing that // window the measurement *describes* (`swin`, the viewport live and the pin when pinned), never
// can move the window: with it, zooming a latched event forty times over left the pane // about the viewport. That distinction is the whole of a reported defect in both directions.
// describing the whole 2 s event (`for=[0.47..2.47]` against a `swin=[1.417..1.467]`, `passes` // This test used to compare the viewport, with a `&& !held` on it: exempting a held event left
// frozen), which is what "the measurements move when zooming or panning" was -- the numbers and // the pane describing the whole 2 s event (`for=[0.47..2.47]`) while a 50 ms slice of it was on
// the time marks belonged to a window the plot had left. The term was aimed at a *pinned* // screen, and *removing* the exemption made the opposite mistake -- the numbers followed the
// window recomputing every frame, and nothing is needed for that: a settled window satisfies // zoom into the slice, so an average or a top moved whenever the user moved the wheel on a
// `swin == measure_window` exactly and so reads `moved == false` on its own. A pause is the // frozen event, which is the report this pins down. Neither is a staleness question: on a
// standing proof, since `display_mode()` reports `Paused` rather than `Held` for one and the // pinned view the pin is what the pane is about, so a gesture cannot move it at all, and the
// statistics probe already requires `n` to advance by at most one across a two-second pause. // numbers a gesture would have disturbed are the frame's and stay put. A pinned view still
// recomputes when it should -- a fresh latch, a re-stated pin, a new snapshot and the mode
// change below all move `swin` -- and a settled one satisfies `swin == measure_window` exactly
// and so reads `moved == false` on its own, which is what keeps it from recomputing every
// frame (457 passes in one run is the shape that mistake took).
const bool moved = app.measure_window_valid && const bool moved = app.measure_window_valid &&
std::fabs(swin.min - app.measure_window.min) + std::fabs(swin.min - app.measure_window.min) +
std::fabs(swin.max - app.measure_window.max) > std::fabs(swin.max - app.measure_window.max) >
@@ -445,7 +475,7 @@ void update_measures(App& app, bool force, bool accumulate) {
std::vector<MeasureBucket>& buckets = app.measure_bucket_scratch; std::vector<MeasureBucket>& buckets = app.measure_bucket_scratch;
for (App::MeasureWindow& w : app.measure_samples) { for (App::MeasureWindow& w : app.measure_samples) {
const gint64 fetch_start_us = g_get_monotonic_time(); const gint64 fetch_start_us = g_get_monotonic_time();
MeasureWindowReader reader(app, static_cast<std::size_t>(w.channel)); MeasureWindowReader reader(app, static_cast<std::size_t>(w.channel), window);
const std::size_t n = reader.size(); const std::size_t n = reader.size();
const std::function<double(std::size_t)> time_of = [&reader](std::size_t i) { const std::function<double(std::size_t)> time_of = [&reader](std::size_t i) {
return reader.time_of(i); return reader.time_of(i);
@@ -653,12 +683,21 @@ gboolean measure_trace(gpointer user) {
// the offset that converts between them: a reading that moved without the view moving is a // the offset that converts between them: a reading that moved without the view moving is a
// change of `s0` (a new snapshot under the same mode), and only these three fields tell the two // change of `s0` (a new snapshot under the same mode), and only these three fields tell the two
// apart. `passes` beside them says whether a recompute answered it. // apart. `passes` beside them says whether a recompute answered it.
//
// `pin` is the fourth, and it is where a *pinned* view's `for` comes from: while the pin is what
// frames the plot the numbers describe it and not `swin`, so a gesture inside the pin leaves
// `for`, `passes` and every value exactly where they were. Both are captured on the tick, which
// is why `for` and `pin` are readable together; `view`/`s0` are read here, as they always were.
const double s0 = a->model->stream_at_x0(); const double s0 = a->model->stream_at_x0();
std::string line = "[measure-trace] mode=" + std::string line = "[measure-trace] mode=" +
std::string(display_mode_name(a->model->display_mode())) + " view=[" + std::string(display_mode_name(a->model->display_mode())) + " view=[" +
std::to_string(view.min) + ".." + std::to_string(view.max) + "] s0=" + std::to_string(view.min) + ".." + std::to_string(view.max) + "] s0=" +
std::to_string(s0) + " swin=[" + std::to_string(view.min + s0) + ".." + std::to_string(s0) + " swin=[" + std::to_string(view.min + s0) + ".." +
std::to_string(view.max + s0) + "] for=[" + std::to_string(view.max + s0) + "] pin=[" +
(a->measure_pin_valid ? std::to_string(a->measure_pin_window.min) + ".." +
std::to_string(a->measure_pin_window.max)
: std::string("off")) +
"] for=[" +
std::to_string(a->measure_window.min) + ".." + std::to_string(a->measure_window.min) + ".." +
std::to_string(a->measure_window.max) + "] passes=" + std::to_string(a->measure_window.max) + "] passes=" +
std::to_string(a->measure_passes); std::to_string(a->measure_passes);
+339
View File
@@ -1326,6 +1326,345 @@ gboolean measure_probe(gpointer user) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
/// `UDPSCOPE_PROBE_MEASUREPIN_MS`: on a **still frame** the pane describes the frame and not the
/// viewport, so a pan or a zoom changes no number -- and on a live view the numbers still follow
/// their window.
///
/// Three legs, each one reading the pane's own results before and after a **gesture**: the range
/// is written on the row's own axis, which is what a drag, a wheel or a box zoom writes and what
/// `Figure::layout()` adopts. Nothing here takes the window-control path, which *unpins* on the
/// way and would be testing something else.
///
/// * `held`: arms the trigger in single mode, polls for a latched event, reads, zooms to a
/// quarter of the event, reads again. **`same=4/4`, `for_same=1` and `pass_delta=0` are the
/// assertion**: the frame's samples did not change, so no number may, and the pane is not
/// even allowed to spend a recompute on the gesture.
/// * `paused`: presses the pause button and does the same, with `for=` at the *snapshot's* own
/// extent.
/// * `live`: resumes and zooms to a sixteenth of the window. The results must move with it --
/// `follows=1`, `for=`'s span narrowed to the axis' -- and the values move with them, which
/// is the case the still-frame rule must not swallow.
struct MeasurePinProbe {
App* app = nullptr;
/// Which leg is running: 0 held, 1 paused, 2 live. It only names the `before` line, since each
/// step is scheduled from the last.
int stage = 0;
std::vector<double> before;
std::vector<bool> before_valid;
plot::Range for_before{};
double span_before = 0.0;
/// `App::measure_passes` is an `int`, and the delta below is a difference of two of them.
int passes_before = 0;
/// The settle guard's own state: the pass count the last poll saw, and how many polls in a row
/// have been quiet.
int passes_seen = -1;
int settled = 0;
int polls = 0;
/// The last state the wait line was printed for: mode, pinned and at-pin in one number.
int last_state = -1;
};
/// Whether two windows are the same window **by the app's own criterion** -- the 2 %-of-span
/// rule `update_measures()` uses to decide that a recompute is not owed. Equality is not that
/// criterion and would never hold: the pin is recomputed from the snapshot's stamps every tick
/// while the pane keeps the window its last pass used, and the two differ in the last decimals
/// without a recompute being owed for it.
bool measurepin_same_window(const plot::Range& a, const plot::Range& b) {
const double span = std::max(1.0e-12, std::max(a.span(), b.span()));
return std::fabs(a.min - b.min) + std::fabs(a.max - b.max) <= 0.02 * span;
}
/// Whether the pane's results describe the pin.
bool measurepin_at_pin(App& a) {
return a.measure_pin_valid && measurepin_same_window(a.measure_window, a.measure_pin_window);
}
/// The pane's values, its window and its pass count, as the pane itself holds them.
void measurepin_read(App& a, std::vector<double>& values, std::vector<bool>& valid,
plot::Range& window, int& passes) {
values.clear();
valid.clear();
for (const MeasureResult& r : a.measure_results) {
values.push_back(r.primary);
valid.push_back(r.valid);
}
window = a.measure_window;
passes = a.measure_passes;
}
/// One leg's line: the state, the window, every value the pane is showing, and -- the frame the
/// measurement is *about* -- the bound event series' own sample count and extent. A pinned view
/// with no event bound, or one bound to an empty snapshot, is the defect that reads as `--` in
/// every column, and nothing else in the line would say which of the two windows it was.
void measurepin_line(App& a, const char* leg, const std::vector<double>& values,
const std::vector<bool>& valid) {
const RowSet::Row& r0 = a.rows->row(0);
const plot::Range view =
r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{0.0, 0.0};
// The pin only means something while it is what frames the plot: a *stale* pin (one that a
// previous mode left behind) printed as a window would read as the answer.
const bool pinned = a.rows->window_pinned();
const plot::Range pin = a.rows->pinned_range();
const EventSeries* ev = r0.slots[0].bound_event();
const plot::Range ev_x = ev != nullptr ? ev->x_bounds() : plot::Range{0.0, 0.0};
std::fprintf(stderr,
"[measurepin-probe] %-13s pinned=%d mode=%-7s view=[%.6f..%.6f] pin=[%s] "
"for=[%.6f..%.6f] passes=%d ev=%zu evx=[%.6f..%.6f]",
leg, pinned ? 1 : 0, display_mode_name(a.model->display_mode()),
static_cast<double>(view.min), static_cast<double>(view.max),
pinned ? (std::to_string(pin.min) + ".." + std::to_string(pin.max)).c_str() : "off",
a.measure_window.min, a.measure_window.max, a.measure_passes,
ev != nullptr ? ev->size() : 0, ev_x.min, ev_x.max);
for (std::size_t i = 0; i < a.measures.size() && i < values.size(); ++i) {
std::fprintf(stderr, " %s=", measure_type_name(a.measures[i].type));
if (valid[i]) {
std::fprintf(stderr, "%.6f", values[i]);
} else {
std::fprintf(stderr, "--");
}
}
}
/// A gesture zoom: `keep` of the span on screen now, about its centre, written where the pointer
/// puts it. The next frame's `hold_pinned_window()` adopts it -- a zoom *in* is allowed, which is
/// what makes this the case that used to move the numbers.
void measurepin_zoom(App& a, double keep) {
RowSet::Row& r0 = a.rows->row(0);
if (r0.plot == nullptr) return;
const plot::Range now = r0.plot->x_range(r0.axis_x);
const double centre = 0.5 * (now.min + now.max);
const double span = now.span() * keep;
r0.plot->set_x_range(r0.axis_x, plot::Range{centre - 0.5 * span, centre + 0.5 * span});
}
/// Reads the leg's second half and compares it with the first. `same` counts the values that are
/// bit-identical, which is what "the measurement did not change" means for a statistic over the
/// same samples; `for_same` says the window is the same, and `pass_delta` that no recompute was
/// spent.
void measurepin_compare(MeasurePinProbe& p, const char* leg) {
App& a = *p.app;
std::vector<double> after;
std::vector<bool> after_valid;
plot::Range window{};
int passes = 0;
measurepin_read(a, after, after_valid, window, passes);
int same = 0;
for (std::size_t i = 0; i < after.size() && i < p.before.size(); ++i) {
if (after_valid[i] && p.before_valid[i] && after[i] == p.before[i]) ++same;
}
measurepin_line(a, leg, after, after_valid);
const bool for_same = measurepin_same_window(window, p.for_before);
const double swin_span = window.span();
std::fprintf(stderr,
" same=%d/%zu for_same=%d pass_delta=%d"
" span_before=%.6f span_after=%.6f follows=%d\n",
same, after.size(), for_same ? 1 : 0, passes - p.passes_before,
p.span_before, swin_span,
// Live, the results *are* the viewport: the pass that follows a gesture must be
// over the narrowed window, which is where "the numbers follow the window" is a
// reading rather than a claim. Pinned, `for` must not have moved at all.
(for_same == false && swin_span > 0.0 && swin_span <= 0.75 * p.span_before) ? 1 : 0);
std::fflush(stderr);
}
/// Starts a leg: what the pane says now, and the gesture that must not move it.
void measurepin_begin(MeasurePinProbe& p, double keep) {
App& a = *p.app;
p.before_valid.clear();
measurepin_read(a, p.before, p.before_valid, p.for_before, p.passes_before);
p.span_before = p.for_before.span();
measurepin_line(a, p.stage == 0 ? "held/before" : (p.stage == 1 ? "paused/before" : "live/before"),
p.before, p.before_valid);
std::fprintf(stderr, "\n");
std::fflush(stderr);
measurepin_zoom(a, keep);
}
/// The wait budget every leg shares: every step is scheduled from the last, so a step that is
/// never reached has to *say* so -- a probe that quietly waits until the window closes reads as a
/// passing run with no lines, which is the worst of both.
bool measurepin_polls(MeasurePinProbe& p, const char* what, App& a) {
if (++p.polls <= 600) return true;
std::fprintf(stderr, "[measurepin-probe] gave up waiting for %s after %d polls (mode=%s pinned=%d)\n",
what, p.polls, display_mode_name(a.model->display_mode()),
a.rows->window_pinned() ? 1 : 0);
std::fflush(stderr);
return false;
}
/// The pause leg: presses the button, waits for the frozen frame to settle, then reads and zooms
/// it like the held leg.
gboolean measurepin_pause_wait(gpointer user);
/// The live leg: resumes, waits for the follow to re-frame, then reads and zooms.
gboolean measurepin_live_wait(gpointer user);
/// Whether a **pinned** pane has settled on the pin: the results describe it, and no pass has
/// landed since the last poll. Both halves are needed, and the second is what makes the reading
/// below about the gesture: a pass landing between the two reads moves a value for a reason that
/// is not the zoom.
bool measurepin_pinned_settled(MeasurePinProbe& p) {
App& a = *p.app;
if (!measurepin_at_pin(a) || a.measure_passes != p.passes_seen) {
p.passes_seen = a.measure_passes;
p.settled = 0;
return false;
}
// Three polls of quiet: one is the pass, the next is the frame it landed in being re-clamped.
return ++p.settled >= 3;
}
gboolean measurepin_pause_wait(gpointer user) {
MeasurePinProbe& p = *static_cast<MeasurePinProbe*>(user);
App& a = *p.app;
if (a.window == nullptr || !measurepin_polls(p, "the pause to settle", a)) {
delete &p;
return G_SOURCE_REMOVE;
}
if (a.model->display_mode() != DisplayMode::Paused ||
!measurepin_pinned_settled(p)) {
return G_SOURCE_CONTINUE;
}
p.stage = 1;
measurepin_begin(p, 0.25);
g_timeout_add(400, +[](gpointer user) {
MeasurePinProbe& probe = *static_cast<MeasurePinProbe*>(user);
measurepin_compare(probe, "paused/after");
// The live leg: the display has to *leave* the event, and a resumed view does not -- a
// `Held` model holds its snapshot with the trigger on, so resuming only un-pins it. Switching
// the trigger off is the user's own way back to the stream, and it is what makes the last leg
// a live view rather than the same event under another name.
App& a = *probe.app;
if (a.pause_button != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.pause_button), FALSE);
}
if (a.trigger_toggle != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.trigger_toggle), FALSE);
}
probe.settled = 0;
probe.polls = 0;
g_timeout_add(50, measurepin_live_wait, &probe);
return G_SOURCE_REMOVE;
}, &p);
return G_SOURCE_REMOVE;
}
gboolean measurepin_live_wait(gpointer user) {
MeasurePinProbe& p = *static_cast<MeasurePinProbe*>(user);
App& a = *p.app;
if (a.window == nullptr || !measurepin_polls(p, "the stream to come back", a)) {
delete &p;
return G_SOURCE_REMOVE;
}
// A resumed view is live again, and there "settled" is not a state: the window scrolls and a
// pass lands every `kMeasureRefreshUs`. What is waited for here is the *resume* itself -- the
// follow re-armed and the pin gone -- and then a few frames for the results to be over the new
// window rather than the frozen one.
const bool live = a.model->display_mode() == DisplayMode::Live && !a.rows->window_pinned();
if (!live || ++p.settled < 8) return G_SOURCE_CONTINUE;
p.stage = 2;
measurepin_begin(p, 0.0625);
g_timeout_add(400, +[](gpointer user) {
MeasurePinProbe& probe = *static_cast<MeasurePinProbe*>(user);
measurepin_compare(probe, "live/after");
delete &probe;
return G_SOURCE_REMOVE;
}, &p);
return G_SOURCE_REMOVE;
}
/// Polls for the held event the first leg needs: a latch happens when the stream says so, and a
/// sweep on a timer has nothing to zoom into. Then for the *settled* pin, which is the state the
/// gesture is read against.
gboolean measurepin_held_wait(gpointer user) {
MeasurePinProbe& p = *static_cast<MeasurePinProbe*>(user);
App& a = *p.app;
if (a.window == nullptr) {
delete &p;
return G_SOURCE_REMOVE;
}
if (!measurepin_polls(p, "a latched event to settle on its pin", a)) {
// A wait that never ends is a *reading* about the state, not a hung probe: say what it was
// waiting for rather than printing nothing at all.
delete &p;
return G_SOURCE_REMOVE;
}
// One line per *change* of the state being waited on, which is a handful in a normal run and the
// whole reading in a stuck one: `mode=held at_pin=0` for ever is a pane whose results never land
// on the pin, and it is invisible in the leg lines because there are none.
const int state = static_cast<int>(a.model->display_mode()) * 4 +
(a.rows->window_pinned() ? 2 : 0) + (measurepin_at_pin(a) ? 1 : 0);
if (state != p.last_state) {
p.last_state = state;
std::fprintf(stderr,
"[measurepin-probe] wait mode=%s pinned=%d at_pin=%d for=[%.6f..%.6f] "
"pin=[%.6f..%.6f] passes=%d\n",
display_mode_name(a.model->display_mode()), a.rows->window_pinned() ? 1 : 0,
measurepin_at_pin(a) ? 1 : 0, a.measure_window.min, a.measure_window.max,
a.measure_pin_window.min, a.measure_pin_window.max, a.measure_passes);
std::fflush(stderr);
}
if (a.model->display_mode() != DisplayMode::Held || !measurepin_pinned_settled(p)) {
if (a.model->display_mode() != DisplayMode::Held) p.settled = 0;
return G_SOURCE_CONTINUE;
}
p.stage = 0;
measurepin_begin(p, 0.25);
g_timeout_add(400, +[](gpointer user) {
MeasurePinProbe& probe = *static_cast<MeasurePinProbe*>(user);
measurepin_compare(probe, "held/after");
App& a = *probe.app;
if (a.pause_button != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.pause_button), TRUE);
}
probe.settled = 0;
probe.polls = 0;
g_timeout_add(50, measurepin_pause_wait, &probe);
return G_SOURCE_REMOVE;
}, &p);
return G_SOURCE_REMOVE;
}
gboolean measurepin_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
// The same four types `measure_probe` adds, on a *channel* -- the first signal of a stream is
// its clock, which is not acquired, so naming it would measure nothing at all and every
// comparison below would be a comparison of two `--`.
{
const std::vector<SignalInfo>& signals = a->model->signals();
for (std::size_t i = 0; i < signals.size(); ++i) {
if (signals[i].name == "sine") a->model->set_signal_enabled(i, true);
}
}
std::string name;
{
std::lock_guard<std::mutex> lock(a->model->mutex());
if (a->model->channels().empty()) return G_SOURCE_REMOVE;
name = a->model->channels().front().name;
}
for (const MeasureType t : {MeasureType::Average, MeasureType::Top, MeasureType::PeakToPeak,
MeasureType::Amplitude}) {
MeasureOptions options;
options.show_on_plot = measure_can_show_on_plot(t);
a->measures.push_back({name, t, options});
}
rebuild_measures_pane(*a);
// Single trigger mode, so one event stays on screen for the whole gesture.
if (a->trigger_mode_drop != nullptr) {
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_mode_drop), 1);
}
if (a->trigger_toggle != nullptr) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
}
std::fprintf(stderr, "[measurepin-probe] %zu measurements on '%s', trigger single\n",
a->measures.size(), name.c_str());
std::fflush(stderr);
auto* probe = new MeasurePinProbe;
probe->app = a;
g_timeout_add(50, measurepin_held_wait, probe);
return G_SOURCE_REMOVE;
}
gboolean stats_probe_report(gpointer user) { gboolean stats_probe_report(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;