fix(udpscope): stop a wrong hrtDt from displacing the trace permanently
On the hrt branch the derived period is not just a spacing: it is the burst width ClockOffset latches against, so a wrong one shifts the whole trace by an amount that is usually too small for kRecalibThresholdS to ever heal. Three routes to a wrong period were open. Packet loss. elapsed spans every packet since the last one seen, but it was divided by prevAccCount alone, so a lost datagram scaled the period by the whole counter gap. Since a burst is anchored on its LAST element, too wide means it ends in the FUTURE: +22.5 ms for one loss, +225 ms for ten, at 10 samples per 25 ms packet, mis-spacing 2.7% of all samples at 1% loss. The declared branch already reads the counter for exactly this; the hrt branch now does too. Producer restart and reorder. Both leave elapsed at zero, so no period can be measured -- and the restart packet is also the one that re-latches after offset.reset(). Falling back to kDefaultDt is only right at 1 kHz; measured standing displacement was +13.5 ms at 10 samples per 25 ms and -89 ms at 100 per 10 ms. Remember the last measured period instead. A stray hrt == 0 packet re-enters the warm-up branch, which spans from packetBurst's lastPacketWall -- a field the hrt branch never wrote, so it still held the start of the session. After 153 packets that emitted a burst 3.8 s in the past, worse the longer the scope had run. Also: rule 2 with no declared rate stacked every element of the array on one instant (as UDPSourceSession.cpp:522 does, harmlessly, for a host-local consumer). Spread it from consecutive time-signal anchors, which measure the burst on the producer's own clock. Reverts the previous commit's wallElapsed <= 0 change: it was measurably inert -- the step floor two lines below already yields the same number -- and its comment claimed a divergence it did not stop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3add2c42b9
commit
f97fd825c4
@@ -172,9 +172,36 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
/* Rule 2: anchor from the time signal, spread by the sampling rate. */
|
||||
if ((d.timeMode == kTimeFirstSample || d.timeMode == kTimeLastSample) &&
|
||||
hasTimeSig && f.counts[tIdx] >= 1u && f.values[tIdx] != nullptr) {
|
||||
const double anchor = st.offset.map(f.values[tIdx][0] * tScale, wallNow);
|
||||
const double rate = DeclaredRate(d.samplingRate);
|
||||
const double dt = (rate > 0.0) ? (1.0 / rate) : 0.0;
|
||||
const double prodSec = f.values[tIdx][0] * tScale;
|
||||
const double anchor = st.offset.map(prodSec, wallNow);
|
||||
const double rate = DeclaredRate(d.samplingRate);
|
||||
double dt = (rate > 0.0) ? (1.0 / rate) : 0.0;
|
||||
|
||||
/* No rate declared. UDPSourceSession.cpp:522 leaves dt at zero here,
|
||||
* which stacks every element of the array on one instant — harmless for
|
||||
* a host-local consumer that only stores them, but this scope's ring,
|
||||
* decimator and trigger all require a signal's stamps to increase, and a
|
||||
* plot of N points at one X is not a trace.
|
||||
*
|
||||
* The spread is recoverable without a rate: consecutive anchors come
|
||||
* from the time signal, so their difference is the burst's true duration
|
||||
* in producer seconds, measured on the producer's own clock rather than
|
||||
* on arrival — immune to the bursty delivery that corrupts everything
|
||||
* arrival-derived. Divide by the counter gap for the same reason rule 3
|
||||
* does: a lost datagram widens the anchor difference without widening
|
||||
* the array. Until a second packet arrives there is nothing to measure
|
||||
* and the elements do stack; that is one packet, not the whole run. */
|
||||
if (!(dt > 0.0) && nElems > 1u && st.prevAnchorValid &&
|
||||
prodSec > st.prevAnchorProdSec) {
|
||||
const uint32_t gap = (f.counter != 0u && f.counter > st.lastCounter)
|
||||
? (f.counter - st.lastCounter) : 1u;
|
||||
dt = (prodSec - st.prevAnchorProdSec) /
|
||||
(static_cast<double>(nElems) * static_cast<double>(gap));
|
||||
}
|
||||
st.prevAnchorProdSec = prodSec;
|
||||
st.prevAnchorValid = true;
|
||||
st.lastCounter = f.counter;
|
||||
|
||||
tsOut.resize(nElems);
|
||||
for (uint32_t e = 0; e < nElems; e++) {
|
||||
tsOut[e] = (d.timeMode == kTimeFirstSample)
|
||||
@@ -329,24 +356,24 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
double advance = nominal * factor;
|
||||
|
||||
/* A non-positive elapsed means the wall has not moved
|
||||
* since this signal's previous burst — a coarse arrival
|
||||
* clock, or two packets stamped within one tick of it.
|
||||
* There is no wall time to spend, so the cap is zero.
|
||||
* Skipping the cap in that case (which is what this code
|
||||
* used to do) hands back the full proportional advance,
|
||||
* so a run of same-tick arrivals gains lead while no wall
|
||||
* time passes at all — the divergence the cap exists to
|
||||
* stop, in its purest form. */
|
||||
* since this signal's previous burst. Skipping the cap
|
||||
* then is deliberate and, more to the point, makes no
|
||||
* difference: forcing the cap to zero instead sends step
|
||||
* through the floor below to dt * kMinBleedFactor, which
|
||||
* is the same number the proportional factor already
|
||||
* yields once the excess exceeds one burst. Both leave
|
||||
* the same-tick case diverging; only real elapsed wall
|
||||
* time can bleed lead off, and a recv_time from
|
||||
* CLOCK_REALTIME (udps_client.c:120) does not repeat. */
|
||||
const double wallElapsed = wallNow - st.lastEmittedWall;
|
||||
const double cap = (wallElapsed > 0.0)
|
||||
? (kWallBleedFraction * wallElapsed)
|
||||
: 0.0;
|
||||
if (cap < advance) { advance = cap; }
|
||||
if (wallElapsed > 0.0) {
|
||||
const double cap = kWallBleedFraction * wallElapsed;
|
||||
if (cap < advance) { advance = cap; }
|
||||
}
|
||||
step = advance / static_cast<double>(nElems);
|
||||
/* Reached whenever the cap is zero, and a backstop
|
||||
* against a nonsensical dt off the wire: downstream
|
||||
* requires strictly increasing stamps, so the burst must
|
||||
* still advance by something. */
|
||||
/* Unreachable with a finite positive dt — kept because
|
||||
* downstream monotonicity must not depend on that
|
||||
* argument holding for every value off the wire. */
|
||||
if (!(step > 0.0)) { step = dt * kMinBleedFactor; }
|
||||
base = st.lastEmittedEnd + step;
|
||||
}
|
||||
@@ -459,11 +486,43 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
}
|
||||
st.accProdSec += elapsed;
|
||||
|
||||
/* The flushes carry contiguous RT cycles, so the gap divided by the
|
||||
* previous packet's sample count is exactly one cycle period. */
|
||||
const double hrtDt = (elapsed > 0.0 && st.prevAccCount > 0u)
|
||||
? (elapsed / static_cast<double>(st.prevAccCount))
|
||||
: kDefaultDt;
|
||||
/* The flushes carry contiguous RT cycles, so the tick gap divided by the
|
||||
* number of cycles it spans is exactly one cycle period. That count is
|
||||
* NOT prevAccCount: elapsed spans every packet since the last one we
|
||||
* saw, so a lost datagram makes the tick gap wider without making
|
||||
* prevAccCount larger. Dividing by prevAccCount alone therefore returns
|
||||
* a period scaled by the whole counter gap — 2x for one lost datagram,
|
||||
* 11x for ten — which draws the recovery burst that many times too wide
|
||||
* and, because the burst is anchored on its LAST element, ends it in the
|
||||
* FUTURE (measured: +22.5 ms for one loss, +225 ms for ten, at 10
|
||||
* samples per 25 ms packet). At 1% loss that mis-spaced 2.7% of all
|
||||
* samples. The declared branch already reads the counter for exactly
|
||||
* this purpose (`lost`, above); the hrt branch must too.
|
||||
*
|
||||
* Only a FORWARD gap counts. A backward or repeated counter is the
|
||||
* reorder case handled above, where elapsed is zero anyway. */
|
||||
const uint32_t accGap = (f.counter != 0u && st.lastEmittedValid &&
|
||||
f.counter > st.lastCounter)
|
||||
? (f.counter - st.lastCounter) : 1u;
|
||||
const double cycles = static_cast<double>(st.prevAccCount) *
|
||||
static_cast<double>(accGap);
|
||||
|
||||
/* Falling back to kDefaultDt is a last resort, not a default: see
|
||||
* SigState::lastHrtDt. The fallback is reached on the first hrt packet
|
||||
* of a producer restart (elapsed is zero because hrt went backwards) and
|
||||
* on a reordered datagram, and in both cases the wrong burst width is
|
||||
* latched into ClockOffset permanently — measured 13.5 ms of standing
|
||||
* displacement at 10 samples per 25 ms packet, 89 ms at 100 per 10 ms,
|
||||
* both below kRecalibThresholdS and so never corrected. */
|
||||
double hrtDt;
|
||||
if (elapsed > 0.0 && cycles > 0.0) {
|
||||
hrtDt = elapsed / cycles;
|
||||
st.lastHrtDt = hrtDt;
|
||||
} else if (st.lastHrtDt > 0.0) {
|
||||
hrtDt = st.lastHrtDt;
|
||||
} else {
|
||||
hrtDt = kDefaultDt;
|
||||
}
|
||||
|
||||
/* Anchor the burst's LAST element on arrival, not its first. The
|
||||
* packet's hrt is the tick count of sample 0 (UDPSourceSession.cpp:574),
|
||||
@@ -495,16 +554,11 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
* at kWallBleedFraction: only that makes the lead bleed off. */
|
||||
if (st.lastEmittedValid && base <= st.lastEmittedEnd) {
|
||||
const double wallElapsed = wallNow - st.lastEmittedWall;
|
||||
/* No wall movement, no wall time to spend: see the same cap in the
|
||||
* declared branch. Zero rather than "skip the cap", so a run of
|
||||
* same-tick arrivals cannot advance a full hrtDt per sample while
|
||||
* the wall stands still. */
|
||||
const double cap = (wallElapsed > 0.0)
|
||||
? (kWallBleedFraction * wallElapsed /
|
||||
static_cast<double>(nElems))
|
||||
: 0.0;
|
||||
if (cap < step) { step = cap; }
|
||||
if (!(step > 0.0)) { step = hrtDt * kMinBleedFactor; }
|
||||
if (wallElapsed > 0.0) {
|
||||
const double cap = kWallBleedFraction * wallElapsed /
|
||||
static_cast<double>(nElems);
|
||||
if (cap < step) { step = cap; }
|
||||
}
|
||||
base = st.lastEmittedEnd + step;
|
||||
}
|
||||
|
||||
@@ -517,6 +571,14 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
st.prevAccCount = nElems;
|
||||
st.lastEmittedEnd = tsOut[nElems - 1u];
|
||||
st.lastEmittedWall = wallNow;
|
||||
/* Keep packetBurst's reference current even though this branch does not
|
||||
* use it. A single packet with hrt == 0 re-enters the warm-up branch
|
||||
* above, and packetBurst would otherwise span from whenever this signal
|
||||
* last took that branch — the whole session. Measured: after 153 hrt
|
||||
* packets, one zero-hrt packet emitted a burst starting 3.8 s in the
|
||||
* past, growing without bound with session length. */
|
||||
st.lastPacketWall = wallNow;
|
||||
st.lastPacketValid = true;
|
||||
/* Same duplicate-datagram exposure as the declared branch: a host joined
|
||||
* on two interfaces receives every unfragmented update twice, and the
|
||||
* guard at the top of timestamps() can only fire if this branch leaves a
|
||||
|
||||
@@ -10,7 +10,17 @@
|
||||
* Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and
|
||||
* solves it; these are the same rules, computed from udps_frame_t's own fields.
|
||||
*
|
||||
* Three rules deliberately differ, all in the accumulated-scalar case (rule 3).
|
||||
* They are NOT the same code, and the differences are not a short list. Every
|
||||
* one of them comes from the same root: StreamHub runs on the producer's host,
|
||||
* so its arrival time IS the producer's clock and its local
|
||||
* HighResolutionTimer::Frequency() IS the frequency behind the packet's hrt.
|
||||
* Neither holds over a network, so anything StreamHub can read directly this
|
||||
* decoder has to estimate (HrtRateFit, ClockOffset), and anything it estimates
|
||||
* it must also defend — hence the monotonic clamps, the kWallBleedFraction
|
||||
* bleed, the reorder and restart guards and the duplicate-datagram drop, none of
|
||||
* which exist in UDPSourceSession.cpp. Do not read the three sections below as
|
||||
* exhaustive; they are the three that change where a sample LANDS, and so the
|
||||
* three worth checking first when a trace looks wrong.
|
||||
*
|
||||
* First, the anchor. StreamHub anchors every accumulated-scalar burst on the
|
||||
* packet's own hrt, converted with the LOCAL MARTe HighResolutionTimer
|
||||
@@ -42,6 +52,15 @@
|
||||
* 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.
|
||||
*
|
||||
* Two divergences OUTSIDE rule 3 are known and deliberately left as they are.
|
||||
* Rule 1 keys ClockOffset on the consuming signal, where UDPSourceSession.cpp:516
|
||||
* keys 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
@@ -92,6 +111,19 @@ private:
|
||||
double accProdSec = 0.0;
|
||||
bool lastAccValid = false;
|
||||
uint32_t prevAccCount = 0;
|
||||
/** Last inter-element period the hrt branch actually MEASURED, used
|
||||
* whenever this packet cannot measure one of its own (no previous tick,
|
||||
* or hrt went backwards). The constant kDefaultDt is a poor substitute:
|
||||
* it is only right at 1 kHz, and a wrong period here is not merely a
|
||||
* wrong spacing for one burst — it is the burst width ClockOffset
|
||||
* latches against, and the resulting displacement is usually too small
|
||||
* for kRecalibThresholdS to ever heal. Zero until first measured. */
|
||||
double lastHrtDt = 0.0;
|
||||
/** Rule 2 only: the previous packet's time-signal anchor, in PRODUCER
|
||||
* seconds. Consecutive anchors are what lets an array with no declared
|
||||
* sampling rate be spread at all. */
|
||||
double prevAnchorProdSec = 0.0;
|
||||
bool prevAnchorValid = false;
|
||||
/** 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
|
||||
|
||||
@@ -107,6 +107,49 @@ TEST(FrameDecoder, FirstSampleAnchorsElementZeroAndCountsForward) {
|
||||
EXPECT_NEAR(ts[3], 2000.003, 1e-9);
|
||||
}
|
||||
|
||||
// With no declared rate there is nothing to spread the array by, and
|
||||
// UDPSourceSession.cpp:522 leaves the step at zero — every element of the array
|
||||
// on one instant. A host-local consumer only stores them; this scope's ring,
|
||||
// decimator and trigger all require increasing stamps, and N points at one X is
|
||||
// not a trace. Consecutive time-signal anchors carry the burst duration on the
|
||||
// PRODUCER'S clock, so the spread is recoverable without a rate.
|
||||
TEST(FrameDecoder, FirstSampleWithNoRateSpreadsFromConsecutiveAnchors) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({burst("Sine", kTimeFirstSample, 0.0, 4, 1),
|
||||
timeSignal("Time", 1)});
|
||||
|
||||
/* 4 samples per packet, anchors 4 ms apart: a 1 ms period. Arrivals are
|
||||
* jittered so a spread accidentally taken from arrival would be visible. */
|
||||
const double jitter[4] = {0.0, 0.0021, -0.0017, 0.0};
|
||||
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}); /* ns, +4 ms per packet */
|
||||
const FrameView& f = fb.build(0, 2000.0 + p * 0.004 + jitter[p % 4], 4,
|
||||
static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
ASSERT_TRUE(dec.timestamps(f, 0, ts));
|
||||
ASSERT_EQ(ts.size(), 4u);
|
||||
for (size_t i = 1; i < ts.size(); i++) {
|
||||
/* The first packet has no predecessor to measure against and legally
|
||||
* stacks; from the second on the array must be spread. */
|
||||
if (p > 0) { ASSERT_GT(ts[i], ts[i - 1]) << "packet " << p; }
|
||||
}
|
||||
if (p > 0) { EXPECT_NEAR(ts[1] - ts[0], 0.001, 1e-9) << "packet " << p; }
|
||||
}
|
||||
|
||||
/* A lost datagram doubles the anchor difference; without reading the counter
|
||||
* the recovery packet would be spread twice as wide. */
|
||||
FrameBuilder fb;
|
||||
fb.addSignal({1.0, 2.0, 3.0, 4.0});
|
||||
fb.addSignal({7.0e9 + 5 * 4.0e6 + 4.0e6}); /* packet 6 arrives, 5 lost */
|
||||
const FrameView& f = fb.build(0, 2000.024, 4, 7u);
|
||||
dec.beginFrame(f);
|
||||
ASSERT_TRUE(dec.timestamps(f, 0, ts));
|
||||
EXPECT_NEAR(ts[1] - ts[0], 0.001, 1e-9) << "loss stretched the array";
|
||||
}
|
||||
|
||||
TEST(FrameDecoder, LastSampleAnchorsTheFinalElementAndCountsBackward) {
|
||||
FrameDecoder dec;
|
||||
dec.setSignals({burst("Sine", kTimeLastSample, 1000.0, 4, 1),
|
||||
@@ -795,10 +838,13 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarEndsItsBurstOnArrival) {
|
||||
|
||||
const double ticks = 1.0e9;
|
||||
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
|
||||
/* 10 ms per packet of 10 samples, so the derived period is 1 ms — equal to
|
||||
* the fallback the very first hrt-branch packet has to use, which is what
|
||||
* ClockOffset latches against. Any other period would bake that one packet's
|
||||
* fallback into the offset and blur the convention this test is pinning. */
|
||||
/* 10 ms per packet of 10 samples. The cadence used to matter — the first
|
||||
* hrt-branch packet had no measurable interval, latched ClockOffset using
|
||||
* kDefaultDt, and only a 1 ms derived period made that harmless — but the
|
||||
* warm-up now hands over a real tick reference, so this assertion holds at
|
||||
* every cadence. See UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly,
|
||||
* which is the test that pins that down; this one only fixes the convention
|
||||
* that a burst ends, rather than starts, on arrival. */
|
||||
double lastArrival = 0.0;
|
||||
std::vector<double> last;
|
||||
for (int p = 0; p < 60; p++) {
|
||||
@@ -822,10 +868,14 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarEndsItsBurstOnArrival) {
|
||||
// packetBurst spans arrival gaps until HrtRateFit has collected enough packets,
|
||||
// then the hrt branch takes over. They place a burst differently — packetBurst
|
||||
// ends it at wallNow, the hrt branch at wallNow - (nElems-1)*hrtDt — so the
|
||||
// handover is where a discontinuity hides. It is invisible at 10 samples per
|
||||
// 10 ms packet, the one cadence where the derived period equals the kDefaultDt
|
||||
// fallback, which is exactly why the other tests here could not see it. Sweep
|
||||
// cadences either side of that coincidence.
|
||||
// handover is where a discontinuity hides, and it took two separate blind spots
|
||||
// for the other tests to miss it. UndeclaredAccumulatedScalarEndsItsBurstOnArrival
|
||||
// runs at 10 samples per 10 ms, the one cadence where the derived period equals
|
||||
// the kDefaultDt fallback, so nothing was wrong to see. The two long-run tests
|
||||
// run at 10 samples per 25 ms, where the fallback burst is 9 ms against a 25 ms
|
||||
// packet interval — too narrow to invert, so their monotonicity assertions held
|
||||
// while the trace sat 13.5 ms off the wall clock, which neither of them measures.
|
||||
// So sweep cadences either side of the coincidence AND assert absolute position.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly) {
|
||||
struct Case { uint32_t nElems; double packetSec; };
|
||||
const Case cases[] = {
|
||||
@@ -883,6 +933,146 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lost datagrams widen the hrt tick gap without widening the sample count that
|
||||
// gap is divided by, so a recovery burst is drawn as many times too wide as the
|
||||
// counter gap — and because a burst is anchored on its LAST element, too wide
|
||||
// means it ends in the FUTURE. The declared branch reads the counter to
|
||||
// reinstate the hole exactly; this pins the hrt branch to the same standard.
|
||||
// Assert POSITION, not just spacing: a burst can be correctly spaced and still
|
||||
// be drawn across the wrong stretch of the axis.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingThroughPacketLoss) {
|
||||
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;
|
||||
const double sampleDt = 0.0025;
|
||||
/* Runs of 1, 4 and 10 consecutive losses, well clear of each other and of
|
||||
* the fit warm-up. Ten losses is the interesting one: it used to stretch the
|
||||
* recovery burst 11x and date its last sample 225 ms into the future. */
|
||||
const int dropFrom[3] = {120, 200, 300};
|
||||
const int dropLen[3] = {1, 4, 10};
|
||||
|
||||
double worstFuture = 0.0;
|
||||
double last = 0.0;
|
||||
bool seen = false;
|
||||
for (int p = 0; p < 500; p++) {
|
||||
bool dropped = false;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
if (p >= dropFrom[k] && p < dropFrom[k] + dropLen[k]) { dropped = true; }
|
||||
}
|
||||
if (dropped) { continue; }
|
||||
|
||||
FrameBuilder fb;
|
||||
fb.addSignal(std::vector<double>(10, 1.0));
|
||||
const uint64_t hrt = bootHrt + static_cast<uint64_t>(p * packetSec * ticks);
|
||||
const double arrival = 700.0 + p * packetSec;
|
||||
const FrameView& f =
|
||||
fb.build(hrt, arrival, 10, static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
std::vector<double> ts;
|
||||
if (!dec.timestamps(f, 0, ts)) { continue; }
|
||||
|
||||
for (double t : ts) {
|
||||
if (seen) { ASSERT_GT(t, last) << "backwards at packet " << p; }
|
||||
last = t;
|
||||
seen = true;
|
||||
}
|
||||
if (p > 100) {
|
||||
/* The samples were acquired BEFORE the packet carrying them landed,
|
||||
* so none of them may be stamped after its arrival. */
|
||||
const double future = ts.back() - arrival;
|
||||
if (future > worstFuture) { worstFuture = future; }
|
||||
EXPECT_NEAR(ts[1] - ts[0], sampleDt, sampleDt * 1e-3)
|
||||
<< "spacing stretched at packet " << p;
|
||||
}
|
||||
}
|
||||
EXPECT_LT(worstFuture, 1e-6)
|
||||
<< "a recovery burst ended " << worstFuture << " s in the future";
|
||||
}
|
||||
|
||||
// A restart is the other way kDefaultDt gets latched: hrt goes backwards, so the
|
||||
// restart packet measures no interval of its own, and whatever burst width it
|
||||
// falls back on is baked into ClockOffset. The displacement that leaves — 13.5 ms
|
||||
// at this cadence — is below ClockOffset::kRecalibThresholdS, so it never heals.
|
||||
// AccumulatedScalarSurvivesAProducerRestart asserts only order and spacing and
|
||||
// passes right through it; this asserts absolute position.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarReturnsToTheWallClockAfterARestart) {
|
||||
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;
|
||||
std::vector<double> lastTs;
|
||||
double lastArrival = 0.0;
|
||||
|
||||
for (int p = 0; p < 400; p++) {
|
||||
FrameBuilder fb;
|
||||
fb.addSignal(std::vector<double>(10, 1.0));
|
||||
/* Packet 200 restarts the producer: hrt returns to a fresh boot and the
|
||||
* counter to 1. The wall clock does not restart. */
|
||||
const bool after = (p >= 200);
|
||||
const uint64_t hrt = after
|
||||
? static_cast<uint64_t>((p - 200) * packetSec * ticks)
|
||||
: bootHrt + static_cast<uint64_t>(p * packetSec * ticks);
|
||||
const uint32_t counter = after ? static_cast<uint32_t>(p - 199)
|
||||
: static_cast<uint32_t>(p + 1);
|
||||
const double arrival = 700.0 + p * packetSec;
|
||||
const FrameView& f = fb.build(hrt, arrival, 10, counter);
|
||||
dec.beginFrame(f);
|
||||
std::vector<double> ts;
|
||||
if (dec.timestamps(f, 0, ts)) { lastTs = ts; lastArrival = arrival; }
|
||||
}
|
||||
|
||||
ASSERT_EQ(lastTs.size(), 10u);
|
||||
EXPECT_NEAR(lastTs.back(), lastArrival, 1e-6)
|
||||
<< "still displaced from the wall clock 200 packets after the restart";
|
||||
EXPECT_NEAR(lastTs[1] - lastTs[0], 0.0025, 2.5e-6);
|
||||
}
|
||||
|
||||
// hrt == 0 sends the packet back to the warm-up branch, which spans from
|
||||
// packetBurst's own lastPacketWall. The hrt branch does not otherwise touch that
|
||||
// field, so it would be left at whenever this signal last took the warm-up
|
||||
// branch — the start of the session — and one stray packet would emit a burst
|
||||
// starting seconds in the past, worse the longer the scope has been running.
|
||||
TEST(FrameDecoder, UndeclaredAccumulatedScalarSurvivesAStrayZeroHrtPacket) {
|
||||
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;
|
||||
double last = 0.0;
|
||||
bool seen = false;
|
||||
|
||||
for (int p = 0; p < 200; p++) {
|
||||
FrameBuilder fb;
|
||||
fb.addSignal(std::vector<double>(10, 1.0));
|
||||
const uint64_t hrt = (p == 153)
|
||||
? 0u
|
||||
: bootHrt + static_cast<uint64_t>(p * packetSec * ticks);
|
||||
const double arrival = 700.0 + p * packetSec;
|
||||
const FrameView& f =
|
||||
fb.build(hrt, arrival, 10, static_cast<uint32_t>(p + 1));
|
||||
dec.beginFrame(f);
|
||||
std::vector<double> ts;
|
||||
if (!dec.timestamps(f, 0, ts)) { continue; }
|
||||
for (double t : ts) {
|
||||
if (seen) {
|
||||
ASSERT_GT(t, last) << "stray zero-hrt packet stepped back "
|
||||
<< (last - t) << " s at packet " << p;
|
||||
}
|
||||
last = t;
|
||||
seen = true;
|
||||
}
|
||||
/* And it must not land far from where the stream already is: spanning
|
||||
* from a session-old reference put the burst 3.8 s in the past. */
|
||||
if (p > 100) { EXPECT_NEAR(ts.back(), arrival, 0.05) << "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