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.
112 lines
5.4 KiB
C++
112 lines
5.4 KiB
C++
/**
|
|
* @file FrameDecoder.h
|
|
* @brief Per-element timestamp reconstruction for UDPS frames.
|
|
*
|
|
* The C client's udps_frame_element_time() is explicitly an arrival-anchored
|
|
* estimate. It is not sufficient: the kernel frequently delivers several queued
|
|
* datagrams in one burst, so two packets are processed microseconds apart even
|
|
* 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.
|
|
*
|
|
* 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
|
|
|
|
#include "TimeBase.h"
|
|
#include "Types.h"
|
|
|
|
#include <vector>
|
|
|
|
namespace udpscope {
|
|
|
|
class FrameDecoder {
|
|
public:
|
|
/** Installs the signal table. Clears all per-signal timing history. */
|
|
void setSignals(const std::vector<SignalMeta>& signals);
|
|
|
|
const std::vector<SignalMeta>& signals() const { return signals_; }
|
|
|
|
/** Call once per frame, before any timestamps() call for that frame. */
|
|
void beginFrame(const FrameView& f);
|
|
|
|
/**
|
|
* @brief Timestamps for every value of signal @p idx in this frame.
|
|
* @return false when the signal produced nothing usable — an empty slot, or
|
|
* the first PACKET burst after connect, which has no previous
|
|
* arrival to span from and would otherwise poison the ring with
|
|
* wrongly spaced timestamps.
|
|
*/
|
|
bool timestamps(const FrameView& f, uint32_t idx, std::vector<double>& tsOut);
|
|
|
|
/** Forgets all timing history; call on reconnect. */
|
|
void reset();
|
|
|
|
private:
|
|
bool packetBurst(uint32_t idx, uint32_t nElems, double wallNow,
|
|
std::vector<double>& tsOut);
|
|
|
|
struct SigState {
|
|
ClockOffset offset;
|
|
double lastPacketWall = 0.0;
|
|
bool lastPacketValid = false;
|
|
/** Raw ticks, not seconds — see the comment in the samplingRate == 0
|
|
* 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;
|
|
/** 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 (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;
|
|
};
|
|
|
|
std::vector<SignalMeta> signals_;
|
|
std::vector<SigState> state_;
|
|
HrtRateFit hrtFit_;
|
|
};
|
|
|
|
} /* namespace udpscope */
|