ethercat-linux/src/stats.rs
Tony Cao 21b2d3bf99 J1900 <-> TwinCAT 1 kHz verification over EL6695 bridge
Re-implementation of rustbootd's ecat_el6695_* examples as a single binary
with fixes found in review and on hardware:

- DC-follow PLL hardened against period-2 hunting: slew-limited anchor
  (+/-50us/cycle), bistable-trap snap re-anchor, re-prime on stale deadline
- drift-free absolute-grid ticker mode; probe mode for timestamp forensics
- bounded-memory online stats (histograms), graceful SIGINT/SIGTERM shutdown
  with full report, error-streak abort
- timestamp plausibility filter comparing against the previous raw sample
  (avoids the deadlock after a startup outlier)
- XFC scope waveform options: --el2202 (with --el2202-dual), --el2262,
  --el1252 latch timestamp readback with per-channel edge statistics
- register access unified in regs.rs: named bit constants everywhere,
  read-modify-write for enable/activation bytes
- vendored patched ethercrab 0.7.1 (sdo_write_complete, send_raw_coe)

Verified on J1900 (PREEMPT_RT 6.6.135): 600k cycles/600s exact 1 kHz,
tx/rx zero errors, phase_err p50=164us std=5us; EL2202<->EL1252 loopback
edge interval mean 2000.24us std=24.65us.
2026-07-22 08:55:41 +08:00

119 lines
3.3 KiB
Rust

//! Bounded-memory online statistics: fixed histogram + Welford mean/variance.
//!
//! Unlike the original implementation (which pushed every sample into a
//! `Vec` and sorted at the end — ~30 MB/hour of RAM per metric at 1 kHz),
//! memory use here is constant, so multi-hour stability runs are safe.
pub struct OnlineStats {
name: &'static str,
/// Histogram bin width in ns.
bin_ns: i64,
/// Values below `low` or above `high` go to under/overflow counters but
/// still feed min/max/mean/variance.
low: i64,
high: i64,
bins: Vec<u64>,
under: u64,
over: u64,
n: u64,
min: i64,
max: i64,
mean: f64,
m2: f64,
}
impl OnlineStats {
/// Track values in `[low, high]` with `bin_ns` resolution (all in ns).
pub fn new(name: &'static str, bin_ns: i64, low: i64, high: i64) -> Self {
let n_bins = ((high - low) / bin_ns + 1).max(1) as usize;
Self {
name,
bin_ns,
low,
high,
bins: vec![0; n_bins],
under: 0,
over: 0,
n: 0,
min: i64::MAX,
max: i64::MIN,
mean: 0.0,
m2: 0.0,
}
}
pub fn push(&mut self, v: i64) {
self.n += 1;
if v < self.min {
self.min = v;
}
if v > self.max {
self.max = v;
}
// Welford
let d = v as f64 - self.mean;
self.mean += d / self.n as f64;
self.m2 += d * (v as f64 - self.mean);
if v < self.low {
self.under += 1;
} else if v > self.high {
self.over += 1;
} else {
let idx = ((v - self.low) / self.bin_ns) as usize;
let last = self.bins.len() - 1;
self.bins[idx.min(last)] += 1;
}
}
pub fn len(&self) -> u64 {
self.n
}
fn percentile(&self, p: f64) -> i64 {
if self.n == 0 {
return 0;
}
let mut target = (self.n as f64 * p).ceil() as u64;
target = target.max(1);
// Mass below the histogram window counts first.
if target <= self.under {
return self.low;
}
target -= self.under;
let mut acc = 0u64;
for (i, &c) in self.bins.iter().enumerate() {
acc += c;
if acc >= target {
return self.low + (i as i64 + 1) * self.bin_ns;
}
}
self.high // fell into overflow region
}
pub fn report(&self, unit_div: i64, unit: &str) {
if self.n == 0 {
println!("{}: no samples", self.name);
return;
}
let std = (self.m2 / self.n as f64).sqrt();
let d = unit_div as f64;
println!(
"{}: n={} min={:.1} p50={:.1} p95={:.1} p99={:.1} p99.9={:.1} max={:.1} mean={:.2} std={:.2} ({}) under={} over={}",
self.name,
self.n,
self.min as f64 / d,
self.percentile(0.50) as f64 / d,
self.percentile(0.95) as f64 / d,
self.percentile(0.99) as f64 / d,
self.percentile(0.999) as f64 / d,
self.max as f64 / d,
self.mean / d,
std / d,
unit,
self.under,
self.over,
);
}
}