docs: the sample budget in samples per signal, the memory limit and the planner probe lines

Converts every --buffer-mb / buffer_mb mention to --buffer-samples / buffer_samples,
records the unit change as a deviation (README, SPECS, CLAUDE.md), documents the
buffer: and planner: dump lines and the new Preferences row, and updates the help
pages and the help-shot script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 02:49:21 +01:00
1 parent 034c82f7d7
commit 1627abcd4a
19 files changed
+166 -93

No files matched your search

+3 -2
View File
@@ -133,8 +133,9 @@ thread.
### Data path
`ScopeModel` holds **one `BufferStore`** (`live()`): one shared time-base ring plus one
value ring per channel, sized by `plan_partition()` to cover `--max-window` seconds
within the byte budget. The producer always writes it, whatever the trigger is doing —
value ring per channel, sized by `plan_partition()`: `--buffer-samples` ticks (rounded down to a power of two) per ring, halved
until the real bytes fit `--memory-limit-mb` (`Partition::memory_limited` records a clip), and the shared
clock decimated until `--max-window` seconds fit. The producer always writes it, whatever the trigger is doing —
that is what guarantees a full pre-trigger history on the *first* latch after arming.
The spec's second buffer (item 1) is the latched `EventSnapshot`, which is already an
immutable copy of the captured window; see README for why two live rings were dropped.
+4 -2
View File
@@ -101,8 +101,10 @@ component, so its behaviour is unchanged.
Options: `--source NAME` and the repeatable `--source-opt k=v` (the source kind and its settings;
`--server`, `--port`, `--multicast` and `--interface` populate the UDPS entries of the same options),
`--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
`--server`, `--port`, `--multicast`, `--interface`, `--buffer-samples` (samples
per acquired signal, default 10M, k/M/G suffixes, rounded down to a power of two),
`--memory-limit-mb` (hard ceiling on the ring bytes, default 8192; also a Preferences row),
`--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards),
`--capture-packets N` (the raw-datagram ring of README's "Packet capture"; 1..100000,
the value is required, and the popover button uses 1000 when the flag was absent).
+3 -3
View File
@@ -20,7 +20,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
comparison on purpose: it is not a size, but it is what the slots *mean*, and keeping samples
under a changed `base_dt` would re-date the whole ring.
**Construction is the cost.** A `RingBuffer<T>` holds a `std::vector<std::atomic<T>>`, which is
value-initialised, so making a store touches every byte of `--buffer-mb` — about a second per
value-initialised, so making a store touches every byte of the planned rings — about a second per
gigabyte, mostly first-touch page faults — while `reset()` is one relaxed store per ring plus an
`invalidate()` and an epoch back to zero. It must leave nothing of the old history behind, which
is why it clears the time ring, the absolute-time ring, every value ring, and both the published
@@ -65,8 +65,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
trade and is why the status line's `rate` field is worth reading.
- **A file is ingested too, so the loader decimates by the same factor.** `load_waveform_into_model()`
sets the target span to the recording's own length and enables every column, and the plan that
comes back may well decimate: 20 s of 10 channels at 500 kS/s is 480 MB of rings, and the budget is
`--buffer-mb`. The loader therefore reads `partition().time_stride` **after** the enables and pushes
comes back may well decimate: 20 s of 10 channels at 500 kS/s is 10 M samples a channel, and the budget is
`--buffer-samples` (rounded down to a power of two, 8.39 M by default). The loader therefore reads `partition().time_stride` **after** the enables and pushes
one slot per that many rows, carrying the group's mean at the group's mean stamp — the packet path's
filter, written a second time because a file does not arrive in packets. Pushing every row instead
overruns the ring and leaves the file as **its own tail**, with nothing to say so: a 20 s event
+1 -1
View File
@@ -334,7 +334,7 @@ the assertion: `finite` must be about `M` minus the hole share, and `finite=0` b
before the ring first wraps** (`base == 0`, where the two numberings coincide), which is why it reads
as intermittent and uptime-dependent. The dump's `math '<name>': N samples vs M time slots` is the
reading -- both numbers are `produced()`, so a mismatch *is* this defect -- but only on a run whose
ring has wrapped: give it a small `--buffer-mb`. `UDPSCOPE_PROBE_MATHWRAP_MS` is the probe that
ring has wrapped: give it a small `--buffer-samples`. `UDPSCOPE_PROBE_MATHWRAP_MS` is the probe that
arranges exactly that, and it exists because **`math_probe` structurally cannot** reach this state:
every edit that probe makes follows a structural change, which replans the buffer and restarts every
ring at absolute 0, where both halves of this are identities.
+1 -1
View File
@@ -121,7 +121,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **A pass costs the window's buckets times the pane's *measured channels* and its *distinct
levels*, and the window's own size multiplies all of it.** The cadence above bounds *how
often*; this is *how much*, and it is the term a big time scale buys. Measured on the mock at
the user's scale -- a **15 s** window, `--buffer-mb 4000` (so `time_stride` stays 1 and the
the user's scale -- a **15 s** window, `--buffer-samples 100M` (so `time_stride` stays 1 and the
window is 11.8 M samples, 184 k level-0 buckets), four signals, one row -- with
`[levels-probe]` (see [verification.md](verification.md)), `measure cost:` reading
`recomputes`/`each`/`covered`/`buckets`/`raw`:
+3 -3
View File
@@ -145,7 +145,7 @@ expression of the scheme, and the name is `sanitize()`d exactly as the host is,
with a slash cannot escape the directory.
A configuration carries the whole session *and* the settings that can only be decided before the
first socket: `buffer_mb`, `relative_time`, `capture_packets`. The startup chooser applies them
first socket: `buffer_samples`, `relative_time`, `capture_packets`. The startup chooser applies them
(`apply_source()`), which works because `choose_source()` runs before `ScopeModel` is constructed
— the one moment a buffer size can be applied at all. Switching configurations inside a running
app applies what can change live and says in the status line that the rest takes effect on the
@@ -156,9 +156,9 @@ A configuration's *startup* settings reach the model from two directions, and bo
named with `--server`, which never reaches the chooser at all. Without the second, a saved host's
buffer size, clock and capture ring were silently ignored — the layout still applied, because the
app loads that separately once a CONFIG names the signals — so an edit to any of the three read as
one that never took effect. **A flag the user typed wins over the file** (`--buffer-mb`,
one that never took effect. **A flag the user typed wins over the file** (`--buffer-samples`,
`--relative-time`, `--capture-packets`): the file fills in what the command line did not say, and
`Config::buffer_mb_specified` is what tells "not asked" from "asked for the default".
`Config::buffer_samples_specified` is what tells "not asked" from "asked for the default".
The `active` file is written on every save and by the chooser's pick, so "which configuration was
I on" survives a restart. A session written before configurations existed is **imported once** as
@@ -356,7 +356,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`snapshot_bytes()` is the honest cost of one -- both stamp arrays, every channel's `f32` samples
and the LOD pyramids built for them, i.e. ~4.5 bytes per sample per channel plus 8 per sample of
clock -- and `prune_persist_locked()` cuts the set to **both** bounds: the depth, and
`persist_budget_bytes()`, which is the ring's own `--buffer-mb`, because persistence is a copy
`persist_budget_bytes()`, which is the planned ring bytes (`Partition::bytes()`), because persistence is a copy
*of* what the rings hold and holding as much again is the honest ceiling for a display of it.
`kMaxPersistDepth` is 8 for the different reason the panel needs one: the layer there is already
invisible (0.8 %) and seven is all the free slots a one-signal row has. The depth is not the only
+46 -18
View File
@@ -75,7 +75,7 @@ Two lines of the self-check output are assertions in disguise:
a live axis narrower than the requested window is the same symptom from the other
cause (see the `clamp_to_data()` invariant in
[invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)). Both need a *saturated* ring to
show up, so run them under `--buffer-mb 1`, which wraps in a few seconds.
show up, so run them under `--buffer-samples 65536`, which wraps in a few seconds.
- `trigger: edge at X ... want [-a..b] got [-c..d] n=N` (stderr) — `got` must equal
`want` and `N` must be exactly the window times the stream's real rate, plus one
(2 s at 10 kS/s is `n=20001`, not 19999). `got` shorter than `want` on the *left*
@@ -206,10 +206,10 @@ Two lines of the self-check output are assertions in disguise:
decimation is for, and a `buffered` short of the window is the old "the control is capped
to what the ring holds" behaviour coming back. `decim` must be a power of two,
`stored × decim` must equal the wire rate, and `aa` must be exactly `stored / 2`. Run it
under `--buffer-mb 1`, which saturates in a few seconds and makes windows past ~6 s
decimate; at 200 MB nothing inside `--max-window` needs to.
under `--buffer-samples 65536`, which saturates in a few seconds and makes windows past ~6 s
decimate; at the default 10M nothing inside `--max-window` needs to.
- **The anti-alias check** is the same probe run against a waveform above the stored
Nyquist: `--buffer-mb 1 --freq 2000` with the probe ending on a 60 s window gives
Nyquist: `--buffer-samples 65536 --freq 2000` with the probe ending on a 60 s window gives
`decim=16`, a 625 S/s stored rate and a 312.5 Hz corner, and the channel dump's
`ch 0 'sine' ... y=[-0.059, 0.079]` is the assertion — a boxcar of 16 at 10 kS/s passes
2 kHz at about 6%. **A `y=[-1, 1]` there means the filter is gone** and the 2 kHz sine has
@@ -763,7 +763,7 @@ Two lines of the self-check output are assertions in disguise:
it exists to answer: freezing a whole `BufferStore` copies nothing and inherits the pyramid a
`ChannelBuffer` already maintains incrementally, so both `worst` figures would go to **0** — at
the price of a second budget's worth of memory (README records that one live ring is why
`--buffer-mb 200` really is 200 MB) and a second per-sample write on the producer. What the line
the planned bytes are the whole memory cost) and a second per-sample write on the producer. What the line
says about that trade is that the stall is real but **costs no packets**: an A/B of the same run
with and without a pause read `gaps=0` both ways, the paused one with an 86 ms publish, so a
second ring would buy a smoother frame at a latch rather than recovered data. (An earlier run
@@ -776,7 +776,7 @@ Two lines of the self-check output are assertions in disguise:
for the window**, one fetch and one scan per *measured channel*; **`L` is its price for the
pane's shares**, one accumulator per distinct one, walked over the window's buckets; and the
window's samples are the multiplier on both, so a sweep at one scale says nothing about
another. Measured at the user's scale (15 s window, `--buffer-mb 4000` -> `stride=1`, 11.8 M
another. Measured at the user's scale (15 s window, `--buffer-samples 100M` -> `stride=1`, 11.8 M
samples in the window = 184 k buckets, four signals, mock at ~0.5 MS/s per signal, llvmpipe),
with `UDPSCOPE_PROBE_LEVELS_CHANNELS` set:
* `50`, 1 channel -> `1 window, 1 level`, `measure cost: 226 recomputes, 6.58 ms each`, `raw=0.1%`,
@@ -1407,7 +1407,7 @@ popover row reads `failed: <strerror>` — one stderr line, no retry storm again
[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
[configs] round trip read=1 comment='written by the configs probe' buffer_mb=8 relative=0 capture=0 plots=1 signals=4 measures=0 math=0 noise=-1 hf=-1
[configs] round trip read=1 comment='written by the configs probe' buffer_samples=10000000 relative=0 capture=0 plots=1 signals=4 measures=0 math=0 noise=-1 hf=-1
[configs] after forget names=default
```
`noise=` and `hf=` are the first measurement's band and corner as they came back from the file. The probe
@@ -1422,7 +1422,7 @@ sets them to **7.5 and 250** before saving, so a key that never reached the file
| `[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 |
| `[configs] round trip` | `read=1`, the note verbatim, `buffer_samples` 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 |
## `[rowmenu]`
@@ -1524,8 +1524,8 @@ actually use.
```
Printed by `apply_stored_settings()` when a source is named on the command line: the stored
configuration's startup settings, filled in where the command line was silent. A `buffer=` here
that disagrees with `--buffer-mb` is a flag being overruled by the file; a *missing* line, with a
configuration's startup settings, filled in where the command line was silent. A `samples=` here
that disagrees with `--buffer-samples` is a flag being overruled by the file; a *missing* line, with a
configuration that carries settings, is the defect this exists for — a saved host opened with
`--server` ran on the defaults, so an edit to its buffer or clock never took effect.
@@ -2170,7 +2170,7 @@ of `S` signals shows `floor((8 - S) / S)` ages of each, so the default four-sign
**`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
~7 MB each) against `budget=` the planned ring bytes (`persist_budget_bytes()`, `Partition::bytes()`). `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:
@@ -2182,7 +2182,7 @@ budget bit — which is the run to take next, on the fast mock with a long windo
```
`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`
acquisition is 72.8 MB, so the budget (the planned rings, ~201 MB for four channels at the default 10M) 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
@@ -2374,7 +2374,7 @@ refusal it may still hold the previous text.
## `[enable-all-probe]` and switching a signal off
```
[enable-all-probe] 4 channel(s) enabled and assigned to plots, notice='connecting to 127.0.0.1:21023 -- planning the 200 MB buffer'
[enable-all-probe] 4 channel(s) enabled and assigned to plots, notice='connecting to 127.0.0.1:21023 -- planning the buffer: 8.39 M samples per signal'
[enable-all-probe] switched 'sine' off: traces_left=0 (want 0) notice=''sine' is off: 1 trace left the plots'
```
@@ -2383,7 +2383,7 @@ earliest thing that reads the status bar. It is set as the window is presented -
CONFIG can take the model's mutex, so before anything can block -- and it names the host and the
buffer being planned, because that is the window's only answer to "why is nothing happening yet".
An empty `notice` here is the window that used to come up silent while a large buffer was allocated:
at `--buffer-mb 4000` that is the dialog disappearing and then over a second of a frozen, blank
at `--buffer-samples 100M` that is the dialog disappearing and then over a second of a frozen, blank
window. (The buffer is allocated on the producer thread under `ScopeModel::mutex()`, which the frame
tick also takes, so the freeze is real and cannot be papered over in the UI -- see the
`[udpscope] buffer allocated:` line below.)
@@ -2452,6 +2452,34 @@ rate whenever data arrives at all:
Neither can be read off the engine's own fields, which is why the store's pair is here.
## `[udpscope] buffer:` and `[udpscope] planner:`
```
[udpscope] buffer: requested=10000000 used=8388608 bytes=167772160 limit=8589934592 limited=0
[udpscope] planner: unclipped got=67108864 pow2=1 limited=0 | clipped(1GiB) got=33554432 bytes=671088640 fits=1 pow2=1 limited=1 | clipped_rel got=33554432 bytes=939524096 fits=1 | too_small_throws=1
```
Printed by the exit dump (`src/ui.cpp`), next to the per-channel `produced` lines. The first is the live
plan: `requested` is `Config::buffer_samples`, `used` is `Partition::time_capacity` (the request rounded
down to a power of two, then halved under the memory limit), `bytes` is `Partition::bytes()` (the rings'
real size -- 8 388 608 ticks x (8 + 3 x 4) = 167 772 160 for the mock's three channels) and `limit` is
`memory_limit_bytes()`. **`used` must be a power of two no larger than `requested`'s floor, `bytes` must be
<= `limit`, and `limited=1` only when `used` is below that floor.** A run with `--buffer-samples 100M
--memory-limit-mb 256` reads `used=8388608 limited=1` and prints the `buffer limited to ...` stderr line;
`used=0 bytes=0` means no plan was made yet (no channel enabled -- add `UDPSCOPE_ENABLE_ALL_MS`).
The second line is not about the run: it feeds `plan_partition()` three channels and a 100 M request on
fixed inputs, so a change to the rounding, the halving or the byte formula reads here even when the run's
own plan is not clipped. `unclipped` (16 GiB limit) must give the pow2 floor of 100 M, **67108864**, with
`pow2=1 limited=0`. `clipped(1GiB)` must give a smaller power of two whose `bytes` are <= 1 GiB
(**33554432**, 671 088 640 bytes: 67 108 864 would need 1 342 177 280), `fits=1 pow2=1 limited=1`.
`clipped_rel` is the same in relative time, where the clock ring is doubled, so a tick costs 8 more bytes:
the same 33 554 432 ticks but 939 524 096 bytes, still `fits=1`. `too_small_throws=1` is a 1000-byte limit
refused (it cannot hold 128 ticks). A `fits=0` is the halving loop stopping early; `limited=0` on the clipped
case is the clip not being reported to the UI; `too_small_throws=0` is a planner that allocates past the limit.
The Preferences *Memory limit* row is not probed: change it by hand and read the new `buffer allocated:` line
and the status popover.
## `[udpscope] buffer allocated:`
```
@@ -2460,7 +2488,7 @@ Neither can be read off the engine's own fields, which is why the store's pair i
Not a probe — it is printed by `ScopeModel::rebuild_locked()` in every build, because this is the one
operation in the application whose duration a user can see with no instrument: the store's rings are
`std::vector<std::atomic<T>>` and so **value-initialised**, which touches the whole `--buffer-mb`
`std::vector<std::atomic<T>>` and so **value-initialised**, which touches the whole planned ring storage
(roughly a second per GB, mostly first-touch page faults), and it runs on the producer thread holding
`ScopeModel::mutex()` — which the frame tick also takes. So the line's `ms` is a frozen window, not
bookkeeping.
@@ -2469,7 +2497,7 @@ bookkeeping.
rebuild whose plan describes the storage already allocated clears the store instead
(`Partition::same_storage_as()` → `BufferStore::reset()`) and prints nothing, so a startup or a mode
change that reads more than one or two of these is a re-plan that should have been coalesced. Startup
asked for **fifteen**; it now asks for two or three. A run at `--buffer-mb 4000` measured those
asked for **fifteen**; it now asks for two or three. A run at `--buffer-samples 100M` measured those
coalesced allocations at 1148 ms each and a max frame tick of 1186 ms — the same number, which is the
evidence that the freeze *is* this call and not something downstream of it.
@@ -2495,8 +2523,8 @@ while the edit runs) and never falls to 0, which is what a rebuild would do. Eve
[mathwrap-probe] after edit: produced=72400 slots=72400 (want equal) y=[-0.5000,0.5000] (want +-0.5, was +-2.0000)
```
`UDPSCOPE_PROBE_MATHWRAP_MS`, and it must be run with a **small `--buffer-mb`** and given time to
saturate it: `--buffer-mb 1` against `tools/mock_udpstreamer.py --rate 20000 --batch 200`, with
`UDPSCOPE_PROBE_MATHWRAP_MS`, and it must be run with a **small `--buffer-samples`** and given time to
saturate it: `--buffer-samples 65536` against `tools/mock_udpstreamer.py --rate 20000 --batch 200`, with
`UDPSCOPE_PROBE_MATHWRAP_MS=1500 UDPSCOPE_EXIT_AFTER_MS=25000`, reads the lines above. It installs one
`Gain` graph of 2 over the mock's unit sine, polls (100 ms) until the ring has **wrapped**, and only then
edits the gain to 0.5 — the same channel list with one node changed, which is the in-place recompute and
+15 -15
View File
@@ -46,7 +46,7 @@ when only `src/` changed, so its `-Woverlength-strings` does not reappear.
A third reading every run offers for free: **how many `[udpscope] buffer allocated:` lines it
printed.** The line is not behind a probe — it is in every build — and one line is one *real* change
of buffer plan, each of which blocks the main thread for the time it takes to touch the whole
`--buffer-mb`. A normal run prints two or three. Fifteen at startup is the defect it was added for;
planned buffer. A normal run prints two or three. Fifteen at startup is the defect it was added for;
see [probe-readings.md](probe-readings.md).
That second baseline used to be "exactly two, `gtk_widget_get_height` and `gtk_widget_get_width`,
@@ -342,7 +342,7 @@ precedent, not an exception.
*tick*, one frame after the ask: a `g_timeout` was the first version and it drifted past the
paused/resumed boundary under load. The probe then asks for a **zoom-out** ten times the current span and reports
the span the next frame produced: **`limited` must be 1**, i.e. `span <= window`. Run it with
a ring that holds *more* than the window (`--buffer-mb 64` past the window's own seconds) —
a ring that holds *more* than the window (`--buffer-samples 2M` past the window's own seconds) —
that is the state the old `max(retained, window_s_)` bound leaked in, and it showed up here as
`span=2.502550 window=2.000000 limited=0` against `span=2.000000` after the fix.
@@ -617,7 +617,7 @@ as distinct from the fault injection below, which exercises ingest:
`Gain` graph over the mock's sine, waits for the ring to **saturate**, and only then edits the gain,
which is the one state `UDPSCOPE_PROBE_MATH_MS` cannot reach (each of its edits follows a structural
change, and a replan restarts every ring at absolute 0). It therefore needs a **small buffer and a long
run**: `--buffer-mb 1` against `--rate 20000 --batch 200`, with
run**: `--buffer-samples 65536` against `--rate 20000 --batch 200`, with
`UDPSCOPE_PROBE_MATHWRAP_MS=1500 UDPSCOPE_EXIT_AFTER_MS=25000`. `base > 0` on its second line is the
precondition — **`base=0` means the run tested nothing** — and the third line's `produced == slots` and
`y=±0.5` are the two assertions; both are in [probe-readings.md](probe-readings.md).
@@ -625,7 +625,7 @@ as distinct from the fault injection below, which exercises ingest:
note, through `save_configuration_as()` (the same entry point the dialog's button uses), then
reads it back off disk. Reading: `save-as=1`; `names=` gains `probe`; `active=probe`; and the
round trip must carry the note **and** the startup settings —
`comment='written by the configs probe' buffer_mb=8 relative=0 capture=0`. A `buffer_mb=0` there
`comment='written by the configs probe' buffer_samples=10000000 relative=0 capture=0`. A `buffer_samples=0` there
is the per-source buffer size not being written, which is the one setting a user notices only
after a capture has been sized wrong. Run it with a throwaway `XDG_CONFIG_HOME`; the probe
deletes its own configuration at the end (`after forget names=…`).
@@ -681,9 +681,9 @@ as distinct from the fault injection below, which exercises ingest:
`[defaults] calibrated: 2.5 -1.25 model=2.5 -1.25` — **the dialog must show the signal's stored
calibration**, and the two halves must match.
- The **chooser**, display-free: `UDPSCOPE_PROBE_SOURCES=1` prints `[sources] N known:` with one
entry per configuration (`host:port/name … buffer= relative= note='…'`), and
entry per configuration (`host:port/name … samples= relative= note='…'`), and
`UDPSCOPE_PROBE_PICK_MS=800` opens the chooser, picks the first row and prints
`[sources] picked host:port … buffer=64 relative=0 capture=0 config=… note='…'` — the settings the
`[sources] picked host:port … samples=2097152 relative=0 capture=0 config=… note='…'` — the settings the
configuration carried, into the `Config` the model is built from. **With no `--server` and no probe
the chooser blocks in its own nested loop** (no frame to end and no dump), so a chooser run is
always this probe. It also prints the dialog's frame *and its own help layer*:
@@ -764,7 +764,7 @@ as distinct from the fault injection below, which exercises ingest:
python3 -c "import numpy as np; n=1000000; t=-4.0+np.arange(n)/50000.0; \
a=np.zeros(n, dtype=[('time','<f8'),('abs_seconds','<f8'),('a','<f4'),('b','<f4'),('c','<f4'),('d','<f4')]); \
a['time']=t; a['abs_seconds']=1000.0+t; a['d']=np.linspace(-1,1,n); np.save('/tmp/long.npy', a)"
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=4000 ./build/udpscope --open /tmp/long.npy --buffer-mb 1
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=4000 ./build/udpscope --open /tmp/long.npy --buffer-samples 32768
```
must read `samples=31250 rows=1000000 decim=32 span=19.999980s time=[-4.000000..15.999980]` and
`stored=31250 … published=31250` per channel — **the whole span, at a thirty-second of the rate**.
@@ -797,21 +797,21 @@ as distinct from the fault injection below, which exercises ingest:
the field the file dialog's callback sets and prints `[sources] picked waveform <path>` — then the
load line above, with no socket involved.
- **A `--server` launch reads the stored configuration.** With a throwaway `XDG_CONFIG_HOME`
holding `127.0.0.1_8101/bench.toml` (`buffer_mb = 64`, `relative_time = true`,
holding `127.0.0.1_8101/bench.toml` (`buffer_samples = 2097152`, `relative_time = true`,
`capture_packets = 500`, `active` = `bench`), `./build/udpscope --server 127.0.0.1 --port 8101`
must print `[sources] settings from configuration 'bench': buffer=64 relative=1 capture=500`, the
`relative time:` line, and a `span=` of about 209 s (64 MB at 10 kS/s) — **not** the 200 MB
default's ~655 s. The same command with `--buffer-mb 8` must read `buffer=8` and `span=26.214s`:
must print `[sources] settings from configuration 'bench': samples=2097152 relative=1 capture=500`, the
`relative time:` line, and a `span=` of about 209 s (2 097 152 ticks at 10 kS/s) — **not** the 10M default's ~839 s. The same command with
`--buffer-samples 262144` must read `samples=262144` and `span=26.214s`:
**an explicit flag wins over the file**. Without this, the settings a host was edited to have were
ignored whenever the address came from the command line.
- The chooser's **form**, with `UDPSCOPE_PROBE_PICK_MS` scheduling it:
`UDPSCOPE_PROBE_NEWHOST=host:port` fills it and prints
`[sources] form: name='probe-host' note='…' buffer=32 relative=1 connect_sensitive=1`, then
`[sources] configuration created: host:port/probe-host … buffer=32 relative=1` and a
`[sources] form: name='probe-host' note='…' samples_m=0.5 relative=1 connect_sensitive=1`, then
`[sources] configuration created: host:port/probe-host … samples=500000 relative=1` and a
`picked … config=probe-host`; a following `UDPSCOPE_PROBE_SOURCES=1` must list it
(`buffer=32 relative=1 note='…'`) — a configuration the form wrote that the chooser does not
(`samples=500000 relative=1 note='…'`) — a configuration the form wrote that the chooser does not
list cannot be edited next time. `UDPSCOPE_PROBE_EDITHOST=host:port` opens the same form on that
configuration and prints `[sources] edit form: name='…' note='…' buffer=… relative=… button='Save'`
configuration and prints `[sources] edit form: name='…' note='…' samples_m=… relative=… button='Save'`
— **the pre-fill is the assertion**: an edit form that opens empty cannot edit anything — then
saves and reports the new values. `UDPSCOPE_PROBE_EDITNAME=new-name` renames it as well: one
`known:` row afterwards, with the old file gone.