fixed issue on udpstreamer trigger logic
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -58,11 +58,15 @@
|
||||
* CONSUMING signal, where UDPSourceSession.cpp:538 and :516 key it on the
|
||||
* time-signal index, so signals sharing a time signal share an offset there and
|
||||
* not here — immaterial, since the mapping they compute is the same. And a
|
||||
* FIRST_SAMPLE/LAST_SAMPLE signal whose time signal is absent falls through to
|
||||
* rule 4 rather than using its declared rate; that is a malformed CONFIG, and
|
||||
* spanning arrivals is the more honest answer than trusting a rate whose anchor
|
||||
* never arrived. Neither this list nor the three above is closed: any other
|
||||
* difference you find is unexamined, not sanctioned.
|
||||
* FIRST_SAMPLE/LAST_SAMPLE signal of DECLARED ARRAY shape whose time signal is
|
||||
* absent falls through to rule 4 rather than using its declared rate; that is a
|
||||
* malformed CONFIG, and spanning arrivals is the more honest answer than
|
||||
* trusting a rate whose anchor never arrived. Note the shape qualifier: an
|
||||
* accumulated SCALAR is declared with one element, so a frame that carries no
|
||||
* time signal for it routes to rule 3, which does use the declared rate — the
|
||||
* normal case, since Accumulate flushes per signal on a timer. Neither this list
|
||||
* nor the three above is closed: any other difference you find is unexamined,
|
||||
* not sanctioned.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
@@ -100,7 +104,16 @@ private:
|
||||
std::vector<double>& tsOut);
|
||||
|
||||
struct SigState {
|
||||
/** Rules 1 and 2: maps the TIME SIGNAL's epoch onto the wall clock. */
|
||||
ClockOffset offset;
|
||||
/** Rule 3's hrt branch, which maps a different epoch — accProdSec counts
|
||||
* from this signal's first usable packet, not from whatever zero the
|
||||
* time signal uses. One signal can reach both: a declared scalar with
|
||||
* FIRST_SAMPLE takes rule 2 in frames where its time signal carries an
|
||||
* element and rule 3 in frames where it does not, which Accumulate's
|
||||
* per-signal timer flushing makes ordinary. Sharing one ClockOffset
|
||||
* across the two forces a re-latch on every alternation. */
|
||||
ClockOffset accOffset;
|
||||
double lastPacketWall = 0.0;
|
||||
bool lastPacketValid = false;
|
||||
/** Raw ticks, not seconds — see the comment in the samplingRate == 0
|
||||
|
||||
@@ -184,6 +184,84 @@ TEST(FrameDecoder, FirstSampleWithNoRateSpreadsFromConsecutiveAnchors) {
|
||||
EXPECT_NEAR(ts[1] - ts[0], 0.001, 1e-9) << "loss stretched the array";
|
||||
}
|
||||
|
||||
// Rule 2 faces the same reorder-or-restart question as rule 3, asked of the time
|
||||
// signal rather than of hrt, and the two halves need opposite answers. A late
|
||||
// datagram is DROPPED: its anchor is genuine producer time, so emitting it would
|
||||
// place a whole array before stamps already handed out, and this rule keeps no
|
||||
// emitted chain to clamp it against. Dropping also protects the next packet,
|
||||
// which would otherwise divide one packet's worth of anchor difference by a
|
||||
// counter gap of two and halve its spacing.
|
||||
TEST(FrameDecoder, FirstSampleDropsAReorderedAnchor) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({burst("Sine", kTimeFirstSample, 0.0, 4, 1),
|
||||
timeSignal("Time", 1)});
|
||||
|
||||
/* Anchors 4 ms apart, packets 2 and 3 delivered in the opposite order. */
|
||||
const int order[5] = {0, 1, 3, 2, 4};
|
||||
double lastEnd = 0.0;
|
||||
bool seen = false;
|
||||
std::vector<double> ts;
|
||||
for (int slot = 0; slot < 5; slot++) {
|
||||
const int p = order[slot];
|
||||
FrameBuilder fb;
|
||||
fb.addSignal({1.0, 2.0, 3.0, 4.0});
|
||||
fb.addSignal({7.0e9 + p * 4.0e6});
|
||||
const FrameView& f = fb.build(0, 2000.0 + slot * 0.004, 4,
|
||||
static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
if (!dec.timestamps(f, 0, ts)) { continue; }
|
||||
|
||||
ASSERT_EQ(ts.size(), 4u);
|
||||
if (seen) {
|
||||
EXPECT_GT(ts[0], lastEnd)
|
||||
<< "slot " << slot << " stepped back " << (lastEnd - ts[0]) << " s";
|
||||
}
|
||||
if (slot > 0) {
|
||||
EXPECT_NEAR(ts[1] - ts[0], 0.001, 1e-9) << "slot " << slot;
|
||||
}
|
||||
lastEnd = ts[3];
|
||||
seen = true;
|
||||
}
|
||||
}
|
||||
|
||||
// The counterweight, exactly as in rule 3: a restart drops the time signal to a
|
||||
// fresh epoch, and refusing every backward anchor would then freeze the anchor
|
||||
// pair for the rest of the session — no period could ever be measured again and
|
||||
// the rule would run on whatever it derived before the restart.
|
||||
TEST(FrameDecoder, FirstSampleRebasesAfterAProducerRestart) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({burst("Sine", kTimeFirstSample, 0.0, 4, 1),
|
||||
timeSignal("Time", 1)});
|
||||
|
||||
std::vector<double> ts;
|
||||
for (int p = 0; p < 5; p++) {
|
||||
FrameBuilder fb;
|
||||
fb.addSignal({1.0, 2.0, 3.0, 4.0});
|
||||
fb.addSignal({7.0e9 + p * 4.0e6});
|
||||
const FrameView& f = fb.build(0, 2000.0 + p * 0.004, 4,
|
||||
static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
ASSERT_TRUE(dec.timestamps(f, 0, ts));
|
||||
}
|
||||
|
||||
/* Producer restarts: the time signal comes back near zero, and the counter
|
||||
* with it. Anchors now step 2 ms, a different period from before. */
|
||||
for (int p = 0; p < 5; p++) {
|
||||
FrameBuilder fb;
|
||||
fb.addSignal({1.0, 2.0, 3.0, 4.0});
|
||||
fb.addSignal({0.5e9 + p * 2.0e6});
|
||||
const FrameView& f = fb.build(0, 2000.020 + p * 0.002, 4,
|
||||
static_cast<uint32_t>(p + 1 + 100));
|
||||
dec.beginFrame(f);
|
||||
ASSERT_TRUE(dec.timestamps(f, 0, ts)) << "restart packet " << p;
|
||||
/* From the second post-restart packet the NEW period must be measured;
|
||||
* a frozen anchor pair would keep returning the old 1 ms. */
|
||||
if (p > 0) {
|
||||
EXPECT_NEAR(ts[1] - ts[0], 0.0005, 1e-9) << "restart packet " << p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FrameDecoder, LastSampleAnchorsTheFinalElementAndCountsBackward) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({burst("Sine", kTimeLastSample, 1000.0, 4, 1),
|
||||
@@ -311,6 +389,48 @@ TEST(FrameDecoder, AccumulatedScalarReinstatesLostPacketsFromTheCounterGap) {
|
||||
EXPECT_NEAR(ts[9], 501.100, 1e-9);
|
||||
}
|
||||
|
||||
// The lockstep rule applies to the declared branch too, and there the damage is
|
||||
// permanent rather than transient. A late datagram that rolls lastCounter back
|
||||
// gives the NEXT packet a gap of dist+1, so `lost` reinstates dist bursts of
|
||||
// duration that were never lost. Unlike the absurd gap a reorder itself
|
||||
// produces, that prediction is close enough to arrival to pass the resync
|
||||
// backstop and be accepted — after which the chain is self-consistent, base
|
||||
// stays above lastEmittedEnd, the squeeze never fires and nothing ever pulls it
|
||||
// back. Measured +10 ms at distance 1 and +200 ms at distance 20, held to the
|
||||
// end of the session.
|
||||
TEST(FrameDecoder, AccumulatedScalarKeepsItsChainThroughAReorder) {
|
||||
for (uint32_t dist : {uint32_t(1), uint32_t(20)}) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({accSignal()});
|
||||
|
||||
/* 300 contiguous 10-sample bursts at 1 kHz, one payload delayed by
|
||||
* `dist` delivery slots at packet 150. Arrival times belong to the slot;
|
||||
* only the counter travels, since the declared branch reads no hrt. */
|
||||
std::vector<uint32_t> counters(300);
|
||||
for (uint32_t p = 0; p < 300u; p++) { counters[p] = p + 1u; }
|
||||
for (uint32_t i = 0; i < dist; i++) {
|
||||
std::swap(counters[150u + i], counters[151u + i]);
|
||||
}
|
||||
|
||||
std::vector<double> ts;
|
||||
double lastArrival = 0.0;
|
||||
for (uint32_t p = 0; p < 300u; p++) {
|
||||
FrameBuilder fb;
|
||||
fb.addSignal(std::vector<double>(10, 1.0));
|
||||
lastArrival = 500.0 + p * 0.010;
|
||||
const FrameView& f = fb.build(0, lastArrival, 10, counters[p]);
|
||||
dec.beginFrame(f);
|
||||
if (!dec.timestamps(f, 0, ts)) { continue; }
|
||||
}
|
||||
|
||||
/* A healthy chain ends its burst on the moment the packet landed, less
|
||||
* the burst it spans. */
|
||||
EXPECT_NEAR(ts[9], lastArrival, 1.0e-6)
|
||||
<< "distance " << dist << " left the chain "
|
||||
<< (ts[9] - lastArrival) << " s ahead for good";
|
||||
}
|
||||
}
|
||||
|
||||
// A producer that never advances the counter, or restarts it, leaves nothing to
|
||||
// reconstruct from. Arrival time is then the better of two bad answers, and the
|
||||
// chain has to be abandoned rather than left to drift forever.
|
||||
@@ -810,20 +930,29 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarIgnoresReorderedDatagrams) {
|
||||
// squeezes bursts, because the late datagram's samples belong in the past and
|
||||
// downstream demands increasing stamps, so the monotonic clamp walks them
|
||||
// forward instead — and the timeline it leaves ahead of the producer takes a few
|
||||
// packets to bleed off, squeezing those too. But that clamp has an exact floor:
|
||||
// its cap is kWallBleedFraction * wallElapsed / nElems, and wallElapsed / nElems
|
||||
// IS the producer's true period at steady cadence, so no burst it touches can
|
||||
// ever be narrower than kWallBleedFraction of true. Anything below that floor
|
||||
// did not come from the clamp; it came from a mis-derived period. That is what
|
||||
// separates the defect from the design, and it is why the check is a floor
|
||||
// rather than a target.
|
||||
// packets to bleed off, squeezing those too. The clamp's cap is
|
||||
// kWallBleedFraction * wallElapsed / nElems, so on THIS schedule — uniform 25 ms
|
||||
// arrivals, where wallElapsed / nElems is exactly the producer's period — no
|
||||
// burst it touches can come out below 0.5x true. A burst narrower than that did
|
||||
// not come from the clamp; it came from a mis-derived period.
|
||||
//
|
||||
// That floor is a property of the SCHEDULE, not of the decoder, and must not be
|
||||
// read as a general invariant: the cap is proportional to the ARRIVAL gap, and
|
||||
// the premise of this whole file is that arrival gaps are not uniform. Under a
|
||||
// drained delivery — a reorder plus a queue flushed at 100 us per datagram — a
|
||||
// clamped burst measures 0.002x true, and that is the design working as
|
||||
// intended, because the lead can only bleed off against wall time that has
|
||||
// really passed. Keep the arrivals here uniform, or the bound stops meaning
|
||||
// anything.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingAfterAReorder) {
|
||||
const double trueDt = 0.0025; /* 10 samples per 25 ms packet */
|
||||
const double floorDt = 0.5 * trueDt; /* kWallBleedFraction * trueDt */
|
||||
|
||||
/* Distance 1 sits exactly ON the floor either way and is here to pin it;
|
||||
* 5 and 20 are where the defect drops through it, to 0.167x and 0.048x. */
|
||||
for (size_t dist : {size_t(1), size_t(5), size_t(20)}) {
|
||||
/* Distance 1 is deliberately absent: the defect and the clamp both yield
|
||||
* exactly 0.5x there, so it discriminates nothing while sitting on the
|
||||
* assertion boundary with zero margin. 5 and 20 drop through the floor, to
|
||||
* 0.167x and 0.048x. */
|
||||
for (size_t dist : {size_t(5), size_t(20)}) {
|
||||
const std::vector<std::vector<double> > out =
|
||||
runUndeclaredPerPacket(delayOne(cleanUndeclaredStream(), 150, dist));
|
||||
|
||||
@@ -840,9 +969,10 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingAfterAReorder) {
|
||||
}
|
||||
|
||||
// The same defect under a network that reorders continuously rather than once.
|
||||
// Same floor, applied to every burst in the run including the late datagrams'
|
||||
// own — under sustained reordering there is no quiet packet to exempt, and the
|
||||
// floor holds for all of them anyway.
|
||||
// Same floor and the same caveat about it being a property of these uniform
|
||||
// arrivals, applied to every burst in the run including the late datagrams' own
|
||||
// — under sustained reordering there is no quiet packet to exempt, and the floor
|
||||
// holds for all of them anyway.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingUnderSustainedReordering) {
|
||||
std::vector<HrtPacket> pkts = cleanUndeclaredStream();
|
||||
|
||||
@@ -1233,6 +1363,112 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarSurvivesAStrayZeroHrtPacket) {
|
||||
}
|
||||
}
|
||||
|
||||
// prevAccCount is the third member of the lockstep group and the one easiest to
|
||||
// miss, because every other test in this file sends bursts of a fixed length,
|
||||
// which makes it invisible. It is the OTHER factor of the denominator —
|
||||
// cycles = prevAccCount * gap — and it means "cycles spanned by the reference
|
||||
// packet", so a packet that cannot become the reference must not set it either.
|
||||
// Accumulate flushes on a TIMER, so a short packet is ordinary rather than
|
||||
// exotic: a 2-sample stray with hrt == 0 that moved prevAccCount alone left the
|
||||
// next real burst dividing 25 ms of ticks by 2 cycles instead of 10, drawing it
|
||||
// five times too wide and — burst anchored on its last element — ending it
|
||||
// +90 ms in the future.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarSurvivesAShortStrayZeroHrtPacket) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({undeclaredAcc()});
|
||||
|
||||
const double ticks = 1.0e9;
|
||||
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
|
||||
const double packetSec = 0.025;
|
||||
|
||||
for (int p = 0; p < 200; p++) {
|
||||
/* The stray carries two samples, not ten: a short RT cycle flushed by
|
||||
* the timer, with no hrt on it. */
|
||||
const bool stray = (p == 153);
|
||||
const uint32_t nElems = stray ? 2u : 10u;
|
||||
|
||||
FrameBuilder fb;
|
||||
fb.addSignal(std::vector<double>(nElems, 1.0));
|
||||
const uint64_t hrt = stray
|
||||
? 0u
|
||||
: bootHrt + static_cast<uint64_t>(p * packetSec * ticks);
|
||||
const double arrival = 700.0 + p * packetSec;
|
||||
const FrameView& f =
|
||||
fb.build(hrt, arrival, nElems, static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
std::vector<double> ts;
|
||||
if (!dec.timestamps(f, 0, ts)) { continue; }
|
||||
|
||||
/* The packet after the stray is the one that divides by the reference,
|
||||
* so check every burst from there on: a burst drawn too wide ends in the
|
||||
* future and then takes several squeezed bursts to bleed back. */
|
||||
if (p > 100) {
|
||||
EXPECT_NEAR(ts.back(), arrival, packetSec / 5.0)
|
||||
<< "at packet " << p << " (" << ts.size() << " samples)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One signal can reach both rule 2 and rule 3's hrt branch, and they map
|
||||
// DIFFERENT epochs: rule 2 maps the time signal's own zero, the hrt branch maps
|
||||
// accProdSec, which counts from this signal's first usable packet. A declared
|
||||
// scalar with FIRST_SAMPLE alternates between them whenever its time signal is
|
||||
// empty in some frames — ordinary, since Accumulate flushes per signal on a
|
||||
// timer. Sharing one ClockOffset across the two puts the epochs' whole
|
||||
// difference through the recalibration threshold on every single alternation.
|
||||
TEST(FrameDecoder, AlternatingBetweenTheAnchorAndHrtRulesDoesNotRelatch) {
|
||||
SignalMeta acc;
|
||||
acc.name = "Acc";
|
||||
acc.typeCode = 9;
|
||||
acc.numRows = 1; /* scalar: rule 3 is reachable */
|
||||
acc.timeMode = kTimeFirstSample; /* rule 2 is reachable too */
|
||||
acc.samplingRate = 0.0; /* rule 3 takes the hrt branch */
|
||||
acc.timeSignalIdx = 1;
|
||||
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({acc, timeSignal("Time", 1)});
|
||||
|
||||
const double ticks = 1.0e9;
|
||||
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
|
||||
double last = 0.0;
|
||||
bool seen = false;
|
||||
|
||||
for (int p = 0; p < 120; p++) {
|
||||
FrameBuilder fb;
|
||||
fb.addSignal(std::vector<double>(10, 1.0));
|
||||
/* Even packets carry the anchor (epoch 5 s); odd packets do not, and
|
||||
* fall through to the hrt branch (epoch 0 s at first packet). */
|
||||
if (p % 2 == 0) {
|
||||
fb.addSignal({5.0e9 + p * 0.025e9});
|
||||
} else {
|
||||
fb.addSignal({});
|
||||
}
|
||||
const double arrival = 700.0 + p * 0.025;
|
||||
const FrameView& f =
|
||||
fb.build(bootHrt + static_cast<uint64_t>(p * 0.025 * ticks),
|
||||
arrival, 10, static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
std::vector<double> ts;
|
||||
if (!dec.timestamps(f, 0, ts)) { continue; }
|
||||
|
||||
/* Packet 0 is rule 2's legal first-packet stack — no predecessor to
|
||||
* measure a period from — so ordering is only checked from packet 1. */
|
||||
for (double t : ts) {
|
||||
if (seen && p > 0) {
|
||||
ASSERT_GT(t, last) << "packet " << p << " stepped back "
|
||||
<< (last - t) << " s";
|
||||
}
|
||||
last = t;
|
||||
seen = true;
|
||||
}
|
||||
/* Neither rule may be dragged onto the other's epoch: both must stay
|
||||
* within a packet of the moment the datagram landed. */
|
||||
if (p > 40) {
|
||||
EXPECT_NEAR(ts.back(), arrival, 0.025) << "at packet " << p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The same double delivery that the declared branch guards against — a host
|
||||
// joined on two interfaces receives every unfragmented update twice — reaches an
|
||||
// undeclared-rate signal identically. The guard can only fire if this branch
|
||||
|
||||
Reference in New Issue
Block a user