fix(udpscope): keep the accumulated-scalar timeline monotonic and bounded

Round 4 of Task 4 review. Four defects in FrameDecoder's rule 3:

- The undeclared-rate (hrt) path positioned each burst at an ABSOLUTE
  hrt/ticksPerSecond(). hrt counts from the producer's boot, so it is ~1e11
  ticks by the time a scope attaches, and the rate is refitted every packet
  with a few parts in 1e4 of wobble. The product is tens of milliseconds of
  jitter in BOTH directions -- not merely imprecise, non-monotonic. Integrate
  short tick deltas into accProdSec instead and let ClockOffset latch the
  epoch that leaves behind.
- The lead bleed used a fixed 0.9 factor, which converges only while the
  declared rate is within ~10%. Squeeze proportionally to the excess instead
  (floored at kMinBleedFactor), settling it in a single burst.
- A single-sample flush fell through to the plain-scalar rule, dating it from
  arrival and leaving lastCounter stale so the next real burst reinstated a
  hole that never existed. Accumulate mode flushes on a timer, so a short
  cycle legitimately yields one sample; keep it on the chain.
- kMaxCounterGap was inert: an absurd gap yields an absurd prediction that the
  arrival backstop already rejects, and no input can distinguish the two
  rules. Removed rather than left implying a behaviour it did not have.

FrameDecoder.h now states the deliberate divergence from StreamHub -- which
converts hrt with the LOCAL MARTe timer frequency, valid only because it runs
on the producer's host -- and why a remote scope's drift is irreducible.

Three new tests, each sabotage-proven non-vacuous: producer restart, short
flushes staying on the chain, and a 20000-packet undeclared run after a
day of producer uptime that asserts SPACING as well as ordering (the
monotonic guard alone restores order while leaving positions wrong).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-08-27 22:16:11 +02:00
co-authored by Claude Opus 4.6
parent 3270284cfe
commit a2efc142c3
4 changed files with 421 additions and 123 deletions
+17 -4
View File
@@ -8,8 +8,19 @@
* though each represents ~10 ms of signal, and arrival-time interpolation then
* crams a packet's samples into that tiny gap — the trace renders as a sawtooth.
* Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and
* solves it; these are the same rules, computed from udps_frame_t's own fields
* so the scope and StreamHub agree on the same stream.
* solves it; these are the same rules, computed from udps_frame_t's own fields.
*
* One rule deliberately differs. StreamHub anchors every accumulated-scalar
* burst on the packet's own hrt, converted with the LOCAL MARTe
* HighResolutionTimer frequency — correct only because StreamHub runs on the
* producer's host. A bench scope attaches over the network and has no access to
* that frequency; it can only regress hrt against arrival time, which is
* exactly what the bursty delivery above corrupts. So when a SamplingRate is
* declared this decoder chains bursts instead, using the packet counter to
* account for loss and arrival time only as a backstop. The consequence is that
* a declared rate measured against the producer's crystal rather than ours makes
* the reconstructed timeline drift, and drift that only arrival time can
* observe must be corrected against arrival time — see rule 3.
*/
#pragma once
@@ -54,8 +65,10 @@ private:
* branch for why a tick difference is the only safe way to measure a
* producer-side interval while the rate is still being re-estimated. */
uint64_t lastAccHrt = 0u;
uint64_t hrtRef = 0u;
bool hrtRefValid = false;
/** Producer seconds since this signal's first usable packet, built by
* SUMMING short tick deltas. Never recomputed from an absolute tick
* count; see the samplingRate == 0 branch. */
double accProdSec = 0.0;
bool lastAccValid = false;
uint32_t prevAccCount = 0;
/** For accumulated scalars with a declared sampling rate: end timestamp