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:
1 parent
034c82f7d7
commit
1627abcd4a
19 files changed
+166
-93
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user