refactor(irtt-stats): extract time stats module
This commit is contained in:
2 files changed
+148
-139
No files matched your search
@@ -12,6 +12,11 @@ use irtt_client::{
|
|||||||
SignedDuration,
|
SignedDuration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod time_stats;
|
||||||
|
|
||||||
|
use time_stats::TimeMetric;
|
||||||
|
pub use time_stats::TimeStats;
|
||||||
|
|
||||||
const CONTINUOUS_SEQUENCE_LIMIT: usize = 4096;
|
const CONTINUOUS_SEQUENCE_LIMIT: usize = 4096;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -228,27 +233,6 @@ pub struct ServerProcessingStats {
|
|||||||
pub processing: TimeStats,
|
pub processing: TimeStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct TimeStats {
|
|
||||||
pub count: u64,
|
|
||||||
pub total_ns: i128,
|
|
||||||
pub min_ns: Option<i128>,
|
|
||||||
pub max_ns: Option<i128>,
|
|
||||||
pub mean_ns: f64,
|
|
||||||
pub median_ns: Option<f64>,
|
|
||||||
pub variance_ns2: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeStats {
|
|
||||||
pub fn stddev_ns(&self) -> f64 {
|
|
||||||
self.variance_ns2.sqrt()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.count == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
struct CoreStats {
|
struct CoreStats {
|
||||||
events: EventCounts,
|
events: EventCounts,
|
||||||
@@ -761,69 +745,6 @@ struct CompletedIpdvPair {
|
|||||||
receive_ipdv_ns: Option<i128>,
|
receive_ipdv_ns: Option<i128>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
struct TimeMetric {
|
|
||||||
running: RunningStats,
|
|
||||||
samples: Option<Vec<i128>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeMetric {
|
|
||||||
fn new(retain_samples: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
running: RunningStats::default(),
|
|
||||||
samples: retain_samples.then(Vec::new),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_ns(&mut self, value: i128) {
|
|
||||||
self.running.push(value);
|
|
||||||
if let Some(samples) = self.samples.as_mut() {
|
|
||||||
samples.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stats(&self) -> TimeStats {
|
|
||||||
self.running
|
|
||||||
.stats(self.samples.as_ref().and_then(|samples| median_ns(samples)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
|
||||||
struct RunningStats {
|
|
||||||
count: u64,
|
|
||||||
total_ns: i128,
|
|
||||||
min_ns: Option<i128>,
|
|
||||||
max_ns: Option<i128>,
|
|
||||||
mean_ns: f64,
|
|
||||||
m2_ns2: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunningStats {
|
|
||||||
fn push(&mut self, value: i128) {
|
|
||||||
self.count += 1;
|
|
||||||
self.total_ns = self.total_ns.saturating_add(value);
|
|
||||||
self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value)));
|
|
||||||
self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value)));
|
|
||||||
let x = value as f64;
|
|
||||||
let delta = x - self.mean_ns;
|
|
||||||
self.mean_ns += delta / self.count as f64;
|
|
||||||
let delta2 = x - self.mean_ns;
|
|
||||||
self.m2_ns2 += delta * delta2;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stats(&self, median_ns: Option<f64>) -> TimeStats {
|
|
||||||
TimeStats {
|
|
||||||
count: self.count,
|
|
||||||
total_ns: self.total_ns,
|
|
||||||
min_ns: self.min_ns,
|
|
||||||
max_ns: self.max_ns,
|
|
||||||
mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns },
|
|
||||||
median_ns,
|
|
||||||
variance_ns2: sample_variance(self.count, self.m2_ns2),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn snapshot_window(events: &VecDeque<StatsEvent>) -> Snapshot {
|
fn snapshot_window(events: &VecDeque<StatsEvent>) -> Snapshot {
|
||||||
let mut core = CoreStats::new(SampleMode::RunningOnly);
|
let mut core = CoreStats::new(SampleMode::RunningOnly);
|
||||||
for event in events {
|
for event in events {
|
||||||
@@ -832,28 +753,6 @@ fn snapshot_window(events: &VecDeque<StatsEvent>) -> Snapshot {
|
|||||||
core.snapshot()
|
core.snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_variance(count: u64, m2: f64) -> f64 {
|
|
||||||
if count < 2 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
m2 / (count - 1) as f64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn median_ns(samples: &[i128]) -> Option<f64> {
|
|
||||||
if samples.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut sorted = samples.to_vec();
|
|
||||||
sorted.sort_unstable();
|
|
||||||
let mid = sorted.len() / 2;
|
|
||||||
Some(if sorted.len() % 2 == 1 {
|
|
||||||
sorted[mid] as f64
|
|
||||||
} else {
|
|
||||||
(sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn loss_stats(packets: PacketCounts) -> LossStats {
|
fn loss_stats(packets: PacketCounts) -> LossStats {
|
||||||
let lost = packets.packets_sent.saturating_sub(packets.unique_replies);
|
let lost = packets.packets_sent.saturating_sub(packets.unique_replies);
|
||||||
let packet_loss_percent = if packets.packets_sent == 0 {
|
let packet_loss_percent = if packets.packets_sent == 0 {
|
||||||
@@ -1016,39 +915,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn running_duration_stats_use_sample_variance() {
|
|
||||||
let mut metric = TimeMetric::new(false);
|
|
||||||
metric.push_ns(1);
|
|
||||||
metric.push_ns(2);
|
|
||||||
metric.push_ns(3);
|
|
||||||
let stats = metric.stats();
|
|
||||||
assert_eq!(stats.count, 3);
|
|
||||||
assert_eq!(stats.total_ns, 6);
|
|
||||||
assert_eq!(stats.min_ns, Some(1));
|
|
||||||
assert_eq!(stats.max_ns, Some(3));
|
|
||||||
assert_eq!(stats.mean_ns, 2.0);
|
|
||||||
assert_eq!(stats.variance_ns2, 1.0);
|
|
||||||
assert_eq!(stats.stddev_ns(), 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn exact_median_handles_odd_and_even_samples() {
|
|
||||||
assert_eq!(median_ns(&[3, 1, 2]), Some(2.0));
|
|
||||||
assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5));
|
|
||||||
assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0));
|
|
||||||
assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_sample_stddev_is_zero() {
|
|
||||||
let mut metric = TimeMetric::new(false);
|
|
||||||
metric.push_ns(42);
|
|
||||||
let stats = metric.stats();
|
|
||||||
assert_eq!(stats.variance_ns2, 0.0);
|
|
||||||
assert_eq!(stats.stddev_ns(), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn system_time_before_epoch_is_supported() {
|
fn system_time_before_epoch_is_supported() {
|
||||||
let before = UNIX_EPOCH - Duration::from_nanos(7);
|
let before = UNIX_EPOCH - Duration::from_nanos(7);
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct TimeStats {
|
||||||
|
pub count: u64,
|
||||||
|
pub total_ns: i128,
|
||||||
|
pub min_ns: Option<i128>,
|
||||||
|
pub max_ns: Option<i128>,
|
||||||
|
pub mean_ns: f64,
|
||||||
|
pub median_ns: Option<f64>,
|
||||||
|
pub variance_ns2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeStats {
|
||||||
|
pub fn stddev_ns(&self) -> f64 {
|
||||||
|
self.variance_ns2.sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.count == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub(crate) struct TimeMetric {
|
||||||
|
running: RunningStats,
|
||||||
|
samples: Option<Vec<i128>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeMetric {
|
||||||
|
pub(crate) fn new(retain_samples: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
running: RunningStats::default(),
|
||||||
|
samples: retain_samples.then(Vec::new),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push_ns(&mut self, value: i128) {
|
||||||
|
self.running.push(value);
|
||||||
|
if let Some(samples) = self.samples.as_mut() {
|
||||||
|
samples.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stats(&self) -> TimeStats {
|
||||||
|
self.running
|
||||||
|
.stats(self.samples.as_ref().and_then(|samples| median_ns(samples)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
struct RunningStats {
|
||||||
|
count: u64,
|
||||||
|
total_ns: i128,
|
||||||
|
min_ns: Option<i128>,
|
||||||
|
max_ns: Option<i128>,
|
||||||
|
mean_ns: f64,
|
||||||
|
m2_ns2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunningStats {
|
||||||
|
fn push(&mut self, value: i128) {
|
||||||
|
self.count += 1;
|
||||||
|
self.total_ns = self.total_ns.saturating_add(value);
|
||||||
|
self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value)));
|
||||||
|
self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value)));
|
||||||
|
let x = value as f64;
|
||||||
|
let delta = x - self.mean_ns;
|
||||||
|
self.mean_ns += delta / self.count as f64;
|
||||||
|
let delta2 = x - self.mean_ns;
|
||||||
|
self.m2_ns2 += delta * delta2;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stats(&self, median_ns: Option<f64>) -> TimeStats {
|
||||||
|
TimeStats {
|
||||||
|
count: self.count,
|
||||||
|
total_ns: self.total_ns,
|
||||||
|
min_ns: self.min_ns,
|
||||||
|
max_ns: self.max_ns,
|
||||||
|
mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns },
|
||||||
|
median_ns,
|
||||||
|
variance_ns2: sample_variance(self.count, self.m2_ns2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_variance(count: u64, m2: f64) -> f64 {
|
||||||
|
if count < 2 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
m2 / (count - 1) as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn median_ns(samples: &[i128]) -> Option<f64> {
|
||||||
|
if samples.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut sorted = samples.to_vec();
|
||||||
|
sorted.sort_unstable();
|
||||||
|
let mid = sorted.len() / 2;
|
||||||
|
Some(if sorted.len() % 2 == 1 {
|
||||||
|
sorted[mid] as f64
|
||||||
|
} else {
|
||||||
|
(sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn running_duration_stats_use_sample_variance() {
|
||||||
|
let mut metric = TimeMetric::new(false);
|
||||||
|
metric.push_ns(1);
|
||||||
|
metric.push_ns(2);
|
||||||
|
metric.push_ns(3);
|
||||||
|
let stats = metric.stats();
|
||||||
|
assert_eq!(stats.count, 3);
|
||||||
|
assert_eq!(stats.total_ns, 6);
|
||||||
|
assert_eq!(stats.min_ns, Some(1));
|
||||||
|
assert_eq!(stats.max_ns, Some(3));
|
||||||
|
assert_eq!(stats.mean_ns, 2.0);
|
||||||
|
assert_eq!(stats.variance_ns2, 1.0);
|
||||||
|
assert_eq!(stats.stddev_ns(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_median_handles_odd_and_even_samples() {
|
||||||
|
assert_eq!(median_ns(&[3, 1, 2]), Some(2.0));
|
||||||
|
assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5));
|
||||||
|
assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0));
|
||||||
|
assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_sample_stddev_is_zero() {
|
||||||
|
let mut metric = TimeMetric::new(false);
|
||||||
|
metric.push_ns(42);
|
||||||
|
let stats = metric.stats();
|
||||||
|
assert_eq!(stats.variance_ns2, 0.0);
|
||||||
|
assert_eq!(stats.stddev_ns(), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user