Feature/source plugins #1
No files matched your search
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
udpscope is a **GTK4 oscilloscope-style viewer for a UDPStreamer source**. It receives one
|
udpscope is a **GTK4 oscilloscope-style viewer for a UDPStreamer source**. It receives one
|
||||||
data source -- a UDPStreamer stream through `deps/udpsclient` by default, or another registered
|
data source -- a UDPStreamer stream through `deps/udpsclient` by default, or another registered
|
||||||
source kind (see `docs/claude/architecture.md`) -- buffers it in a large ring (default 200 MB), and draws it
|
source kind (see `docs/claude/architecture.md`) -- buffers it in a large ring (default 10 M samples per signal, under an 8 GiB memory limit), and draws it
|
||||||
with `deps/plotter` — a figure of up to eight stacked rows sharing one x window. On top of
|
with `deps/plotter` — a figure of up to eight stacked rows sharing one x window. On top of
|
||||||
the live view it provides the things a bench scope provides: a trigger with a pre/post event
|
the live view it provides the things a bench scope provides: a trigger with a pre/post event
|
||||||
window, a pause that freezes the visible window, cursors, statistical measurements,
|
window, a pause that freezes the visible window, cursors, statistical measurements,
|
||||||
@@ -19,6 +19,11 @@ Two documents bound the work and are **not** interchangeable with this one:
|
|||||||
- **`README.md`** documents what was actually *built*, including deliberate deviations from
|
- **`README.md`** documents what was actually *built*, including deliberate deviations from
|
||||||
the spec and known limitations. **Read it before concluding something is a bug.**
|
the spec and known limitations. **Read it before concluding something is a bug.**
|
||||||
|
|
||||||
|
**Deliberate deviation from `SPECS.md` line 1:** the sample buffer is budgeted in *samples per signal*
|
||||||
|
(`--buffer-samples`, rounded down to a power of two), not in megabytes, under a separate hard
|
||||||
|
`--memory-limit-mb` (also a Preferences row). There is no `--buffer-mb`; see README and `SPECS.md`'s
|
||||||
|
"Deviations from the spec".
|
||||||
|
|
||||||
Three things shape almost every change here, and the linked files expand on each:
|
Three things shape almost every change here, and the linked files expand on each:
|
||||||
|
|
||||||
- **There are exactly two threads** — a producer that owns the data source and the GTK main
|
- **There are exactly two threads** — a producer that owns the data source and the GTK main
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ It is used for exactly one widget — the shortcut overview is an `AdwShortcutsD
|
|||||||
GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and `AdwShortcutsDialog` is its
|
GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and `AdwShortcutsDialog` is its
|
||||||
replacement.
|
replacement.
|
||||||
|
|
||||||
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb`
|
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-samples`
|
||||||
(size of the sample buffer, default 200), `--max-window` (largest time
|
(samples kept per acquired signal, default 10M; see "The sample buffer is budgeted in samples" below),
|
||||||
|
`--memory-limit-mb` (hard ceiling on the ring storage, default 8192), `--max-window` (largest time
|
||||||
window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
|
window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
|
||||||
`--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are
|
`--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are
|
||||||
added and removed from the **Layout editor** afterwards), `--relative-time` (see below),
|
added and removed from the **Layout editor** afterwards), `--relative-time` (see below),
|
||||||
@@ -33,16 +34,36 @@ added and removed from the **Layout editor** afterwards), `--relative-time` (see
|
|||||||
`GSK_RENDERER` and `LIBGL_ALWAYS_SOFTWARE` before GTK initialises — see the SSH
|
`GSK_RENDERER` and `LIBGL_ALWAYS_SOFTWARE` before GTK initialises — see the SSH
|
||||||
limitation below).
|
limitation below).
|
||||||
|
|
||||||
**What a large `--buffer-mb` costs at startup, and what the window says while it happens.** The
|
**The sample buffer is budgeted in samples per signal, under a memory limit.** `--buffer-samples N`
|
||||||
buffer's rings are value-initialised, so allocating one touches every byte of `--buffer-mb` --- about
|
(`250000`, `64k`, `100M`, `1.5G`; k, M and G are decimal, a *sample* count and not a byte size) is how
|
||||||
|
many samples to keep for **every acquired signal**, and for the shared time ring. A ring is a power of
|
||||||
|
two, so the request is rounded **down** to one --- "up to" --- and the UI shows both figures: the
|
||||||
|
default 10 M asks for, and gets, 8 388 608 per signal, and `--buffer-samples 100M` gets 67 108 864. What that
|
||||||
|
costs in bytes follows from the channel count (`ticks x 8` for the clock, doubled in relative mode, plus
|
||||||
|
`ticks x 4` per channel), and the status popover and the badge text quote the *planned* bytes rather than a
|
||||||
|
figure the user typed. The ceiling on those bytes is a separate application setting,
|
||||||
|
**`--memory-limit-mb`** (default 8192 MiB; also a *Memory limit* row in **Preferences**, which re-plans the
|
||||||
|
live buffer when changed). `plan_partition()` starts at the power-of-two floor of the request and
|
||||||
|
**halves** it until the rings fit under the limit, and throws only when 128 samples do not fit. A clipped
|
||||||
|
plan is never silent: stderr gets one line (`[udpscope] buffer limited to 8.39 M samples per signal (100 M
|
||||||
|
requested) by the 256 MiB memory limit; the rings take 192 MiB`), the status popover's buffer row names the
|
||||||
|
limit, and the source form's note says so, assuming one signal there because the channel count is not
|
||||||
|
known before a stream is. `--buffer-mb` and the stored `buffer_mb` key are gone, with no compatibility
|
||||||
|
shim: the flag is an unknown argument, and an old `buffer_mb` in a saved configuration is ignored (the
|
||||||
|
source falls back to the default until it is saved again as `buffer_samples`). **This is a deliberate
|
||||||
|
change of the budget's unit** (it was megabytes of ring, which made the retained history depend on the
|
||||||
|
channel count and on relative time); it is recorded under "Deviations from the spec" in `SPECS.md`.
|
||||||
|
|
||||||
|
**What a large buffer costs at startup, and what the window says while it happens.** The
|
||||||
|
buffer's rings are value-initialised, so allocating one touches every byte of the planned rings --- about
|
||||||
a second per gigabyte, mostly first-touch page faults --- and it happens on the producer thread
|
a second per gigabyte, mostly first-touch page faults --- and it happens on the producer thread
|
||||||
holding the lock the frame tick also takes. It is therefore a real freeze and not a slow paint. Two
|
holding the lock the frame tick also takes. It is therefore a real freeze and not a slow paint. Two
|
||||||
things follow. First, a rebuild whose plan describes the storage already allocated **clears** the
|
things follow. First, a rebuild whose plan describes the storage already allocated **clears** the
|
||||||
store instead of allocating a new one, which is what most structural changes are: startup used to ask
|
store instead of allocating a new one, which is what most structural changes are: startup used to ask
|
||||||
for fifteen allocations and now asks for two or three (3.36 s → 1.18 s at `--buffer-mb 1000`), and
|
for fifteen allocations and now asks for two or three (3.36 s → 1.18 s at a gigabyte of rings), and
|
||||||
every enable of a signal is applied in one batch rather than re-planning per signal. Second, the
|
every enable of a signal is applied in one batch rather than re-planning per signal. Second, the
|
||||||
window is **presented before** the buffer is planned and carries a notice naming what it is waiting
|
window is **presented before** the buffer is planned and carries a notice naming what it is waiting
|
||||||
for --- `connecting to 127.0.0.1:8000 — planning the 4000 MB buffer` --- because the remaining second
|
for --- `connecting to 127.0.0.1:8000 — planning the buffer: 134 M samples per signal` --- because the remaining second
|
||||||
or so at a multi-gigabyte buffer is unavoidable, and a blank window that says nothing is the part of
|
or so at a multi-gigabyte buffer is unavoidable, and a blank window that says nothing is the part of
|
||||||
it that reads as a hang. Each allocation also names itself on stderr
|
it that reads as a hang. Each allocation also names itself on stderr
|
||||||
(`[udpscope] buffer allocated: 4 channels, 839 s span, 74 ms`), so a run that prints a column of them
|
(`[udpscope] buffer allocated: 4 channels, 839 s span, 74 ms`), so a run that prints a column of them
|
||||||
@@ -740,8 +761,12 @@ The history cost of relative mode is measured rather than computed:
|
|||||||
| stream time (default) | **83.886 s** |
|
| stream time (default) | **83.886 s** |
|
||||||
| `--relative-time` | **41.943 s** |
|
| `--relative-time` | **41.943 s** |
|
||||||
|
|
||||||
Conditions: the mock at 100 kS/s, 4 channels at stride 1, the default `--buffer-mb 200`.
|
Conditions: the mock at 100 kS/s, 4 channels at stride 1, the old byte budget (`--buffer-mb 200`, since removed). **That
|
||||||
Relative mode retains **exactly half** the history — not the ~22% the original spec claimed, and
|
no longer happens**: the buffer is budgeted in samples per signal, so both modes retain the same span
|
||||||
|
(8 388 608 ticks at the default) and relative mode instead costs a third more *memory* (32 against 24
|
||||||
|
bytes a tick with four channels; the `planner:` dump line in `docs/claude/probe-readings.md` reads it); only a memory limit that
|
||||||
|
bites clips it first. The paragraph below is the measurement that motivated it.
|
||||||
|
Under the old byte budget relative mode retained **exactly half** the history — not the ~22% the original spec claimed, and
|
||||||
not the 75% a per-byte calculation suggests. The reason is the floor: relative mode keeps a second
|
not the 75% a per-byte calculation suggests. The reason is the floor: relative mode keeps a second
|
||||||
f64 ring for absolute time beside the stream clock's, so a tick's cost goes from
|
f64 ring for absolute time beside the stream clock's, so a tick's cost goes from
|
||||||
`8 + 4×4 = 24` bytes to `16 + 4×4 = 32` bytes — a third more. But `Partition::time_capacity` is
|
`8 + 4×4 = 24` bytes to `16 + 4×4 = 32` bytes — a third more. But `Partition::time_capacity` is
|
||||||
@@ -889,7 +914,7 @@ Three things worth knowing:
|
|||||||
replayed samples age out of the ring as the stream refills it.
|
replayed samples age out of the ring as the stream refills it.
|
||||||
- **Opening an event, a comparison or going back to *Live* is one re-plan of the buffer**, not one
|
- **Opening an event, a comparison or going back to *Live* is one re-plan of the buffer**, not one
|
||||||
per channel. Each of those switches rebuilds the signal set, and a rebuild that moves the plan
|
per channel. Each of those switches rebuilds the signal set, and a rebuild that moves the plan
|
||||||
allocates --- and so touches --- the whole of `--buffer-mb`; enabling a comparison's eight channels
|
allocates --- and so touches --- the whole of the planned rings; enabling a comparison's eight channels
|
||||||
one at a time was eight of those, which is most of what made the switch feel slow. They are now
|
one at a time was eight of those, which is most of what made the switch feel slow. They are now
|
||||||
enabled in a single call, and a rebuild whose plan is unchanged clears the buffer instead of
|
enabled in a single call, and a rebuild whose plan is unchanged clears the buffer instead of
|
||||||
allocating a new one.
|
allocating a new one.
|
||||||
@@ -923,15 +948,15 @@ Only `.npy` is read: it is the format the item asked for and the one that carrie
|
|||||||
export is for other tools.
|
export is for other tools.
|
||||||
|
|
||||||
**A recording longer than the buffer can hold at its own rate is resampled, never cut short.** The
|
**A recording longer than the buffer can hold at its own rate is resampled, never cut short.** The
|
||||||
file is pushed into the same ring a stream fills, and that ring is sized by `--buffer-mb`: 20 s of ten
|
file is pushed into the same ring a stream fills, and that ring is sized by `--buffer-samples`: 20 s of ten
|
||||||
channels at 500 kS/s is 480 MB of rings, which a 200 MB buffer cannot hold. The loader therefore reads
|
channels at 500 kS/s is 10 M samples a channel, which the default 8.39 M cannot hold. The loader therefore reads
|
||||||
the decimation the plan chose and pushes one stored sample per group of file rows, carrying the
|
the decimation the plan chose and pushes one stored sample per group of file rows, carrying the
|
||||||
group's **mean** — the same anti-aliased filter the live ingest applies, so a long event opens as the
|
group's **mean** — the same anti-aliased filter the live ingest applies, so a long event opens as the
|
||||||
whole recording at a lower sample rate instead of as a fraction of itself. It used to push every row
|
whole recording at a lower sample rate instead of as a fraction of itself. It used to push every row
|
||||||
regardless, which overran the ring and left the file as **its own tail**: a 20 s event opened showing
|
regardless, which overran the ring and left the file as **its own tail**: a 20 s event opened showing
|
||||||
only its last 4 s, with a correct-looking 20 s header line above it. The load line now names both
|
only its last 4 s, with a correct-looking 20 s header line above it. The load line now names both
|
||||||
numbers, `samples=31250 rows=1000000 decim=32`, so what is on screen is never in doubt; give the run a
|
numbers, `samples=31250 rows=1000000 decim=32`, so what is on screen is never in doubt; give the run a
|
||||||
larger `--buffer-mb` (the source's configuration carries one) to see such a file sample for sample.
|
larger `--buffer-samples` (the source's configuration carries one) to see such a file sample for sample.
|
||||||
|
|
||||||
## Named configurations
|
## Named configurations
|
||||||
|
|
||||||
@@ -953,18 +978,18 @@ measurements — **and the settings that can only be decided at startup**, which
|
|||||||
here rather than only on the command line:
|
here rather than only on the command line:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
comment = "5 kHz sine, trigger on Ch1, 8 MB is plenty"
|
comment = "5 kHz sine, trigger on Ch1, 256 k samples is plenty"
|
||||||
buffer_mb = 8
|
buffer_samples = 262144
|
||||||
relative_time = false
|
relative_time = false
|
||||||
capture_packets = 0
|
capture_packets = 0
|
||||||
```
|
```
|
||||||
|
|
||||||
The **buffer size** is the one that matters most: two sources on one machine rarely want the same
|
The **buffer size** (samples per signal) is the one that matters most: two sources on one machine rarely want the same
|
||||||
ring, and the value is applied before the model exists, which is the only moment it can be — by
|
ring, and the value is applied before the model exists, which is the only moment it can be — by
|
||||||
whichever of the chooser's rows opened the session, **and by `apply_stored_settings()` when the
|
whichever of the chooser's rows opened the session, **and by `apply_stored_settings()` when the
|
||||||
source was named on the command line**. A saved host's settings are read either way: opening
|
source was named on the command line**. A saved host's settings are read either way: opening
|
||||||
`udpscope --server 10.0.0.5 --port 8000` gets that source's stored buffer size, clock and capture
|
`udpscope --server 10.0.0.5 --port 8000` gets that source's stored buffer size, clock and capture
|
||||||
ring, and a flag the user actually typed wins over the file (`--buffer-mb`, `--relative-time`,
|
ring, and a flag the user actually typed wins over the file (`--buffer-samples`, `--relative-time`,
|
||||||
`--capture-packets`) because it is the more recent statement of intent. Switching configurations *inside* a running
|
`--capture-packets`) because it is the more recent statement of intent. Switching configurations *inside* a running
|
||||||
app applies everything that can change live (layout, plots, math, measurements, enables) and says
|
app applies everything that can change live (layout, plots, math, measurements, enables) and says
|
||||||
so in the status line about the rest (buffer, clock, packet capture): they take effect on the next
|
so in the status line about the rest (buffer, clock, packet capture): they take effect on the next
|
||||||
@@ -1004,7 +1029,11 @@ applies as it is made — there is no Apply button, and a **Reset to defaults**
|
|||||||
dialog puts everything back — and the rows are the ones every GNOME application shows: a title, a
|
dialog puts everything back — and the rows are the ones every GNOME application shows: a title, a
|
||||||
subtitle under it where the setting needs explaining, and the control on the right. A key that is
|
subtitle under it where the setting needs explaining, and the control on the right. A key that is
|
||||||
absent means its default, so the file only mentions what changed, and a missing file is a first run
|
absent means its default, so the file only mentions what changed, and a missing file is a first run
|
||||||
rather than an error. The directory field shows the directory in use: the default
|
rather than an error. The **Sample buffer** group holds the *Memory limit (MB)* row (`memory_limit_mb`,
|
||||||
|
default 8192): the hard ceiling on the ring storage for the whole application, which the planner halves the
|
||||||
|
sample budget to fit. It is the one planner input that changes while the app runs, so changing it
|
||||||
|
re-plans the live buffer --- dropping history only if the storage actually changes --- except while a
|
||||||
|
recording or a replay is on screen, where it is stored for the next plan. The directory field shows the directory in use: the default
|
||||||
is filled in rather than described, and typing it back leaves the setting unset, so it goes on
|
is filled in rather than described, and typing it back leaves the setting unset, so it goes on
|
||||||
following the cache directory instead of pinning one path for good. It can be typed, or picked with the folder
|
following the cache directory instead of pinning one path for good. It can be typed, or picked with the folder
|
||||||
button beside it, which writes what it found into the same field -- the field stays the one value, and
|
button beside it, which writes what it found into the same field -- the field stays the one value, and
|
||||||
@@ -1247,11 +1276,11 @@ allocates one `BufferStore` per CONFIG: one shared time-base ring plus one value
|
|||||||
ring per channel (N enabled signals -- see the limitation on multi-element signals
|
ring per channel (N enabled signals -- see the limitation on multi-element signals
|
||||||
below). The spec's second buffer is the latched `EventSnapshot`, which is already
|
below). The spec's second buffer is the latched `EventSnapshot`, which is already
|
||||||
an immutable copy of the captured window (see "Trigger" below); a second live ring
|
an immutable copy of the captured window (see "Trigger" below); a second live ring
|
||||||
bought nothing and cost half the memory budget, so `--buffer-mb 200` really is
|
bought nothing and cost half the memory, so the planned bytes the status line quotes are
|
||||||
200 MB. What it *would* buy is priced in the dump's `publish cost:` line — see
|
the whole of it. What it *would* buy is priced in the dump's `publish cost:` line — see
|
||||||
"There is one live buffer, not two" under the review findings.
|
"There is one live buffer, not two" under the review findings.
|
||||||
`plan_partition()` sizes the rings to cover `--max-window` seconds and then grows
|
`plan_partition()` sizes the rings to the sample budget (rounded down to a power of two, halved
|
||||||
them with the rest of the budget. When a channel's declared sample rate is lower
|
until it fits the memory limit) and then decimates the shared clock until `--max-window` seconds fit. When a channel's declared sample rate is lower
|
||||||
than the fastest channel's, it is decimated by the integer ratio
|
than the fastest channel's, it is decimated by the integer ratio
|
||||||
(`Partition::stride`); when the requested window does not fit the budget, the shared
|
(`Partition::stride`); when the requested window does not fit the budget, the shared
|
||||||
clock is resampled too (`Partition::time_stride`, an averaging decimation — see
|
clock is resampled too (`Partition::time_stride`, an averaging decimation — see
|
||||||
@@ -1437,8 +1466,8 @@ tick and 12.8 ms of draw a frame and 4.9 MB uploaded** on llvmpipe, and reads **
|
|||||||
5.6 ms and 1.0 MB** at the cap.
|
5.6 ms and 1.0 MB** at the cap.
|
||||||
|
|
||||||
**And it says what it costs.** A retained acquisition is a whole window copied out of the ring, so
|
**And it says what it costs.** A retained acquisition is a whole window copied out of the ring, so
|
||||||
the retention is bounded by two things: the depth, and **`--buffer-mb`** — persistence may hold at
|
the retention is bounded by two things: the depth, and **the planned buffer** — persistence may hold at
|
||||||
most as much again as the buffer does, which is the bound a long window on a fast stream reaches
|
most as many bytes again as the rings take (`persist_budget_bytes()`), which is the bound a long window on a fast stream reaches
|
||||||
long before the eighth layer. The other bound is the plot's own: a layer takes a *free trace slot
|
long before the eighth layer. The other bound is the plot's own: a layer takes a *free trace slot
|
||||||
in the row that plots its signal*, layer by layer, so a row of four signals — the layout the app
|
in the row that plots its signal*, layer by layer, so a row of four signals — the layout the app
|
||||||
starts with — has room for one older acquisition of each, while a row per signal has room for all
|
starts with — has room for one older acquisition of each, while a row per signal has room for all
|
||||||
@@ -1654,7 +1683,7 @@ enabled only to feed a math channel.
|
|||||||
several, and there was no way to see which sources the app already knew about. The chooser lists
|
several, and there was no way to see which sources the app already knew about. The chooser lists
|
||||||
one row per stored session: the server and port it was named after, how many plots and signals it
|
one row per stored session: the server and port it was named after, how many plots and signals it
|
||||||
held, and its multicast group when it had one, newest first. `New host…` is a four-field form —
|
held, and its multicast group when it had one, newest first. `New host…` is a four-field form —
|
||||||
server, port, multicast group, interface — and nothing more: `--buffer-mb` and the rest are tuning
|
server, port, multicast group, interface — and nothing more: `--buffer-samples` and the rest are tuning
|
||||||
rather than "which source", and a form that asks for them is a form you have to read before using
|
rather than "which source", and a form that asks for them is a form you have to read before using
|
||||||
the app. Cancel exits before anything is allocated, because declining to choose a source is not a
|
the app. Cancel exits before anything is allocated, because declining to choose a source is not a
|
||||||
failure.
|
failure.
|
||||||
@@ -1858,7 +1887,7 @@ fixes above bound *how often* and *how much per sample*; what is left is the ari
|
|||||||
scale: a pass is priced per measured **channel** (one fetch of the window's level-0 buckets, one
|
scale: a pass is priced per measured **channel** (one fetch of the window's level-0 buckets, one
|
||||||
statistics scan) and per **distinct share** in the pane (one level accumulator, walked over those
|
statistics scan) and per **distinct share** in the pane (one level accumulator, walked over those
|
||||||
buckets), and the window's own sample count multiplies both. Measured with the new
|
buckets), and the window's own sample count multiplies both. Measured with the new
|
||||||
`[levels-probe]` sweep on the mock at a **15 s** window and `--buffer-mb 4000` (so `time_stride`
|
`[levels-probe]` sweep on the mock at a **15 s** window and `--buffer-samples 100M` (so `time_stride`
|
||||||
stays 1 and the window holds 11.8 M samples, 184 k buckets), four signals, one row, on llvmpipe
|
stays 1 and the window holds 11.8 M samples, 184 k buckets), four signals, one row, on llvmpipe
|
||||||
— `measure cost:`'s `each` per recompute, and the tick's mean:
|
— `measure cost:`'s `each` per recompute, and the tick's mean:
|
||||||
|
|
||||||
@@ -2001,9 +2030,9 @@ a sample and runs out on a clock. Widening only the clock keeps the cost where i
|
|||||||
the rings are the memory budget, and the values are the overwhelming majority of them. The
|
the rings are the memory budget, and the values are the overwhelming majority of them. The
|
||||||
one consequence worth knowing is that `plan_partition()`'s growth loop can no longer count
|
one consequence worth knowing is that `plan_partition()`'s growth loop can no longer count
|
||||||
slots, because a time slot and a value sample are now different sizes — it weighs the
|
slots, because a time slot and a value sample are now different sizes — it weighs the
|
||||||
budget in **bytes** (`sizeof(f64)` for the shared clock plus `sizeof(f32) / stride[i]` per
|
budget in **bytes** against the memory limit (`sizeof(f64)` for the shared clock plus `sizeof(f32)` per
|
||||||
channel) and the power-of-two rounding absorbs the rest: a three-channel stream at
|
channel); the sample budget itself is a count of ticks, so a three-channel stream at
|
||||||
`--buffer-mb 200` retains the same 8.39 s span it did before. `EventSnapshot::stamps` is
|
`--buffer-samples 10M` retains the same 8.39 M ticks it did before. `EventSnapshot::stamps` is
|
||||||
`f64` for the same reason rather than for symmetry — an *event's* stamps are rebased onto
|
`f64` for the same reason rather than for symmetry — an *event's* stamps are rebased onto
|
||||||
its edge and so are small, but a *pause* view is deliberately not rebased and carries full
|
its edge and so are small, but a *pause* view is deliberately not rebased and carries full
|
||||||
stream time, which quantised at exactly the rate the live ring did.
|
stream time, which quantised at exactly the rate the live ring did.
|
||||||
@@ -2646,7 +2675,7 @@ This reverses the earlier "the app does no decimation at all" decision, and keep
|
|||||||
reason: a naive stride is what made a 2 kHz sine on a 1 MHz clock plot as a
|
reason: a naive stride is what made a 2 kHz sine on a 1 MHz clock plot as a
|
||||||
full-amplitude 125 Hz sine — a plausible-looking waveform that was never in the data.
|
full-amplitude 125 Hz sine — a plausible-looking waveform that was never in the data.
|
||||||
The headless check is the same aliasing case with the filter in place: at
|
The headless check is the same aliasing case with the filter in place: at
|
||||||
`--buffer-mb 1 --freq 2000` with a 60 s window the clock decimates by 16 to a 625 S/s
|
`--buffer-samples 65536 --freq 2000` with a 60 s window the clock decimates by 16 to a 625 S/s
|
||||||
stored rate (312.5 Hz corner) and the channel dump reads `y=[-0.059, 0.079]`, roughly
|
stored rate (312.5 Hz corner) and the channel dump reads `y=[-0.059, 0.079]`, roughly
|
||||||
the boxcar's predicted 6%. A `y=[-1, 1]` there would mean the filter had been lost.
|
the boxcar's predicted 6%. A `y=[-1, 1]` there would mean the filter had been lost.
|
||||||
|
|
||||||
@@ -2912,7 +2941,7 @@ now — they are listed so they are not mistaken for anything else:
|
|||||||
smaller than the stall: an A/B with and without a pause read `gaps=0` both ways, so
|
smaller than the stall: an A/B with and without a pause read `gaps=0` both ways, so
|
||||||
what a second ring would buy is a smoother frame at a latch, not recovered packets.
|
what a second ring would buy is a smoother frame at a latch, not recovered packets.
|
||||||
- **The buffer is allocated at the first CONFIG, not at initialisation.**
|
- **The buffer is allocated at the first CONFIG, not at initialisation.**
|
||||||
The spec's "N+1 split" needs N, which only a CONFIG gives; `--buffer-mb` is the
|
The spec's "N+1 split" needs N, which only a CONFIG gives; `--buffer-samples` is the
|
||||||
*budget*, and the power-of-two ring rounding means the final
|
*budget*, and the power-of-two ring rounding means the final
|
||||||
allocation lands in `(budget/2, budget]` rather than exactly on it. Every CONFIG,
|
allocation lands in `(budget/2, budget]` rather than exactly on it. Every CONFIG,
|
||||||
enable/disable and calibration re-partitions (and drops the history) because the
|
enable/disable and calibration re-partitions (and drops the history) because the
|
||||||
|
|||||||
@@ -42,9 +42,18 @@
|
|||||||
The following were built differently from what was asked for, each for a reason `README.md` states
|
The following were built differently from what was asked for, each for a reason `README.md` states
|
||||||
in full:
|
in full:
|
||||||
|
|
||||||
- **`todo.md` item 2 — relative timing.** Implemented as `--relative-time`. The retained history
|
- **The buffer is budgeted in samples per signal, not megabytes** (line 1 asked for "default 200MB
|
||||||
in this mode is **41.943 s** against the default **83.886 s** (measured at 100 kS/s, 4 channels,
|
each, configurable as cli argument"). `--buffer-samples N` (default 10M, k/M/G suffixes) is the
|
||||||
`--buffer-mb 200`): exactly half, because a second f64 ring for absolute time raises a tick's
|
number of samples kept for every acquired signal and for the shared time ring, rounded **down** to a
|
||||||
|
power of two ("up to"; 10M gives 8 388 608), and a separate application setting, `--memory-limit-mb`
|
||||||
|
(default 8192, also in Preferences), is the hard ceiling on the ring bytes, halving the sample budget
|
||||||
|
when the rings would not fit. The retained history therefore no longer depends on the channel count
|
||||||
|
or on relative time. There is no `--buffer-mb` and no compatibility shim; a stored `buffer_mb` is
|
||||||
|
ignored. The "2 identical buffers" were already a single ring plus latched snapshots (see below).
|
||||||
|
- **`todo.md` item 2 — relative timing.** Implemented as `--relative-time`. *As first built* (under
|
||||||
|
the old byte budget; superseded by the item above, where both modes keep the same span and only
|
||||||
|
the memory differs) the retained history in this mode was **41.943 s** against the default
|
||||||
|
**83.886 s** (measured at 100 kS/s, 4 channels, `--buffer-mb 200`): exactly half, because a second f64 ring for absolute time raises a tick's
|
||||||
cost from 24 to 32 bytes — a third more — and `Partition::time_capacity` is floored to a power
|
cost from 24 to 32 bytes — a third more — and `Partition::time_capacity` is floored to a power
|
||||||
of two, so a third more weight costs a full halving. The design document and the implementation
|
of two, so a third more weight costs a full halving. The design document and the implementation
|
||||||
plan both put the cost at ~22%, which is wrong; the per-byte figure of 75% is wrong for the same
|
plan both put the cost at ~22%, which is wrong; the per-byte figure of 75% is wrong for the same
|
||||||
|
|||||||
@@ -133,8 +133,9 @@ thread.
|
|||||||
### Data path
|
### Data path
|
||||||
|
|
||||||
`ScopeModel` holds **one `BufferStore`** (`live()`): one shared time-base ring plus one
|
`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
|
value ring per channel, sized by `plan_partition()`: `--buffer-samples` ticks (rounded down to a power of two) per ring, halved
|
||||||
within the byte budget. The producer always writes it, whatever the trigger is doing —
|
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.
|
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
|
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.
|
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;
|
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` and `--interface` populate the UDPS entries of the same options),
|
||||||
`--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
|
`--server`, `--port`, `--multicast`, `--interface`, `--buffer-samples` (samples
|
||||||
buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
|
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),
|
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,
|
`--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).
|
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
|
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.
|
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
|
**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
|
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
|
`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
|
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.
|
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()`
|
- **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
|
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
|
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-mb`. The loader therefore reads `partition().time_stride` **after** the enables and pushes
|
`--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
|
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
|
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
|
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
|
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
|
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
|
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:
|
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
|
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.
|
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
|
- **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
|
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
|
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
|
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
|
`[levels-probe]` (see [verification.md](verification.md)), `measure cost:` reading
|
||||||
`recomputes`/`each`/`covered`/`buckets`/`raw`:
|
`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.
|
with a slash cannot escape the directory.
|
||||||
|
|
||||||
A configuration carries the whole session *and* the settings that can only be decided before the
|
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
|
(`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
|
— 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
|
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
|
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
|
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
|
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
|
`--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
|
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
|
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
|
`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
|
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
|
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.
|
*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
|
`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
|
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
|
a live axis narrower than the requested window is the same symptom from the other
|
||||||
cause (see the `clamp_to_data()` invariant in
|
cause (see the `clamp_to_data()` invariant in
|
||||||
[invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)). Both need a *saturated* ring to
|
[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
|
- `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
|
`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*
|
(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
|
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,
|
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
|
`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
|
under `--buffer-samples 65536`, which saturates in a few seconds and makes windows past ~6 s
|
||||||
decimate; at 200 MB nothing inside `--max-window` needs to.
|
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
|
- **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
|
`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
|
`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
|
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
|
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
|
`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
|
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
|
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
|
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
|
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
|
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
|
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
|
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),
|
samples in the window = 184 k buckets, four signals, mock at ~0.5 MS/s per signal, llvmpipe),
|
||||||
with `UDPSCOPE_PROBE_LEVELS_CHANNELS` set:
|
with `UDPSCOPE_PROBE_LEVELS_CHANNELS` set:
|
||||||
* `50`, 1 channel -> `1 window, 1 level`, `measure cost: 226 recomputes, 6.58 ms each`, `raw=0.1%`,
|
* `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
|
[defaults] calibrated: 2.5 -1.25 model=2.5 -1.25
|
||||||
[configs] before=default active=default
|
[configs] before=default active=default
|
||||||
[configs] save-as=1 names=probe,default active=probe
|
[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
|
[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
|
`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 |
|
| `[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] 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] 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]`
|
## `[rowmenu]`
|
||||||
|
|
||||||
@@ -1524,8 +1524,8 @@ actually use.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Printed by `apply_stored_settings()` when a source is named on the command line: the stored
|
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
|
configuration's startup settings, filled in where the command line was silent. A `samples=` here
|
||||||
that disagrees with `--buffer-mb` is a flag being overruled by the file; a *missing* line, with a
|
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
|
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.
|
`--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
|
**`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,
|
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
|
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:
|
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
|
`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
|
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
|
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
|
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]` 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'
|
[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
|
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".
|
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:
|
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
|
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
|
tick also takes, so the freeze is real and cannot be papered over in the UI -- see the
|
||||||
`[udpscope] buffer allocated:` line below.)
|
`[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.
|
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:`
|
## `[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
|
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
|
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
|
(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
|
`ScopeModel::mutex()` — which the frame tick also takes. So the line's `ms` is a frozen window, not
|
||||||
bookkeeping.
|
bookkeeping.
|
||||||
@@ -2469,7 +2497,7 @@ bookkeeping.
|
|||||||
rebuild whose plan describes the storage already allocated clears the store instead
|
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
|
(`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
|
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
|
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.
|
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)
|
[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
|
`UDPSCOPE_PROBE_MATHWRAP_MS`, and it must be run with a **small `--buffer-samples`** and given time to
|
||||||
saturate it: `--buffer-mb 1` against `tools/mock_udpstreamer.py --rate 20000 --batch 200`, with
|
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
|
`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
|
`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
|
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
|
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
|
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
|
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).
|
see [probe-readings.md](probe-readings.md).
|
||||||
|
|
||||||
That second baseline used to be "exactly two, `gtk_widget_get_height` and `gtk_widget_get_width`,
|
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
|
*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
|
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
|
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
|
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.
|
`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,
|
`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
|
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
|
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
|
`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
|
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).
|
`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
|
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
|
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 —
|
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
|
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
|
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=…`).
|
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
|
`[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.
|
calibration**, and the two halves must match.
|
||||||
- The **chooser**, display-free: `UDPSCOPE_PROBE_SOURCES=1` prints `[sources] N known:` with one
|
- 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
|
`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
|
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
|
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*:
|
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; \
|
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=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)"
|
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
|
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**.
|
`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
|
the field the file dialog's callback sets and prints `[sources] picked waveform <path>` — then the
|
||||||
load line above, with no socket involved.
|
load line above, with no socket involved.
|
||||||
- **A `--server` launch reads the stored configuration.** With a throwaway `XDG_CONFIG_HOME`
|
- **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`
|
`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
|
must print `[sources] settings from configuration 'bench': samples=2097152 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
|
`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
|
||||||
default's ~655 s. The same command with `--buffer-mb 8` must read `buffer=8` and `span=26.214s`:
|
`--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
|
**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.
|
ignored whenever the address came from the command line.
|
||||||
- The chooser's **form**, with `UDPSCOPE_PROBE_PICK_MS` scheduling it:
|
- The chooser's **form**, with `UDPSCOPE_PROBE_PICK_MS` scheduling it:
|
||||||
`UDPSCOPE_PROBE_NEWHOST=host:port` fills it and prints
|
`UDPSCOPE_PROBE_NEWHOST=host:port` fills it and prints
|
||||||
`[sources] form: name='probe-host' note='…' buffer=32 relative=1 connect_sensitive=1`, then
|
`[sources] form: name='probe-host' note='…' samples_m=0.5 relative=1 connect_sensitive=1`, then
|
||||||
`[sources] configuration created: host:port/probe-host … buffer=32 relative=1` and a
|
`[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
|
`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
|
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
|
— **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
|
saves and reports the new values. `UDPSCOPE_PROBE_EDITNAME=new-name` renames it as well: one
|
||||||
`known:` row afterwards, with the old file gone.
|
`known:` row afterwards, with the old file gone.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<p>udpscope plots a live UDPStreamer source. One window holds up to eight rows of plots,
|
<p>udpscope plots a live UDPStreamer source. One window holds up to eight rows of plots,
|
||||||
each with up to four traces, and every row shares one time axis, so the whole figure
|
each with up to four traces, and every row shares one time axis, so the whole figure
|
||||||
scrolls together. Acquisition runs on its own thread and writes a ring buffer sized from
|
scrolls together. Acquisition runs on its own thread and writes a ring buffer sized from
|
||||||
<cmd>--buffer-mb</cmd>; the drawing thread reads whatever that ring holds when a frame
|
<cmd>--buffer-samples</cmd>; the drawing thread reads whatever that ring holds when a frame
|
||||||
starts, which is why a trace never makes the window stutter.</p>
|
starts, which is why a trace never makes the window stutter.</p>
|
||||||
|
|
||||||
<p>The stream is the clock. Every interval you read on screen is measured between
|
<p>The stream is the clock. Every interval you read on screen is measured between
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
they open.</p>
|
they open.</p>
|
||||||
|
|
||||||
<p>A configuration also carries the settings that can only be decided when the buffers are
|
<p>A configuration also carries the settings that can only be decided when the buffers are
|
||||||
allocated: the <gui>buffer size</gui> (<cmd>--buffer-mb</cmd>), relative time, and the
|
allocated: the <gui>samples per signal</gui> (<cmd>--buffer-samples</cmd>), relative time, and the
|
||||||
packet capture's ring size. Opening a configuration from the chooser applies all three;
|
packet capture's ring size. Opening a configuration from the chooser applies all three;
|
||||||
switching inside a running app applies the rest and says in the status line that these take
|
switching inside a running app applies the rest and says in the status line that these take
|
||||||
effect on the next start.</p>
|
effect on the next start.</p>
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
<list>
|
<list>
|
||||||
<item><p>What a configuration persists: the time window, the rows and each row's slot
|
<item><p>What a configuration persists: the time window, the rows and each row's slot
|
||||||
signals, the calibration, the trace colours, which signals were enabled, the math
|
signals, the calibration, the trace colours, which signals were enabled, the math
|
||||||
channels with their graphs, the measurements pane — and the startup settings: buffer
|
channels with their graphs, the measurements pane — and the startup settings: samples
|
||||||
size, relative time and the packet capture's ring size.</p></item>
|
per signal, relative time and the packet capture's ring size.</p></item>
|
||||||
<item><p>Signal identity is the <em>name</em> throughout, because a new CONFIG renumbers
|
<item><p>Signal identity is the <em>name</em> throughout, because a new CONFIG renumbers
|
||||||
the channels.</p></item>
|
the channels.</p></item>
|
||||||
<item><p>A file written by a run against a source whose streamer was down would name no
|
<item><p>A file written by a run against a source whose streamer was down would name no
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
stored <em>configuration</em> (<code>host:port · name</code>, its note, and the
|
stored <em>configuration</em> (<code>host:port · name</code>, its note, and the
|
||||||
settings it carries), newest first, plus a <gui>New host…</gui> form that takes the
|
settings it carries), newest first, plus a <gui>New host…</gui> form that takes the
|
||||||
address <em>and</em> the configuration it belongs to — its name and note, the
|
address <em>and</em> the configuration it belongs to — its name and note, the
|
||||||
<gui>buffer size</gui>, and whether time is relative or absolute. A row's pencil
|
<gui>samples per signal</gui>, and whether time is relative or absolute. A row's pencil
|
||||||
button opens the same form on that configuration, pre-filled, and changes to its name
|
button opens the same form on that configuration, pre-filled, and changes to its name
|
||||||
or address move it (the old file is deleted).</p>
|
or address move it (the old file is deleted).</p>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
layout the app starts with — has room for one older acquisition of each of them; a row per
|
layout the app starts with — has room for one older acquisition of each of them; a row per
|
||||||
signal has room for all seven.</p></item>
|
signal has room for all seven.</p></item>
|
||||||
<item><p><em>Memory.</em> A retained acquisition is a whole window copied out of the ring,
|
<item><p><em>Memory.</em> A retained acquisition is a whole window copied out of the ring,
|
||||||
so persistence may hold at most as much again as the buffer (<gui>--buffer-mb</gui>) does.
|
so persistence may hold at most as much again as the buffer takes (see <cmd>--buffer-samples</cmd> and <cmd>--memory-limit-mb</cmd>).
|
||||||
On a long window at a high sample rate that bound is reached before the eighth layer.</p></item>
|
On a long window at a high sample rate that bound is reached before the eighth layer.</p></item>
|
||||||
</list>
|
</list>
|
||||||
<section id="persistence">
|
<section id="persistence">
|
||||||
|
|||||||
@@ -56,11 +56,10 @@
|
|||||||
|
|
||||||
<section id="example">
|
<section id="example">
|
||||||
<title>Example</title>
|
<title>Example</title>
|
||||||
<p>1 MS/s with 200 MB of buffer. A 2 s window stores every sample
|
<p>1 MS/s with the default buffer of 8 388 608 samples per signal (8.39 s). A 2 s window
|
||||||
(<code>time x1</code>). Widen it to 600 s and the stride becomes <code>x16</code>: 625 S/s
|
stores every sample (<code>time x1</code>). Widen it to 600 s and the stride becomes
|
||||||
stored, anti-aliased at 312.5 Hz. A 2 kHz sine seen through that window reads
|
<code>x128</code>: 7.8 kS/s stored, anti-aliased at 3.9 kHz. A 5 kHz sine seen through that
|
||||||
<code>y=[-0.059, 0.079]</code> — attenuated to about 6%, not folded down to a
|
window is attenuated by the filter, not folded down to a full-amplitude low-frequency one.</p>
|
||||||
full-amplitude 125 Hz one.</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</page>
|
</page>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
- user is acquiring a signal with 1MSps and 100_000 samples per udp frame
|
- user is acquiring a signal with 1MSps and 100_000 samples per udp frame
|
||||||
- last frame as number <N> and previous <N-2> in this case a 100_000 indexes must be skipped
|
- last frame as number <N> and previous <N-2> in this case a 100_000 indexes must be skipped
|
||||||
- time is simply: `((<N_frame> - <N_last>) * <n_samples_per_frame> - <sample_index>) / <sample rate>` (or something like that)
|
- time is simply: `((<N_frame> - <N_last>) * <n_samples_per_frame> - <sample_index>) / <sample rate>` (or something like that)
|
||||||
- [x] (3) ~~named configurations~~ **done**: one directory per source holding `<name>.toml` per configuration plus `active`; a configuration carries a note and the startup settings (`buffer_mb`, `relative_time`, `capture_packets`); the chooser lists them and the menu's _Configurations…_ opens/saves/deletes them.
|
- [x] (3) ~~named configurations~~ **done**: one directory per source holding `<name>.toml` per configuration plus `active`; a configuration carries a note and the startup settings (`buffer_samples`, `relative_time`, `capture_packets`); the chooser lists them and the menu's _Configurations…_ opens/saves/deletes them.
|
||||||
- [x] (4) ~~optional udp frame storage for debugging of strange behaviours~~ **done**: `--capture-packets N`; see README and the design note in `docs/superpowers/specs/` for the ring file, the popover toggle and `tools/capture_to_pcap.py`.
|
- [x] (4) ~~optional udp frame storage for debugging of strange behaviours~~ **done**: `--capture-packets N`; see README and the design note in `docs/superpowers/specs/` for the ring file, the popover toggle and `tools/capture_to_pcap.py`.
|
||||||
- [x] (5) ~~open exported binary waveforms to view/analyse~~ **done**: `--open FILE` and the chooser's **Waveforms** page (the files opened before, newest first, plus _Open…_); the `.npy`'s dtype names the columns, so a load restores the signals, the samples and the axis the export had, and every instrument works on it — see README and `docs/claude/invariants-data-path.md`.
|
- [x] (5) ~~open exported binary waveforms to view/analyse~~ **done**: `--open FILE` and the chooser's **Waveforms** page (the files opened before, newest first, plus _Open…_); the `.npy`'s dtype names the columns, so a load restores the signals, the samples and the axis the export had, and every instrument works on it — see README and `docs/claude/invariants-data-path.md`.
|
||||||
- [x] (6) ~~imporve right click menu on the plots~~ **done**: the plot's context menu uses the main menu's row style (`menu_button` + `.udpscope-menu`, asserted by `UDPSCOPE_PROBE_ROWMENU_MS`: uniform `label_x=26`, `h=18`, the shortcut flush right), and deleting a signal is a **Remove signal** submenu listing that plot's traces plus _All traces on this plot_.
|
- [x] (6) ~~imporve right click menu on the plots~~ **done**: the plot's context menu uses the main menu's row style (`menu_button` + `.udpscope-menu`, asserted by `UDPSCOPE_PROBE_ROWMENU_MS`: uniform `label_x=26`, `h=18`, the shortcut flush right), and deleting a signal is a **Remove signal** submenu listing that plot's traces plus _All traces on this plot_.
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ pick_other_window() {
|
|||||||
start_app() { # LOGFILE ENVS... ; extra args via APP_ARGS
|
start_app() { # LOGFILE ENVS... ; extra args via APP_ARGS
|
||||||
local log=$1; shift
|
local log=$1; shift
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
env "$@" "$app" --server 127.0.0.1 --port "$port" --buffer-mb 16 ${APP_ARGS:-} >"$log" 2>&1 &
|
env "$@" "$app" --server 127.0.0.1 --port "$port" --buffer-samples 512k ${APP_ARGS:-} >"$log" 2>&1 &
|
||||||
app_pid=$!
|
app_pid=$!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user