fix(udpscope): carry warm-up state across the hrt handover

An undeclared-rate accumulated scalar is served by packetBurst until
HrtRateFit is ready, then by the hrt branch. The two place a burst
differently -- packetBurst ends it at wallNow, the hrt branch at
wallNow - (nElems-1)*hrtDt -- and the warm-up left no state behind, so
the handover packet skipped the monotonic clamp and stepped the signal
backwards by up to a burst width (-6.5 ms at 10 samples per 2.5 ms
packet, -0.99 s at 1000 samples per 10 ms).

Seeding lastEmitted* alone would only restore ordering. Without
lastAccHrt/prevAccCount the first hrt packet also has no tick delta to
measure, falls back to kDefaultDt and latches ClockOffset against a
burst width that is wrong whenever the cadence is not 1 kHz -- 89 ms of
permanent displacement at 100 samples per 10 ms, below the
recalibration threshold that would otherwise heal it. Seed both.

Also close the wallElapsed <= 0 bypass in both branches: skipping the
bleed cap when the wall has not moved hands back the full proportional
advance, letting a run of same-tick arrivals gain lead while no wall
time passes at all.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-08-28 05:49:01 +02:00
co-authored by Claude Opus 4.6
parent 440b805afd
commit 3add2c42b9
4 changed files with 288 additions and 34 deletions
+56 -11
View File
@@ -328,15 +328,25 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
if (factor < kMinBleedFactor) { factor = kMinBleedFactor; }
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. */
const double wallElapsed = wallNow - st.lastEmittedWall;
if (wallElapsed > 0.0) {
const double cap = kWallBleedFraction * wallElapsed;
const double cap = (wallElapsed > 0.0)
? (kWallBleedFraction * wallElapsed)
: 0.0;
if (cap < advance) { advance = cap; }
}
step = advance / static_cast<double>(nElems);
/* Unreachable with a finite positive dt — kept because
* downstream monotonicity must not depend on that
* argument holding for every value off the wire. */
/* 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. */
if (!(step > 0.0)) { step = dt * kMinBleedFactor; }
base = st.lastEmittedEnd + step;
}
@@ -356,7 +366,37 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
/* No declared rate: need hrt-derived dt. */
if (!hrtFit_.ready() || f.hrt == 0u) {
return packetBurst(idx, nElems, wallNow, tsOut);
const bool ok = packetBurst(idx, nElems, wallNow, tsOut);
/* Carry the warm-up's state into the hrt branch, or the handover
* from one to the other is a discontinuity in both directions.
*
* The producer-clock reference (lastAccHrt, prevAccCount) matters
* most. Without it the first hrt packet has no previous tick to
* subtract, falls back to kDefaultDt for its inter-element step and
* latches ClockOffset against wallNow - (nElems-1)*kDefaultDt.
* kDefaultDt is only right when the burst happens to run at 1 kHz;
* at 100 samples per 10 ms packet it is ten times too wide and the
* latch lands 89 ms in the past — permanently, since it is below
* ClockOffset's recalibration threshold. Seeding here means the
* first hrt packet measures a real tick delta and latches correctly.
*
* The emitted-timeline reference (lastEmitted*) then only has to
* cover residual disagreement, but it is what keeps the handover
* MONOTONIC: packetBurst ends its burst at wallNow while the hrt
* branch ends at wallNow - (nElems-1)*hrtDt, and without a previous
* end to clamp against the first hrt packet steps the signal
* backwards by up to a whole burst width. */
if (f.hrt != 0u) {
st.lastAccHrt = f.hrt;
st.lastAccValid = true;
}
st.prevAccCount = nElems;
if (!ok) { return false; }
st.lastEmittedEnd = tsOut[nElems - 1u];
st.lastEmittedWall = wallNow;
st.lastCounter = f.counter;
st.lastEmittedValid = true;
return true;
}
const double rate = hrtFit_.ticksPerSecond();
@@ -455,11 +495,16 @@ 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;
if (wallElapsed > 0.0) {
const double cap = kWallBleedFraction * wallElapsed /
static_cast<double>(nElems);
/* 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; }
base = st.lastEmittedEnd + step;
}
+10 -2
View File
@@ -10,7 +10,7 @@
* 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).
* Three rules deliberately differ, all 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
@@ -24,7 +24,15 @@
* 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
* Second, which end of the burst is anchored. StreamHub converts the packet's
* hrt into the position of sample 0 and steps forward, so the burst STARTS at
* the anchor. Here the anchor is arrival time, and the samples were acquired
* before the packet carrying them landed — so the burst must END there instead.
* Both branches of rule 3 do this, or two accumulated scalars in one scope, one
* with a declared rate and one without, would sit a whole burst apart on the
* shared X axis.
*
* Third, 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
+73 -1
View File
@@ -429,7 +429,14 @@ TEST(FrameDecoder, AccumulatedScalarWithANonFiniteRateFallsBackToTheHrtPath) {
}
ASSERT_EQ(last.size(), 10u);
EXPECT_NEAR(last[1] - last[0], 0.0025, 2e-5)
/* The tolerance is bounded from both sides and neither bound is arbitrary.
* Below: hrtDt divides a tick delta by HrtRateFit's fitted rate, and the fit
* regresses hrt against arrivals carrying the +/-3 ms jitter above, so ~2 us
* of residual is inherent — 1e-8 fails. Above: the degenerate declared branch
* would span those same jittered gaps and answer 2.2 or 2.8 ms, 300 us out.
* 1e-5 sits two orders below the thing it must reject and five times above
* the noise it must tolerate. */
EXPECT_NEAR(last[1] - last[0], 0.0025, 1e-5)
<< "an unusable declared rate must fall through to the hrt path";
}
@@ -811,6 +818,71 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarEndsItsBurstOnArrival) {
EXPECT_NEAR(last[0], lastArrival - 0.009, 1e-9);
}
// An undeclared-rate signal is served by TWO different mechanisms in sequence:
// 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.
TEST(FrameDecoder, UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly) {
struct Case { uint32_t nElems; double packetSec; };
const Case cases[] = {
{10u, 0.0025}, /* 4 kHz: burst wider than the packet interval */
{100u, 0.010 }, /* 10 kHz */
{1000u, 0.010 }, /* 100 kHz: a burst is 100x the kDefaultDt guess */
{10u, 0.050 }, /* 200 Hz: burst narrower than the packet interval */
};
for (const Case& c : cases) {
FrameDecoder dec;
dec.setSignals({undeclaredAcc()});
const double ticks = 1.0e9;
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
const double sampleDt = c.packetSec / static_cast<double>(c.nElems);
double last = 0.0;
bool seen = false;
double lastArrival = 0.0;
std::vector<double> lastTs;
for (int p = 0; p < 200; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(c.nElems, 1.0));
const uint64_t hrt =
bootHrt + static_cast<uint64_t>(p * c.packetSec * ticks);
const double arrival = 700.0 + p * c.packetSec;
const FrameView& f = fb.build(hrt, arrival, c.nElems,
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)
<< "handover stepped back " << (last - t) << " s with "
<< c.nElems << " samples per " << c.packetSec << " s packet";
}
last = t;
seen = true;
}
lastTs = ts;
lastArrival = arrival;
}
/* Monotonic is necessary but not sufficient: a clamp restores ordering
* while leaving the whole trace parked in the past. The producer clock
* here is exact, so once settled the burst must still end on arrival and
* step at the true sample period. */
ASSERT_EQ(lastTs.size(), c.nElems);
EXPECT_NEAR(lastTs.back(), lastArrival, 1e-6)
<< "trace drifted off the wall clock with " << c.nElems
<< " samples per " << c.packetSec << " s packet";
EXPECT_NEAR(lastTs[1] - lastTs[0], sampleDt, sampleDt * 1e-3);
}
}
// 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
+145 -16
View File
@@ -1861,7 +1861,14 @@ TEST(FrameDecoder, AccumulatedScalarWithANonFiniteRateFallsBackToTheHrtPath) {
}
ASSERT_EQ(last.size(), 10u);
EXPECT_NEAR(last[1] - last[0], 0.0025, 2e-5)
/* The tolerance is bounded from both sides and neither bound is arbitrary.
* Below: hrtDt divides a tick delta by HrtRateFit's fitted rate, and the fit
* regresses hrt against arrivals carrying the +/-3 ms jitter above, so ~2 us
* of residual is inherent — 1e-8 fails. Above: the degenerate declared branch
* would span those same jittered gaps and answer 2.2 or 2.8 ms, 300 us out.
* 1e-5 sits two orders below the thing it must reject and five times above
* the noise it must tolerate. */
EXPECT_NEAR(last[1] - last[0], 0.0025, 1e-5)
<< "an unusable declared rate must fall through to the hrt path";
}
@@ -2243,6 +2250,71 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarEndsItsBurstOnArrival) {
EXPECT_NEAR(last[0], lastArrival - 0.009, 1e-9);
}
// An undeclared-rate signal is served by TWO different mechanisms in sequence:
// 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.
TEST(FrameDecoder, UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly) {
struct Case { uint32_t nElems; double packetSec; };
const Case cases[] = {
{10u, 0.0025}, /* 4 kHz: burst wider than the packet interval */
{100u, 0.010 }, /* 10 kHz */
{1000u, 0.010 }, /* 100 kHz: a burst is 100x the kDefaultDt guess */
{10u, 0.050 }, /* 200 Hz: burst narrower than the packet interval */
};
for (const Case& c : cases) {
FrameDecoder dec;
dec.setSignals({undeclaredAcc()});
const double ticks = 1.0e9;
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
const double sampleDt = c.packetSec / static_cast<double>(c.nElems);
double last = 0.0;
bool seen = false;
double lastArrival = 0.0;
std::vector<double> lastTs;
for (int p = 0; p < 200; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(c.nElems, 1.0));
const uint64_t hrt =
bootHrt + static_cast<uint64_t>(p * c.packetSec * ticks);
const double arrival = 700.0 + p * c.packetSec;
const FrameView& f = fb.build(hrt, arrival, c.nElems,
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)
<< "handover stepped back " << (last - t) << " s with "
<< c.nElems << " samples per " << c.packetSec << " s packet";
}
last = t;
seen = true;
}
lastTs = ts;
lastArrival = arrival;
}
/* Monotonic is necessary but not sufficient: a clamp restores ordering
* while leaving the whole trace parked in the past. The producer clock
* here is exact, so once settled the burst must still end on arrival and
* step at the true sample period. */
ASSERT_EQ(lastTs.size(), c.nElems);
EXPECT_NEAR(lastTs.back(), lastArrival, 1e-6)
<< "trace drifted off the wall clock with " << c.nElems
<< " samples per " << c.packetSec << " s packet";
EXPECT_NEAR(lastTs[1] - lastTs[0], sampleDt, sampleDt * 1e-3);
}
}
// 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
@@ -2383,7 +2455,7 @@ Create `Client/udpscope/FrameDecoder.h`:
* 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).
* Three rules deliberately differ, all 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
@@ -2397,7 +2469,15 @@ Create `Client/udpscope/FrameDecoder.h`:
* 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
* Second, which end of the burst is anchored. StreamHub converts the packet's
* hrt into the position of sample 0 and steps forward, so the burst STARTS at
* the anchor. Here the anchor is arrival time, and the samples were acquired
* before the packet carrying them landed — so the burst must END there instead.
* Both branches of rule 3 do this, or two accumulated scalars in one scope, one
* with a declared rate and one without, would sit a whole burst apart on the
* shared X axis.
*
* Third, 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
@@ -2819,15 +2899,25 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
if (factor < kMinBleedFactor) { factor = kMinBleedFactor; }
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. */
const double wallElapsed = wallNow - st.lastEmittedWall;
if (wallElapsed > 0.0) {
const double cap = kWallBleedFraction * wallElapsed;
const double cap = (wallElapsed > 0.0)
? (kWallBleedFraction * wallElapsed)
: 0.0;
if (cap < advance) { advance = cap; }
}
step = advance / static_cast<double>(nElems);
/* Unreachable with a finite positive dt — kept because
* downstream monotonicity must not depend on that
* argument holding for every value off the wire. */
/* 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. */
if (!(step > 0.0)) { step = dt * kMinBleedFactor; }
base = st.lastEmittedEnd + step;
}
@@ -2847,7 +2937,37 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
/* No declared rate: need hrt-derived dt. */
if (!hrtFit_.ready() || f.hrt == 0u) {
return packetBurst(idx, nElems, wallNow, tsOut);
const bool ok = packetBurst(idx, nElems, wallNow, tsOut);
/* Carry the warm-up's state into the hrt branch, or the handover
* from one to the other is a discontinuity in both directions.
*
* The producer-clock reference (lastAccHrt, prevAccCount) matters
* most. Without it the first hrt packet has no previous tick to
* subtract, falls back to kDefaultDt for its inter-element step and
* latches ClockOffset against wallNow - (nElems-1)*kDefaultDt.
* kDefaultDt is only right when the burst happens to run at 1 kHz;
* at 100 samples per 10 ms packet it is ten times too wide and the
* latch lands 89 ms in the past — permanently, since it is below
* ClockOffset's recalibration threshold. Seeding here means the
* first hrt packet measures a real tick delta and latches correctly.
*
* The emitted-timeline reference (lastEmitted*) then only has to
* cover residual disagreement, but it is what keeps the handover
* MONOTONIC: packetBurst ends its burst at wallNow while the hrt
* branch ends at wallNow - (nElems-1)*hrtDt, and without a previous
* end to clamp against the first hrt packet steps the signal
* backwards by up to a whole burst width. */
if (f.hrt != 0u) {
st.lastAccHrt = f.hrt;
st.lastAccValid = true;
}
st.prevAccCount = nElems;
if (!ok) { return false; }
st.lastEmittedEnd = tsOut[nElems - 1u];
st.lastEmittedWall = wallNow;
st.lastCounter = f.counter;
st.lastEmittedValid = true;
return true;
}
const double rate = hrtFit_.ticksPerSecond();
@@ -2946,11 +3066,16 @@ 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;
if (wallElapsed > 0.0) {
const double cap = kWallBleedFraction * wallElapsed /
static_cast<double>(nElems);
/* 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; }
base = st.lastEmittedEnd + step;
}
@@ -3002,7 +3127,7 @@ set(CORE_SOURCES
cd Client/udpscope && cmake --build build -j && ./build/udpscope_tests --gtest_filter='FrameDecoder*'
```
Expected: PASS, 25 `FrameDecoder` tests — 54 across the whole `udpscope_tests` binary.
Expected: PASS, 26 `FrameDecoder` tests — 55 across the whole `udpscope_tests` binary.
If `AccumulatedScalarSurvivesBurstyDelivery` fails, do NOT reach for the hrt fit: with a declared `samplingRate` rule 3 never consults it, precisely because the fit is not ready for the first 32 packets and — since `HrtRateFit` regresses `hrt` against ARRIVAL time — is itself corrupted by the very bursts it would be asked to survive. Check instead that `lastEmittedEnd`, `lastCounter`, `prevAccCount` and `lastEmittedValid` are updated on every emitted burst.
@@ -3022,7 +3147,11 @@ If `UndeclaredAccumulatedScalarSurvivesAProducerRestart` fails with the spacing
If `UndeclaredAccumulatedScalarRecoversFromABackwardWallStep` fails, the hrt branch's `base <= lastEmittedEnd` clamp has gone back to a bare `lastEmittedEnd + hrtDt`, which is one-directional and leaves an NTP correction or a suspend/resume as a permanent lead. Note the test's geometry is deliberate: the step is 0.6 s (it must exceed `ClockOffset::kRecalibThresholdS` or the recalibration that puts `base` behind `lastEmittedEnd` never happens at all), and it lands after the 256-sample rate-fit window is full. `HrtRateFit` regresses `hrt` against ARRIVAL, so it eventually absorbs the step too, at roughly `step / kWindow` per packet — a much slower second correction that would swamp the measurement if the step were placed early or the run continued for hundreds of packets past it.
If `UndeclaredAccumulatedScalarEndsItsBurstOnArrival` fails by exactly `(nElems - 1) * hrtDt`, `ClockOffset::map()` is being latched against raw `wallNow` again, which puts the burst's FIRST element on arrival while the declared branch puts its LAST one there — two accumulated scalars in one scope, one with a declared rate and one without, then sit a whole burst apart on the shared X axis. The test's 10 ms packet of 10 samples is chosen so the derived period equals `kDefaultDt`: the very first hrt-branch packet has no measurable interval and latches the offset using that fallback, and any other period would bake the difference into the offset for the rest of the run.
If `UndeclaredAccumulatedScalarEndsItsBurstOnArrival` fails by exactly `(nElems - 1) * hrtDt`, `ClockOffset::map()` is being latched against raw `wallNow` again, which puts the burst's FIRST element on arrival while the declared branch puts its LAST one there — two accumulated scalars in one scope, one with a declared rate and one without, then sit a whole burst apart on the shared X axis.
That test alone is NOT sufficient cover for the hrt branch's anchoring, which is why `UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly` exists beside it. Its 10 samples per 10 ms packet make the derived period exactly `kDefaultDt`, the one cadence at which the warm-up-to-hrt handover cannot misbehave, so it passes against a decoder that steps backwards by up to a whole burst at that handover. The handover is the sharp edge here: an undeclared-rate signal is served by `packetBurst` until `HrtRateFit` is ready and by the hrt branch afterwards, and the two place a burst differently — `packetBurst` ends it at `wallNow`, the hrt branch at `wallNow - (nElems - 1) * hrtDt`.
If `UndeclaredAccumulatedScalarCrossesTheHrtHandoverCleanly` fails on the monotonicity assertion, the warm-up branch is returning `packetBurst()`'s result directly without recording state. Two different fields matter and they fix two different symptoms. `lastEmittedEnd`/`lastEmittedWall`/`lastEmittedValid` are what the monotonic clamp needs; without them the first hrt packet skips the clamp entirely and steps back by up to a burst width (-6.5 ms at 10 samples per 2.5 ms, -0.99 s at 1000 samples per 10 ms). `lastAccHrt`/`prevAccCount` are what stops the failure being merely hidden: without a previous tick to subtract, the first hrt packet has no measurable interval, falls back to `kDefaultDt` and latches `ClockOffset` against `wallNow - (nElems - 1) * kDefaultDt`. At 100 samples per 10 ms packet that is ten times too wide and parks the trace 89 ms in the past permanently, since 89 ms is below `ClockOffset::kRecalibThresholdS`. That is why the test asserts the settled burst still ends on arrival and steps at the true sample period, not just that it never goes backwards — the clamp on its own would satisfy monotonicity while leaving the trace displaced.
- [ ] **Step 8: Commit**