Compare commits
2
Commits
e83ad86b8f
...
27c9b34277
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c9b34277 | ||
|
|
22e7a8efcc |
No files matched your search
@@ -57,7 +57,7 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
|
||||
| [invariants-rendering.md](docs/claude/invariants-rendering.md) | How the renderer maps a visible range onto samples (never the `dx()` grid), what `gather()` emits and relative to which origin, why every series needs a real `MinMaxPyramid`, and why a dense trace is drawn one pixel column at a time. |
|
||||
| [invariants-trigger-and-pinning.md](docs/claude/invariants-trigger-and-pinning.md) | The event window counted in stream seconds end to end, what pins an axis in each mode, and what each pin is measured *from* (the snapshot's own stamps, not the request). |
|
||||
| [invariants-time-axis-and-window.md](docs/claude/invariants-time-axis-and-window.md) | **No plot interaction changes the time scale**; `apply_time_window()` is the only path that does; how the x labels pick their form; what a fit restores. |
|
||||
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, and how a crossing is counted with hysteresis. |
|
||||
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted with hysteresis, and which mean over a split is which. |
|
||||
| [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. |
|
||||
| [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. |
|
||||
| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and how the Mallard manual is opened. |
|
||||
@@ -236,6 +236,8 @@ measurement.
|
||||
| `max` / `min` | the extremes | — | a ruler at the value **and** a marker where it happened |
|
||||
| `peak to peak` | the swing, `max - min` — the extremes, ripple and overshoot included | — | a ruler at each extreme |
|
||||
| `amplitude` | the two flat levels' delta, `top - bottom` | — | a ruler at each flat level |
|
||||
| `above` / `below` | the mean of the samples **above** / **below** a share — a mean *over a side*, not a level (see below) | split % | a ruler at the mean |
|
||||
| `between` | the mean of the samples **between** two shares | from %, to % | a ruler at the mean |
|
||||
| `pulse duration` | the mean width of the pulse-side runs | split %, which side is the pulse | markers at the pulse's two edges |
|
||||
| `period` / `frequency` | the spacing of the threshold crossings / its reciprocal, in Hz | — | markers at two edges of a cycle |
|
||||
|
||||
@@ -263,6 +265,39 @@ for it at all. Where the pyramid cannot summarize the window — an `hf`-filtere
|
||||
shorter than a bucket — there is no density estimate, and the types fall back to the old
|
||||
behaviour: the mean of each side of the split, and `max - min` for `amplitude`.
|
||||
|
||||
**`above`, `below` and `between` are those side means, exposed.** The flat-level change kept the
|
||||
split's own two means as its *fallback* for a window the pyramid cannot summarize; these three
|
||||
types are that fallback, named, so the two different questions about one split can be asked side by
|
||||
side. On a unit sine `above 50` reads `0.636620` — 2/π, exactly what `top` reported before it became
|
||||
a level — where `top` reads `0.957988`; on the mock's band-limited square `above 50` reads
|
||||
`0.986496`, where the plateau is 1 and `top` is `0.999983`, because the mean *over a side* carries
|
||||
the edges' ring and the level the signal dwells at does not. `between` is new arithmetic in one
|
||||
sense only: it is the samples above the lower edge **and not** above the upper one, so it is a
|
||||
*difference of two levels* — no wider split can answer a band, because a band's two edges are two
|
||||
thresholds — and it rides the same bucket walk (a bucket wholly inside the band folds into both
|
||||
accumulators whole), which is why `measure cost:`'s `raw` is unchanged by it. The readings, on the
|
||||
fast mock at a 2 s window, `[bandavg-probe]`: sine `above 50` `0.636620`, `below 50` `-0.636620`,
|
||||
`between 50 75` `0.255873` (3(2−√3)/π — neither the window's mean nor the band's edge); rippled
|
||||
square `above 50` `0.986496` and `between 50 75` `0.302744` (the two edges' transitions);
|
||||
`ramp` `between 50 75` `0.250000` and `above 50` `0.500000`, because a uniform density makes every
|
||||
band's mean that band's **own centre**; ideal square (`--square-harmonics 0`) `above 50` `1.000000`
|
||||
and `between 50 75` `--`, because every sample is exactly ±1 and the band holds none.
|
||||
|
||||
Three things to know before reading one of those numbers:
|
||||
|
||||
- **The pair is ordered on read, not refused.** "between 80 % and 20 %" is the band between 20 % and
|
||||
80 %: the two shares name two *edges* and the band does not depend on which was typed first, so a
|
||||
reversed pair is normalized rather than turned into a silent `--`.
|
||||
- **Nothing to measure reads `--`**, like every other type: a share at `100 %` has no sample above
|
||||
it (the level is the window's own maximum) and a band whose interior holds none — the ideal
|
||||
square above — is `invalid`/`--`, never `0`.
|
||||
- **The sixth decimal follows the window's boundary.** The live window is closed at both ends, so
|
||||
its oldest and newest sample share a phase, and one extra sample out of ~2.6 M moves the mean by
|
||||
about one unit in the sixth decimal: over one run `above 50` on the sine printed `0.636620` on
|
||||
most `[measure-trace]` lines and `0.636621` on a few, `above 50` on the square `0.986496` on most
|
||||
and `0.986495` on the rest. The old `top`/`bottom` spread exactly the same way — it is a property
|
||||
of a mean over a finite window, not of these types.
|
||||
|
||||
`period` and `frequency` deliberately take **no options**: the engine counts *rising*
|
||||
crossings of the *mid* level, one edge per cycle, and a split or a side to choose from could
|
||||
only make that count wrong — a split near either extreme leaves the hysteresis band below no room
|
||||
@@ -272,7 +307,7 @@ The split is a share of the **window's own** min..max, not volts: a measurement
|
||||
signal in this window, and a level in volts would have to be typed for every signal. For
|
||||
`top`/`bottom` it says **which side** the level is read from, so any split between the two
|
||||
plateaus reads the same number — it is no longer a mean *over* the side, which is the reading a
|
||||
ringing edge used to drag.
|
||||
ringing edge used to drag — and for `above`/`below` it is the level that mean is taken about.
|
||||
|
||||
Two more options ride on it, both at their old behaviour by default:
|
||||
|
||||
@@ -337,7 +372,9 @@ Each measurement's row carries **two badges**: its own colour (the same one its
|
||||
the plot) and the signal's chip, in the colour every other view names that signal with —
|
||||
through `RowSet::signal_color()`, so a signal the user re-coloured is re-coloured here too and
|
||||
not left at the palette entry. The dialog's fields and their wording follow the type — the split row is absent for
|
||||
`average`, `min` and `max`, the side row only for `pulse duration`, and the "show on plot"
|
||||
`average`, `min` and `max`, the side row only for `pulse duration`, the second share ("to % of the
|
||||
range") only for `between`, which is the one type whose two edges are both parameters, and the
|
||||
words on the split row name its side ("above % of the range" for `above`) — and the "show on plot"
|
||||
line says what would be drawn.
|
||||
|
||||
The values are **window statistics over exactly the visible range, at the stored rate**: the
|
||||
@@ -382,7 +419,8 @@ Both walks go through it:
|
||||
and one raw scan of that bucket alone then pins it to a sample — two buckets at most, whatever
|
||||
the window's size. The two stamps the scan needed are read at the end rather than at every
|
||||
chunk that moved an extreme, since a stamp is a ring read and an extreme moves many times.
|
||||
- **Pass two** (the crossing types — `pulse duration`, `period`, `frequency` — and, as a fallback
|
||||
- **Pass two** (the crossing types — `pulse duration`, `period`, `frequency` — the band averages,
|
||||
and, as a fallback
|
||||
for a window with no histogram, `top`/`bottom`)
|
||||
folds a bucket's sums whenever it lies *strictly* on one side of its level — then every sample
|
||||
in it falls in the same bin and none is *at* the level, so the fold is exact. It then asks
|
||||
@@ -396,12 +434,17 @@ samples produced since this frame's `acquire_all()` (bounded by one frame of pro
|
||||
for those opened buckets. Nothing is decimated or strided: `peak to peak` still reads
|
||||
exactly `2` on a unit sine, and the shared pass that carries it is untouched — `top` has no
|
||||
*exact* reading to protect any more, because it is a flat level read from a density rather than
|
||||
the half-wave mean it used to be (`0.636620` on a unit sine; it is `0.957988` now, near the peak,
|
||||
because a sine has no plateau). A window shorter than a bucket, or a signal
|
||||
the half-wave mean it used to be (that mean is now `above 50`, which reads `0.636620` on a unit
|
||||
sine where `top` reads `0.957988`, near the peak, because a sine has no plateau). A window shorter
|
||||
than a bucket, or a signal
|
||||
noisy enough to cross a threshold everywhere, degrades back towards the O(window) cost and nothing
|
||||
else changes. The dump's `measure cost:` line reports `covered`, `buckets` and `raw` for exactly
|
||||
this: `raw` near `covered` means the pyramid bought nothing — and `raw` is unchanged by the flat
|
||||
levels, which read the *same* bucket list a second time and never a sample.
|
||||
levels, which read the *same* bucket list a second time and never a sample, and by the band
|
||||
averages for the same reason (`between` is a **second level in the same walk**: with the
|
||||
measurements probe adding one of every type at a 2 s window, `raw` went from `113966` of
|
||||
`240719031` covered — `0.0%` — with ten types to `134978` of `255372033` — `0.1%` — with
|
||||
thirteen).
|
||||
|
||||
### What the math channels cost
|
||||
|
||||
@@ -511,6 +554,17 @@ missing shades solidly, even if all but one slot are already repaired. And the t
|
||||
yet configurable; the preferences dialog in `todo.md` is where that would land. Do not use a
|
||||
command-line flag for either; none exists.
|
||||
|
||||
**The counter is a claim, and the wire is checked against it.** A gap is only booked when the
|
||||
arrival times corroborate it: `lost` packets would have taken that many packet intervals to
|
||||
arrive, and a claim of less than half that is refused, as is a hole bigger than the ring could
|
||||
hold. The status bar's `gaps` counts what the *stream* says either way; the refused ones are
|
||||
counted separately and said once on stderr. That matters for one kind of source: a device whose
|
||||
counter counts something other than packets — samples, or frames of an inner layer — reports a
|
||||
forward jump on every datagram. Believing it reserved a NaN run per packet, which on an intact
|
||||
link reads as one missing-data band over the whole window with the samples washed out and nothing
|
||||
lost to backfill it, while the status bar says `live`. The samples are now kept and the gap shows
|
||||
as the x discontinuity it is.
|
||||
|
||||
**`--relative-time` makes the x axis follow the sample count and the declared rate**, rather than
|
||||
the stream's own clock. The packet counter says exactly how many samples each gap spans, so a
|
||||
reserved hole is a precise arithmetic range. In stream mode the same hole is estimated from the
|
||||
|
||||
@@ -169,7 +169,36 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
(`kGlitchPackets`, 256) is counted in **packets, not samples**, so one hole's sample cost
|
||||
is `256 × batch_` and scales with the stream's batch — 25 600 samples at a batch of 100,
|
||||
2.56 M at a batch of 10 000 — which is the number to weigh against the ring, not the 256.
|
||||
- **A re-plan drops every hole and restarts the producer's tick.** `rebuild_locked()` replaces
|
||||
- **A reservation is weighed against the wire, and never booked on the counter's word alone.**
|
||||
The counter is a *claim* about the stream; the arrival clock is a *measurement* of it, and only
|
||||
the two together justify writing NaN into the ring (`Acquisition::gap_plausible()`, called
|
||||
before anything is booked *and* before relative mode's `slot_base_` advance, because the advance
|
||||
and the reservation are the same claim). Two tests, either one refusing is enough:
|
||||
- **The arrival clock.** `missing` packets would have taken `missing` frame intervals to arrive,
|
||||
so a gap whose elapsed arrival time is under half of that is refused. Half and not one because
|
||||
the socket clock jitters and a drain can compress two frames' arrival times -- and a refused
|
||||
gap costs a *shading*, never data, so the test is deliberately permissive.
|
||||
- **The ring.** `missing × batch_` samples become `missing × batch_ / time_stride` slots, and the
|
||||
hole may not lap `time_capacity`. This is the *sample* form of the rule the 256-packet cap
|
||||
states in packets -- the cap's own comment says 256 packet spans in one go is an outage, and
|
||||
the invariant above says the number to weigh against the ring is `256 × batch_` and not the
|
||||
256 -- and it has to exist in samples because a hole that laps the ring erases the history it
|
||||
is meant to annotate, can never be backfilled (its slots are already overwritten) and is never
|
||||
forgotten (`forget_holes_before()` waits for slots the ring has passed), so it leaves a
|
||||
*permanent* band over a buffer that is all NaN.
|
||||
What its absence looks like, which is why it is written down: a device whose counter counts
|
||||
*samples*, or frames of an inner layer, reports a forward jump on every datagram while the wire
|
||||
delivers on time. Every frame then reserves, and with `relative_time` every frame books NaN
|
||||
slots by construction -- one band over the whole window with the samples washed out,
|
||||
`backfilled=0` (no packet was ever missing, so nothing can repair it), and the status bar reading
|
||||
`live` at the right rate the whole time. The refusal is counted in `StreamStats::gaps_refused`
|
||||
and said once per connection with the numbers, because the status bar's `gaps` climbs either way
|
||||
and the state is otherwise invisible; `tools/mock_udpstreamer.py --counter-step N` is the mock
|
||||
shape (its counter is otherwise always +1, so this is the one wire shape it could not produce),
|
||||
and `gaps_refused` rising while `nan_slots` stays 0 is the reading. A refused gap is not a silent
|
||||
one: the timeline is not stretched over loss that did not happen, so it shows as the x
|
||||
discontinuity it is.
|
||||
- **A re-plan drops every hole and restarts the producer's tick.** `rebuild_locked()` replaces
|
||||
the store, so `produced()` restarts at 0 and a hole's `slot_begin`/`tick_begin`/`x_begin`
|
||||
describe a buffer that is gone; it therefore clears `holes_` and bumps
|
||||
`ScopeModel::plan_generation()`, which the producer compares against the value it last saw
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Invariants: the measurements pane
|
||||
|
||||
Why the pane is refreshed on a cadence, why neither pass may read its window raw, and how a crossing is counted.
|
||||
Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted, and which mean over a split is which.
|
||||
|
||||
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
||||
|
||||
@@ -66,9 +66,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
* **Exactness is the point, so nothing may stride or decimate.** `peak to peak` must still read
|
||||
exactly `2` on a unit sine, and it and `average` still come straight out of the one shared
|
||||
`measure_stats()` pass. A bucket's sum is a sum. (`top` has no *exact* reading to protect any
|
||||
more: it is a density estimate and not a mean, and the two places that used to assert
|
||||
`top == 0.636620` on a sine now assert what the flat level actually reads -- see the flat-level
|
||||
invariant below. Everything above is about the *bucket* path being a fast path and not an
|
||||
more: it is a density estimate and not a mean, so the assertion that used to be
|
||||
`top == 0.636620` on a sine now belongs to `above 50`, which *is* a mean over a side and reads
|
||||
exactly 2/pi again -- see the band-average invariant below, and the flat-level one for what
|
||||
`top` reads instead. Everything above is about the *bucket* path being a fast path and not an
|
||||
approximation, and that is unchanged.)
|
||||
* **The bucket index space must be the window's.** `ChannelBuffer::acquire()` calls
|
||||
`pyramid_.drop_before(values_->base())`, so a channel's pyramid is in the value ring's
|
||||
@@ -139,6 +140,60 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
`top=0.973824 bottom=-0.991731 amplitude=2.354173 peak to peak=2.354173`, `buckets=0
|
||||
raw=200.0%` -- two raw walks and no density estimate, which is the honest reading of a window
|
||||
the pyramid cannot summarize.
|
||||
- **`Above`/`Below`/`Between` are the *means over a side of a share*, and they are the readings
|
||||
`Top`/`Bottom` gave before a flat level replaced them.** That change kept the split's two means as
|
||||
its *internal fallback* for a window the pyramid cannot summarize (above); these three types are
|
||||
that fallback, exposed and named, which is what lets the two different questions about one split be
|
||||
asked side by side -- on a unit sine `above 50` reads `0.636620` (2/pi) where `top` reads
|
||||
`0.957988`, and on the mock's band-limited square `above 50` reads `0.986496` where the plateau is
|
||||
`1.000000` and `top` `0.999983`: the edge's ring is *inside* this mean and not inside that level.
|
||||
Five rules, and the first two are what a reader is most likely to assume wrongly:
|
||||
* **A band is not a wider split, and needs no walk of its own.** `Above`/`Below` are
|
||||
`MeasureLevel::sum[1]`/`count[1]` and `sum[0]`/`count[0]` at the measurement's own share.
|
||||
`Between` is the **difference of two levels' upper sides** -- `sum[1](lo) - sum[1](hi)` over
|
||||
`count[1](lo) - count[1](hi)`, i.e. the samples above the lower edge and *not* above the upper
|
||||
one -- and no single level's two sums can bound a band, because a band's two edges are two
|
||||
thresholds. Both accumulators come out of one pass two over one window (the pane registers one
|
||||
level per edge, `w.pct`, and both are deduplicated into it), so the difference is exact rather
|
||||
than approximate. `v == level` is the below side, as everywhere, so the band is `(lo, hi]`.
|
||||
* **It rides the bucket fast path unchanged.** A bucket wholly inside the band is strictly above
|
||||
the lower level *and* strictly below the upper one, so both accumulators fold it whole; only the
|
||||
buckets that straddle either edge are opened, exactly as for one level. `measure cost:` is the
|
||||
guard: adding all three types to a pane of every type left `raw` at `0.1%` of `covered`
|
||||
(`113966` of `240719031` with the ten types, `134978` of `255372033` with the thirteen), and a
|
||||
band is *two entries of the level list*, not a second pass.
|
||||
* **`measure_level_pcts()` is the only place the shares are read**, and it clamps both to `[0,100]`
|
||||
and puts `Between`'s pair **in order**: "between 80 % and 20 %" is the band between 20 % and
|
||||
80 %. Normalized rather than refused because the shares name two *edges*, and the band does not
|
||||
depend on which the user typed first; the pane's dedup key and both accumulators are keyed on the
|
||||
ordered pair, so one band is one walk however it was written.
|
||||
* **Nothing to measure reads `--`**, exactly as for every other type: a side holding no sample (a
|
||||
share at `100 %`, whose level is the window's own maximum) and an *interior* holding none are
|
||||
both `invalid`/`--` and never `0`. The ideal `--square-harmonics 0` square is where the second
|
||||
shows -- every sample is exactly +/-1, so the band `(0, 0.5]` holds nothing -- against the same
|
||||
band on the band-limited square, which holds the edges' transitions (`between 50 75` =
|
||||
`0.302744`, `between 25 75` = `0.000000`: the symmetric band's two halves cancel, the transition
|
||||
being odd about its own midpoint).
|
||||
* **The reading is a mean over the window's *samples*, so its sixth decimal follows the window's
|
||||
boundary.** A live window is closed at both ends, so its oldest and newest sample share a phase
|
||||
and the mean carries one sample that is not in an open interval -- about one unit in the sixth
|
||||
decimal out of 2.6 M samples. Measured over one fast-mock run, `[measure-trace]`: `above 50` on
|
||||
the sine printed `0.636620` on 48 lines and `0.636621` on 2, `below 50` `-0.636620` on 37 and
|
||||
`-0.636619` on 18, `above 50` on the square `0.986496` on 32 and `0.986495` on 18, and
|
||||
`between 50 75` on the ramp `0.250000`/`0.250001`/`0.249999`. The old `Top`/`Bottom` spread the
|
||||
same way -- it is a property of a mean over a finite window, not of these types, and a reading
|
||||
that has to be exact to six figures is read off `[bandavg-probe]`'s nine.
|
||||
Measured on the fast mock, 2 s window, the mock's default 30-harmonic square, `[bandavg-probe]`
|
||||
(nine significant digits, then what the pane prints):
|
||||
`above 50` on the sine `0.636620262` -> `636.62 mV`; `below 50` on the sine `-0.636619283`;
|
||||
`between 50 75` on the sine `0.255873597` -- the analytic `3(2-sqrt 3)/pi = 0.255873`, which is
|
||||
neither the window's mean (`0`) nor the band's edge (`0.5`); `above 50` on the square
|
||||
`0.986494746` / `0.986495505` (one or two samples at the window's end); `between 50 75` on the
|
||||
`ramp` `0.250000385` and `above 50` `0.500000` -- a uniform density makes every band's mean that
|
||||
band's own centre, so a quarter and a half of the range are the shape's prediction and not a
|
||||
coincidence; `above 100` on the `ramp` `--`; and the ideal square's `between 50 75` `--`.
|
||||
Editing the sine's band through the dialog to `50/90` takes it to `0.431361697`
|
||||
(`0.8 / (2 asin 0.8)`), which is the check that the second share is the one the engine reads.
|
||||
- **`MeasureOptions::noise_pct` is a percentage, clamped in `measure_set_level()` to half the room to the
|
||||
nearer extreme**, so a band can never invert its own thresholds. It moves the **edge decision only**: the
|
||||
sums still split at the bare level, which is the *fallback* `Top`/`Bottom` read on a window with no
|
||||
@@ -170,12 +225,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
decides that side and the measurement reads nothing at all, which is worse than reading
|
||||
something noisy. On a ringing 3.3 V logic signal at a 90 % split: `enters=0` with a fixed band,
|
||||
`enters=4 period=0.200000` with the clamp.
|
||||
* **The `sum`/`count` split stays at the bare `level`.** Two things read it and neither may be
|
||||
* **The `sum`/`count` split stays at the bare `level`.** Four things read it and none may be
|
||||
moved by a band: a crossing's *side* is decided by the sample's own value against the level
|
||||
(`v == level` is the below side, as it always was), and `Top`/`Bottom` fall back to the mean of
|
||||
each side on a window with no bucket histogram. The band is about *deciding a crossing*, not
|
||||
about where a sample lives, so folding it in would change a filtered window's `top` for no gain
|
||||
to any crossing count.
|
||||
(`v == level` is the below side, as it always was), `Top`/`Bottom` fall back to the mean of
|
||||
each side on a window with no bucket histogram, and `Above`/`Below`/`Between` read it on
|
||||
*every* window -- their whole definition is that split, so a band folded into it would move a
|
||||
documented exact reading (2/pi on a sine) for no gain to any crossing count. The band is about
|
||||
*deciding a crossing*, not about where a sample lives.
|
||||
* **`h = 0` must reduce exactly to the single-level walk**, including the `v == level` skip, so the
|
||||
band is a parameter of the engine rather than a mode of it.
|
||||
The cost is the noise's, not the band's: the clean fast mock reads `raw=119412 (0.1%)` of 215 M at
|
||||
|
||||
@@ -42,9 +42,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
was parsed into the last calibration record and the enable state was lost without a word.
|
||||
`mkdirs()` used to create only components with a trailing slash, so `~/.config/udpscope` was never
|
||||
made and the *first* save failed silently -- worth knowing when a layout change "does not stick".
|
||||
- **Measurement options persist under *named* keys -- `noise` and `hf` -- so a file written before them
|
||||
loads at the defaults**, which are exactly the behaviour it was written against: `noise = 10` is the
|
||||
hysteresis band the app has always counted with, and `hf = 0` is no filter.
|
||||
- **Measurement options persist under *named* keys -- `threshold`, `threshold2`, `noise` and `hf` -- so a
|
||||
file written before them loads at the defaults**, which are exactly the behaviour it was written against:
|
||||
`noise = 10` is the
|
||||
hysteresis band the app has always counted with, `hf = 0` is no filter, and a file with no
|
||||
`threshold2` -- every file written before `between` existed -- leaves that measurement at
|
||||
`threshold2 = 75`, which is the value the dialog offers a new one. The *pair* is stored as written
|
||||
rather than as ordered: `measure_level_pcts()` is what puts a reversed pair in order on read, so a
|
||||
file records what the user typed and a measurement file round-trips unchanged.
|
||||
- **The math node string is thirteen pipe fields, and the reader accepts twelve *or* thirteen.** `med_n` is
|
||||
the thirteenth, after `y`; a twelve-field node is a file written before the de-noise node existed, and its
|
||||
`med_n` stays at the struct's default -- which is what such a file meant. Field 0 is the op as a
|
||||
|
||||
@@ -218,6 +218,9 @@ Two lines of the self-check output are assertions in disguise:
|
||||
pane_h=H switch_on=X reset_sensitive=Y`
|
||||
(stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first
|
||||
plottable signal and lets the tick compute them, so what it prints is what a user reads.
|
||||
The dump line carries one more field for the one type that reads *two* shares:
|
||||
`measure N: 'sig' between thr=50.0% thr2=75.0% above=1 show=1 -> ok ...`, where `thr2` appears
|
||||
only for `between` (see the band-average probe below).
|
||||
On the test stream's 1 Hz ±1 sine the values must be: average ≈ 0, `top` ≈ 0.96 and
|
||||
`bottom` ≈ -0.96, `amplitude` ≈ 1.92, max/min ≈ ±1, `peak to peak` **exactly 2**,
|
||||
`pulse duration` ≈ 0.5 s
|
||||
@@ -226,7 +229,9 @@ Two lines of the self-check output are assertions in disguise:
|
||||
window rather than from a mean over a side, and a sine has no plateau — so the densest band
|
||||
sits at its peak and the reading is near it, not the 0.636620 a mean over a half would give.
|
||||
`amplitude` is that pair's delta (`top - bottom`), which is why it reads about 1.92 rather than
|
||||
the half-swing it used to: a bench scope's `Vamplitude` is `Vtop - Vbase`. The measured
|
||||
the half-swing it used to: a bench scope's `Vamplitude` is `Vtop - Vbase`. The mean over a half
|
||||
is not gone, though: it is `above 50` on this stream (and `-above 50` is `below 50`), and its
|
||||
own probe asserts it — see `[bandavg-probe]` below. The measured
|
||||
figures, and the reason they are what they are, are in
|
||||
[invariants-measurements.md](invariants-measurements.md) — on the mock's `square` the
|
||||
separation is the visible one (`top`/`bottom` `0.999983`/`-0.999983`, `amplitude` `1.999966`,
|
||||
@@ -275,10 +280,15 @@ Two lines of the self-check output are assertions in disguise:
|
||||
(`an_unselectable_cursor_is_neither_hovered_nor_grabbed`) is the other half. The probe's last
|
||||
step still drags a mark *programmatically* and requires the next frame to put it back
|
||||
(`restored=1`), which now checks the placement path rather than the read-only property. The dialog report's `type= thr=
|
||||
side= show=` is the per-type behaviour: `period` and `frequency` must show **neither** the
|
||||
band= side= show= shares=A/B` is the per-type behaviour: `period` and `frequency` must show **neither** the
|
||||
split nor the side row (they compute from the mid level's rising crossings, and an option
|
||||
there is one the user can only get wrong), `average`/`min`/`max` no split either, and only
|
||||
`pulse duration` a side. A measurement whose signal is not acquired, or
|
||||
`pulse duration` a side. `band` is the *second* share's row, which only `between` shows -- it is
|
||||
the one type whose two edges are both parameters -- and `shares` is what the two spin fields are
|
||||
*showing*, read back out of the widgets: a share that is stored but not seeded into its field, or
|
||||
a field that is built but not read back on apply, is then visible in this line rather than only in
|
||||
the reading it produces. `above`/`below` show the threshold row and no band row, with their own
|
||||
label (`above % of the range`). A measurement whose signal is not acquired, or
|
||||
a window with too few crossings, must read `invalid`/`--` rather than a made-up number
|
||||
(`frequency` on a window holding one edge is the case: `duty` is reported only when a
|
||||
period exists to divide by). The probe then **walks the dialog's lifecycle**: open the edit
|
||||
@@ -862,10 +872,11 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
The `m`/`g` pairs are the *tab name* before and after, which is what says those two keys select a tab rather than toggling a pane; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
|
||||
- `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit.
|
||||
- `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N
|
||||
open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` (stderr,
|
||||
gaps_refused=N open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D`
|
||||
(stderr,
|
||||
`UDPSCOPE_PROBE_RESEQ_MS`) —
|
||||
the resequencing path's mid-run state; the measured readings and the per-case command are in
|
||||
[verification.md](verification.md). Eight things to read carefully:
|
||||
[verification.md](verification.md). Nine things to read carefully:
|
||||
|
||||
1. **`backfilled` and `hole_slots` are not comparable directly.** `hole_slots` counts
|
||||
*time-ring* slots once per hole; `backfilled` counts *value-ring* slots repaired and
|
||||
@@ -931,6 +942,29 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
slots. A non-zero reading beside a healthy `backfilled` is a note about the ring's size,
|
||||
not a defect.
|
||||
|
||||
9. **`gaps_refused` counts reservations the wire contradicted, and it is the field that reads
|
||||
a counter which is not counting packets.** A gap is booked only if it passes two tests
|
||||
(`Acquisition::gap_plausible()`): the *arrival clock* has to corroborate the elapsed time
|
||||
(`missing` packets would have taken `missing` frame intervals; anything under half of that
|
||||
is refused) and the *ring* has to hold the hole (`missing * batch_` samples must not lap
|
||||
`time_capacity`). The shape this exists for is a device whose counter advances by samples,
|
||||
or by frames of an inner layer: every datagram reports a forward jump while the wire
|
||||
delivers on time, so *every* frame reserves -- and, with `relative_time`, every frame
|
||||
books NaN slots, which is a band over the whole window with the samples washed out and no
|
||||
packet ever missing to backfill it. `tools/mock_udpstreamer.py --counter-step N` produces
|
||||
exactly that on an intact link. The reading's two sides, both measured with
|
||||
`--relative-time` and no session: a *fixed* run reads
|
||||
`holes=1 hole_slots=900 gaps_refused=419 open_holes=0 bands=0 nan_slots=0 scanned=21700`
|
||||
with the data channels full and a once-per-connection stderr warning naming the numbers,
|
||||
while the unfixed one read `holes=420 hole_slots=378000 open_holes=64 bands=64
|
||||
bands_repaired=0 nan_slots=198000 scanned=220000` -- `nan_slots` at 90% of `scanned`, which
|
||||
is the whole-window band. The refusal is **not relative-mode-only**: the same command without
|
||||
`--relative-time` reads `holes=1 gaps_refused=419 nan_slots=0 scanned=21800`, because a
|
||||
counter that lies is not a mode. On the honest shapes
|
||||
(`--drop-every`, `--drop-burst 200`, `--reorder-delay-ms`) this field must stay **0**:
|
||||
a non-zero `gaps_refused` there means the arrival test is refusing real loss, and the cost
|
||||
is a gap with no shading (never lost data).
|
||||
|
||||
- `[zoomkeys] ctrl+plus x=[..]->[..] y_held=1 | alt+plus y=[..]->[..] x_held=1` (stderr, `UDPSCOPE_PROBE_ZOOMKEYS_MS`) — **both `_held` must be 1 and both zooms must still happen.** Run it inside a pinned state, where a frame cannot re-frame the other axis for its own reasons: `UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_ZOOMKEYS_MS=2200`. A `_held=0` is the modifier not reaching the plotter's controller; an unchanged range on the *zoomed* axis is the opposite failure, the key doing nothing. Ctrl restricts to x and Alt to y; Ctrl+Alt zooms neither (no special case needed — neither flag alone fires).
|
||||
- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set (`connecting`, `live`, `paused`, `armed`, `capturing`, `event held`, `disarmed` — a word added to it must be **no wider than the widest already there**, which is what let `connecting` join at ten characters beside `event held`; the dead-port glance reads `max_glance_chars=51` against a live run's 54), the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the two fields a *state* adds are both elapsed times — trigger mode's `last event … ago` and a pause's `for <elapsed>` — which step units rather than growing digits (`format_elapsed()`). A **paused** glance is not in the dump at all: it is read by `[pause-probe]`'s `glance=` field, since the dump's own line is taken live. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it.
|
||||
|
||||
@@ -1040,6 +1074,70 @@ phase=2 … n=0 mean=0 sd=0 lo=0 hi=0 (all four)
|
||||
- `phase=2` — after Reset. **`n` must be 0 or 1** (0 if the report ran before the next recompute,
|
||||
1 if after); anything larger means Reset cleared nothing.
|
||||
|
||||
## `[bandavg-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_BANDAVG_MS`. Needs `UDPSCOPE_ENABLE_ALL_MS` ahead of it, and it must **not** be
|
||||
scheduled with `UDPSCOPE_PROBE_MEASURE_MS` or `UDPSCOPE_PROBE_MEASURES_MS`: all three add
|
||||
measurements to the same pane, and the later one would report the earlier one's rows as its own.
|
||||
Run it against the fast mock (`--rate 1300000 --batch 1000 --freq 1`) **at a 2 s window** — the
|
||||
figures below are shares of the range, and a window that holds a fifth of a cycle is a different
|
||||
question, not a worse answer. Its own sequence is about 12 s, so `UDPSCOPE_EXIT_AFTER_MS=17000`.
|
||||
|
||||
It adds seven measurements, one per shape whose band mean can be computed by hand, and every one of
|
||||
them is a *mean over a side or a band* rather than a level. `--square-harmonics 30` and
|
||||
`--square-harmonics 0` are two runs — the second is the ideal step, where the band holds nothing:
|
||||
|
||||
| measurement | shape | reading | why that shape |
|
||||
|---|---|---|---|
|
||||
| `above 50` | 1 Hz unit sine | `0.636620` | 2/pi, the figure the old `top` documented |
|
||||
| `below 50` | 1 Hz unit sine | `-0.636620` | its other side: exactly `-above 50` |
|
||||
| `between 50/75` | 1 Hz unit sine | `0.255873` | `3(2-sqrt 3)/pi`, and neither 0 (the window's mean) nor 0.5 (the band's edge) |
|
||||
| `above 50` | `square`, 30 harmonics / `0` | `0.986496` / `1.000000` | the reading that commit recorded as the *old* `top`; the ideal step has no ring to put inside it |
|
||||
| `between 50/75` | `square`, 30 harmonics / `0` | `0.302744` / `--` | the two edges' transitions the band holds; **every** sample is exactly +/-1 on the ideal step, so it holds nothing |
|
||||
| `between 50/75` | `ramp` | `0.250000` | uniform density: a band's mean *is* that band's centre |
|
||||
| `above 100` | `ramp` | `--` | a share at the top of the range has no sample above it |
|
||||
|
||||
Four line kinds, and the reading is the third:
|
||||
|
||||
```
|
||||
[bandavg-probe] waiting stored=2108000 of 2600000 slots for a 2.000 s window
|
||||
[bandavg-probe] readings window channel=0 lo=-1.000000 hi=1.000000 samples=2600000 levels=2
|
||||
[bandavg-probe] readings above 'sine' shares=50% -> ok 0.636620262 text=[636.62 mV]
|
||||
[bandavg-probe] edit opened band_row=1 shares=50.0/75.0 (as stored)
|
||||
[bandavg-probe] after delete measures=6 -> 5 (want one fewer)
|
||||
```
|
||||
|
||||
- `waiting` — the probe reads nothing until the ring holds a **full window**: a share of a *partial*
|
||||
window is a different number (and one that keeps moving while the ring fills), and the mock
|
||||
delivers about 0.5 MS/s of the 1.3 MS/s it declares, so a ring the probe has just re-enabled needs
|
||||
seconds of wall clock before a 2 s window is there. The wait is read off the store
|
||||
(`stored_slots()` against `window / dt`), not guessed from a stopwatch. **A report taken before it
|
||||
finishes is the one reading this probe can produce that is not the share's** — the settled lines
|
||||
are the ones to quote.
|
||||
- `window` — the window each reading was taken over, per channel: `lo`/`hi` are that window's own
|
||||
extremes (so `lo`/`hi` of the split are visible here), `samples` is what it holds, and `levels`
|
||||
is how many level accumulators the pane registered for that channel. **`between` on the sine makes
|
||||
`levels=2` where `top`/`between` alone would make one**: a band is two entries of the level list,
|
||||
and a second measurement of the same band does not add a third.
|
||||
- the value line — the pane's own numbers, nine significant digits and then the row's formatted text
|
||||
(`text=`), with the shares it was given. `-> invalid` with `text=[--]` is the nothing-to-measure
|
||||
reading (the last two rows of the table). The value carries the measurement's **unit**; the three
|
||||
new types are signal-valued like `top`/`average`, so a sine reads `mV` and a quantity with no unit
|
||||
reads the bare number.
|
||||
- `edit opened` — the dialog's second-share row for a `between`: **`band_row` must be 1** (only
|
||||
`between` offers it) and `shares` must be the pair that was *stored*, read back out of the two
|
||||
spin fields. The probe then sets them to `50/90` and saves, and the next report is the proof the
|
||||
field is wired: the sine's `between` reads `0.431361697` (`0.8 / (2 asin 0.8)`), where neither of
|
||||
its sides would.
|
||||
- `after delete` — the cycle's last leg through the same dialog: **one fewer** measurement, process
|
||||
alive. The two shares are the thing a delete could drop on the floor.
|
||||
- The `[measure-trace]` line the probe starts carries the same five readings every 250 ms, which is
|
||||
where the *sixth* decimal's spread is visible (see the band-average invariant in
|
||||
[invariants-measurements.md](invariants-measurements.md)): `above 50` on the sine reads
|
||||
`0.636620` on almost every line and `0.636621` on a few, `above 50` on the square `0.986496` on
|
||||
most and `0.986495` on the rest. **A line's `--` on a *lagging* trace is the trace's, not the
|
||||
reading's** — the pane is refreshed on a cadence, so quote an earlier line beside its own `view=`.
|
||||
|
||||
## `[search-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is
|
||||
|
||||
@@ -120,13 +120,45 @@ precedent, not an exception.
|
||||
thing that reads a second time: `top`/`bottom`/`amplitude` fold a histogram out of the **same**
|
||||
fetched bucket list, so they add buckets and not samples (measured: `raw=0.1%` and `2.1%` with
|
||||
the interferer, against `0.1%`/`2.1%` before those types had a flat level, for 0.44→0.50 ms and
|
||||
0.52→0.65 ms a pass -- the buckets are the budget).
|
||||
0.52→0.65 ms a pass -- the buckets are the budget). It is the guard on the band averages for the
|
||||
same reason: `between` is a *second level in the same walk*, not a second pass, and measured on
|
||||
the fast mock at a 2 s window with the measurements probe adding one of **every** type, `raw`
|
||||
stayed at a fraction of a percent of `covered` —
|
||||
`covered=240719031 buckets=3760828 raw=113966 (0.0%)` for the ten types before, against
|
||||
`covered=255372033 buckets=3989978 raw=134978 (0.1%)` for the thirteen after, at 0.8 → 1.1
|
||||
ns/covered sample (`data` 156.85 → 178.38 ms: the pane holds three more measurements and one more
|
||||
level, and both walk the same fetched bucket list).
|
||||
Note the **final** `measurements:` line and `[measure-trace]`'s last lines may read `--`:
|
||||
`UDPSCOPE_EXIT_AFTER_MS` arms the trigger before it dumps, so the dump can land in `Waiting`
|
||||
with nothing bound and the axis at its default. The race-free reading is an earlier
|
||||
`[measure-trace]` line, which carries its own `view=`; those lines are also where the
|
||||
level-mark fallback is visible (an invalid pass must still print its `level:` marks).
|
||||
|
||||
- `[bandavg-probe]` (stderr, `UDPSCOPE_PROBE_BANDAVG_MS`) — the **band averages**:
|
||||
`above`/`below`/`between`, the plain means over one side of a share and over a band, which are
|
||||
*different numbers* from `top`/`bottom`'s flat levels (see the band-average invariant in
|
||||
[invariants-measurements.md](invariants-measurements.md)). It adds seven measurements, on the three
|
||||
shapes whose band means can be computed by hand, and the four figures to read are:
|
||||
|
||||
* the 1 Hz unit sine at a **2 s window**: `above 50` = **`0.636620`** (2/pi) and `below 50` =
|
||||
**`-0.636620`** — the exact figures the old `top`/`bottom` documented, and the reason this type
|
||||
exists at all; `between 50 75` = `0.255873` (`3(2-sqrt 3)/pi`), an interior mean no side and no
|
||||
whole-window statistic reports;
|
||||
* the mock's band-limited `square` (30 harmonics, its default): `above 50` = **`0.986496`**, the
|
||||
reading the commit that made `top` a flat level recorded as the *old* `top`;
|
||||
* the mock's `ramp`, whose density is uniform: `between 50 75` = `0.250000` and `above 50` =
|
||||
`0.500000` — a band's mean *is* that band's centre, so the shape predicts the number;
|
||||
* and the two nothing-to-measure readings: `above 100` on the `ramp` and `between 50 75` on
|
||||
`--square-harmonics 0` (whose samples are all exactly +/-1) must be `invalid`/`--`, never 0.
|
||||
|
||||
**Run it at a 2 s window** (the probe waits for a full one before it reads anything — the mock
|
||||
delivers about 0.5 MS/s of the 1.3 MS/s it declares, so a freshly enabled ring needs seconds of
|
||||
wall clock), and **not** with `UDPSCOPE_PROBE_MEASURE_MS` or `UDPSCOPE_PROBE_MEASURES_MS`: all
|
||||
three add measurements to one pane. `UDPSCOPE_EXIT_AFTER_MS=17000` covers its sequence. The
|
||||
command, both mock flavours and the lines are in [probe-readings.md](probe-readings.md):
|
||||
`--square-harmonics 30` for the `0.986496` and the `0.302744`, `--square-harmonics 0` for the
|
||||
`1.000000` and the empty band.
|
||||
|
||||
- `[pan-probe] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then
|
||||
`[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L`
|
||||
(stderr, `UDPSCOPE_PROBE_PAN_MS`) — **`held` must be 1**: a gesture must not move a window
|
||||
@@ -558,7 +590,7 @@ as distinct from the fault injection below, which exercises ingest:
|
||||
at 64. The `mixed` case's `open_holes=43` against `bands=43` has no such spread because
|
||||
nothing re-planned. The per-field reference is in [probe-readings.md](probe-readings.md).
|
||||
|
||||
The mock also carries three **fault injections** that no probe schedules, because what they exercise
|
||||
The mock also carries four **fault injections** that no probe schedules, because what they exercise
|
||||
is the ingest rather than the UI. The first: `--time-glitch-at <stream seconds> --time-glitch-by <seconds>` makes
|
||||
**one** packet's time signal jump ahead and then carries on from where the true timeline was, which
|
||||
is what a device whose clock glitches puts on the wire. Run it with the trigger armed, e.g.
|
||||
@@ -593,6 +625,41 @@ arrives within the 20 ms reorder window is resequenced with no hole, and one tha
|
||||
leaves a committed hole that it then backfills (`UDPSCOPE_PROBE_RESEQ_MS` exercises both paths).
|
||||
There is **no interpolation** anywhere below the ring.
|
||||
|
||||
The fourth is the counter itself: **`--counter-step N`** advances the header counter by `N` per
|
||||
datagram instead of one -- a device whose counter counts *samples*, or frames of an inner layer,
|
||||
rather than packets. Nothing is withheld and every datagram goes out on schedule, so the link is
|
||||
intact and the status bar is right to say `live`; what is wrong is the counter's claim, which the
|
||||
client cannot check (it has no clock) and only the app can (it has the arrival times). It is the
|
||||
one wire shape the mock could not otherwise produce, and the run that reads it is the reseq probe
|
||||
with relative time on (the mode that frames a counter gap as NaN by construction):
|
||||
|
||||
```sh
|
||||
# mock: --counter-step 10
|
||||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=600 \
|
||||
UDPSCOPE_PROBE_RESEQ_MS=5000 UDPSCOPE_EXIT_DUMP=1 \
|
||||
./build/udpscope --server 127.0.0.1 --port 8000 --relative-time
|
||||
```
|
||||
|
||||
```
|
||||
# fixed: [reseq-probe] … holes=1 hole_slots=900 backfilled=0 gaps_refused=419 open_holes=0
|
||||
# bands=0 bands_repaired=0 nan_slots=0 scanned=21700 trigger_in_hole=0
|
||||
# status bar: glance=[● live · 9.97 kS/s · 2 s window]
|
||||
# broken: [reseq-probe] … holes=420 hole_slots=378000 backfilled=0 open_holes=64 bands=64
|
||||
# bands_repaired=0 nan_slots=198000 scanned=220000 trigger_in_hole=0
|
||||
# status bar: glance=[● live · 101 kS/s · 2 s window]
|
||||
```
|
||||
|
||||
**`gaps_refused` above 0 with `nan_slots` at 0 is the assertion** — the gap was refused and the
|
||||
samples were kept. The broken reading is `nan_slots` at 90% of `scanned` with `backfilled=0`: a
|
||||
reserved hole per datagram, `relative_time` framing each one as NaN, and nothing ever missing to
|
||||
backfill it, i.e. one missing-data band over the whole window with no trace in it, under a status
|
||||
bar reading `live` at ten times the stream's real rate. The same field
|
||||
must be **0** on every honest shape above (`--drop-every`, `--drop-burst 200`,
|
||||
`--reorder-delay-ms 50`, at 10 kS/s: `holes=210 hole_slots=21000 nan_slots=11000 scanned=22000
|
||||
gaps_refused=0` for `--drop-every 2`, `holes=3 hole_slots=60000 nan_slots=20000` for the 200-packet
|
||||
burst at 100 kS/s) — a non-zero reading there means the arrival test is refusing real loss, whose
|
||||
only cost is a gap with no shading.
|
||||
|
||||
All of the readings below are off `UDPSCOPE_EXIT_DUMP`'s `[udpscope] frames= packets= gaps=` line,
|
||||
over an 8 s run against `--rate 10000 --batch 100 --freq 5` (100 packets a second) except where the
|
||||
burst needs the faster stream. **`gaps` is the exact reading** and repeats to the packet; `frames`
|
||||
@@ -691,8 +758,10 @@ have run. `--spike-every`/`--spike-amp`/
|
||||
|
||||
The three lines and what each must read are in `probe-readings.md`.
|
||||
|
||||
`UDPSCOPE_PROBE_NOISE_MS`, `UDPSCOPE_PROBE_MEASURE_MS` and `UDPSCOPE_PROBE_MATH_MS` must **not** be scheduled
|
||||
in the same run. The noise and measurement probes both push onto `app.measures`, and the later one would read
|
||||
`UDPSCOPE_PROBE_NOISE_MS`, `UDPSCOPE_PROBE_MEASURE_MS`, `UDPSCOPE_PROBE_MEASURES_MS`,
|
||||
`UDPSCOPE_PROBE_BANDAVG_MS` and `UDPSCOPE_PROBE_MATH_MS` must **not** be scheduled
|
||||
in the same run. The noise, measurement, measurements and band-average probes all push onto
|
||||
`app.measures`, and the later one would read
|
||||
the earlier one's rows as its own; the noise and math probes both write `app.math`, so the later one's graph
|
||||
lands on top of the earlier one's and both sets of readings describe a state neither probe set up. The noise probe carries
|
||||
its own measurement legs (see `probe-readings.md`), so nothing is lost by keeping them apart.
|
||||
@@ -26,8 +26,8 @@
|
||||
</item>
|
||||
<item>
|
||||
<p>The pane's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
|
||||
the type, the threshold share and the on-plot marks are set, with <gui>Save</gui> and
|
||||
<gui>Delete</gui>.</p>
|
||||
the type, the threshold share or shares and the on-plot marks are set, with
|
||||
<gui>Save</gui> and <gui>Delete</gui>.</p>
|
||||
</item>
|
||||
</steps>
|
||||
</section>
|
||||
@@ -35,14 +35,32 @@
|
||||
<section id="knowing">
|
||||
<title>Worth knowing</title>
|
||||
<list>
|
||||
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, pulse
|
||||
duration, period and frequency. <gui>top</gui> and <gui>bottom</gui> are the waveform's
|
||||
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, above,
|
||||
below, between, pulse duration, period and frequency. <gui>top</gui> and <gui>bottom</gui> are the waveform's
|
||||
<em>flat</em> high and low levels: the level the signal dwells at, taken from the densest
|
||||
band of the window on each side of the split. A band-limited edge overshoots and rings —
|
||||
a nominal 2 V square reaches about ±1.18 at its edges — and a ripple or an overshoot is
|
||||
therefore <em>not</em> what these read. <gui>amplitude</gui> is the delta between those two
|
||||
levels, and <gui>peak to peak</gui> is the swing between the extremes (so on a rippled
|
||||
square <gui>amplitude</gui> reads 2 V where <gui>peak to peak</gui> reads 2.36 V).</p></item>
|
||||
<item><p><gui>above</gui>, <gui>below</gui> and <gui>between</gui> are the opposite kind of
|
||||
number: the plain <em>mean of the samples</em> above a share, below it, or between two
|
||||
shares. That is a different question from <gui>top</gui>/<gui>bottom</gui> even about the
|
||||
same split, and it is the older one — on a unit sine <gui>above 50%</gui> reads 0.64 V
|
||||
(2/π, the mean of a half cycle) where <gui>top</gui> reads 0.96 V (near the peak, which is
|
||||
where a sine dwells most), and on a band-limited square <gui>above 50%</gui> reads 0.99 V
|
||||
where <gui>top</gui> reads the plateau, 1.00 V: the edge's ring is inside the first and not
|
||||
inside the second. <gui>between</gui> takes <em>two</em> shares — <gui>from %</gui> and
|
||||
<gui>to %</gui> — and reports the mean of the samples inside that band, which no single
|
||||
split can answer; the order you type them in does not matter, since a band between 80 % and
|
||||
20 % is the band between 20 % and 80 %.</p></item>
|
||||
<item><p>A mean over a side carries whatever is on that side, so these three are the right
|
||||
numbers when you want the <em>energy</em> or the <em>offset</em> of part of the waveform,
|
||||
and the wrong ones when you want the level a plateau sits at. Like every other measurement,
|
||||
one with nothing to measure reads <code>--</code> rather than a made-up number: a share at
|
||||
100 % has no sample above it, and a band can hold none at all — on an ideal
|
||||
(non-ringing) square every sample is exactly ±1, so a band between 50 % and 75 % is
|
||||
empty.</p></item>
|
||||
<item><p><gui>top</gui> and <gui>bottom</gui> also read <em>near the peak</em> on a
|
||||
waveform that has no flat part, which is the honest reading rather than a failure: a unit
|
||||
sine has no plateau, so the densest band is at its peak and these read about ±0.96 — not
|
||||
@@ -95,7 +113,9 @@
|
||||
not the raw window: a bucket's sum is a sum, so the mean, the extremes and a level's own
|
||||
mean are exact. <gui>top</gui>, <gui>bottom</gui> and <gui>amplitude</gui> read the
|
||||
<em>density</em> of the window out of that same summary — which is what tells a flat level
|
||||
from a ripple — so those three cost a second read of the summary and never of a sample.</p></item>
|
||||
from a ripple — so those three cost a second read of the summary and never of a sample, and
|
||||
<gui>between</gui>'s second share is one more level in the same summary walk rather than a
|
||||
walk of its own.</p></item>
|
||||
<item><p><gui>Statistics</gui> adds a second line to every measurement showing
|
||||
<code>n</code>, the mean, the standard deviation and the range. Each observation is one
|
||||
<em>recompute</em> of the pane, so <code>n</code> counts windows measured, not samples —
|
||||
@@ -117,6 +137,13 @@
|
||||
<code>bottom=-0.999983</code>, <code>amplitude=1.999966</code> against
|
||||
<code>peak to peak=2.358701</code> — the levels are the plateau's and the extremes are the
|
||||
edges'. Over a 2 M-sample window those passes read about 0.1% of the samples.</p>
|
||||
<p>The mean-over-a-side types on the same two windows: on the sine
|
||||
<code>above 50=0.636620</code>, <code>below 50=-0.636620</code> and
|
||||
<code>between 50 75=0.255873</code>; on the rippled square <code>above 50=0.986496</code> and
|
||||
<code>between 50 75=0.302744</code>, against <code>1.000000</code> for the same
|
||||
<code>above 50</code> on an ideal (non-ringing) square. A <code>ramp</code>, whose density is
|
||||
uniform, is the shape that names what a band mean <em>is</em>: <code>above 50=0.500000</code>
|
||||
and <code>between 50 75=0.250000</code>, each band's own centre.</p>
|
||||
</section>
|
||||
|
||||
</page>
|
||||
+96
-6
@@ -203,6 +203,7 @@ void Acquisition::on_config(const udps_signal_t* signals, std::uint32_t count,
|
||||
// next session is the failure mode worth avoiding in a feature whose whole job is diagnosis.
|
||||
warned_batch_ = false;
|
||||
warned_outage_ = false;
|
||||
warned_phantom_gap_ = false;
|
||||
warned_bridge_ = false;
|
||||
warned_time_source_ = false;
|
||||
// The clock choice is per connection too: a reconnect re-derives it from the frames
|
||||
@@ -243,9 +244,61 @@ void Acquisition::reset_session_state() noexcept {
|
||||
have_arrival_interval_ = false;
|
||||
arrival_interval_ = 0.0;
|
||||
arrival_prev_ = 0.0;
|
||||
have_prev_frame_recv_ = false;
|
||||
prev_frame_recv_ = 0.0;
|
||||
}
|
||||
|
||||
std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_signal) {
|
||||
void Acquisition::note_frame_arrival(double recv_time) noexcept {
|
||||
const bool first = !have_prev_frame_recv_;
|
||||
const double elapsed = first ? 0.0 : recv_time - prev_frame_recv_;
|
||||
prev_frame_recv_ = recv_time;
|
||||
have_prev_frame_recv_ = true;
|
||||
// The arrival *cadence* belongs to the arrival clock, and `build_stamps()`'s arrival branch keeps
|
||||
// it whenever that clock is the one in play (`time_source_ == 2`, decided once per connection and
|
||||
// sticky). Every other case needs it here: relative mode returns before that branch, and a stream
|
||||
// whose declared time signal checks out never reaches it either -- while the reservation test
|
||||
// below weighs the counter's claim against a measured cadence whatever clock the display uses,
|
||||
// because a counter that does not count packets lies in all of them. The two writers are therefore
|
||||
// exclusive by `time_source_`, and this is the branch's own update (0.8/0.2) so the estimate does
|
||||
// not jump at the one frame where the clock changes hands.
|
||||
if (first || time_source_ == 2) return;
|
||||
if (elapsed > 0.0 && std::isfinite(elapsed)) {
|
||||
arrival_interval_ =
|
||||
have_arrival_interval_ ? 0.8 * arrival_interval_ + 0.2 * elapsed : elapsed;
|
||||
have_arrival_interval_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Acquisition::gap_plausible(std::uint32_t missing, double elapsed, const Partition& part,
|
||||
u64 g) const {
|
||||
if (missing == 0) return true;
|
||||
// The ring cannot hold a hole bigger than itself, and this test needs no measurement: the
|
||||
// invariant's own rule is that the reservation's *sample* cost -- `missing * batch_`, not the
|
||||
// packet cap -- is what has to be weighed against the ring. A hole that laps the ring erases the
|
||||
// history it is meant to annotate, can never be backfilled (its slots are already overwritten),
|
||||
// and is never forgotten (`forget_holes_before()` waits for slots the ring has passed), so it
|
||||
// leaves a band over a buffer that is all NaN.
|
||||
if (g == 0 || part.time_capacity == 0) return false;
|
||||
if (u64(missing) * u64(batch_) / g >= part.time_capacity) return false;
|
||||
// And the counter has to agree with the wire about how long the loss took. The counter is a claim
|
||||
// about the stream; the arrival clock is a *measurement* of it: `missing` packets would have taken
|
||||
// `missing` frame intervals to arrive, and the frame before this one is the witness of how long
|
||||
// actually passed. A device whose counter counts samples, or frames of an inner layer, reports a
|
||||
// forward jump on *every* datagram while the wire delivers on time -- and believing that booked a
|
||||
// NaN hole per packet, i.e. a band over the whole window filled with samples that were never
|
||||
// missing, which no backfill can repair because no packet was ever lost. The factor is a half and
|
||||
// not one: the socket clock jitters and a drain can compress two frames' arrival times, so only a
|
||||
// claim the wire flatly contradicts (half the time it needs, or less) is refused. A refused gap
|
||||
// costs a shading, never data.
|
||||
if (have_arrival_interval_ && arrival_interval_ > 0.0 && elapsed > 0.0 &&
|
||||
elapsed < 0.5 * double(missing) * arrival_interval_) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_signal,
|
||||
bool counter_gap_refused) {
|
||||
// The frame's span in base ticks: a scalar signal carries `num_samples` values
|
||||
// (one per batched cycle), a block signal (rows*cols > 1) carries its whole
|
||||
// element block. The largest of those is how many base ticks this frame spans --
|
||||
@@ -294,8 +347,11 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
|
||||
if (!(dt > 0.0)) dt = 1.0e-3;
|
||||
// The reserved hole for the missing packets advances `slot_base_` so this frame's
|
||||
// own stamps land *after* the hole rather than on top of it. The same conditions
|
||||
// the reservation call uses: not late, gap present, batch known, within the cap.
|
||||
if (!frame->late && frame->lost > 0 && batch_ > 0 &&
|
||||
// the reservation call uses: not late, gap present, batch known, within the cap --
|
||||
// and not one the arrival clock or the ring refused, because the advance and the
|
||||
// reservation are the *same* claim about the wire: believing a phantom gap here
|
||||
// would stretch the timeline by exactly the loss that did not happen.
|
||||
if (!frame->late && frame->lost > 0 && batch_ > 0 && !counter_gap_refused &&
|
||||
double(frame->lost) <= kGlitchPackets) {
|
||||
slot_base_ += u64(frame->lost) * batch_;
|
||||
}
|
||||
@@ -615,7 +671,20 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::uint32_t n = build_stamps(frame, time_signal);
|
||||
// The wire's own cadence, and the verdict on the counter's claim, both taken *before* the
|
||||
// stamps: relative mode's `slot_base_` advance and the reservation below are the same claim
|
||||
// about the wire (see `build_stamps`), so they have to agree about it. The verdict is taken
|
||||
// before the cadence is updated, too: folding this frame's own arrival into the estimate would
|
||||
// let one burst raise the very number that is supposed to corroborate it -- a 200-packet outage
|
||||
// smooths the interval to 40 ms and then refuses itself.
|
||||
const bool first_frame = !have_prev_frame_recv_;
|
||||
const double since_prev = first_frame ? 0.0 : frame->recv_time - prev_frame_recv_;
|
||||
const std::uint32_t missing = frame->lost;
|
||||
const bool gap_refused =
|
||||
missing > 0 && !gap_plausible(missing, since_prev, part, part.time_stride);
|
||||
note_frame_arrival(frame->recv_time);
|
||||
|
||||
const std::uint32_t n = build_stamps(frame, time_signal, gap_refused);
|
||||
if (n == 0) return;
|
||||
|
||||
if (model_.epoch() == 0.0) {
|
||||
@@ -695,8 +764,29 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
||||
// through the same decimation accumulators (value_acc_, value_acc_n_, math_sample_),
|
||||
// which must already be sized to the current channel count and stride before any
|
||||
// per-sample write.
|
||||
const std::uint32_t missing = frame->lost;
|
||||
if (missing > 0 && batch_ > 0 && double(missing) <= kGlitchPackets) {
|
||||
if (missing > 0 && gap_refused) {
|
||||
// A claim the wire contradicted, or a hole the ring cannot hold: framing it as NaN is what
|
||||
// turns one phantom gap into a band over the whole window (and, at a batch big enough, into
|
||||
// minutes of the producer's time under this lock). The samples are kept, no hole is booked,
|
||||
// and the timeline above was not advanced either -- so the picture shows the data and the gap
|
||||
// shows as the x discontinuity it is. Said once per connection with the numbers, because it is
|
||||
// otherwise invisible: the status bar's `gaps` climbs either way.
|
||||
model_.stats().gaps_refused.fetch_add(1, std::memory_order_relaxed);
|
||||
if (!warned_phantom_gap_) {
|
||||
warned_phantom_gap_ = true;
|
||||
std::fprintf(stderr,
|
||||
"[udpscope] warning: the counter reports %u packet(s) missing (%llu samples, "
|
||||
"%.3f s at the measured cadence of %.6f s/frame) but only %.3f s passed since "
|
||||
"the previous frame, or the hole is larger than the ring (%llu slots); "
|
||||
"refusing the reservation and keeping the samples. A counter that does not "
|
||||
"count packets -- samples, or frames of an inner layer -- reports a jump on "
|
||||
"every datagram and reads exactly like this.\n",
|
||||
missing, static_cast<unsigned long long>(std::uint64_t(missing) * batch_),
|
||||
double(missing) * arrival_interval_, arrival_interval_, since_prev,
|
||||
static_cast<unsigned long long>(u64(missing) * u64(batch_) / part.time_stride));
|
||||
std::fflush(stderr);
|
||||
}
|
||||
} else if (missing > 0 && batch_ > 0 && double(missing) <= kGlitchPackets) {
|
||||
reserve_hole(missing, part, channels, store, g);
|
||||
} else if (missing > 0 && !warned_outage_) {
|
||||
warned_outage_ = true;
|
||||
|
||||
+25
-2
@@ -77,7 +77,8 @@ class Acquisition {
|
||||
/// Fills `stamps_` with this frame's base-tick timestamps, in stream seconds,
|
||||
/// and returns how many were written (`frame->num_samples` unless the frame is
|
||||
/// malformed).
|
||||
std::uint32_t build_stamps(const udps_frame_t* frame, int time_signal);
|
||||
std::uint32_t build_stamps(const udps_frame_t* frame, int time_signal,
|
||||
bool counter_gap_refused);
|
||||
|
||||
/// Pushes NaN for the `missing` packets' worth of samples that were never delivered,
|
||||
/// advances the decimation accumulators and tick_, records the hole in the model, and
|
||||
@@ -92,6 +93,15 @@ class Acquisition {
|
||||
void backfill_from(const udps_frame_t* frame, const Partition& part,
|
||||
const std::vector<ChannelInfo>& channels, BufferStore& store, u64 g);
|
||||
|
||||
/// Keeps the wire's own cadence up to date from the arrival times, in the modes where
|
||||
/// `build_stamps()`'s arrival branch does not (see the definition).
|
||||
void note_frame_arrival(double recv_time) noexcept;
|
||||
|
||||
/// Whether the counter's claim of `missing` lost packets can be believed *and* booked: the
|
||||
/// arrival clock has to corroborate it and the ring has to be able to hold it. See the
|
||||
/// definition for both tests and for what believing a claim that fails them looks like.
|
||||
bool gap_plausible(std::uint32_t missing, double elapsed, const Partition& part, u64 g) const;
|
||||
|
||||
ScopeModel& model_;
|
||||
Config cfg_;
|
||||
udps_client* client_ = nullptr;
|
||||
@@ -125,10 +135,20 @@ class Acquisition {
|
||||
double arrival_epoch_ = 0.0;
|
||||
bool have_arrival_epoch_ = false;
|
||||
/// Smoothed interval between two frames' arrivals, and the previous arrival: the
|
||||
/// spine of the arrival-anchored clock (see `build_stamps`).
|
||||
/// spine of the arrival-anchored clock (see `build_stamps`). In relative mode no frame
|
||||
/// reaches that branch, so `on_data` keeps the same pair up to date itself -- the
|
||||
/// reservation's plausibility test below needs a measured cadence whatever clock the
|
||||
/// user chose, and the two writers are mutually exclusive by mode.
|
||||
double arrival_interval_ = 0.0;
|
||||
double arrival_prev_ = 0.0;
|
||||
bool have_arrival_interval_ = false;
|
||||
/// The previous *accepted* frame's receive time, for the one measurement the reservation
|
||||
/// test needs: how much wall time actually passed. Kept apart from `arrival_prev_` on
|
||||
/// purpose -- that one is the arrival clock's own bookkeeping and is re-anchored by the
|
||||
/// clock-choice code, while this is a bare witness of the wire and must never be moved by
|
||||
/// a mode change.
|
||||
double prev_frame_recv_ = 0.0;
|
||||
bool have_prev_frame_recv_ = false;
|
||||
/// The stream's time source, chosen once and sticky: 0 undecided, 1 the declared
|
||||
/// time signal, 2 arrival. Re-evaluated only on a CONFIG. A per-frame choice
|
||||
/// interleaved two timelines in one ring -- some frames stamped from a declared
|
||||
@@ -158,6 +178,9 @@ class Acquisition {
|
||||
/// Warn-once latch for a loss past the reservation cap. A member, not a function-local
|
||||
/// `static`, matching the latch Task 2 added for the same reason.
|
||||
bool warned_outage_ = false;
|
||||
/// Warn-once latch for a reservation the arrival clock did not corroborate, or that the ring
|
||||
/// could not hold (see `gap_plausible()`). Once per connection, like the two above.
|
||||
bool warned_phantom_gap_ = false;
|
||||
/// Warn-once latch for the one bridged packet whose time signal glitched.
|
||||
bool warned_bridge_ = false;
|
||||
/// Warn-once latch for the declared time signal failing for good, so the arrival clock is
|
||||
|
||||
@@ -612,6 +612,14 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
m.options.threshold_pct = std::stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
} else if (key == "threshold2") {
|
||||
// The band's other edge. A file written before `Between` existed has no such key, and the
|
||||
// struct's own default is what such a measurement then reads -- which is the same value the
|
||||
// dialog offers a new one.
|
||||
try {
|
||||
m.options.threshold_pct2 = std::stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
} else if (key == "noise") {
|
||||
try {
|
||||
m.options.noise_pct = std::stod(value);
|
||||
@@ -752,6 +760,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
}
|
||||
o << "\n[[measure]]\nsignal = " << toml_escape(m.signal) << "\ntype = " << kind
|
||||
<< "\nthreshold = " << m.options.threshold_pct
|
||||
<< "\nthreshold2 = " << m.options.threshold_pct2
|
||||
<< "\nabove = " << (m.options.above ? "true" : "false") << "\nshow = "
|
||||
<< (m.options.show_on_plot ? "true" : "false") << "\nnoise = " << m.options.noise_pct
|
||||
<< "\nhf = " << m.options.hf_fc << "\n";
|
||||
|
||||
+92
-28
@@ -342,21 +342,36 @@ MeasureFlatLevels measure_flat_levels(const MeasureStats& b, double pct) {
|
||||
return out;
|
||||
}
|
||||
|
||||
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct) {
|
||||
int measure_level_pcts(MeasureType type, const MeasureOptions& options, double pct[2]) {
|
||||
const auto share = [](double v) { return std::clamp(v, 0.0, 100.0) / 100.0; };
|
||||
switch (type) {
|
||||
case MeasureType::Top:
|
||||
case MeasureType::Bottom:
|
||||
pct = std::clamp(options.threshold_pct, 0.0, 100.0) / 100.0;
|
||||
return true;
|
||||
case MeasureType::Above:
|
||||
case MeasureType::Below:
|
||||
pct[0] = share(options.threshold_pct);
|
||||
return 1;
|
||||
case MeasureType::Between: {
|
||||
// The band's two edges, clamped and *ordered*: the shares name a lower and an upper edge, so
|
||||
// "between 80 % and 20 %" is the band between 20 % and 80 %. Normalizing rather than refusing
|
||||
// is the honest reading of a band -- the set of samples does not depend on which edge the user
|
||||
// typed first -- and the two accumulators the pair is answered from are keyed on these ordered
|
||||
// values, so the same band is one walk however it was written.
|
||||
const double a = share(options.threshold_pct);
|
||||
const double b = share(options.threshold_pct2);
|
||||
pct[0] = std::min(a, b);
|
||||
pct[1] = std::max(a, b);
|
||||
return 2;
|
||||
}
|
||||
case MeasureType::PulseDuration:
|
||||
case MeasureType::Period:
|
||||
case MeasureType::Frequency:
|
||||
// The *mid* level, for all three. No option here on purpose: a period is the spacing of
|
||||
// one crossing per cycle, and a threshold near either extreme counts noise as crossings.
|
||||
pct = 0.5;
|
||||
return true;
|
||||
pct[0] = 0.5;
|
||||
return 1;
|
||||
default:
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,11 +388,14 @@ const char* measure_type_name(MeasureType type) {
|
||||
case MeasureType::PulseDuration: return "pulse duration";
|
||||
case MeasureType::Period: return "period";
|
||||
case MeasureType::Frequency: return "frequency";
|
||||
case MeasureType::Above: return "above";
|
||||
case MeasureType::Below: return "below";
|
||||
case MeasureType::Between: return "between";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
int measure_type_count() { return 10; }
|
||||
int measure_type_count() { return 13; }
|
||||
|
||||
MeasureType measure_type_at(int index) {
|
||||
switch (index) {
|
||||
@@ -390,15 +408,24 @@ MeasureType measure_type_at(int index) {
|
||||
case 6: return MeasureType::Amplitude;
|
||||
case 7: return MeasureType::PulseDuration;
|
||||
case 8: return MeasureType::Frequency;
|
||||
default: return MeasureType::Period;
|
||||
case 9: return MeasureType::Period;
|
||||
// Appended rather than filed beside `Top`/`Bottom`, where they read best in the dropdown:
|
||||
// `layout_store` writes *this* index into the session file, so renumbering would turn a saved
|
||||
// `max` into an `above` on the next start -- silently, and only for users who had one.
|
||||
case 10: return MeasureType::Above;
|
||||
case 11: return MeasureType::Below;
|
||||
default: return MeasureType::Between;
|
||||
}
|
||||
}
|
||||
|
||||
bool measure_uses_threshold(MeasureType type) {
|
||||
return type == MeasureType::Top || type == MeasureType::Bottom ||
|
||||
type == MeasureType::PulseDuration;
|
||||
type == MeasureType::PulseDuration || type == MeasureType::Above ||
|
||||
type == MeasureType::Below || type == MeasureType::Between;
|
||||
}
|
||||
|
||||
bool measure_uses_band(MeasureType type) { return type == MeasureType::Between; }
|
||||
|
||||
bool measure_uses_side(MeasureType type) { return type == MeasureType::PulseDuration; }
|
||||
|
||||
bool measure_uses_noise(MeasureType type) {
|
||||
@@ -408,8 +435,8 @@ bool measure_uses_noise(MeasureType type) {
|
||||
|
||||
bool measure_can_show_on_plot(MeasureType) { return true; }
|
||||
|
||||
MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level, MeasureType type,
|
||||
const MeasureOptions& options) {
|
||||
MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* const levels[2],
|
||||
MeasureType type, const MeasureOptions& options) {
|
||||
MeasureResult out;
|
||||
if (!b.valid) return out;
|
||||
|
||||
@@ -475,10 +502,10 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
||||
// is the value. The band is a *value* band, so the reading does not follow the window's
|
||||
// alignment, and it excludes both a plateau's ripple and an edge's overshoot -- which is the
|
||||
// whole point, because those are exactly what a mean over a side reports instead.
|
||||
double pct = 0.0;
|
||||
if (!measure_level_pct(type, options, pct)) return out;
|
||||
double pct[2] = {0.0, 0.0};
|
||||
if (measure_level_pcts(type, options, pct) == 0) return out;
|
||||
const bool want_top = type == MeasureType::Top;
|
||||
const MeasureFlatLevels flat = measure_flat_levels(b, pct);
|
||||
const MeasureFlatLevels flat = measure_flat_levels(b, pct[0]);
|
||||
if (want_top ? flat.top_ok : flat.bottom_ok) {
|
||||
out.primary = want_top ? flat.top : flat.bottom;
|
||||
out.marks.push_back({true, out.primary});
|
||||
@@ -487,11 +514,47 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
||||
}
|
||||
// No histogram: the mean of one side of the level, straight out of the second pass' running
|
||||
// sums. A window the pyramid cannot summarize is one bucket or two of a zoomed-in view, or
|
||||
// an HF-rejected walk, and on those the split's own means are the honest reading.
|
||||
if (level == nullptr) return out;
|
||||
// an HF-rejected walk, and on those the split's own means are the honest reading -- and they
|
||||
// are what `Above`/`Below` report on *every* window.
|
||||
const MeasureLevel* lv = levels != nullptr ? levels[0] : nullptr;
|
||||
const std::size_t s = want_top ? 1 : 0;
|
||||
if (level->count[s] == 0) return out;
|
||||
out.primary = level->sum[s] / static_cast<double>(level->count[s]);
|
||||
if (lv == nullptr || lv->count[s] == 0) return out;
|
||||
out.primary = lv->sum[s] / static_cast<double>(lv->count[s]);
|
||||
out.marks.push_back({true, out.primary});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
case MeasureType::Above:
|
||||
case MeasureType::Below: {
|
||||
// The plain mean of the samples on one side of the share -- what `Top`/`Bottom` reported
|
||||
// before they became flat levels, and a different question from them: a mean over a side
|
||||
// carries the ripple and the edge's overshoot (0.986496 on the rippled square where the
|
||||
// plateau is 1), which is exactly why the flat-level types exist beside these.
|
||||
const MeasureLevel* lv = levels != nullptr ? levels[0] : nullptr;
|
||||
const std::size_t s = type == MeasureType::Above ? 1 : 0;
|
||||
if (lv == nullptr || lv->count[s] == 0) return out;
|
||||
out.primary = lv->sum[s] / static_cast<double>(lv->count[s]);
|
||||
out.marks.push_back({true, out.primary});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
case MeasureType::Between: {
|
||||
// The band's interior: what lies above the lower edge and *not* above the upper one. Both
|
||||
// accumulators split the same samples the same way -- pass two walks one bucket list and one
|
||||
// filter for the whole window -- so the band is the *difference* of the two upper sides, and
|
||||
// it is exact rather than approximate. It needs no third walk of its own, and the bucket
|
||||
// fast path serves it unchanged: a bucket wholly inside the band is strictly above the lower
|
||||
// level and strictly below the upper, so both accumulators fold it whole.
|
||||
const MeasureLevel* lower = levels != nullptr ? levels[0] : nullptr;
|
||||
const MeasureLevel* upper = levels != nullptr ? levels[1] : nullptr;
|
||||
if (lower == nullptr || upper == nullptr) return out;
|
||||
// Equal counts mean an empty interior: nothing lies in the band, so there is nothing to
|
||||
// average -- the pane reads `--`, as it does for any other type with no samples on the side
|
||||
// it was asked about. `lower` may not count *fewer*, which would say the two accumulators
|
||||
// saw different samples.
|
||||
if (lower->count[1] <= upper->count[1]) return out;
|
||||
out.primary = (lower->sum[1] - upper->sum[1]) /
|
||||
static_cast<double>(lower->count[1] - upper->count[1]);
|
||||
out.marks.push_back({true, out.primary});
|
||||
out.valid = true;
|
||||
return out;
|
||||
@@ -499,18 +562,19 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
||||
case MeasureType::PulseDuration:
|
||||
case MeasureType::Period:
|
||||
case MeasureType::Frequency: {
|
||||
if (level == nullptr) return out;
|
||||
if (levels == nullptr || levels[0] == nullptr) return out;
|
||||
const MeasureLevel& level = *levels[0];
|
||||
// The pulse is one side of the mid level: its widths run from the edge that enters it to
|
||||
// the edge that leaves it, and a period is the spacing of the edges that *enter* it -- one
|
||||
// per cycle, whatever else the waveform does in between.
|
||||
const std::size_t s = options.above ? 1 : 0;
|
||||
const double enter_span = level->last_enter[s] - level->first_enter[s];
|
||||
const double enter_period = level->enters[s] >= 2
|
||||
? enter_span / static_cast<double>(level->enters[s] - 1)
|
||||
const double enter_span = level.last_enter[s] - level.first_enter[s];
|
||||
const double enter_period = level.enters[s] >= 2
|
||||
? enter_span / static_cast<double>(level.enters[s] - 1)
|
||||
: 0.0;
|
||||
if (type == MeasureType::PulseDuration) {
|
||||
if (level->widths[s] == 0) return out;
|
||||
out.primary = level->width_sum[s] / static_cast<double>(level->widths[s]);
|
||||
if (level.widths[s] == 0) return out;
|
||||
out.primary = level.width_sum[s] / static_cast<double>(level.widths[s]);
|
||||
// Duty as width/period, not as pulse-time over the window: a window that starts or
|
||||
// ends mid-pulse holds partial cycles, and counting only the complete ones against
|
||||
// the whole span would report a duty that depends on where the window happened to
|
||||
@@ -518,15 +582,15 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
||||
// them -- a window holding one pulse reports the width alone.
|
||||
out.secondary = enter_period > 0.0 ? out.primary / enter_period : 0.0;
|
||||
out.has_secondary = enter_period > 0.0;
|
||||
out.marks.push_back({false, level->first_enter[s]});
|
||||
out.marks.push_back({false, level->first_enter[s] + out.primary});
|
||||
out.marks.push_back({false, level.first_enter[s]});
|
||||
out.marks.push_back({false, level.first_enter[s] + out.primary});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
if (!(enter_period > 0.0)) return out;
|
||||
out.primary = type == MeasureType::Period ? enter_period : 1.0 / enter_period;
|
||||
out.marks.push_back({false, level->first_enter[s]});
|
||||
out.marks.push_back({false, level->first_enter[s] + enter_period});
|
||||
out.marks.push_back({false, level.first_enter[s]});
|
||||
out.marks.push_back({false, level.first_enter[s] + enter_period});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
+51
-18
@@ -38,16 +38,37 @@ enum class MeasureType {
|
||||
PulseDuration, ///< how long the signal spends on one side of the threshold
|
||||
Period, ///< the time between two successive crossings of the threshold
|
||||
Frequency, ///< 1 / period
|
||||
/// The mean of the samples **above** `threshold_pct` -- the split's own upper mean, which is
|
||||
/// what `Top` reported before a flat level replaced it. `Top`/`Bottom` are the level the signal
|
||||
/// *dwells at* (a density question, so a ringing edge does not drag them); these two are the
|
||||
/// plain mean of the samples on one side of the share, ripple and overshoot included, which is
|
||||
/// a *different* question about the same split and the reason the two coexist: on a unit sine
|
||||
/// this reads `0.636620` (2/pi) where `Top` reads `0.957988`, and on the mock's band-limited
|
||||
/// square `0.986496` where its plateau is 1.
|
||||
Above,
|
||||
/// The same from the split's lower side.
|
||||
Below,
|
||||
/// The mean of the samples **between** `threshold_pct` and `threshold_pct2`: the band's
|
||||
/// interior, which neither of the two means above can answer (one of them gives the two sides
|
||||
/// and the other the whole window, because the band's two edges are not one split).
|
||||
Between,
|
||||
};
|
||||
|
||||
/// Type-specific behaviour. Every field is meaningful only for the types named below; the
|
||||
/// others ignore it (an option that does nothing for a type is left out of the dialog
|
||||
/// rather than stored and ignored).
|
||||
struct MeasureOptions {
|
||||
/// `Top`/`Bottom`/`PulseDuration`/`Period`/`Frequency`: where the split is, as a share of
|
||||
/// `Top`/`Bottom`/`PulseDuration`/`Above`/`Below`/`Between`: where the split is, as a share of
|
||||
/// the window's own min..max. The middle by default, which is what a symmetric waveform
|
||||
/// wants; a 3.3 V logic signal with ringing wants it nearer the top.
|
||||
/// wants; a 3.3 V logic signal with ringing wants it nearer the top. For `Above`/`Below` it is
|
||||
/// the level the mean is taken on one side of; for `Between` it is the band's *lower* edge.
|
||||
double threshold_pct = 50.0;
|
||||
/// `Between`: the band's other edge, the same unit. The two are read through
|
||||
/// `measure_level_pcts()`, which clamps both and puts the pair in order, so the band between
|
||||
/// 80 % and 20 % is the band between 20 % and 80 % -- the shares name two *edges* and the order
|
||||
/// they were typed in carries no meaning, which is why a reversed pair is normalized rather
|
||||
/// than refused.
|
||||
double threshold_pct2 = 75.0;
|
||||
/// `PulseDuration`: which side of the threshold is "the pulse". `Period`/`Frequency`
|
||||
/// use it as the edge direction, so one cycle is counted once.
|
||||
bool above = true;
|
||||
@@ -268,11 +289,13 @@ struct MeasureLevel {
|
||||
/// fact rather than a threshold decision. `measure_set_level()` is what sizes it; zero reduces
|
||||
/// the walk exactly to a single level.
|
||||
double hysteresis = 0.0;
|
||||
/// The samples either side of `level`, exactly: `Top`/`Bottom`'s reading on a window with no
|
||||
/// bucket histogram to read a flat level from (shorter than one bucket, or an HF-rejected
|
||||
/// walk). They are folded on every window -- pass two walks it for the crossing types anyway
|
||||
/// -- but a window that has a histogram is read from that instead, because a mean over a side
|
||||
/// reports the ripple and the overshoot rather than the level the signal dwells at.
|
||||
/// The samples either side of `level`, exactly: the reading of `Above`/`Below` is one of these
|
||||
/// two means, `Between`'s is the difference of two *levels'* upper sides, and `Top`/`Bottom` fall
|
||||
/// back to them on a window with no bucket histogram to read a flat level from (shorter than one
|
||||
/// bucket, or an HF-rejected walk). They are folded on every window -- pass two walks it for the
|
||||
/// crossing types anyway -- but a window that has a histogram is read from that instead, because
|
||||
/// a mean over a side reports the ripple and the overshoot rather than the level the signal
|
||||
/// dwells at.
|
||||
/// Index 0 is the side below the level (a sample exactly *at* it counts here), index 1 the
|
||||
/// side above it.
|
||||
double sum[2] = {0.0, 0.0};
|
||||
@@ -326,13 +349,18 @@ bool measure_accumulate_bucket(const MeasureBucket& bucket,
|
||||
const std::function<double(std::size_t)>& time_of,
|
||||
MeasureLevel& level);
|
||||
|
||||
/// The level a type reads, as a share of the window's own min..max, or false when it reads none
|
||||
/// (the pure statistics, answered from `MeasureStats` alone). `Top`/`Bottom` read the
|
||||
/// measurement's own threshold; the crossing types read the mid level, deliberately -- see
|
||||
/// `measure_uses_threshold()`. `Amplitude` reads no level either: its value is the delta between
|
||||
/// the two flat levels, which the histogram answers, and the split's two means are only a
|
||||
/// fallback it takes from the window's own extremes -- see `compute_measure()`.
|
||||
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct);
|
||||
/// The levels a type reads, as shares of the window's own min..max, **lower first**, or 0 when it
|
||||
/// reads none (the pure statistics, answered from `MeasureStats` alone). `pct` must hold two: a
|
||||
/// level type fills `pct[0]` and answers 1, `Between` fills both and answers 2.
|
||||
///
|
||||
/// `Top`/`Bottom`/`Above`/`Below` read the measurement's own threshold; the crossing types read the
|
||||
/// mid level, deliberately -- see `measure_uses_threshold()`. `Amplitude` reads no level either: its
|
||||
/// value is the delta between the two flat levels, which the histogram answers, and the split's two
|
||||
/// means are only a fallback it takes from the window's own extremes -- see `compute_measure()`.
|
||||
///
|
||||
/// This is the one place `Between`'s pair is clamped and ordered, so the pane's dedup key, the
|
||||
/// engine's two accumulators and the dialog all describe the same band whatever was typed.
|
||||
int measure_level_pcts(MeasureType type, const MeasureOptions& options, double pct[2]);
|
||||
|
||||
/// The window's two flat levels, from the bucket histogram in `stats.flat`. `top` is the densest
|
||||
/// band of the bins *above* `lo + pct * (hi - lo)`, `bottom` the same below it -- for a
|
||||
@@ -352,10 +380,11 @@ struct MeasureFlatLevels {
|
||||
};
|
||||
MeasureFlatLevels measure_flat_levels(const MeasureStats& stats, double pct);
|
||||
|
||||
/// The engine: one measurement's answer, O(1), from the two passes' results. `level` is the
|
||||
/// accumulator for `measure_level_pct(type, options)` and may be null for the types that read
|
||||
/// none. The function reads nothing else, which is what keeps the answer independent of the plot.
|
||||
MeasureResult compute_measure(const MeasureStats& stats, const MeasureLevel* level,
|
||||
/// The engine: one measurement's answer, O(1), from the two passes' results. `levels` is the
|
||||
/// accumulators `measure_level_pcts(type, options)` asked for, in the same order -- `nullptr` for
|
||||
/// the types that read none, and either entry may be null when the caller could not resolve it.
|
||||
/// The function reads nothing else, which is what keeps the answer independent of the plot.
|
||||
MeasureResult compute_measure(const MeasureStats& stats, const MeasureLevel* const levels[2],
|
||||
MeasureType type, const MeasureOptions& options);
|
||||
|
||||
/// A measurement type's name, as the pane's row and the dialog's dropdown spell it.
|
||||
@@ -368,6 +397,10 @@ MeasureType measure_type_at(int index);
|
||||
/// spacing of the crossings of the *mid* level, one rising edge per cycle, and offering a
|
||||
/// threshold there only invites a setting that makes the count wrong.
|
||||
bool measure_uses_threshold(MeasureType type);
|
||||
/// Whether a type reads a *pair* of shares, so the dialog shows its second share only then: the
|
||||
/// two edges of a band are one measurement's two parameters, and one field of the two is a
|
||||
/// measurement of a different band than the user asked for.
|
||||
bool measure_uses_band(MeasureType type);
|
||||
/// Whether a type uses the `above` side (only `PulseDuration`, where it says which half of
|
||||
/// the waveform is the pulse).
|
||||
bool measure_uses_side(MeasureType type);
|
||||
|
||||
@@ -268,6 +268,15 @@ struct StreamStats {
|
||||
/// a reading of 0 beside a non-zero `holes` is exactly the honest answer until the
|
||||
/// backfill lands.
|
||||
std::atomic<std::uint64_t> backfilled{0};
|
||||
/// Reservations refused because the counter's claim was not corroborated by the arrival
|
||||
/// cadence, or because the hole it would book is bigger than the ring can hold. A counter
|
||||
/// is a claim about the wire and the arrival clock is the measurement of it: a stream whose
|
||||
/// counter does not count packets (a sample counter, say) reports a forward jump on every
|
||||
/// datagram, and believing it booked a NaN hole per packet -- a band over the whole window
|
||||
/// filled with samples that were never missing, which the backfill cannot repair because no
|
||||
/// packet was ever lost. Counted here because it is otherwise *invisible*: the status bar's
|
||||
/// `gaps` climbs either way, and only this field says the gap was refused.
|
||||
std::atomic<std::uint64_t> gaps_refused{0};
|
||||
};
|
||||
|
||||
/// What the plot is showing, which is one decision made once per frame: the bind, the
|
||||
|
||||
+24
-8
@@ -1374,12 +1374,8 @@ GtkWidget* measures_row(App& app, std::size_t index, const Measure& m) {
|
||||
gtk_widget_set_halign(kind, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(kind, true);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(kind), PANGO_ELLIPSIZE_END);
|
||||
char tip[160];
|
||||
std::snprintf(tip, sizeof(tip), "%s\n%s", m.signal.c_str(),
|
||||
measure_uses_threshold(m.type)
|
||||
? (m.options.above ? "the above-threshold side" : "the below-threshold side")
|
||||
: "");
|
||||
gtk_widget_set_tooltip_text(kind, tip);
|
||||
const std::string tip = m.signal + "\n" + measure_row_summary(m);
|
||||
gtk_widget_set_tooltip_text(kind, tip.c_str());
|
||||
gtk_box_append(GTK_BOX(box), kind);
|
||||
GtkWidget* value = gtk_label_new("--");
|
||||
gtk_widget_set_halign(value, GTK_ALIGN_END);
|
||||
@@ -1684,6 +1680,17 @@ void open_measure_dialog(App& app, int edit_index) {
|
||||
GtkWidget* threshold_row = labelled_row("threshold %", threshold, &threshold_label);
|
||||
gtk_box_append(GTK_BOX(box), threshold_row);
|
||||
|
||||
// `Between`'s second share: the dialog offers it only for the one type that reads two (see
|
||||
// `measure_uses_band()`), and the label is retitled per type with the field above it. The 75 is
|
||||
// `MeasureOptions::threshold_pct2`'s default -- a default the *reader* sees, so it is seeded here
|
||||
// as well as in the struct.
|
||||
GtkWidget* band = spin(0.0, 100.0, 1.0, 1, 75.0);
|
||||
gtk_widget_set_tooltip_text(band,
|
||||
"the band's other edge, the same share of the visible range");
|
||||
GtkWidget* band_label = nullptr;
|
||||
GtkWidget* band_row = labelled_row("to %", band, &band_label);
|
||||
gtk_box_append(GTK_BOX(box), band_row);
|
||||
|
||||
GtkWidget* above = gtk_switch_new();
|
||||
gtk_widget_set_halign(above, GTK_ALIGN_START);
|
||||
gtk_widget_set_tooltip_text(above, "which half of the waveform is the pulse");
|
||||
@@ -1714,8 +1721,9 @@ void open_measure_dialog(App& app, int edit_index) {
|
||||
gtk_box_append(GTK_BOX(box), show);
|
||||
|
||||
auto* d = new MeasureDialog{&app, edit_index, signal_drop, type_drop,
|
||||
threshold, above, show, window,
|
||||
threshold_row, threshold_label, side_row, side_label,
|
||||
threshold, band, above, show,
|
||||
window, threshold_row, threshold_label, band_row,
|
||||
band_label, side_row, side_label,
|
||||
noise, noise_row, hf, hf_row,
|
||||
sig_names};
|
||||
app.measure_dialog = window;
|
||||
@@ -1739,12 +1747,14 @@ void open_measure_dialog(App& app, int edit_index) {
|
||||
}
|
||||
}
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), editing->options.threshold_pct);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(band), editing->options.threshold_pct2);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(noise), editing->options.noise_pct);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(hf), editing->options.hf_fc);
|
||||
gtk_switch_set_active(GTK_SWITCH(above), editing->options.above);
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(show), editing->options.show_on_plot);
|
||||
} else {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), 50.0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(band), MeasureOptions{}.threshold_pct2);
|
||||
gtk_switch_set_active(GTK_SWITCH(above), true);
|
||||
}
|
||||
g_signal_connect(type_drop, "notify::selected", G_CALLBACK(on_measure_type_changed), d);
|
||||
@@ -5769,6 +5779,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), measures_probe_step, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_BANDAVG_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), bandavg_probe_step, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIT_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), fit_probe_step, &app);
|
||||
}
|
||||
@@ -6379,6 +6392,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
i, m.signal.c_str(), measure_type_name(m.type), m.options.threshold_pct,
|
||||
m.options.above ? 1 : 0, m.options.show_on_plot ? 1 : 0,
|
||||
r.valid ? "ok" : "invalid", r.valid ? r.primary : 0.0);
|
||||
// The band's other edge, only for the type that reads two shares: the reading of a `between`
|
||||
// is a function of the pair, so a line that printed one of them could not say what it is.
|
||||
if (measure_uses_band(m.type)) std::printf(" thr2=%.1f%%", m.options.threshold_pct2);
|
||||
// What the row *reads*: the formatted string with its unit (so `frequency` showing Hz
|
||||
// is asserted here, not inferred), and the row's chip count (two: the measurement's
|
||||
// colour and the signal's).
|
||||
|
||||
@@ -584,6 +584,10 @@ struct App {
|
||||
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
|
||||
GtkWidget* measure_dialog = nullptr;
|
||||
int dbg_measure_step = 0;
|
||||
/// The band-average probe's own step counter. Its own rather than the measurement pane's: the two
|
||||
/// probes add measurements, so they must not be scheduled in one run (see verification.md), and a
|
||||
/// shared counter would make that a silent mis-step rather than a documented prohibition.
|
||||
int dbg_bandavg_step = 0;
|
||||
int cursors_pane_mode = -1;
|
||||
int cursors_pane_rows = -1;
|
||||
unsigned cursors_pane_generation = ~0u;
|
||||
@@ -1174,6 +1178,9 @@ struct MeasureDialog {
|
||||
GtkWidget* signal_drop = nullptr;
|
||||
GtkWidget* type_drop = nullptr;
|
||||
GtkWidget* threshold = nullptr;
|
||||
/// `Between`'s *other* edge: the same unit as `threshold`, shown only for the one type that reads
|
||||
/// two shares (see `measure_uses_band()`).
|
||||
GtkWidget* band = nullptr;
|
||||
GtkWidget* above = nullptr;
|
||||
GtkWidget* show = nullptr;
|
||||
GtkWidget* window = nullptr;
|
||||
@@ -1182,6 +1189,8 @@ struct MeasureDialog {
|
||||
/// and the difference has to be in words, not just in visibility.
|
||||
GtkWidget* threshold_row = nullptr;
|
||||
GtkWidget* threshold_label = nullptr;
|
||||
GtkWidget* band_row = nullptr;
|
||||
GtkWidget* band_label = nullptr;
|
||||
GtkWidget* side_row = nullptr;
|
||||
GtkWidget* side_label = nullptr;
|
||||
/// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in
|
||||
@@ -1502,6 +1511,10 @@ void on_layout_drag_update(GtkGestureDrag* gesture, double offset_x, double offs
|
||||
void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
||||
void on_trigger_toggled(GtkToggleButton* button, gpointer user);
|
||||
plot::u32 measure_color(std::size_t index);
|
||||
/// One line naming what a measurement's type is measuring, with the shares it reads them at: the
|
||||
/// pane's row tooltip. The row's own label is the type alone (as for every type), so this is where
|
||||
/// a user sees the share an `above`/`below`/`between` row is averaging over.
|
||||
std::string measure_row_summary(const Measure& m);
|
||||
bool visible_samples(App& app, std::size_t channel, std::vector<double>& time, std::vector<double>& value);
|
||||
int measure_channel(App& app, const std::string& signal);
|
||||
int measure_row(App& app, const std::string& signal);
|
||||
@@ -1731,6 +1744,10 @@ void measure_dialog_click(App& a, const char* label);
|
||||
void measure_dialog_report(App& a, const char* what);
|
||||
void measures_probe_next(App& a, int step, guint delay_ms);
|
||||
gboolean measures_probe_step(gpointer user);
|
||||
void bandavg_probe_next(App& a, int step, guint delay_ms);
|
||||
void bandavg_report(App& a, const char* what);
|
||||
int bandavg_index(App& a, MeasureType type, const char* signal);
|
||||
gboolean bandavg_probe_step(gpointer user);
|
||||
gboolean zoomkeys_probe(gpointer user);
|
||||
void menus_probe_next(App& a, int step, guint delay_ms);
|
||||
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
||||
|
||||
+88
-14
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
@@ -21,6 +22,35 @@ plot::u32 measure_color(std::size_t index) {
|
||||
return kPalette[index % (sizeof(kPalette) / sizeof(kPalette[0]))];
|
||||
}
|
||||
|
||||
/// One line naming what a measurement's type measures, with the shares it reads them at. The pane's
|
||||
/// row shows the type alone, as it does for every type, so this is the only place a reader sees
|
||||
/// *which* side of *which* split an `above`/`below` is averaging over, or how wide a `between`
|
||||
/// band is -- and the reading of those three is entirely a function of those two numbers.
|
||||
std::string measure_row_summary(const Measure& m) {
|
||||
const auto pct_text = [](double v) {
|
||||
char text[32];
|
||||
std::snprintf(text, sizeof(text), "%g%% of the range", v);
|
||||
return std::string(text);
|
||||
};
|
||||
switch (m.type) {
|
||||
case MeasureType::Above:
|
||||
return "the mean of the samples above " + pct_text(m.options.threshold_pct);
|
||||
case MeasureType::Below:
|
||||
return "the mean of the samples below " + pct_text(m.options.threshold_pct);
|
||||
case MeasureType::Between:
|
||||
return "the mean of the samples between " + pct_text(m.options.threshold_pct) + " and " +
|
||||
pct_text(m.options.threshold_pct2);
|
||||
case MeasureType::Top:
|
||||
case MeasureType::Bottom:
|
||||
return "the flat level on one side of the split at " + pct_text(m.options.threshold_pct);
|
||||
case MeasureType::PulseDuration:
|
||||
return std::string(m.options.above ? "the pulse above " : "the pulse below ") +
|
||||
pct_text(m.options.threshold_pct);
|
||||
default:
|
||||
return std::string("the ") + measure_type_name(m.type) + " of this window";
|
||||
}
|
||||
}
|
||||
|
||||
/// The channel a measurement's signal resolved to, or -1 while the stream does not carry it.
|
||||
int measure_channel(App& app, const std::string& signal) {
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
@@ -94,6 +124,8 @@ void measure_dialog_sync(MeasureDialog* d) {
|
||||
const bool side = measure_uses_side(type);
|
||||
gtk_widget_set_visible(d->threshold_row, threshold);
|
||||
gtk_widget_set_visible(d->side_row, side);
|
||||
// The band's second share: a `Between` is the one type whose two edges are *both* parameters.
|
||||
gtk_widget_set_visible(d->band_row, measure_uses_band(type));
|
||||
// The band moves the edge decision, so it is shown for exactly the crossing types. The HF reject sits
|
||||
// before both walks, so it is shown for all of them.
|
||||
gtk_widget_set_visible(d->noise_row, measure_uses_noise(type));
|
||||
@@ -138,6 +170,24 @@ void measure_dialog_sync(MeasureDialog* d) {
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range (picks the side)");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the level)");
|
||||
break;
|
||||
case MeasureType::Above:
|
||||
case MeasureType::Below: {
|
||||
// These *are* a mean over one side of the split -- the reading the old `top`/`bottom` gave --
|
||||
// so the words have to say which side of which share, and they have to say it beside the
|
||||
// field rather than only in the docs: the same spin means a different side for each type.
|
||||
const bool above = type == MeasureType::Above;
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), above ? "above % of the range"
|
||||
: "below % of the range");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
|
||||
break;
|
||||
}
|
||||
case MeasureType::Between:
|
||||
// Two shares, so two rows: the band's edges, lower first (the engine orders a reversed pair
|
||||
// rather than refusing it, so either order the user types reads the same band).
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "from % of the range");
|
||||
gtk_label_set_text(GTK_LABEL(d->band_label), "to % of the range");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
|
||||
break;
|
||||
case MeasureType::PulseDuration:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
|
||||
gtk_label_set_text(GTK_LABEL(d->side_label), "pulse above the level");
|
||||
@@ -177,6 +227,7 @@ void on_measure_dialog_apply(GtkButton*, gpointer user) {
|
||||
m.signal = d->signals[sig];
|
||||
m.type = measure_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(d->type_drop)));
|
||||
m.options.threshold_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold));
|
||||
m.options.threshold_pct2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->band));
|
||||
m.options.above = gtk_switch_get_active(GTK_SWITCH(d->above));
|
||||
m.options.show_on_plot = gtk_check_button_get_active(GTK_CHECK_BUTTON(d->show));
|
||||
m.options.noise_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->noise));
|
||||
@@ -308,6 +359,10 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
std::vector<int> chan(app.measures.size(), -1);
|
||||
std::vector<int> want_window(app.measures.size(), -1);
|
||||
std::vector<int> want_level(app.measures.size(), -1);
|
||||
// `Between` reads two levels, and they are two ordinary entries of the same list: one walk answers
|
||||
// every level in the pane, so a band costs the same pass two as any other measurement plus one
|
||||
// more accumulator in it.
|
||||
std::vector<int> want_level2(app.measures.size(), -1);
|
||||
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
||||
const Measure& m = app.measures[i];
|
||||
const int channel = measure_channel(app, m.signal);
|
||||
@@ -335,20 +390,26 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
m.type == MeasureType::Amplitude) {
|
||||
w.flat_wanted = true;
|
||||
}
|
||||
double pct = 0.0;
|
||||
if (!measure_level_pct(m.type, m.options, pct)) continue;
|
||||
double pct[2] = {0.0, 0.0};
|
||||
const int level_count = measure_level_pcts(m.type, m.options, pct);
|
||||
// A type that does not read the band must not split the accumulator over it: its sums split at the bare
|
||||
// level, so a `Top` is the same walk whatever the band is. Grouping those at the default is what keeps
|
||||
// the common pane at exactly one level.
|
||||
const double noise =
|
||||
measure_uses_noise(m.type) ? m.options.noise_pct : MeasureOptions{}.noise_pct;
|
||||
std::size_t k = 0;
|
||||
while (k < w.pct.size() && (w.pct[k] != pct || w.noise[k] != noise)) ++k;
|
||||
if (k == w.pct.size()) {
|
||||
w.pct.push_back(pct);
|
||||
w.noise.push_back(noise);
|
||||
for (int j = 0; j < level_count; ++j) {
|
||||
std::size_t k = 0;
|
||||
while (k < w.pct.size() && (w.pct[k] != pct[j] || w.noise[k] != noise)) ++k;
|
||||
if (k == w.pct.size()) {
|
||||
w.pct.push_back(pct[j]);
|
||||
w.noise.push_back(noise);
|
||||
}
|
||||
if (j == 0) {
|
||||
want_level[i] = static_cast<int>(k);
|
||||
} else {
|
||||
want_level2[i] = static_cast<int>(k);
|
||||
}
|
||||
}
|
||||
want_level[i] = static_cast<int>(k);
|
||||
}
|
||||
|
||||
// The data work: two walks per channel, whatever the pane holds. The first fixes the window's
|
||||
@@ -483,11 +544,15 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
continue;
|
||||
}
|
||||
const App::MeasureWindow& w = *wp;
|
||||
const MeasureLevel* level =
|
||||
want_level[i] >= 0 && static_cast<std::size_t>(want_level[i]) < w.level.size()
|
||||
? &w.level[static_cast<std::size_t>(want_level[i])]
|
||||
: nullptr;
|
||||
const MeasureResult r = compute_measure(w.stats, level, m.type, m.options);
|
||||
// In the order `measure_level_pcts()` asked for them: the band's lower edge first.
|
||||
const MeasureLevel* levels[2] = {nullptr, nullptr};
|
||||
const int want[2] = {want_level[i], want_level2[i]};
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
if (want[j] >= 0 && static_cast<std::size_t>(want[j]) < w.level.size()) {
|
||||
levels[j] = &w.level[static_cast<std::size_t>(want[j])];
|
||||
}
|
||||
}
|
||||
const MeasureResult r = compute_measure(w.stats, levels, m.type, m.options);
|
||||
app.measure_results[i] = r;
|
||||
if (r.valid) app.measure_last_valid[i] = r;
|
||||
// Time-valued types carry seconds and a frequency carries hertz whatever the signal's
|
||||
@@ -646,11 +711,20 @@ void measure_dialog_report(App& a, const char* what) {
|
||||
if (dd != nullptr) {
|
||||
// What the dialog is offering for the type on screen: the option rows' visibility and
|
||||
// the words on them, which is the per-type behaviour a user sees.
|
||||
std::snprintf(options, sizeof(options), "type=%d thr=%d side=%d show=%d '%s'",
|
||||
std::snprintf(options, sizeof(options), "type=%d thr=%d band=%d side=%d show=%d "
|
||||
"shares=%.1f/%.1f '%s'",
|
||||
static_cast<int>(gtk_drop_down_get_selected(GTK_DROP_DOWN(dd->type_drop))),
|
||||
dd->threshold_row != nullptr && gtk_widget_get_visible(dd->threshold_row) ? 1 : 0,
|
||||
dd->band_row != nullptr && gtk_widget_get_visible(dd->band_row) ? 1 : 0,
|
||||
dd->side_row != nullptr && gtk_widget_get_visible(dd->side_row) ? 1 : 0,
|
||||
dd->show != nullptr && gtk_widget_get_visible(dd->show) ? 1 : 0,
|
||||
// What the two share fields are *showing*: the pair a `between` reads, read back
|
||||
// out of the widgets, so a field that is built but not seeded or not read back is
|
||||
// visible in the line rather than only in the number it produces.
|
||||
dd->threshold != nullptr
|
||||
? gtk_spin_button_get_value(GTK_SPIN_BUTTON(dd->threshold))
|
||||
: 0.0,
|
||||
dd->band != nullptr ? gtk_spin_button_get_value(GTK_SPIN_BUTTON(dd->band)) : 0.0,
|
||||
dd->show != nullptr && gtk_check_button_get_child(GTK_CHECK_BUTTON(dd->show)) !=
|
||||
nullptr
|
||||
? gtk_label_get_text(GTK_LABEL(gtk_check_button_get_child(
|
||||
|
||||
+223
-2
@@ -2842,6 +2842,225 @@ gboolean measures_probe_step(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// The band-average readings, on the shapes whose band means can be computed by hand.
|
||||
///
|
||||
/// `above`, `below` and `between` are plain means of the samples on one side of a share -- what
|
||||
/// `top`/`bottom` reported before a flat level replaced them -- and a mean over a side is a
|
||||
/// *different number* from the level the signal dwells at, so each reading is asserted where the
|
||||
/// shape predicts it. Seven measurements are added on the mock's three scalar signals at once,
|
||||
/// because the trio's point is that a *pair* of levels answers a band no single split can:
|
||||
/// * the 1 Hz unit sine: `above 50` = 2/pi = 0.636620 and `below 50` = -2/pi, the figures the old
|
||||
/// `top`/`bottom` documented (and *not* `top`/`bottom`'s 0.957988, the level a sine dwells at
|
||||
/// near its peak), and `between 50 75` = 3(2-sqrt 3)/pi = 0.255873 -- the mean of the samples in
|
||||
/// the band (0, 0.5], which is neither 0 (the whole window's mean) nor 0.5 (the band's edge);
|
||||
/// * the mock's band-limited `square` (30 harmonics, its default): `above 50` = 0.986496, the
|
||||
/// reading the commit that made `top` a flat level recorded as the *old* `top` -- the edges'
|
||||
/// ring is inside this mean, and the plateau the signal dwells at is not;
|
||||
/// * the mock's `ramp`, whose density is uniform: every band's mean is that band's own centre, so
|
||||
/// `above 50` = 0.5 and `between 50 75` = 0.25 of the range -- a pair of figures no other
|
||||
/// measurement type on this shape reports (`average` is 0 there, `top`/`bottom` its edges).
|
||||
/// The dialog leg then edits the sine's band through the real widgets -- the second share is the
|
||||
/// one field only `between` offers -- and re-reads the value, so a share that is built but not
|
||||
/// seeded or not read back shows up as a reading that did not move.
|
||||
gboolean bandavg_probe_step(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
switch (a->dbg_bandavg_step) {
|
||||
case 0: {
|
||||
// The three shapes are enabled *here* rather than assumed: `enable_all_probe` switches a
|
||||
// signal off two seconds after it runs, and a probe that read another probe's leftovers
|
||||
// would measure whatever was left. `set_signals_enabled()` is one structural change and a
|
||||
// no-op when the state already holds, so the usual case costs nothing and the ring keeps
|
||||
// its history; when it does rebuild, the report below is far enough out for a full window.
|
||||
std::vector<std::size_t> wanted;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
for (std::size_t s = 0; s < signals.size(); ++s) {
|
||||
if (signals[s].name == "sine" || signals[s].name == "square" ||
|
||||
signals[s].name == "ramp") {
|
||||
wanted.push_back(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
a->model->set_signals_enabled(wanted, true);
|
||||
struct Leg {
|
||||
const char* signal;
|
||||
MeasureType type;
|
||||
double lo;
|
||||
double hi;
|
||||
};
|
||||
const Leg legs[] = {
|
||||
{"sine", MeasureType::Above, 50.0, 0.0},
|
||||
{"sine", MeasureType::Below, 50.0, 0.0},
|
||||
{"sine", MeasureType::Between, 50.0, 75.0},
|
||||
{"square", MeasureType::Above, 50.0, 0.0},
|
||||
// The same band on the square: on the band-limited one it holds the two edges' transitions
|
||||
// -- a real interior whose mean is ~0, because a transition is odd about its own midpoint --
|
||||
// and on the ideal `--square-harmonics 0` step it holds *nothing at all*, because every
|
||||
// sample is exactly +/-1. That pair is the empty-interior rule from both sides.
|
||||
{"square", MeasureType::Between, 50.0, 75.0},
|
||||
{"ramp", MeasureType::Between, 50.0, 75.0},
|
||||
// A share at the top of the range has no sample above it, so this one must read what any
|
||||
// other measurement with nothing to measure reads -- `invalid`/`--`, never 0. The empty
|
||||
// *interior* is the same rule from the other side, and the ideal square below is where it
|
||||
// shows: its samples are all exactly +/-1, so the band (0, 0.5] holds none of them.
|
||||
{"ramp", MeasureType::Above, 100.0, 0.0},
|
||||
};
|
||||
for (const Leg& leg : legs) {
|
||||
Measure m;
|
||||
m.signal = leg.signal;
|
||||
m.type = leg.type;
|
||||
m.options.threshold_pct = leg.lo;
|
||||
if (leg.hi > 0.0) m.options.threshold_pct2 = leg.hi;
|
||||
// The marks are part of the reading for these types too: one ruler at the mean.
|
||||
m.options.show_on_plot = measure_can_show_on_plot(m.type);
|
||||
a->measures.push_back(std::move(m));
|
||||
}
|
||||
select_side_tab(*a, "measure");
|
||||
rebuild_measures_pane(*a);
|
||||
g_timeout_add(250, measure_trace, a);
|
||||
bandavg_probe_next(*a, 1, 1500);
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// A *full* window before any reading. The window is what the pane last described -- two seconds
|
||||
// on the documented run -- and the mock delivers about 0.5 MS/s of the 1.3 MS/s it declares, so
|
||||
// a ring the probe has just re-enabled needs seconds of wall clock to hold a whole one. A short
|
||||
// ring reads a *partial* window, which is a different number and keeps moving while it fills,
|
||||
// and the ring is capacity-bound as well, so the wait is read off the store rather than guessed:
|
||||
// `stored_slots` is how much history there is and `window / dt` is what the pane is measuring.
|
||||
double slot_dt = 0.0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
slot_dt = a->model->partition().slot_dt();
|
||||
}
|
||||
const double window_s = a->measure_window_valid
|
||||
? a->measure_window.max - a->measure_window.min
|
||||
: 0.0;
|
||||
const std::uint64_t want =
|
||||
slot_dt > 0.0 && window_s > 0.0 ? static_cast<std::uint64_t>(window_s / slot_dt) : 0;
|
||||
const std::uint64_t have = a->model->stored_slots();
|
||||
if (have < want) {
|
||||
std::fprintf(stderr, "[bandavg-probe] %-15s stored=%llu of %llu slots for a %.3f s window\n",
|
||||
"waiting", static_cast<unsigned long long>(have),
|
||||
static_cast<unsigned long long>(want), window_s);
|
||||
std::fflush(stderr);
|
||||
bandavg_probe_next(*a, 1, 500);
|
||||
break;
|
||||
}
|
||||
bandavg_report(*a, "readings");
|
||||
bandavg_probe_next(*a, 2, 400);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// The `between` on the sine, through the ordinary edit path: its dialog is the one that
|
||||
// offers the second share, and this is where the field is read back out of the widgets.
|
||||
const int at = bandavg_index(*a, MeasureType::Between, "sine");
|
||||
if (at < 0) {
|
||||
std::fprintf(stderr, "[bandavg-probe] no sine/between measurement to edit\n");
|
||||
std::fflush(stderr);
|
||||
break;
|
||||
}
|
||||
open_measure_dialog(*a, at);
|
||||
bandavg_probe_next(*a, 3, 400);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
bandavg_report(*a, "edit opened");
|
||||
auto* d = static_cast<MeasureDialog*>(
|
||||
g_object_get_data(G_OBJECT(a->measure_dialog), "udpscope-measure-ctx"));
|
||||
if (d != nullptr) {
|
||||
// The two fields as the *stored* parameters came back into them, and the band row's
|
||||
// visibility: a share that is stored but not seeded, or offered for a type that does not
|
||||
// read it, is a field the user cannot edit back to what the pane is showing.
|
||||
std::fprintf(stderr,
|
||||
"[bandavg-probe] %-15s band_row=%d shares=%.1f/%.1f (as stored)\n",
|
||||
"edit opened", gtk_widget_get_visible(d->band_row) ? 1 : 0,
|
||||
gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold)),
|
||||
gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->band)));
|
||||
// From the mid split to 90 %: a band whose mean neither of its sides reports (the readings
|
||||
// above 50 and below 90 are 0.636620 and 0.431362), so the value after the save can only
|
||||
// come from the pair the two fields now hold.
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->threshold), 50.0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->band), 90.0);
|
||||
}
|
||||
measure_dialog_click(*a, "Save");
|
||||
bandavg_probe_next(*a, 4, 500);
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
bandavg_report(*a, "after band edit");
|
||||
bandavg_probe_next(*a, 5, 300);
|
||||
break;
|
||||
case 5: {
|
||||
// The delete half of the cycle, through the dialog's own button: a measurement that carries
|
||||
// two shares is shown to survive add, edit and delete like the ones that carry none.
|
||||
const int at = bandavg_index(*a, MeasureType::Between, "sine");
|
||||
if (at >= 0) open_measure_dialog(*a, at);
|
||||
bandavg_probe_next(*a, 6, 400);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const std::size_t before = a->measures.size();
|
||||
// `measure_dialog_click()` falls through to the emit when it cannot find its button, so the
|
||||
// dialog being there is this side's to check, not the helper's.
|
||||
if (a->measure_dialog != nullptr) measure_dialog_click(*a, "Delete");
|
||||
std::fprintf(stderr, "[bandavg-probe] %-15s measures=%zu -> %zu (want one fewer)\n",
|
||||
"after delete", before, a->measures.size());
|
||||
std::fflush(stderr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void bandavg_probe_next(App& a, int step, guint delay_ms) {
|
||||
a.dbg_bandavg_step = step;
|
||||
g_timeout_add(delay_ms, bandavg_probe_step, &a);
|
||||
}
|
||||
|
||||
/// The index of one of the probe's measurements, by the pair that names it: a `between` on the sine
|
||||
/// and a `between` on the ramp are two measurements of one type.
|
||||
int bandavg_index(App& a, MeasureType type, const char* signal) {
|
||||
for (std::size_t i = 0; i < a.measures.size(); ++i) {
|
||||
if (a.measures[i].type == type && a.measures[i].signal == signal) return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// Reads the pane out: one line per measurement the probe added, with the shares it was given and
|
||||
/// the two readings a user could compare (the value and the row's own formatted text). The numbers
|
||||
/// come from a *forced* recompute over the window on screen now, so they are what the pane says at
|
||||
/// this instant rather than whatever the last moving frame left behind.
|
||||
void bandavg_report(App& a, const char* what) {
|
||||
update_measures(a, /*force=*/true, /*accumulate=*/false);
|
||||
// The window each reading was taken over, beside it: a mean over a share of a *partial* window is
|
||||
// a different number from the same share of a full one, and nothing in the value says which it
|
||||
// was. `samples` against the ring's span is what a short ring looks like from here.
|
||||
for (const App::MeasureWindow& w : a.measure_samples) {
|
||||
std::fprintf(stderr,
|
||||
"[bandavg-probe] %-15s window channel=%d lo=%.6f hi=%.6f samples=%zu levels=%zu\n",
|
||||
what, w.channel, w.stats.lo, w.stats.hi, w.stats.count, w.level.size());
|
||||
}
|
||||
for (std::size_t i = 0; i < a.measures.size(); ++i) {
|
||||
const Measure& m = a.measures[i];
|
||||
const MeasureResult& r = i < a.measure_results.size() ? a.measure_results[i] : MeasureResult{};
|
||||
char shares[64];
|
||||
if (measure_uses_band(m.type)) {
|
||||
std::snprintf(shares, sizeof(shares), "%.0f/%.0f%%", m.options.threshold_pct,
|
||||
m.options.threshold_pct2);
|
||||
} else {
|
||||
std::snprintf(shares, sizeof(shares), "%.0f%%", m.options.threshold_pct);
|
||||
}
|
||||
std::fprintf(stderr, "[bandavg-probe] %-15s %-7s '%s' shares=%-9s -> %-8s %.9g text=[%s]\n",
|
||||
what, measure_type_name(m.type), m.signal.c_str(), shares,
|
||||
r.valid ? "ok" : "invalid", r.valid ? r.primary : 0.0,
|
||||
i < a.measure_text.size() ? a.measure_text[i].c_str() : "");
|
||||
}
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
gboolean zoomkeys_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->rows->row_count() == 0 || a->rows->row(0).plot == nullptr) return G_SOURCE_REMOVE;
|
||||
@@ -3192,7 +3411,8 @@ gboolean reseq_probe(gpointer user) {
|
||||
std::fprintf(stderr,
|
||||
"[reseq-probe] reseq=%llu late=%llu stale=%llu overflows=%llu holes=%llu "
|
||||
"hole_slots=%llu "
|
||||
"backfilled=%llu open_holes=%zu bands=%zu bands_repaired=%zu nan_slots=%llu "
|
||||
"backfilled=%llu gaps_refused=%llu open_holes=%zu bands=%zu bands_repaired=%zu "
|
||||
"nan_slots=%llu "
|
||||
"scanned=%llu trigger_in_hole=%d\n",
|
||||
static_cast<unsigned long long>(m.stats().resequenced.load()),
|
||||
static_cast<unsigned long long>(m.stats().late.load()),
|
||||
@@ -3200,7 +3420,8 @@ gboolean reseq_probe(gpointer user) {
|
||||
static_cast<unsigned long long>(m.stats().overflows.load()),
|
||||
static_cast<unsigned long long>(m.stats().holes.load()),
|
||||
static_cast<unsigned long long>(m.stats().hole_slots.load()),
|
||||
static_cast<unsigned long long>(m.stats().backfilled.load()), open_holes, bands,
|
||||
static_cast<unsigned long long>(m.stats().backfilled.load()),
|
||||
static_cast<unsigned long long>(m.stats().gaps_refused.load()), open_holes, bands,
|
||||
bands_repaired, static_cast<unsigned long long>(nan_slots),
|
||||
static_cast<unsigned long long>(scanned), trigger_in_hole);
|
||||
std::fflush(stderr);
|
||||
|
||||
@@ -337,6 +337,17 @@ def main():
|
||||
"or the client reconnects and the mock restarts its clock, which is a different "
|
||||
"test",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--counter-step",
|
||||
type=int,
|
||||
default=1,
|
||||
help="advance the header counter by this many per datagram instead of one: a device "
|
||||
"whose counter is not a packet counter (it counts samples, or frames of an inner "
|
||||
"layer). Nothing is withheld -- every datagram is sent on schedule -- but every one "
|
||||
"of them reports a forward jump, which is the *phantom* gap the receiver has to "
|
||||
"refuse: 1 is a per-packet counter, 2 and up is the shape that reads as loss on an "
|
||||
"intact link",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--reorder-every",
|
||||
type=int,
|
||||
@@ -537,7 +548,7 @@ def main():
|
||||
# The counter and the stream clock advance whatever became of the datagram. That
|
||||
# is what makes a withheld packet a *lost* one and not a slower producer: nothing
|
||||
# in a payload says which packet it is, only the header's counter does.
|
||||
counter += 1
|
||||
counter += max(1, args.counter_step)
|
||||
t_stream += args.batch * pdt
|
||||
next_send += period
|
||||
if (
|
||||
|
||||
Reference in new issue
Block a user