test: a probe reads the hidden tab's accumulator, and the docs follow the tabs

`UDPSCOPE_PROBE_SIDE_TAB_MS` is the one reading for the design's §5 rule -- the statistics
accumulator keeps running on the hidden page -- which nothing tested before. It switches to
Measure, forces one pass so there is a baseline, switches away to Signals and reads again two
seconds later: `advanced=4` is the assertion, and `advanced=0` is the defect where the
accumulation is gated on the visible tab, which would leave a gap in min/max/avg for exactly as
long as the trigger was on screen.

Two things the probe has to do for itself, and getting either wrong makes `advanced=0` a lie
about the tab:

- it **enables its own signal**, because a signal starts disabled (enabling is what starts
  acquisition) and a probe that waited for one to be on would be reading another probe's side
  effects -- and `UDPSCOPE_ENABLE_ALL_MS` is not part of this run;
- it **assigns that channel to a plot**, because a channel with no trace leaves the live window
  *still*, and a still window is precisely the state `update_measures()` is supposed to skip.
  Measured the hard way: without the assignment the probe read `advanced=0 min_n=0` with the view
  pinned and only four passes in twelve seconds, none of them the tab's doing.

Docs: `probe-readings.md` gains the probe's entry and the signals panel's new shape
(`rows == signals + 1 + math`, `divider=0` a list that lost its `+`), loses the whole `math
panel:` entry, and follows `pane_sensitive=`, the gone `revealed=` field, the six toolbar
controls and the tab-reading `m`/`g` key pairs. `verification.md` gains the probe's bullet and
the re-measured warning figures. The invariants record the sidebar as a switcher above a stack
with a scroller per page, the hidden-page trap that replaces the unrevealed-revealer one for it,
that a feature's on/off is sensitivity rather than visibility, that a math channel is a row of
the signal list, and that a cursors-off readout blanks to `--` with the panel left on screen.
`shortcuts.ui` and the help page's window and sidebar bullets stop describing five revealers.
The README records the deviation from `SPECS.md` that names the panes as separate collapsible
sections.

Verified: `[side-tab-probe] page=signals rows=4 accums=4 advanced=4 min_n=16`;
`signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1`; `panels: … (page_current=1)
(sensitive=0) switcher=1`; `toolbar icons:` six controls with every name `(ok)`, with the
Preferences dialog opened in the same run; zero `Gtk-CRITICAL` and no missing icon.
This commit is contained in:
Martino Ferrari committed 2026-10-14 12:56:43 +02:00
1 parent cb866774c6
commit 6492bb910f
9 files changed
+197 -41

No files matched your search

+10
View File
@@ -2388,6 +2388,16 @@ now — they are listed so they are not mistaken for anything else:
progress. What the spec was protecting still holds — a *held* event is bound to its progress. What the spec was protecting still holds — a *held* event is bound to its
snapshot alone, and the axis is the event's own window from the edge onwards rather snapshot alone, and the axis is the event's own window from the edge onwards rather
than a sliding live view. than a sliding live view.
- **The sidebar is two tabs, and a math channel is a row of the signal list.** `SPECS.md`
names the panes as separate collapsible sections — trigger, cursors, measurements, math,
signal list — in one scrolling column; with four of them open in a 320 px sidebar every
section was short of room, and finding one meant scrolling past the others. The built
sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals**
tab (the trigger and the channels), with the math list folded into the signal list under a
`MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in
the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no
toggle hides a pane any more: trigger-off and cursors-off **disable** their pane and leave
it in place, and the measurements' and math panes' toolbar toggles are gone with the panes.
- **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot` - **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot`
cannot give up a trace and the slots are therefore created up front. cannot give up a trace and the slots are therefore created up front.
- **One toggle drives both cursor orientations.** The vertical markers and the - **One toggle drives both cursor orientations.** The vertical markers and the
+20 -4
View File
@@ -49,13 +49,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
one of them with reality): it is computed on the first *tick*, because the dump one of them with reality): it is computed on the first *tick*, because the dump
runs after the window is destroyed and asking GTK about a widget then is four criticals, which is runs after the window is destroyed and asking GTK about a widget then is four criticals, which is
how the first version of that line earned them. What stays in the menu is what acts on the *data* how the first version of that line earned them. What stays in the menu is what acts on the *data*
(Fit, Export) and the two editors that are not panes (Layout, Signal list). The Arm/Disarm button in the trigger panel still has text. (Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list
is a *tab* of the sidebar, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text.
- **Seven of this app's icons are its own, and they are a `GResource` compiled into the - **Seven of this app's icons are its own, and they are a `GResource` compiled into the
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
measurement ruler, and the stock names those buttons used to carry said something else measurement ruler, and the stock names those buttons used to carry said something else
entirely: a *record dot* for the trigger and three different **magnifiers** for cursors, entirely: a *record dot* for the trigger and three different **magnifiers** for cursors,
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,math,events}-symbolic.svg` measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows}-symbolic.svg`
are the five action icons; beside them in `src/icons/scalable/apps/` are the two are the five action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last
user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it --
an icon with a live user is not an orphan, whatever the toggle that introduced it did. Beside them in
`src/icons/scalable/apps/` are the two
**application** icons, `org.udpscope.app.svg` (128px colour artwork) and **application** icons, `org.udpscope.app.svg` (128px colour artwork) and
`org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an `org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an
application's own icon and is where the **in-process** window icon resolves from; it is also application's own icon and is where the **in-process** window icon resolves from; it is also
@@ -63,7 +67,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
into `$XDG_DATA_HOME/icons/hicolor/scalable/apps` and `.../symbolic/apps` — those installed into `$XDG_DATA_HOME/icons/hicolor/scalable/apps` and `.../symbolic/apps` — those installed
copies are what makes `Icon=org.udpscope.app` in the desktop entry resolve (a desktop shell copies are what makes `Icon=org.udpscope.app` in the desktop entry resolve (a desktop shell
cannot read a GResource compiled into the binary). cannot read a GResource compiled into the binary).
All six are bundled by `src/icons/udpscope.gresource.xml` (CMake runs All seven are bundled by `src/icons/udpscope.gresource.xml` (CMake runs
`glib-compile-resources`) and reached through **one** `glib-compile-resources`) and reached through **one**
`gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top `gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top
of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon, of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon,
@@ -179,6 +183,18 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not
take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane
*is* open. The allocated width is how this was found -- the revealer's own measure said 0. *is* open. The allocated width is how this was found -- the revealer's own measure said 0.
**The sidebar is a tab stack now, and has no `GtkRevealer` left in it**: an `AdwViewSwitcher`
above an `AdwViewStack`, each page a `GtkScrolledWindow` of its own (the two pages are different
heights, so one shared scroller would leave a switch scrolled where the new page never asked to
be). The reason is the one this trap teaches: five sections competing for one 320 px column left
every one of them short of room. A *feature's* on/off is no longer a visibility at all -- the
trigger's and the cursors' toggles set their panel's **sensitivity**, so the panel stays where it
is and the settings you are about to switch back on do not vanish while you look for them. The
trap itself does not go away with the widget: a stack's hidden page measures 0x0 too and is still
given the leftover width, so **ask about state, not about size** carries over unchanged. Every
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not*
current -- a change in what the reading means, not a defect, and no functional code may depend on
those numbers.
- **A list box outlives the children of the pane it sits in, and the teardown order is not yours to - **A list box outlives the children of the pane it sits in, and the teardown order is not yours to
choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the
list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four
+11
View File
@@ -301,6 +301,17 @@ with at most the transient a re-start costs. The exit dump's `math '<name>': fin
the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero
`hole_slots` is this defect returned. `hole_slots` is this defect returned.
- **A math channel is a row of the *signal* list, under a `MATH` divider, and there is no math pane.**
`append_math_signals_locked()` already puts every channel in `signals()`, so the pane was the same
list drawn twice; the divider is what the pane uniquely contributed. It is non-selectable and
non-activatable -- a heading that highlighted under the pointer would read as a row you can plot --
and it carries the `+` that creates a channel, appended even when there are none, because a `+` that
waited for a channel to exist could never create the first one. A math row's menu is *Edit graph…*,
*Default colour*, *Delete*: no Information and no Calibrate, because calibration is applied at
ingest and a derived channel has no stream to describe, and no rename, because the node editor owns
a channel's name and unit and a second place to change them would be a second answer to the same
question. A row's tick is the channel's own switch and must reach `App::math` -- the copy the
session file is written from -- or the list and the file disagree about what "enabled" was.
- **A graph *edit* does not move the plan; adding or removing a channel does.** The plan is a partition over - **A graph *edit* does not move the plan; adding or removing a channel does.** The plan is a partition over
the channels, so `set_math_channels()` takes a surgical path whenever the *set* is unchanged -- same channels, the channels, so `set_math_channels()` takes a surgical path whenever the *set* is unchanged -- same channels,
names and enables: the store is never touched, every other channel keeps its samples **and** its runtimes, and names and enables: the store is never touched, every other channel keeps its samples **and** its runtimes, and
@@ -54,6 +54,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
unit and a spaced `m` alone reads as metres; with one it stays spaced and the prefix joins the unit and a spaced `m` alone reads as metres; with one it stays spaced and the prefix joins the
unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so
that range falls back to `%.3g`. that range falls back to `%.3g`.
- **With the instruments off the readout blanks, and the panel stays on screen.** Cursors-off makes the
pane *insensitive* rather than hiding it -- a panel you can still see is a panel you can read, and the
settings you are about to switch back on must not vanish while you look for them -- and every cell in
it is written `"--"`, the spelling an unavailable cell already uses. A readout quotes a stored sample;
with nothing bracketing one there is no sample to quote, and keeping the last values would be quoting
a sample the cursors no longer name. Two spellings of one state in one grid would be the defect.
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x` - **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
+45 -24
View File
@@ -161,10 +161,12 @@ Two lines of the self-check output are assertions in disguise:
*adoption* half -- a gesture that does move a cursor, and the shared value reaching every row *adoption* half -- a gesture that does move a cursor, and the shared value reaching every row
-- is asserted in the `check_host` harness, which can call `sync_cursors(row)` directly; a -- is asserted in the `check_host` harness, which can call `sync_cursors(row)` directly; a
synthetic press here cannot reach the adapter's own event pump.) synthetic press here cannot reach the adapter's own event pump.)
- `[cursors-probe] <action> mode=.. on=D pane=D t0=.. t1=.. dt=.. T=[..] lines=N [..]` - `[cursors-probe] <action> mode=.. on=D pane_sensitive=D t0=.. t1=.. dt=.. T=[..] lines=N [..]`
(stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` → (stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` →
`mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is `mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is
what a drag does) and reports again. **The times must survive every mode change**: they what a drag does) and reports again. `pane_sensitive` is the cursors *panel's* sensitivity, not a
revealer's child: with the cursors off the panel stays exactly where it is and its cells blank to `--`,
because a readout whose instruments are off has no sample to quote. **The times must survive every mode change**: they
are the `RowSet`'s one pair, and a mode that parks the markers must not read them back are the `RowSet`'s one pair, and a mode that parks the markers must not read them back
(a parked cursor reports a real number, so a read-back would adopt it as a gesture — the (a parked cursor reports a real number, so a read-back would adopt it as a gesture — the
bug that made `T0` read 0). **`lines`** must be 0 for `vertical`, one per row for bug that made `T0` read 0). **`lines`** must be 0 for `vertical`, one per row for
@@ -195,7 +197,7 @@ Two lines of the self-check output are assertions in disguise:
numbers and the plot's state agreeing is the difference between "the app placed them" and numbers and the plot's state agreeing is the difference between "the app placed them" and
"the app placed them and they are on screen". "the app placed them and they are on screen".
- `[measures-probe]` + the dump's `measure N: 'sig' type thr=..% above=D show=D -> ok|invalid value text=[..] - `[measures-probe]` + the dump's `measure N: 'sig' type thr=..% above=D show=D -> ok|invalid value text=[..]
[duty=..] [marks=..]` and `measures pane: n=N revealed=D marks_shown=M pane_rows=R pane_w=W [duty=..] [marks=..]` and `measures pane: n=N marks_shown=M pane_rows=R pane_w=W
pane_h=H switch_on=X reset_sensitive=Y` pane_h=H switch_on=X reset_sensitive=Y`
(stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first (stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first
plottable signal and lets the tick compute them, so what it prints is what a user reads. plottable signal and lets the tick compute them, so what it prints is what a user reads.
@@ -345,9 +347,9 @@ Two lines of the self-check output are assertions in disguise:
and an expanding one is measured into the title's centre box, so it stretched from the and an expanding one is measured into the title's centre box, so it stretched from the
label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that
grows with the window width is that bug back. grows with the window width is that bug back.
- `toolbar icons: trigger=NAME(ok) pause=... cursors=... measures=... math=... sidebar=... - `toolbar icons: trigger=NAME(ok) events=... pause=... cursors=... sidebar=... menu=...` (in the
menu=...` (in the exit dump, beside `panel controls:`) — the icon name every toolbar button exit dump, beside `panel controls:`) — the icon name every toolbar button
carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Four carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Three
of the names are this app's own (`udpscope-trigger-symbolic` and friends, a `GResource` of the names are this app's own (`udpscope-trigger-symbolic` and friends, a `GResource`
compiled into the binary — see the toolbar-icon invariant in compiled into the binary — see the toolbar-icon invariant in
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)), and a `(MISSING)` on one of [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)), and a `(MISSING)` on one of
@@ -598,7 +600,7 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses
itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores
it afterwards. it afterwards.
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values). - `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: tab signals->measure | g: tab measure->signals | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. The `m`/`g` pairs are the *tab name* before and after, which is what says those two keys select a tab rather than toggling a pane; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
- `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit. - `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit.
- `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N - `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N
open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` (stderr, open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` (stderr,
@@ -1144,6 +1146,30 @@ and `regained` reads 400 ms later.
- `collapsed=1` means the window is narrower than `kSidebarRoom` (900), which is the expected reading on a - `collapsed=1` means the window is narrower than `kSidebarRoom` (900), which is the expected reading on a
scaled display, where the logical width is half what the screen looks like. scaled display, where the logical width is half what the screen looks like.
## `[side-tab-probe]`
```
[side-tab-probe] page=signals accums=4 baseline recorded
[side-tab-probe] page=signals rows=4 accums=4 advanced=4 min_n=18
```
`UDPSCOPE_PROBE_SIDE_TAB_MS` is the one reading for the rule that **the statistics accumulator keeps running
on the hidden page**. It switches *to* the Measure tab, forces one pass so there is a baseline, then switches
away to Signals and reads again two seconds later -- with the measurements' page not current, so its widgets
are unrealized and measure 0x0.
- **`advanced=4` is the assertion**: every accumulator gained passes while its page was off screen, and
`min_n` (the smallest `n` seen) must be non-zero too. **`advanced=0` is the defect**: something gated the
accumulation on the visible tab, so the readout would gain a gap in its min/max/avg for exactly as long as
the trigger was on screen. A `Gtk-CRITICAL` here is the other half of the same rule -- a label or an
allocation read on the unrealized page.
- The probe enables a signal **and assigns it to a plot** itself, both deliberately. A signal starts disabled
(enabling is what starts acquisition), and a channel with no trace leaves the live window *still* -- and a
still window is the state `update_measures()` is *supposed* to skip, so a probe that left it that way would
read `advanced=0` for a reason that has nothing to do with the tab.
- `rows` is the measurement count and `accums` the accumulator count; the two must agree, because one
accumulator per measurement row is what makes `advanced` a statement about every row.
## `[prefs-probe]` ## `[prefs-probe]`
``` ```
@@ -1252,25 +1278,20 @@ the two lines together read what the file gave and what the code put in it.
## `signals panel:` ## `signals panel:`
``` ```
[udpscope] signals panel: labels=2 list=1 [udpscope] signals panel: labels=2 list=1 rows=5 signals=4 math=0 divider=1
[udpscope] signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1
``` ```
The signals panel's frame read from the window: the heading and the hint the `.ui` gives it, and the list The signals panel's frame read from the window, and in the same line the *shape* of the one channel list:
the rows go in. `labels=1` is a `.ui` that lost one of the two, `list=0` one that lost the list -- both of the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds
which otherwise show as a panel quietly missing a piece. `panels: … signals=WxH row_h=26` is the other half: (`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`),
the list's own geometry and the height a row really takes, which is what the compact-row CSS rule is for. and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. `labels=1` is a `.ui` that lost
one of the two labels, `list=0` one that lost the list -- both of which otherwise show as a panel quietly
## `math panel:` missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create
a math channel. The first line above is a run with no math channels at all: the divider is there anyway,
``` because a `+` that only appeared once a channel existed could never create the first one. `panels: …
[udpscope] math panel: head=2 buttons=1 list=1 title='Math channels' signals=WxH row_h=26` is the other half: the list's own geometry and the height a row really takes, which
``` is what the compact-row CSS rule is for.
The math panel's frame read from the window: the head row the `.ui` gives it (the title and the add
button), and the list the channel rows go in. `buttons=0` is a `.ui` that lost the `+` -- a panel that
cannot add a channel, with nothing else complaining -- and `title=` is the file's own text read back, so a
`.ui` that lost the label shows as `title='-'`. `[math-probe]` is the other half of the reading: it drives
the add and the editor through the same code a user does.
## `events pane:` ## `events pane:`
+11 -3
View File
@@ -15,9 +15,11 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
Two baselines every run is read against. A clean build emits **four** warnings and no more: two Two baselines every run is read against. A clean build emits **four** warnings and no more: two
`-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both** `-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both**
generated GResource `.c` files — the help pages' and the icons', whose blob crossed C99's generated GResource `.c` files — the help pages' (**514532** bytes) and the icons', whose blob is
4095-byte string-literal limit when the two app icons joined the four action ones (a note about a **8613** against C99's 4095-byte string-literal limit with the five action icons and the two app icons
generated file, which GCC itself supports). The fifth warning that used to appear here was an unused that remain (a note about a generated file, which GCC itself supports) -- re-measured when
`udpscope-math-symbolic` was deleted, which did not move the count; re-measure by touching the two
`*.gresource.xml` files and rebuilding. The fifth warning that used to appear here was an unused
`output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the `output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the
`output_after_add` already printed on the same line — and that deletion is what dropped the baseline `output_after_add` already printed on the same line — and that deletion is what dropped the baseline
from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at
@@ -205,6 +207,12 @@ precedent, not an exception.
pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three
phases: `phase=0`, `phase=1` (after pause) and `phase=2` (after Reset). Needs phases: `phase=0`, `phase=1` (after pause) and `phase=2` (after Reset). Needs
`UDPSCOPE_ENABLE_ALL_MS` ahead of it and `UDPSCOPE_PROBE_MEASURE_MS` to add measurements first. `UDPSCOPE_ENABLE_ALL_MS` ahead of it and `UDPSCOPE_PROBE_MEASURE_MS` to add measurements first.
- `UDPSCOPE_PROBE_SIDE_TAB_MS=3000` with `UDPSCOPE_EXIT_AFTER_MS=11000` — the measurements on the
Measure tab, a forced pass, then the tab switched away and read again two seconds later. **`advanced=4`
is the assertion** (every accumulator gained passes while its page was off screen); `advanced=0` is the
defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other
probe ahead of it. The reading's own entry is in `probe-readings.md`.
- `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live - `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live
trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to
hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`. hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`.
+7 -6
View File
@@ -31,13 +31,14 @@ panel is working correctly, not failing to connect.
<b>The window</b> <b>The window</b>
• <b>The header bar</b> -- left to right: the time window (<tt>2 s</tt>), the trigger and the • <b>The header bar</b> -- left to right: the time window (<tt>2 s</tt>), the trigger and the
pause, then the pane switches -- signal list, cursors, measurements, math, side panel -- and pause, the events, cursors and side-panel switches, and the menu, which holds Fit to data, the
the menu, which holds Fit to data, the layout editor, Export, Keyboard shortcuts, Help and layout editor, Export, Keyboard shortcuts, Help and About.
About.
• <b>The side panel</b> -- the trigger, cursors, measurements and math panes at the top, then • <b>The side panel</b> -- two tabs. <b>Measure</b> holds the cursors readout and the
the signal list: one row per signal with its colour, an enable switch, its name and its unit. measurements derived from it; <b>Signals</b> holds the trigger and the channel list, where a
Signals are dragged from here onto a plot row. math channel is a row of that one list under a <tt>MATH</tt> heading whose <tt>+</tt> creates
one. One row per channel: its colour, an enable switch, its name and its unit. Channels are
dragged from here onto a plot row.
• <b>The plot</b> -- up to 8 rows, each with its own y axis and up to 4 traces, all on one • <b>The plot</b> -- up to 8 rows, each with its own y axis and up to 4 traces, all on one
shared time axis. Right-click a row for its own menu (fit, add a signal, remove a trace). shared time axis. Right-click a row for its own menu (fit, add a signal, remove a trace).
+4 -4
View File
@@ -46,7 +46,7 @@
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="visible">1</property>
<property name="accelerator">t</property> <property name="accelerator">t</property>
<property name="title" translatable="yes">Show or hide the trigger panel</property> <property name="title" translatable="yes">Enable or disable trigger mode</property>
</object> </object>
</child> </child>
</object> </object>
@@ -66,21 +66,21 @@
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="visible">1</property>
<property name="accelerator">c</property> <property name="accelerator">c</property>
<property name="title" translatable="yes">Show or hide the cursors pane</property> <property name="title" translatable="yes">Enable or disable the cursors</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="visible">1</property>
<property name="accelerator">m</property> <property name="accelerator">m</property>
<property name="title" translatable="yes">Show or hide the measurements pane</property> <property name="title" translatable="yes">Show the Measure tab</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="visible">1</property>
<property name="accelerator">g</property> <property name="accelerator">g</property>
<property name="title" translatable="yes">Show or hide the math pane</property> <property name="title" translatable="yes">Show the Signals tab</property>
</object> </object>
</child> </child>
<child> <child>
+83
View File
@@ -1124,6 +1124,89 @@ gboolean stats_probe(gpointer user) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
/// Phase two of the tab-switch probe: the hidden page's accumulator must have advanced. A statistics
/// readout is *history*, so min/max/avg would gain a gap for exactly as long as the trigger was on
/// screen -- which is the one defect the design's "what runs on a hidden page" rule is there to
/// prevent, and the reason no frame-rate code may be gated on which tab is current.
gboolean side_tab_probe_report(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
std::uint64_t advanced = 0;
std::uint64_t min_n = 0;
bool first = true;
for (std::size_t i = 0; i < a->measure_stats.size(); ++i) {
const std::uint64_t n = a->measure_stats[i].n;
if (i < g_side_tab_n_before.size() && n > g_side_tab_n_before[i]) ++advanced;
if (first || n < min_n) {
min_n = n;
first = false;
}
}
std::fprintf(stderr,
"[side-tab-probe] page=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
" min_n=%" G_GUINT64_FORMAT "\n",
a->side_tab.c_str(), a->measures.size(), a->measure_stats.size(), advanced, min_n);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Phase one: four measurements on the first channel, statistics on, then switch *away* from the
/// Measure tab and come back in two seconds to read what ran while it was not on screen.
gboolean side_tab_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
// One signal on, and on a plot, the way `enable_all_probe` does it and a restored session arrives.
// Both halves matter here: a signal starts disabled (enabling is what starts acquisition), and a
// channel with no trace leaves the live window *still* -- and a still window is exactly the state
// the tick is supposed to skip, so a probe that left it that way would be testing nothing. This
// probe is about the case where the window does move while the page is somewhere else.
std::size_t pick = a->model->signals().size();
{
const std::vector<SignalInfo>& signals = a->model->signals();
for (std::size_t i = 0; i < signals.size(); ++i) {
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not acquired
pick = i;
break;
}
}
if (pick >= a->model->signals().size()) return G_SOURCE_REMOVE;
a->model->set_signals_enabled({pick}, true);
std::string name;
{
const std::vector<ChannelInfo>& channels = a->model->channels();
if (channels.empty()) return G_SOURCE_REMOVE;
name = channels.front().name;
a->rows->assign(0, 0, channels.front().name, channels.front().name,
signal_color_index(a->model->signals(), channels.front().signal));
}
a->relayout_pending = true;
if (a->measures.empty()) {
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);
}
if (a->measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
}
select_side_tab(*a, "measure");
update_measures(*a, /*force=*/true, /*accumulate=*/true);
g_side_tab_n_before.clear();
for (const MeasureStatsAccum& s : a->measure_stats) g_side_tab_n_before.push_back(s.n);
// Away from the page the measurements are on: its widgets are now unrealized and measure 0x0, and
// the tick keeps calling `update_measures()` regardless.
select_side_tab(*a, "signals");
std::fprintf(stderr, "[side-tab-probe] page=%s accums=%zu baseline recorded\n",
a->side_tab.c_str(), g_side_tab_n_before.size());
std::fflush(stderr);
// Long enough for several throttled recomputes (one per 100 ms while the window moves).
g_timeout_add(2000, side_tab_probe_report, a);
return G_SOURCE_REMOVE;
}
gboolean pan_probe(gpointer user) { gboolean pan_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE; if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;