fix: a restarted client is a new stream, so its history and its monotonic mark go
Going live from a file view left the display frozen for seconds: the per-second line showed
`ring=[2.5200..2.5200] n=567000 -> n=1131000 -> n=1689000` — the sample count climbing at the stream's rate
while the retained span was one value, the file view's last stamp. The status bar was green because
everything about the *connection* was fine.
It is the poisoning `invariants-data-path.md` already describes, entered through a different door. The
new client's stream clock sits *behind* the stamps the old stream left in `Acquisition::last_stamp_` (the
file view's samples are still in the store), so the advance test rejects every packet of a perfectly good
stream, four rejections abandon the declared time signal, and the arrival-clock fallback derives its epoch
from the surviving mark — every sample stamped flat at the old value. The guard did exactly what it was
built to do; the *stream* was the thing that had changed and nothing had told it.
So `Acquisition::start()` — which is the one place a client (re)starts — now says so:
`ScopeModel::reset_stream()` drops the previous stream's history (`store_->reset()`, which the data-path
invariants require to leave nothing behind) and moves `plan_generation_`, which is what the producer
already watches to drop the state tied to the old store; and `last_stamp_` is re-based to `-infinity` so
the monotonic guard re-bases on the new clock instead of pinning it. The declaration is kept: the first
packet of the new stream passes, and monotonicity holds from there.
One self-inflicted crash on the way, fixed before this: `reset_stream()` dereferenced `store_`, which is
**null until the first CONFIG has planned anything** — at start-up there is none — so the app died with
SIGSEGV and no output at all. Caught by running it plainly rather than trusting the change.
Verified with the events probe (a replay and a group, then *Live*):
live restored (after replay): … pending=0
trig engine: … history=0.4377/0.4000 s now_t=0.298 ring=[-0.1392..0.2985] n=569000
trig engine: … history=0.8723/0.4000 s now_t=0.733 ring=[-0.1392..0.7331] n=1134000 (pending=1: it latched)
trig engine: … history=1.2954/0.4000 s now_t=1.156 ring=[-0.1392..1.1562] n=1684000
`ring` moves within a round-trip, `history` fills, and the trigger latches the frame after the restore;
`make test-ui` 14 files, zero `Gtk-CRITICAL`, and the plain run is clean. Docs: the restart's own door into
the poisoning in `invariants-data-path.md`, and the `ring=` reading's entry in `probe-readings.md` now says
what it must do after *Go live*.
This commit is contained in:
1 parent
62b5f14504
commit
761615c2fe
4 files changed
+39
-1
No files matched your search
@@ -94,6 +94,18 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
comparison is off by 1e9, so every packet would read as an implausible jump. The guard is
|
||||
`tools/mock_udpstreamer.py --time-glitch-at`/`--time-glitch-by` (see
|
||||
[verification.md](verification.md)).
|
||||
- **A *restarted* client is the other way into the same poisoning, and it is not a glitch.** Going live
|
||||
from a file view starts a new `udps_client`, and the new stream's stamps can sit **behind** the ones the
|
||||
old stream left in `last_stamp_` — the file view's samples are still in the store. The advance test then
|
||||
rejects every packet of a perfectly good stream, four rejections abandon the declared time signal, and
|
||||
the arrival clock derives its epoch from the stale mark: every sample is stamped **flat at the old
|
||||
value**. `trig engine: … ring=[2.5200..2.5200] n=567000 → n=1131000 → n=1689000` is what that looks
|
||||
like — the count climbing at the stream's rate while the retained span is one value — until some later
|
||||
rebuild resets the store, which is where the "few seconds of nothing" came from.
|
||||
`Acquisition::start()` therefore calls `ScopeModel::reset_stream()` and re-bases `last_stamp_` to
|
||||
`-infinity`: a started or restarted client is a *new stream*, so the previous one's history goes (which
|
||||
is what `reset()` is for) and the monotonic guard re-bases on the new clock instead of pinning it.
|
||||
`ring` moving within a stream round-trip of *Go live* is the assertion.
|
||||
- **The arrival-clock fallback does not drop history.** `arrival_epoch_` is derived from
|
||||
`last_stamp_`, so the arrival clock *continues* from the monotonic mark and the two
|
||||
timelines share a zero by construction — the recorded stamps are therefore still on one
|
||||
|
||||
@@ -1500,7 +1500,11 @@ which of two failures a stalled display is.** `n` counts every stored sample, so
|
||||
rate whenever data arrives at all:
|
||||
|
||||
- **`ring` standing still while `n` climbs** is the *clock*: samples are stored but every one carries the
|
||||
same stamp, so the axis cannot advance and the trigger's `history` never fills. Measured after going live
|
||||
same stamp, so the axis cannot advance and the trigger's `history` never fills. The stamps are pinned at
|
||||
the file view's last value because a restarted client's stream clock sits behind it: the advance test
|
||||
rejects the new packets, four rejections abandon the declared time signal, and the arrival-clock fallback
|
||||
derives its epoch from the surviving mark. `Acquisition::start()` now resets the store and re-bases that
|
||||
mark, so `ring` must **move** within a stream round-trip of *Go live*. Measured after going live
|
||||
from an event view: `ring=[2.5200..2.5200] n=567000 → n=1131000 → n=1689000`, one second apart, with
|
||||
`now_t` and `holdoff_until` both pinned at `2.520` — the file view's last stamp. The producer's clock was
|
||||
frozen, not lagging, and the display stayed blank until something re-planned the store.
|
||||
|
||||
Reference in new issue
Block a user