feat(udpscope): producer-clock calibration and hrt tick-rate fit
Adds TimeBase.h/cpp with ClockOffset (latched wall-clock offset with one-sided recalibration on positive drift only, so early-arriving packets do not wobble the trace) and HrtRateFit (sliding-window OLS that recovers an unknown hrt tick rate from receive timestamps). Also adds TimeSignalScale() which maps UDPS type codes to seconds-per-count. 9 new tests, all 28 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ea9689591d
commit
41ab151a2f
@@ -0,0 +1,72 @@
|
||||
#include "TimeBase.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace udpscope {
|
||||
|
||||
/* UDPS_T_UINT64 == 6 in Common/UDP/UDPSProtocol.h. Spelled numerically so this
|
||||
* translation unit stays free of the C client header. */
|
||||
static constexpr uint8_t kTypeUint64 = 6u;
|
||||
|
||||
double TimeSignalScale(uint8_t typeCode) {
|
||||
return (typeCode == kTypeUint64) ? 1.0e-9 : 1.0e-6;
|
||||
}
|
||||
|
||||
double ClockOffset::map(double producerSec, double wallSec) {
|
||||
/* Recalibrate only when wall is significantly ahead of the prediction.
|
||||
* Early-arriving packets (wall < prediction) are normal network jitter and
|
||||
* must not reset the offset — doing so would wobble the trace. A large
|
||||
* positive error means the producer clock jumped back or restarted. */
|
||||
if (!valid_ || (wallSec - (offset_ + producerSec)) > kRecalibThresholdS) {
|
||||
offset_ = wallSec - producerSec;
|
||||
valid_ = true;
|
||||
}
|
||||
return offset_ + producerSec;
|
||||
}
|
||||
|
||||
void HrtRateFit::reset() {
|
||||
samples_.clear();
|
||||
n_ = 0;
|
||||
rate_ = 0.0;
|
||||
}
|
||||
|
||||
void HrtRateFit::add(uint64_t hrt, double wallSec) {
|
||||
samples_.push_back(Sample{static_cast<double>(hrt), wallSec});
|
||||
if (samples_.size() > kWindow) { samples_.pop_front(); }
|
||||
n_++;
|
||||
if (n_ >= kMinSamples) { refit(); }
|
||||
}
|
||||
|
||||
void HrtRateFit::refit() {
|
||||
const size_t n = samples_.size();
|
||||
if (n < 2) { return; }
|
||||
|
||||
/* Least squares slope of hrt against wall time. Both are subtracted from
|
||||
* their first value first: raw hrt counts and epoch seconds are large
|
||||
* enough that the naive sums lose precision. */
|
||||
const double h0 = samples_.front().hrt;
|
||||
const double w0 = samples_.front().wall;
|
||||
|
||||
double sw = 0.0, sh = 0.0, sww = 0.0, swh = 0.0;
|
||||
for (const Sample& s : samples_) {
|
||||
const double w = s.wall - w0;
|
||||
const double h = s.hrt - h0;
|
||||
sw += w;
|
||||
sh += h;
|
||||
sww += w * w;
|
||||
swh += w * h;
|
||||
}
|
||||
const double dn = static_cast<double>(n);
|
||||
const double denom = dn * sww - sw * sw;
|
||||
if (std::fabs(denom) < 1e-12) { return; }
|
||||
|
||||
const double slope = (dn * swh - sw * sh) / denom;
|
||||
if (slope > 0.0 && std::isfinite(slope)) { rate_ = slope; }
|
||||
}
|
||||
|
||||
double HrtRateFit::toSeconds(uint64_t hrt) const {
|
||||
if (rate_ <= 0.0) { return 0.0; }
|
||||
return static_cast<double>(hrt) / rate_;
|
||||
}
|
||||
|
||||
} /* namespace udpscope */
|
||||
Reference in New Issue
Block a user