fix(udpscope): make rule 3 converge in both branches and survive reordering
Eight review findings on FrameDecoder's accumulated-scalar rule. The squeeze that pulls a leading timeline back was expressed as a fraction of the NOMINAL burst width, which cannot converge: inside one timestamps() call the wall clock is frozen, so any positive step raises the lead measured at that instant, and the lead only falls because the wall advances between packets. At the kMinBleedFactor floor the timeline still gained 0.05 * nominal per packet, so a declared SamplingRate of 30 against a producer really flushing 10 samples at 1 kHz ran away without bound (667 s of lead after 1000 s of stream). Cap the burst's total advance at half the wall time really elapsed since this signal's previous burst instead, and the lead strictly falls for any declared rate. SigState gained lastEmittedWall for that reference; lastPacketWall could not be reused because it belongs to packetBurst. The hrt branch contributed zero elapsed for a late datagram but still wrote the hrt reference back to it, so the next packet's delta spanned two intervals and fabricated a whole extra packet of producer time — permanently, since the monotonic clamp discards the correction ClockOffset would have made. Reordering is reachable in production: udps_client.c only counts counter gaps. Simply never regressing the reference is not the fix either, because a producer restart would then freeze the signal forever, so the two are now separated by the size of the backward jump. The hrt branch's clamp was also one-directional, reintroducing on that branch exactly the defect the declared branch's squeeze exists to prevent: a backward wall step (NTP, suspend/resume) left a permanent lead. It now shares the same wall-elapsed cap. Also: anchor an hrt-branch burst's LAST element on arrival, matching the declared branch, so two accumulated scalars in one scope do not sit a burst apart on the shared X axis; treat a non-finite samplingRate off the wire as undeclared, since +inf produced a 0.0/0.0 factor the floor could not catch and turned every stamp NaN; write lastCounter on the hrt branch so duplicate datagrams are dropped there too; and correct two comments that argued for the current code with claims that are false (a counter-gap clamp reaches the opposite outcome, not the same one earlier, and the squeeze's steady state is a sawtooth, not a fixed offset). Seven new tests, each proven non-vacuous by sabotage; 54 pass. Plan document Task 4 re-synced and its stale test count and "agree on the same stream" claim corrected.
This commit is contained in:
@@ -10,17 +10,30 @@
|
||||
* Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and
|
||||
* 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.
|
||||
* Two rules deliberately differ, both in the accumulated-scalar case (rule 3).
|
||||
*
|
||||
* First, the anchor. 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.
|
||||
*
|
||||
* Second, the entry condition. UDPSourceSession.cpp:554 routes any update
|
||||
* carrying nElems <= 1 to plain arrival time. That is safe for a host-local
|
||||
* consumer whose arrival time is the producer's own clock, but wrong here:
|
||||
* Accumulate mode flushes on a TIMER, so a short RT cycle legitimately delivers
|
||||
* a single sample between two full bursts. Dating that one sample from arrival
|
||||
* while its neighbours are chained puts it off the chain, and — worse — leaves
|
||||
* lastCounter behind, so the next full burst reads the skipped counter as a lost
|
||||
* datagram and reinstates a hole that never existed. So a signal that has
|
||||
* already burst keeps every later update on rule 3 regardless of its length; a
|
||||
* signal that has never burst is a genuine scalar and is left to rule 5.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
@@ -71,12 +84,22 @@ private:
|
||||
double accProdSec = 0.0;
|
||||
bool lastAccValid = false;
|
||||
uint32_t prevAccCount = 0;
|
||||
/** For accumulated scalars with a declared sampling rate: end timestamp
|
||||
* of the most recently emitted burst, and the packet counter it came
|
||||
* from. The next burst is chained onto that end, with the counter gap
|
||||
* reinstating the exact duration of any lost datagrams. */
|
||||
/** For accumulated scalars (rule 3, either branch): end timestamp of the
|
||||
* most recently emitted burst, and the packet counter it came from. The
|
||||
* next burst is chained onto that end, with the counter gap reinstating
|
||||
* the exact duration of any lost datagrams. */
|
||||
double lastEmittedEnd = 0.0;
|
||||
uint32_t lastCounter = 0u;
|
||||
/** ARRIVAL time of the packet that produced lastEmittedEnd. Valid
|
||||
* exactly when lastEmittedValid is, so it needs no flag of its own.
|
||||
* Deliberately not lastPacketWall, which belongs to packetBurst() and
|
||||
* is updated on frames rule 3 never emits. This is the only reference
|
||||
* against which a leading timeline can be pulled back: the correction
|
||||
* has to be expressed as a fraction of the wall time that has really
|
||||
* elapsed since this signal's previous burst, because within a single
|
||||
* timestamps() call the wall clock is frozen and every forward step,
|
||||
* however small, increases the lead measured at that instant. */
|
||||
double lastEmittedWall = 0.0;
|
||||
bool lastEmittedValid = false;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user