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.
119 lines
3.3 KiB
Rust
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,
|
|
);
|
|
}
|
|
}
|