fix: a claimed gap must be corroborated before it is believed, and its hole must fit

The user's real host reported "connected and live" with one missing-data band over the whole window
and no trace in it. The counter is the only thing that claims a packet existed, and a reservation
was booked on its word alone: a source whose DATA counter counts samples, or frames of an inner
layer, advances by more than 1 per datagram, the client cannot check that claim (it has no clock of
its own to compare against), so it holds each packet, saturates its reorder ring, delivers it in
order and reports the skipped counters as lost. Every frame therefore reported a phantom gap, and
`reserve_hole()` wrote `lost * batch_` NaN samples with no bound but the 256-PACKET cap -- the
sample form of that rule ("the number to weigh against the ring is 256 * batch_, not the 256") was
never implemented. With relative time on -- which that session carries -- each phantom gap is
materialised as NaN slots by construction: the window filled with reservations, there was never
anything missing to repair them, and the gaps also stretched the relative clock, which is why the
status bar read live at ten times the true rate.

`Acquisition::gap_plausible()` now gates every reservation on two independent tests: the arrival
clock must corroborate the claim (that many missing packets would take that many frame intervals;
deliberately permissive, because a false refusal costs a shading and never data), and the ring must
be able to hold the hole. The verdict is taken before the stamps, and relative mode's slot advance
is skipped with it, since the advance and the reservation are the same claim. Refusals are counted
in `StreamStats::gaps_refused` and said once per connection with the numbers, naming the wire shape
they describe.

tools/mock_udpstreamer.py gains `--counter-step N`: a counter that is not a packet counter on an
intact link, which is the one shape the mock could not produce and the shape the report came from.

Verified in both directions. A counter stepping 1000 per datagram reads `holes=0 hole_slots=0
backfilled=0` with the traces present (3 traces, 1.4 M samples) against 7.9 M claimed gaps, and the
warning above names the cause. Every other packet withheld -- a real loss -- still reads
`holes=3579`, so genuine gaps are still reserved: a fix that stopped believing real losses would
have traded one wrong picture for another. The user's control read as "the math channel is the
problem" and was confounded -- disabling it is a structural change, which rebuilds the buffer and
clears the accumulated NaN.
This commit is contained in:
Martino Ferrari committed 2026-10-15 08:13:28 +02:00
1 parent 22e7a8efcc
commit 27c9b34277
9 files changed
+249 -15

No files matched your search

+25 -2
View File
@@ -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