fixed issue on udpstreamer trigger logic

This commit is contained in:
Martino Ferrari
2026-08-28 16:53:17 +02:00
parent 1c61e814c0
commit 044ce57ba3
170 changed files with 16958 additions and 24036 deletions
+112 -34
View File
@@ -68,6 +68,34 @@ static constexpr double kWallBleedFraction = 0.5;
*/
static constexpr double kProducerRestartS = 1.0;
/**
* The same reorder/restart question in the DECLARED-rate branch, which has no
* producer clock to ask and must read it off the packet counter instead: a
* counter this far behind the front, or further, is a restart; anything nearer
* is a reordered datagram.
*
* Needed for the same reason kProducerRestartS is, and it is not enough to lean
* on the arrival backstop. Both events make the wrapped gap enormous and both
* are rejected there — so the backstop cannot tell them apart, and whichever
* behaviour the counter update takes unconditionally is wrong for one of them.
* Rolling the counter back on a reorder gives the NEXT packet a gap of dist+1,
* an inflated `lost`, and a prediction wrong by dist burst widths that still
* lands inside the backstop and is accepted: measured +10 ms at distance 1,
* +200 ms at distance 20 (10 samples per 10 ms), never bled off, since an
* accepted chain is self-consistent and the squeeze never fires. Refusing to
* roll it back at all instead strands a restarted producer, whose counter
* begins again from 1: every later packet reads as a reorder and re-anchors on
* arrival, i.e. the sawtooth, until the new counter climbs past the old one.
*
* Both failure modes are bounded by this constant. 64 is far beyond any
* reordering a UDP path produces (a few packet intervals) and far below any
* counter a producer accumulates before restarting, so both bounds are slack.
* The one case it cannot separate is a producer that restarts having sent fewer
* than 64 updates; that costs at most 64 arrival-anchored bursts and then heals
* itself.
*/
static constexpr uint32_t kMaxReorderPackets = 64u;
/**
* The declared sampling rate, or 0 when there is none to trust.
*
@@ -181,6 +209,34 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
if ((d.timeMode == kTimeFirstSample || d.timeMode == kTimeLastSample) &&
hasTimeSig && f.counts[tIdx] >= 1u && f.values[tIdx] != nullptr) {
const double prodSec = f.values[tIdx][0] * tScale;
/* An anchor that has not advanced is the same reorder-or-restart
* question rule 3 answers from hrt, asked of the time signal instead,
* and separated by the same threshold for the same reason: nothing but
* the size of the backward step tells them apart.
*
* A reordered datagram is DROPPED rather than emitted. Its anchor is
* genuine producer time, so emitting it would place the whole array
* before stamps already handed out — measured 7 ms backwards on a
* single swapped anchor — and this rule has no emitted-timeline chain to
* clamp against, so there is nowhere honest to put it. packetBurst()
* makes the same choice for the same reason: drop rather than store at
* made-up positions. Dropping also protects the NEXT packet, which would
* otherwise divide a one-packet anchor difference by a counter gap of
* two and halve its spacing.
*
* A restart must instead rebase, or prodSec sits below prevAnchorProdSec
* for the rest of the session: every later packet reads as a reorder,
* the anchor pair never advances, and this rule runs on a period
* measured before the restart until the scope is restarted too. */
if (st.prevAnchorValid && prodSec <= st.prevAnchorProdSec) {
if ((st.prevAnchorProdSec - prodSec) <= kProducerRestartS) {
return false;
}
st.offset.reset();
st.prevAnchorValid = false;
}
const double anchor = st.offset.map(prodSec, wallNow);
const double rate = DeclaredRate(d.samplingRate);
double dt = (rate > 0.0) ? (1.0 / rate) : 0.0;
@@ -207,20 +263,19 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
* 10,10,2,10,20 pattern gave 5x, 0.2x and 0.5x the true period and one
* backward step of 3 ms.
*
* Two packets cannot always be measured. The first has no predecessor,
* and a reordered one has an anchor behind its predecessor's; rather
* than stack the whole array on one instant the very defect this
* paragraph exists to remove reuse the last period actually measured,
* exactly as the hrt branch reuses lastHrtDt. Only the genuine first
* packet of a run stacks, and only until the second arrives. */
* Two packets cannot always be measured — the first of a run has no
* predecessor, and neither does the first after a restart. Rather than
* stack the whole array on one instant, the very defect this paragraph
* exists to remove, reuse the last period actually measured, exactly as
* the hrt branch reuses lastHrtDt. Only the genuine first packet stacks,
* and only until the second arrives. */
if (!(dt > 0.0) && nElems > 1u) {
const uint32_t divisor = (d.timeMode == kTimeLastSample)
? nElems : st.prevAnchorCount;
const uint32_t rawGap = f.counter - st.lastCounter;
const bool fwdGap = (f.counter != 0u) && st.counterValid &&
(rawGap != 0u) && (rawGap < 0x80000000u);
if (st.prevAnchorValid && prodSec > st.prevAnchorProdSec &&
divisor > 0u) {
if (st.prevAnchorValid && divisor > 0u) {
dt = (prodSec - st.prevAnchorProdSec) /
(static_cast<double>(divisor) *
static_cast<double>(fwdGap ? rawGap : 1u));
@@ -229,16 +284,15 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
dt = st.prevAnchorDt;
}
}
/* All four move together or not at all: a reordered packet must not
* leave a newer anchor and an older counter behind for the next one to
* divide one by the other. */
if (!st.prevAnchorValid || prodSec > st.prevAnchorProdSec) {
st.prevAnchorProdSec = prodSec;
st.prevAnchorCount = nElems;
st.prevAnchorValid = true;
st.lastCounter = f.counter;
st.counterValid = true;
}
/* Unconditional, and only because the classification above has already
* sent every packet that must not move these either to `return false`
* or through prevAnchorValid = false. All four are one quantity: an
* anchor and the counter its difference is divided by. */
st.prevAnchorProdSec = prodSec;
st.prevAnchorCount = nElems;
st.prevAnchorValid = true;
st.lastCounter = f.counter;
st.counterValid = true;
tsOut.resize(nElems);
for (uint32_t e = 0; e < nElems; e++) {
@@ -423,9 +477,20 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
}
st.lastEmittedEnd = tsOut[nElems - 1u];
st.lastEmittedWall = wallNow;
st.lastCounter = f.counter;
st.counterValid = true;
st.prevAccCount = nElems;
/* The same lockstep rule the hrt branch applies to lastAccHrt, read
* off the counter alone because this branch has no producer clock.
* lastCounter is the reference the next packet's gap is measured
* from and prevAccCount is the burst width that gap is multiplied
* by, so they are one quantity and only a packet that defines the
* new front of the stream may move it. A datagram that arrived late
* is not that packet: see kMaxReorderPackets. */
const uint32_t back = st.lastCounter - f.counter;
if (!st.counterValid || f.counter == 0u || back == 0u ||
back >= kMaxReorderPackets) {
st.lastCounter = f.counter;
st.counterValid = true;
st.prevAccCount = nElems;
}
st.lastEmittedValid = true;
return true;
}
@@ -464,12 +529,21 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
* 22.5 ms in the future at 10 samples per 25 ms packet (6x and
* +112 ms after five such packets). Before any tick reference exists
* nothing is keyed to the counter, so it is free to advance and arm
* the duplicate guard for a producer that never sets hrt at all. */
* the duplicate guard for a producer that never sets hrt at all.
*
* prevAccCount belongs to the same group. It is the OTHER factor of
* the denominator — cycles = prevAccCount * gap — and it means "how
* many cycles the reference packet spanned", so it is keyed to
* lastAccHrt exactly as the counter is. Accumulate flushes on a
* timer, so a short packet here is ordinary: a 2-sample stray with
* hrt == 0 that moved prevAccCount alone drew the next real burst
* five times too wide and ended it +90 ms in the future, then took
* eight squeezed bursts to bleed back. */
if (f.hrt != 0u || !st.lastAccValid) {
st.lastCounter = f.counter;
st.counterValid = true;
st.prevAccCount = nElems;
}
st.prevAccCount = nElems;
if (!ok) { return false; }
st.lastEmittedEnd = tsOut[nElems - 1u];
st.lastEmittedWall = wallNow;
@@ -529,7 +603,7 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
const double backward =
static_cast<double>(st.lastAccHrt - f.hrt) / rate;
if (backward > kProducerRestartS) {
st.offset.reset();
st.accOffset.reset();
} else {
takeHrt = false;
}
@@ -605,7 +679,7 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
* shared X axis — 9 ms for 10 samples at 1 kHz, plain to see at a 200 ms
* window. Since map() latches once, this is a constant shift applied at
* latch and recalibration only; it changes no spacing. */
double base = st.offset.map(
double base = st.accOffset.map(
st.accProdSec,
wallNow - static_cast<double>(nElems - 1u) * hrtDt);
double step = hrtDt;
@@ -637,7 +711,6 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
}
if (takeHrt) { st.lastAccHrt = f.hrt; }
st.lastAccValid = true;
st.prevAccCount = nElems;
st.lastEmittedEnd = tsOut[nElems - 1u];
st.lastEmittedWall = wallNow;
/* Keep packetBurst's reference current even though this branch does not
@@ -648,17 +721,22 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
* behind the trace, and that figure grows with session length. */
st.lastPacketWall = wallNow;
st.lastPacketValid = true;
/* In lockstep with lastAccHrt, and for the same reason: these two are
* the numerator and the denominator of the next packet's period. Only a
* packet that defines the new front of producer time may move either.
* Advancing the counter alone on a reordered datagram halves the next
* packet's spacing; see the gap comment above. Leaving a counter behind
* at all is what lets the duplicate-datagram guard at the top of
* timestamps() fire — a host joined on two interfaces receives every
* unfragmented update twice, and the original always sets takeHrt. */
/* In lockstep with lastAccHrt, and for the same reason: lastAccHrt is
* the numerator of the next packet's period and these two are its
* denominator, so only a packet that defines the new front of producer
* time may move any of them. Advancing the counter alone on a reordered
* datagram halves the next packet's spacing; see the gap comment above.
* prevAccCount is in the group because cycles multiplies the two
* together — it means "cycles spanned by the reference packet", and
* since Accumulate flushes on a timer the count really does vary
* between packets. Leaving a counter behind at all is what lets the
* duplicate-datagram guard at the top of timestamps() fire — a host
* joined on two interfaces receives every unfragmented update twice,
* and the original always sets takeHrt. */
if (takeHrt) {
st.lastCounter = f.counter;
st.counterValid = true;
st.prevAccCount = nElems;
}
st.lastEmittedValid = true;
return true;