fix: a clock that stalled on a re-plan, and a dialog that could not close
**The clock.** Trigger mode showed a blank plot and never latched when the session defined a math
channel, because such a session gives the model a plan *before* the stream's own signals are in it:
one derived channel, declared rate 0, so `plan_partition()`'s 1 kHz fallback is the interval those
early frames are stamped at -- 1300x too fast for a 1.3 MS/s stream -- and they have nowhere to go,
since the value loop skips derived channels. Relative mode then stamped a *position* times the plan's
*current* interval, so when the CONFIG's channels landed and the plan took the real rate (769 ns) the
same positions produced times 1300x *smaller* than what was already in the ring, and the monotonic
clamp held the clock at the old value for the rest of the run: one repeated stamp, `history=0`, no
latch, and a window the renderer cannot draw. The same family as the restarted-client poisoning
fixed in 620473a, through a different door. The filter's own output was finite and correct
throughout, and disabling the math channel only appeared to cure it because that is a structural
change which rebuilds the store -- the third time that channel has looked guilty while being a
bystander.
Relative mode now accumulates *seconds* (`rel_base_s_ += n * dt`), so a re-plan is a slope change and
the clock cannot jump backwards by construction; a frame is only stamped into a model that has a
*streamed* channel; and the epoch is adopted once per connection, with "unset" a state rather than
the value zero. A one-shot warning names the family if it returns: `the clock stalled at ...: N
samples stamped [a..b] are all behind it`.
**The dialog.** The Configurations window's Close button and the window-manager close both did
nothing: `close-request` was connected to a bare `void` lambda, and that signal is `gboolean` -- TRUE
means "handled" and suppresses the default handler that destroys the window, while `G_CALLBACK` is a
cast, so GTK read whatever the compiler left in the return register. A standalone GTK program of that
shape reproduces it: `void` handler -> `window_alive=1` after `gtk_window_close()`, `gboolean FALSE`
-> 0. The same audit found `gtk_window_present(GTK_WINDOW(...))` on an `AdwDialog`, a type assertion
and a no-op; it is `adw_dialog_present()` now. The menu row stays: it is the only way to switch or
save a configuration without restarting, and what made it look useless was that it could not close.
Verified: trigger mode with that session's graph reads `ring=[0.0058..0.2446] -> [0.0058..1.9969]`,
`history 0.237 -> 2.0 s`, `edge at 1.120 s, latched at 2.120 s`, `display=event(frozen) latches=1`,
`traceless paints 266` against 1519 before the fix. Baselines hold: 0 errors, the documented three
warnings, test-ui 13 ok, test-help green, `gtk warnings: 0`.
This commit is contained in:
1 parent
27c9b34277
commit
074cb611bc
9 files changed
+210
-34
No files matched your search
@@ -172,7 +172,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
- **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
|
||||
before anything is booked *and* before relative mode's own clock 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
|
||||
@@ -206,9 +206,36 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
`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.
|
||||
reconnect with an identical CONFIG rebuilds the store leaving both unchanged.
|
||||
- **The relative clock is accumulated in *seconds*, and a frame is only stamped into a model that has a
|
||||
streamed channel.** Relative time's stamp is the declared interval counted from the stream's own
|
||||
origin — `Acquisition::rel_base_s_`, which is advanced by the frame's own samples and by the samples a
|
||||
reported gap spans. It is deliberately **not** `position × Partition::base_dt`, and the difference is
|
||||
one defect: a plan is rebuilt whenever the channel list changes (a CONFIG, an enable/disable, a
|
||||
session's math channels), and `base_dt` is the **1 kHz fallback** until a *streamed* channel declares a
|
||||
rate. Recomputing from a position made a plan that *shortened* the interval put every later stamp
|
||||
behind the one already written, and the frame loop's monotonic clamp then held the clock there for the
|
||||
rest of the run. What that looks like: `ring=[a..a]` standing still while `n` climbs,
|
||||
`history` never reaching the pre-window (so a trigger never latches and the display stays in `Waiting`,
|
||||
which is blank), and — the reading that names it — `[udpscope] warning: the clock stalled ...`, printed
|
||||
once per connection the first time a whole frame's stamps are all behind the clock. Accumulated, a plan
|
||||
change is only a slope change and the clock cannot go backwards. The entry is the *other* half: the
|
||||
ingest used to accept a frame as soon as `channels()` was non-empty, and a session's math channel
|
||||
applied before the stream's own signals arrive is exactly one channel with a fallback plan. Such a
|
||||
frame has nowhere to go (the value loop skips every derived channel) and its plan's interval is not the
|
||||
stream's rate, so `on_data()` now requires a **streamed** channel and drops the frame otherwise, the
|
||||
same way it always did for an empty list. `rel_base_s_` is **not** reset across a reconnect — the
|
||||
origin survives, it is relative mode's clock origin — while `base_dt`'s *use* is what this rule took
|
||||
away: it is read to *advance* the clock, never to recompute where the clock already was.
|
||||
- **The stream's epoch is adopted *once per connection*, and "unset" is a state, not the value zero.**
|
||||
`epoch` is the stream time the display's x = 0 means, taken from the first frame's stamp. The test for
|
||||
"not adopted yet" used to be `model.epoch() == 0.0`, which relative mode breaks by construction: its
|
||||
first frame is stamped exactly `0`, so the *second* frame adopted the origin — and with
|
||||
`rel = max(stamp - epoch, last_stamp_)` that put a whole frame's samples on one stamp and shifted every
|
||||
later x by a frame's span. `Acquisition::epoch_adopted_` is the flag, re-armed by
|
||||
`reset_session_state()` because a new session's clock is a new clock; the warning line above is what
|
||||
caught it.
|
||||
|
||||
- **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
|
||||
@@ -294,8 +321,8 @@ already scaled against, and re-anchoring mid-stream would move x. The clock *cho
|
||||
per connection (`time_source_ = 0` on CONFIG) for the same reason: a session that dropped may
|
||||
come back with a different answer.
|
||||
|
||||
Relative time's `slot_base_` also survives: it is samples-since-the-stream-began, the user's
|
||||
chosen origin, and must not jump backwards because the link blipped.
|
||||
Relative time's clock (`Acquisition::rel_base_s_`, seconds accumulated at the declared interval)
|
||||
also survives: it is the user's chosen origin, and must not jump backwards because the link blipped.
|
||||
|
||||
## There may be no store at all
|
||||
|
||||
|
||||
Reference in new issue
Block a user