trigger: a phosphor display keeps the last N acquisitions; and a Force button
Two changes in the trigger pane, landed together. **Persistence.** Trigger mode keeps the last N published acquisitions and draws them fading toward the background, N being a panel control (1..8, default 1) with N=1 meaning today's behaviour exactly, so the knob is the switch. The layers are the accumulate path's own -- one layer per (signal, age) in the row's free trace slots, named with the accumulate vocabulary so `layer_base_name()` strips the suffix and `signal_color()` resolves the base signal -- so a layer is not a signal: the signal list, the colours and the measurements see one row per signal. `ScopeModel` retains the outgoing snapshot in seven atomic `shared_ptr` slots (no lock, no copy; the snapshot already exists) and a layer's channel is resolved **by name inside the snapshot**, because the channel list is renumbered by any structural change since that acquisition was cut. `RowSet::sync_layers()` re-plans from the rows and the depth alone, so a latch costs no rebuild, and `event_data_window()` skips layer slots so an older acquisition can never re-pin the axis. The schedule is 0.5^age -- the acquisition on screen at full brightness and each older one half the previous, independent of N, so raising the depth adds dimmer acquisitions rather than rescaling the ones already on screen. Memory is bounded by the ring's own `--buffer-mb` rather than by the depth, which is the bound that cannot be driven into an OOM, and the panel says when either bound bites. This also wired up `set_trace_alpha()`, which the accumulate path was setting and nothing was reading -- so the documented 0.35 translucency of an accumulation was opaque in fact. **Force.** In the trigger pane beside Arm/Disarm, shown only while trigger mode is on: one acquisition now, with the configured window. It is a synthetic edge rather than a second publisher -- the engine opens one capture at the present moment and everything else is the ordinary latch, so a forced acquisition is cut, pinned, framed, recorded and cached exactly like a latched one, and the button owns no window and no publish. The pre-trigger gate is deliberately skipped (capture now is a request for the history that exists, and the snapshot reports its own shorter pre_s), a capture already filling is refused rather than restarted, and the state afterwards is the mode's: Normal stays armed, Single disarms. The request is one atomic flag written under the model mutex with `armed` -- the producer reads `armed` at the top of a packet and the request at the bottom, so a request written between the two would be dropped -- and consumed at the end of a packet, after the channel loop and before the pending publish. **A defect fixed in passing, in the function Force was modelled on:** `on_arm_clicked()` passed `TriggerConfig::channel` (an index into `channels()`) to `set_signal_enabled()` (an index into `signals()`). With one signal acquired, arming switched on the stream's clock signal and left the watched signal untouched -- and since a signal the panel was not showing is a change, it rebuilt the buffer under the click, dropping the very pre-trigger window the arming, and now the Force, was about to capture. Measured before: `0.0100 s of history behind it`; after: `2.5299 s`. `trigger_signal_index()` is now the one resolution, used by both buttons.
This commit is contained in:
1 parent
fc4700d1c9
commit
e14e1ee069
22 files changed
+1652
-26
No files matched your search
@@ -94,6 +94,50 @@ Two lines of the self-check output are assertions in disguise:
|
||||
`events` does not move**: no event fired, so the partial publish deliberately leaves
|
||||
`trigger_latches()` alone — which is also what keeps the `trigger events: N total, M at
|
||||
the disarm` line in the dump meaning what it says.
|
||||
- `[force-probe] button off: sensitive=0 visible=1 w=X | on: sensitive=1 w=X | arm w=Y |
|
||||
label_found=1` (stderr, `UDPSCOPE_PROBE_FORCE_MS`) — the Force button's stated state, read on both
|
||||
sides of the trigger toggle. **`sensitive=0` with the trigger off and `1` with it on** is the whole
|
||||
rule (`sync_arm_buttons()` states it from the toggle, and a trigger that is off has no engine to
|
||||
force); `visible=1` in both says it is a *body* control and not a compact-heading one, which is why
|
||||
the off reading must not be `visible=0`; `w` is its allocation a frame after the state it belongs
|
||||
to, and `arm w=Y` the button beside it, so a `w=0` next to a non-zero `Y` is the "width read in the
|
||||
tick the control was stated" trap rather than a collapsed button. `label_found=1` is the button the
|
||||
window shows under the word **Force** *being* the panel's own widget.
|
||||
- `[force-probe] leg A|B want=[-a..b] during=[-c..d] snap=[-e..f] n=N axis=[..] latches=P->Q
|
||||
display=.. matches_request=D pin_is_snapshot=P` (stderr, `UDPSCOPE_PROBE_FORCE_MS`) — one line per
|
||||
leg, taken after the latch the press produced. **`latches` must move by exactly one and `display`
|
||||
must read `event(frozen)`**: the probe sets the condition to a level the mock never reaches (9 V
|
||||
against ±1 V signals), so the engine cannot latch on its own and a forced acquisition is the only
|
||||
thing that *can* move the count — that is also why this probe must run alone, since any other probe
|
||||
that arms a reachable condition would add latches of its own. `want` is the pre/post split the
|
||||
panel was set to at the press, taken from the producer's own `post_span_s()`; `snap` is what the
|
||||
snapshot holds (`[-pre_s..+post_s]`); `during` is the axis while the forced capture was *filling*,
|
||||
which is the *requested* window — a capture has no snapshot to frame from yet. The two legs differ
|
||||
in which of those can agree:
|
||||
- **Leg A (2 s window at 50 % pre, on a ring that has filled)**: `matches_request=1`, i.e.
|
||||
`snap ≈ want` to within a stored interval, and `n` the span times the stream's real rate plus
|
||||
one (`20001` at 10 kS/s for `[-1..+1]`, exactly as the latch line demands).
|
||||
- **Leg B (100 s window at 99 % pre, which no run this short can reach back to)**: the request is
|
||||
99 s of history and the ring holds seconds, so `matches_request=0` **on purpose** and
|
||||
`snap ≈ [-history..+1]`. That is the leg that makes **`pin_is_snapshot=1`** a reading rather than
|
||||
a coincidence: `axis` must equal `[-snap.pre..+snap.post]` (a few seconds wide) while `during`
|
||||
still reads the requested `[-99..+1]`. A pin stated from the *request* — the
|
||||
`hold_pinned_window()`/`pin_event_window()` mistake this probe exists for — reads
|
||||
`axis=[-99.0000..1.0000]` here, i.e. `pin_is_snapshot=0` with the frozen picture a fraction of
|
||||
its own axis.
|
||||
A forced acquisition is an ordinary latch in every other respect, so the `trigger: edge at X ...
|
||||
want [-a..b] got [-c..d]` line appears beside each leg with the *force's* edge as its `X`, and that
|
||||
line's own rule (`got == want`, `n` exact) applies to it unchanged. The line
|
||||
`[udpscope] trigger: forced acquisition, edge at E -- want [-a..b] s, H s of history behind it,
|
||||
mode=M` is the producer accepting the request: `H` is the ring's retained span at the press, which
|
||||
is what caps the pre side in leg B, and `mode=single (disarms at the latch)` says the one
|
||||
acquisition the mode means, so an armed state that reads `0` afterwards is right and not a lost
|
||||
click. `force ignored (enabled=.. armed=.. ch=.. pending=..)` is the refusal — a trigger that is
|
||||
off, a channel that is still `-1`, or **a capture already filling**, which is deliberately *not*
|
||||
restarted (it is the acquisition being asked for); in the `ch=-1` case the click is refused in the
|
||||
handler instead (`force refused -- no signal is being acquired to trigger on`), because a request
|
||||
left to the producer would sit behind `on_data()`'s no-streamed-channel early return and fire on
|
||||
the first signal switched on afterwards.
|
||||
- `[trigrenum-probe] window=W event_span=S pre_pct=P post=O holdoff=H level=L edge=E mode=M
|
||||
enabled=1 armed=1 ch=C channels=N`, then `capture open: t0=T ch=C -- enabling signal 1 now`,
|
||||
`after enable: trigger ch=C' channels=N'`, and `latched t0=T' opened on T t0_held=D` (stderr,
|
||||
@@ -598,7 +642,12 @@ Two lines of the self-check output are assertions in disguise:
|
||||
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the
|
||||
first run's values, and the signals it names must come back *acquired* (a `row 0 slot 0
|
||||
-> channel ... samples=N` line). Delete the file before a run whose layout you did not
|
||||
mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise).
|
||||
mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise). A **persistence
|
||||
layer** would appear in that line as a suffixed name (`sine (A)`, `sine #4`) *while trigger mode
|
||||
is on* — it is not the user's arrangement and is never saved (`collect_layout()` writes a layer
|
||||
slot as empty) — and it is absent from the dump, which is always taken after the exit probe's
|
||||
go-live has cleared the layers. `[persist-probe]` is therefore the only run-in-progress reading of
|
||||
them.
|
||||
- `[udpscope] starved: connected (config=1), reconnects=N, no new frames in the last
|
||||
second ...` (stderr, once per second while the state holds, plus a status-line notice) —
|
||||
"connected but nothing is arriving after a reconnect", which over unicast is the second
|
||||
@@ -1323,7 +1372,7 @@ popover row reads `failed: <strerror>` — one stderr line, no retry storm again
|
||||
## `[defaults]` and `[configs]`
|
||||
|
||||
```
|
||||
[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01
|
||||
[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01 0 0 1
|
||||
[defaults] calibrated: 2.5 -1.25 model=2.5 -1.25
|
||||
[configs] before=default active=default
|
||||
[configs] save-as=1 names=probe,default active=probe
|
||||
@@ -1339,7 +1388,7 @@ sets them to **7.5 and 250** before saving, so a key that never reached the file
|
||||
| --- | --- | --- |
|
||||
| `[defaults] calibrate:` | `1 0 model=1 0` | an unseeded spin button: `gtk_spin_button_new_with_range()` leaves the value at the range minimum, so the offset opened at -1e12 and Apply wrote it |
|
||||
| `[defaults] calibrated:` | `2.5 -1.25 model=2.5 -1.25` — the dialog and the model agreeing after a calibration was set | a dialog that opens at the identity while the trace is drawn through a gain: pressing Apply "confirms" the calibration away. The two halves are printed so the assertion is that they *match*, not that either is a particular number |
|
||||
| `[defaults] trigger:` | `0.001 0.002 0 0.001 0 50 0.01` | the same, per trigger field; a level of `-1e12` is a number nobody chose |
|
||||
| `[defaults] trigger:` | `0.001 0.002 0 0.001 0 50 0.01 0 0 1` | the same, per trigger field — the ten spins in tree order: width, width2, level2, timeout, level, pre-trigger, hold-off, noise band, HF reject, persistence; a level of `-1e12` is a number nobody chose, and the trailing `1` is the persistence depth, whose default *is* the feature off |
|
||||
| `[configs] names` | gains `probe`, and loses it after `forget` | a save that writes somewhere the listing does not read, or a delete that leaves the file |
|
||||
| `[configs] active` | `probe` after the save-as | the `active` file not written, so the next start opens the wrong configuration |
|
||||
| `[configs] round trip` | `read=1`, the note verbatim, `buffer_mb` non-zero | the note or the startup settings not persisted — a configuration that carries a layout but not the buffer size it was tuned with |
|
||||
@@ -1850,7 +1899,7 @@ through one row type, for the reason in `invariants-gtk-and-chrome.md`. The form
|
||||
## `trigger panel:`
|
||||
|
||||
```
|
||||
[udpscope] trigger panel: boxes=15 controls_inside=15/15 search_widgets=4/4 search_inside=4/4
|
||||
[udpscope] trigger panel: boxes=17 controls_inside=16/16 search_widgets=4/4 search_inside=4/4
|
||||
```
|
||||
|
||||
The trigger panel's frame, read from the window: **the `.ui`'s own panel** (`trigger_body`), not the box
|
||||
@@ -1858,18 +1907,101 @@ this app wraps it in — the heading row and the locked summary are the code's a
|
||||
*wrapper* made this line report `boxes=2 … search_inside=0/4` the moment the heading moved out of the file.
|
||||
That is the failure this line is for, so it is worth knowing it can happen to itself. `boxes` is the row
|
||||
boxes the `.ui` holds, `controls_inside`
|
||||
counts the fifteen controls the code built and then appended into those rows -- the last two being the
|
||||
noise band and the HF reject, whose rows arrived with the noise-rejection work, `search_widgets` is the four
|
||||
counts the sixteen controls the code built and then appended into those rows -- the last three being the
|
||||
noise band and the HF reject, whose rows arrived with the noise-rejection work, and the persistence depth
|
||||
whose row arrived with the persistence display (the Arm/Force pair is a row of its own too, so `boxes` counts
|
||||
it while `controls_inside` does not: the pair is a *button* row, not a value) -- `search_widgets` is the four
|
||||
search buttons having resolved by id, and `search_inside` is those four being inside the block the expander
|
||||
opens onto. A control built and never placed -- an id that drifted between the file and the code -- reads as
|
||||
`controls_inside` below 15 while every other line still looks right, which is the panel that has a field
|
||||
`controls_inside` below 16 while every other line still looks right, which is the panel that has a field
|
||||
missing.
|
||||
|
||||
The search walk starts at `gtk_expander_get_child()`, not at the panel: an expander's child is not in the
|
||||
widget-parent chain at all, so walking up from a search button reaches no ancestor of the panel (see
|
||||
`invariants-gtk-and-chrome.md`). `[defaults] trigger:` prints the same panel's seven spins in tree order, so
|
||||
`invariants-gtk-and-chrome.md`). `[defaults] trigger:` prints the same panel's ten spins in tree order, so
|
||||
the two lines together read what the file gave and what the code put in it.
|
||||
|
||||
## `[persist-probe]` and `persistence:`
|
||||
|
||||
```
|
||||
[persist-probe] held depth=4 kept=3 bytes=20998152 budget=209715200 dropped=0 signals=5 channels=4 layers=12 want=12 placed=12 alphas=[0.1250,0.2500,0.5000] ages=[3,2,1] row0=[1:'sine (A)'@0.5000 2:'sine (B)'@0.2500 3:'sine #4'@0.1250] drawn=12
|
||||
[persist-probe] held axis=[-0.2500..0.2500] pin=[-0.2500..0.2500] snap=[-0.2500..0.2500] n=250001 ev=[-0.2500..0.2500] ev_is_snap=1 pin_is_snap=1
|
||||
[udpscope] persistence: depth=4 kept=3 bytes=20998152 budget=209715200 dropped=0 layers=0 want=0 placed=0 schedule=[0.1250,0.2500,0.5000] alphas=[]
|
||||
```
|
||||
|
||||
The persistence display: the last `depth` triggered acquisitions drawn behind the current one, each older one
|
||||
at half the previous one's brightness. Run with one signal per row and a 0.5 s window against the mock at
|
||||
500 kS/s, which is what the two lines above are.
|
||||
|
||||
**`alphas=` is the assertion, oldest first**: `0.5000,0.2500,0.1250` — halving, with `ages=[3,2,1]` matching
|
||||
one for one. A flat, ascending, or age-gapped list is the decay not being applied or applied backwards (the
|
||||
rule is in [invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)). `row0=` is the same
|
||||
schedule read per slot, with each layer's name — `sine (A)` one acquisition back, `sine (B)` two, `sine #4`
|
||||
three, the accumulate path's own suffix vocabulary. `layers=` is the layer slots that exist,
|
||||
`want=` the depth's whole demand across the rows, `placed=` what fitted: **the three agreeing is a display
|
||||
that shows every age it was asked for**, and `placed < want` is a layout with no free trace slot left (a row
|
||||
of `S` signals shows `floor((8 - S) / S)` ages of each, so the default four-signal row shows one).
|
||||
|
||||
**`kept`/`bytes`/`dropped` are the *retention*, and they bound what those slots can draw.** `kept=3` of the
|
||||
3 asked at `bytes=20998152` (21 MB: three acquisitions of a 0.5 s window at 500 kS/s over four channels,
|
||||
~7 MB each) against `budget=209715200`, the ring's own `--buffer-mb`. `drawn=` is how many layer slots are
|
||||
actually *bound* to a retained acquisition, and it is a different number from `placed=` exactly when the
|
||||
budget bit — which is the run to take next, on the fast mock with a long window:
|
||||
|
||||
```
|
||||
[persist-probe] settled: kept=1 of 7 after 270 polls
|
||||
[persist-probe] held depth=8 kept=1 bytes=72798888 budget=209715200 dropped=0 signals=5 channels=4 layers=28 want=28 placed=28 alphas=[0.0078,0.0156,0.0312,0.0625,0.1250,0.2500,0.5000] ages=[7,6,5,4,3,2,1] row0=[1:'sine (A)'@0.5000 … 7:'sine #8'@0.0078] drawn=4
|
||||
[persist-probe] held axis=[-1.0000..1.0000] pin=[-1.0000..1.0000] snap=[-1.0000..1.0000] n=2600000 ev=[-1.0000..1.0000] ev_is_snap=1 pin_is_snap=1
|
||||
[udpscope] persistence: depth=8 kept=2 bytes=145598128 budget=209715200 dropped=2 layers=0 want=0 placed=0 schedule=[0.0078,0.0156,0.0312,0.0625,0.1250,0.2500,0.5000] alphas=[]
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_PERSIST_DEPTH=8 UDPSCOPE_PROBE_PERSIST_WINDOW=2` against the mock at 1.3 MS/s: one retained
|
||||
acquisition is 72.8 MB, so the 200 MB budget keeps two and drops two — `kept` below the depth, `dropped`
|
||||
non-zero, and **`drawn=4` against `placed=28`**: the layer slots exist and the oldest seven ages per row draw
|
||||
nothing at all. That is the memory bound as a reading, and `settled:` is the wait saying so (the retention
|
||||
can never reach the depth, so the probe reports what stopped growing instead of waiting out its cap — its
|
||||
`kept` can therefore be mid-fill, which is why the *dump's* line below is the settled one). The
|
||||
in-doubling of `alphas=` there is the same rule at full depth: 1/128 at the eighth, which is why the depth
|
||||
stops at 8. Do not read this run's `frame cost:` as persistence's: a 2 s window at 1.3 MS/s costs ~90 ms a
|
||||
latch in `publish cost:` on its own, with or without persistence.
|
||||
|
||||
**`ev_is_snap` and `pin_is_snap` must be 1**, and that is the rule about the past: the pin the held event's
|
||||
axis is re-stated from (`event_data_window()`) is the *snapshot's* own window, so the older acquisitions —
|
||||
which are bound event series on the same rows — moved neither the axis nor the framing; both runs above read
|
||||
`axis = pin = snap = [-0.2500..0.2500]` with `n=250001`. They are read on a frame that is *bound* (the probe
|
||||
waits for one), because a latch landing mid-frame leaves the mode `Held` while the rows still hold the live
|
||||
ring's bindings, and a reading taken there says nothing about either.
|
||||
|
||||
**And `N = 1` is the display without the feature**, which is the same command at
|
||||
`UDPSCOPE_PROBE_PERSIST_DEPTH=1`:
|
||||
|
||||
```
|
||||
[persist-probe] held depth=1 kept=0 bytes=0 budget=209715200 dropped=0 signals=5 channels=4 layers=0 want=0 placed=0 alphas=[] ages=[] row0=[] drawn=0
|
||||
[persist-probe] held axis=[-0.2500..0.2500] pin=[-0.2500..0.2500] snap=[-0.2500..0.2500] n=250001 ev=[-0.2500..0.2500] ev_is_snap=1 pin_is_snap=1
|
||||
```
|
||||
|
||||
Nothing retained, nothing planned, nothing bound, no bytes — and the framing line identical field for field,
|
||||
which is what "the same display as before the feature existed" means as a reading. **That run is also the
|
||||
baseline for the exit dump's `frame cost:` line**, because the tick mean is a mean over the whole run and the
|
||||
depth is raised at `UDPSCOPE_PROBE_PERSIST_MS` (2.5 s of 16 s), so the difference between the pair is a
|
||||
*lower bound* on what the layers cost. At the scale above (0.5 s window, 500 kS/s, four rows, one signal
|
||||
each):
|
||||
|
||||
| run | tick mean | draw mean | paint mean | frames |
|
||||
|---|---|---|---|---|
|
||||
| `..._PERSIST_DEPTH=1` | `0.82 ms` | `11.10 ms` | `15.96 ms` | 936 |
|
||||
| `..._PERSIST_DEPTH=4` | `1.18 ms` | `28.92 ms` | `34.60 ms` | 470 |
|
||||
|
||||
Thirteen extra traces across the four rows (four signals plus nine layers, the twelfth layer's signal having
|
||||
no free slot in its row) for **+17.8 ms of draw a frame**, i.e. ~2.6× the ink of the same plot without
|
||||
persistence; `tick` moves with it by a few tenths, mostly because there are half as many frames to average.
|
||||
|
||||
`[udpscope] persistence:` is the same state at the end of the run, and the one line the *standard* self-check
|
||||
prints without a probe: `depth`, the retention (`kept`/`bytes`/`budget`/`dropped`), the plan
|
||||
(`layers`/`want`/`placed`, 0 here because the exit probe's go-live has already cleared the layers —
|
||||
`layers=0` in that line is correct, not a defect), `schedule=` the alphas the depth asks for oldest first
|
||||
(the rule; the probe's `alphas=` is what the rows were built with), and `alphas=` for row 0.
|
||||
|
||||
## `signals panel:`
|
||||
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user