docs: the five reseq probe cases as this build measures them, plus the overflow counter's reading
Re-measured all four recorded cases and added a fifth for the re-plan path, which needs 100 kS/s: at 10 kS/s the ring already covers 838 s, so no window the control can reach moves time_stride and the window probe re-plans nothing. Also notes that `stale` is 0 by construction while resequencing is on, and that the reservation cap is counted in packets, so its sample cost scales with the batch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d2d4b7f204
commit
210d124814
3 files changed
+111
-48
No files matched your search
@@ -115,7 +115,21 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
never amended. `neighbours()' binary search requires a monotonic array, and `window_x0_`
|
||||
must remain valid; pre-stamping the hole at the declared cadence (exact in relative mode,
|
||||
the bridge mechanism in stream mode) is what keeps both invariants, and it is why a late
|
||||
packet's counter, not its arrival stamp, is what addresses the hole.
|
||||
packet's counter, not its arrival stamp, is what addresses the hole. The reservation cap
|
||||
(`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
|
||||
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
|
||||
and resets `tick_` on. That is what keeps `tick_ / period == produced()`, the relation
|
||||
`backfill_from()` derives a slot index from — without it the amend falls outside the ring
|
||||
and repairs nothing while the hole is still marked repaired, a band saying the opposite of
|
||||
the truth. The generation is the signal and the stride/channel-count comparison is not: a
|
||||
reconnect with an identical CONFIG rebuilds the store leaving both unchanged. `slot_base_`
|
||||
is **not** reset — it is relative mode's clock origin, samples since the stream began, and
|
||||
relative time must not jump backwards when the user retypes the window.
|
||||
- **A dropped time slot is clamped, never answered with a sentinel.** `acquire()` pins
|
||||
the published window to the value ring's base and both rings keep sliding afterwards —
|
||||
their capacities are equal, so there is no slack — which means the oldest few samples
|
||||
|
||||
@@ -586,32 +586,37 @@ Two lines of the self-check output are assertions in disguise:
|
||||
it afterwards.
|
||||
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. 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 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, `UDPSCOPE_PROBE_RESEQ_MS`) —
|
||||
- `[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,
|
||||
`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). Six things to read carefully:
|
||||
[verification.md](verification.md). Eight 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
|
||||
therefore sums over every non-math channel. On a stream with 4 non-math channels they
|
||||
stand a factor of 4 apart. `hole_slots=4200 backfilled=16800` in the `late` case is
|
||||
healthy, not an error; `backfilled` below `4 × hole_slots` would mean some channels
|
||||
went unrepaired.
|
||||
stand a factor of 4 apart. `hole_slots=4000 backfilled=15600` in the `late` case is
|
||||
healthy, not an error (the shortfall is the one hole whose late packet is still in
|
||||
flight); `backfilled` well below `4 × hole_slots` would mean some channels went
|
||||
unrepaired.
|
||||
|
||||
2. **`nan_slots=0` beside a large `holes` is healthy, not a contradiction.** A hole is
|
||||
retired from the table only when its slots age out of the ring
|
||||
(`ScopeModel::forget_holes_before()`), *never* on repair. So the `late` case reads
|
||||
`holes=42 nan_slots=0`: every hole was fully repaired, and all 42 entries stay in the
|
||||
table with no placeholder left in the data. What *would* be a defect is `nan_slots > 0`
|
||||
with `bands_repaired == bands`, which would mean a band claimed data was recovered while
|
||||
placeholders remained. The discriminator is `bands_repaired`, not `holes`.
|
||||
(`ScopeModel::forget_holes_before()`) or a re-plan drops it, *never* on repair. So the
|
||||
`late` case reads `holes=40 nan_slots=100`: 39 of the 40 holes were fully repaired and all
|
||||
40 entries stay in the table, the 100 remaining placeholders being the one packet still in
|
||||
flight. What *would* be a defect is `nan_slots > 0` with `bands_repaired == bands`, which
|
||||
would mean a band claimed data was recovered while placeholders remained. The
|
||||
discriminator is `bands_repaired`, not `holes`.
|
||||
|
||||
3. **`bands` is the visible spans, `open_holes` the whole table.** `bands` counts what the
|
||||
plot is actually shading in the visible window; `open_holes` counts every live hole entry
|
||||
whether or not it is in view. `bands` below `open_holes` is normal — in the `mixed` case
|
||||
`open_holes=43` against `bands=42` because the 43rd hole has scrolled out of the visible
|
||||
window and `build_invalid_spans()` drops it. `bands` *above* `open_holes` would name a
|
||||
stale span left on a plot after its hole was retired.
|
||||
whether or not it is in view. `bands` below `open_holes` is normal — a hole that has
|
||||
scrolled out of the visible window is dropped by `build_invalid_spans()` rather than
|
||||
pinned to the edge. `bands` *above* `open_holes` would name a stale span left on a plot
|
||||
after its hole was retired. Neither is comparable with `holes`, which is cumulative: a
|
||||
re-plan clears the table and `kMaxHoles` caps it at 64, so the re-plan case reads
|
||||
`holes=315 open_holes=64`.
|
||||
|
||||
4. **`bands_repaired` is the faint tier.** A hole whose late packets have all arrived is
|
||||
still drawn, in `Style::invalid_repaired_fill` rather than `Style::invalid_fill`, so the
|
||||
@@ -633,6 +638,24 @@ Two lines of the self-check output are assertions in disguise:
|
||||
actually holds, so a reservation that mismeasured the hole shows up there and nowhere
|
||||
else.
|
||||
|
||||
7. **`stale` is 0 by construction whenever resequencing is enabled**, which is every default
|
||||
run. The client increments it only on the `reorder_window_s <= 0` branch, where it names
|
||||
the pre-resequencing behaviour: a packet that did not advance the counter was dropped.
|
||||
With the window open such a packet is delivered flagged instead and counted in `late`, so
|
||||
a reading of 0 here is the feature working and not a dead counter. The only way to see it
|
||||
move is to turn resequencing off.
|
||||
|
||||
8. **`overflows` is how an undersized reorder ring reads.** It counts the times a packet
|
||||
needed a slot in the client's 64-slot / 4 MB reorder ring and found none, so the gap was
|
||||
committed early to make room — the window those packets got was shorter than
|
||||
`reorder_window_s` asked for. Nothing is dropped on that path (the arriving packet is held
|
||||
after the commit, or delivered in order with the counters it skipped reported as `lost`),
|
||||
which is the point: a dropped packet would leave a hole nothing could ever backfill. It
|
||||
reads 0 at the four cases' 10 kS/s and non-zero at the re-plan case's 100 kS/s, where
|
||||
`--reorder-every 10 --reorder-delay-ms 50` keeps about 50 packets in flight against 64
|
||||
slots. A non-zero reading beside a healthy `backfilled` is a note about the ring's size,
|
||||
not a defect.
|
||||
|
||||
- `[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.
|
||||
|
||||
|
||||
+58
-32
@@ -201,58 +201,84 @@ as distinct from the fault injection below, which exercises ingest:
|
||||
- `--quiet-at S` freezes `sine` at its held value from stream second `S` onwards, so it stops
|
||||
crossing zero: the dropout trigger's input.
|
||||
|
||||
- `UDPSCOPE_PROBE_RESEQ_MS` — one mid-run reading of the resequencing path. Four cases,
|
||||
**run each as its own process** — four back to back starve the mock past its 2 s silence
|
||||
- `UDPSCOPE_PROBE_RESEQ_MS` — one mid-run reading of the resequencing path. Five cases,
|
||||
**run each as its own process** — five back to back starve the mock past its 2 s silence
|
||||
timeout and every case then reads a reconnect instead of what it was testing. Common
|
||||
conditions for all four: the mock at `--rate 10000 --batch 100 --freq 5`, udpscope with
|
||||
conditions for the first four: the mock at `--rate 10000 --batch 100 --freq 5`, udpscope with
|
||||
no extra options, under `xvfb-run` with
|
||||
`UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=600 UDPSCOPE_EXIT_DUMP=1
|
||||
UDPSCOPE_PROBE_RESEQ_MS=5000`.
|
||||
UDPSCOPE_PROBE_RESEQ_MS=5000`. Delete `~/.config/udpscope/127.0.0.1_8000.toml` first: a
|
||||
session left by an earlier run restores its math channel, and five channels instead of four
|
||||
change `channels=`, `span=` and every count derived from them.
|
||||
|
||||
| case | mock extra flags | key reading |
|
||||
| --- | --- | --- |
|
||||
| **base** | *(none)* | every counter 0; proves the machinery costs nothing |
|
||||
| **reorder** | `--reorder-every 10` | `reseq=51` (mid-run) / `102` (dump); `holes=0`; resequencing inside the window leaves no reservation |
|
||||
| **late** | `--reorder-every 10 --reorder-delay-ms 50` | `holes=42 bands=42 bands_repaired=42 nan_slots=0`; every band is the faint, repaired tier |
|
||||
| **mixed** | `--drop-every 20 --reorder-every 10 --reorder-delay-ms 50` | `bands=42 bands_repaired=20 nan_slots=2300`; all three band states at once |
|
||||
| **late** | `--reorder-every 10 --reorder-delay-ms 50` | `bands_repaired` one short of `bands` with `nan_slots=100`; the band still solid is the one hole whose late packet is in flight, and it is the *only* unrepaired one |
|
||||
| **mixed** | `--drop-every 20 --reorder-every 10 --reorder-delay-ms 50` | `bands=43 bands_repaired=20 nan_slots=2200`; all three band states at once |
|
||||
| **re-plan** | `--rate 100000` with the `mixed` flags, and `UDPSCOPE_PROBE_WINDOW_MS=4000 UDPSCOPE_PROBE_RESEQ_MS=6000 UDPSCOPE_EXIT_AFTER_MS=9000` | four `buffer rebuilt` lines before the reading, then `bands=64 bands_repaired=16 nan_slots=7800`: **a band is still only marked repaired when its own slots hold values**. A `bands_repaired == bands` beside a non-zero `nan_slots` here is the hole table and the producer's `tick_` surviving a re-plan that reset `produced()` — a band marked settled over samples that are all still NaN. The non-zero `overflows` is this case too: 100 kS/s with a 50 ms reorder delay keeps about 50 packets in flight against 64 slots |
|
||||
|
||||
Full measured readings (probe line first, then exit-dump; `bands_repaired` was added after the
|
||||
original probe and any earlier reading without it is obsolete):
|
||||
The re-plan case needs `--rate 100000`, not the other four's 10 000: at 10 kS/s the ring
|
||||
already covers 838 s, so no window the control can reach moves `time_stride`,
|
||||
`set_target_span()` returns without rebuilding, and the window probe reports `decim=1`
|
||||
throughout — it re-plans nothing. At 100 kS/s the ring covers 83.9 s and the probe's
|
||||
500 ms → 500 s → 120 s → 600 s → 60 s walk crosses the boundary four times.
|
||||
|
||||
Full measured readings (probe line first, then exit-dump):
|
||||
|
||||
```
|
||||
=== base mock: none
|
||||
[reseq-probe] reseq=0 late=0 stale=0 holes=0 hole_slots=0 backfilled=0 open_holes=0 bands=0
|
||||
bands_repaired=0 nan_slots=0 scanned=44000 trigger_in_hole=0
|
||||
[udpscope] frames=1024 packets=1025 gaps=0 channels=4 span=838.861s stride=1 reseq=0 late=0
|
||||
stale=0 holes=0 hole_slots=0 backfilled=0
|
||||
[udpscope] channel 0 'sine' stored=95200 published=95000
|
||||
[reseq-probe] reseq=0 late=0 stale=0 overflows=0 holes=0 hole_slots=0 backfilled=0 open_holes=0
|
||||
bands=0 bands_repaired=0 nan_slots=0 scanned=44200 trigger_in_hole=0
|
||||
[udpscope] frames=1032 packets=1033 gaps=0 channels=4 span=838.861s stride=1 reseq=0 late=0
|
||||
stale=0 overflows=0 holes=0 hole_slots=0 backfilled=0
|
||||
[udpscope] channel 0 'sine' stored=95700 published=95400
|
||||
|
||||
=== reorder mock: --reorder-every 10
|
||||
[reseq-probe] reseq=51 late=0 stale=0 holes=0 hole_slots=0 backfilled=0 open_holes=0 bands=0
|
||||
bands_repaired=0 nan_slots=0 scanned=44000 trigger_in_hole=0
|
||||
[udpscope] frames=1028 packets=1029 gaps=0 channels=4 span=838.861s stride=1 reseq=102 late=0
|
||||
stale=0 holes=0 hole_slots=0 backfilled=0
|
||||
[udpscope] channel 0 'sine' stored=95400 published=95100
|
||||
[reseq-probe] reseq=51 late=0 stale=0 overflows=0 holes=0 hole_slots=0 backfilled=0
|
||||
open_holes=0 bands=0 bands_repaired=0 nan_slots=0 scanned=44200
|
||||
trigger_in_hole=0
|
||||
[udpscope] frames=1027 packets=1028 gaps=0 channels=4 span=838.861s stride=1 reseq=102 late=0
|
||||
stale=0 overflows=0 holes=0 hole_slots=0 backfilled=0
|
||||
[udpscope] channel 0 'sine' stored=95300 published=95100
|
||||
|
||||
=== late mock: --reorder-every 10 --reorder-delay-ms 50
|
||||
[reseq-probe] reseq=142 late=48 stale=0 holes=42 hole_slots=4200 backfilled=16800 open_holes=42
|
||||
bands=42 bands_repaired=42 nan_slots=0 scanned=44000 trigger_in_hole=0
|
||||
[udpscope] frames=1026 packets=1027 gaps=100 channels=4 span=838.861s stride=1 reseq=288
|
||||
late=100 stale=0 holes=94 hole_slots=9400 backfilled=37600
|
||||
[udpscope] channel 0 'sine' stored=95700 published=95500
|
||||
[reseq-probe] reseq=157 late=46 stale=0 overflows=0 holes=40 hole_slots=4000 backfilled=15600
|
||||
open_holes=40 bands=40 bands_repaired=39 nan_slots=100 scanned=44000
|
||||
trigger_in_hole=0
|
||||
[udpscope] frames=1027 packets=1028 gaps=98 channels=4 span=838.861s stride=1 reseq=296
|
||||
late=98 stale=0 overflows=0 holes=91 hole_slots=9100 backfilled=36400
|
||||
[udpscope] channel 0 'sine' stored=95300 published=95000
|
||||
|
||||
=== mixed mock: --drop-every 20 --reorder-every 10 --reorder-delay-ms 50
|
||||
[reseq-probe] reseq=163 late=24 stale=0 holes=43 hole_slots=4300 backfilled=8000 open_holes=43
|
||||
bands=42 bands_repaired=20 nan_slots=2300 scanned=44100 trigger_in_hole=0
|
||||
[udpscope] frames=974 packets=975 gaps=101 channels=4 span=838.861s stride=1 reseq=314 late=50
|
||||
stale=0 holes=94 hole_slots=9400 backfilled=18400
|
||||
[udpscope] channel 0 'sine' stored=95100 published=94900
|
||||
[reseq-probe] reseq=159 late=25 stale=0 overflows=0 holes=43 hole_slots=4300 backfilled=8400
|
||||
open_holes=43 bands=43 bands_repaired=20 nan_slots=2200 scanned=44100
|
||||
trigger_in_hole=0
|
||||
[udpscope] frames=978 packets=981 gaps=101 channels=4 span=838.861s stride=1 reseq=309 late=50
|
||||
stale=0 overflows=0 holes=94 hole_slots=9400 backfilled=18400
|
||||
[udpscope] channel 0 'sine' stored=95400 published=95400
|
||||
|
||||
=== re-plan mock: --rate 100000 --drop-every 20 --reorder-every 10 --reorder-delay-ms 50
|
||||
[reseq-probe] reseq=4374 late=72 stale=0 overflows=36 holes=315 hole_slots=31136
|
||||
backfilled=26800 open_holes=64 bands=64 bands_repaired=16 nan_slots=7800
|
||||
scanned=156600 trigger_in_hole=0
|
||||
[udpscope] frames=8949 packets=8953 gaps=614 channels=4 span=83.886s stride=1 reseq=8028
|
||||
late=144 stale=0 overflows=36 holes=603 hole_slots=59936 backfilled=55600
|
||||
[udpscope] channel 0 'sine' stored=588600 published=584600
|
||||
```
|
||||
|
||||
The `mixed` case's `open_holes=43` against `bands=42` is correct: the 43rd hole has scrolled
|
||||
out of the visible window, and `build_invalid_spans()` drops a span outside the window rather
|
||||
than pinning it to the edge. The per-field reference is in
|
||||
[probe-readings.md](probe-readings.md).
|
||||
The `late` case's `bands_repaired` is one short of `bands` with `nan_slots=100` — exactly one
|
||||
packet's worth of slots at stride 1 — because the probe fires 50 ms-delayed releases into a
|
||||
window that always has one in flight. An earlier recording of this case read
|
||||
`bands_repaired=42 nan_slots=0`; both are healthy, and which one a run gets is where the
|
||||
probe's instant falls relative to the newest release. A `bands_repaired` *above* the count of
|
||||
holes whose slots hold values is the defect, in either direction.
|
||||
|
||||
`holes` far above `open_holes` in the re-plan case is the two bounds working: a re-plan clears
|
||||
the table (the old entries address a buffer that is gone) and `kMaxHoles` caps what survives
|
||||
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
|
||||
is the ingest rather than the UI. The first: `--time-glitch-at <stream seconds> --time-glitch-by <seconds>` makes
|
||||
|
||||
Reference in new issue
Block a user