use super::funejson::Value; use super::math_round_div::round_div; use super::stats_percentile::percentile; /// Summarise a run of probes the way ping does, in whole microseconds. /// /// Integer microseconds rather than float milliseconds, so every language /// gives the same answer and a vector can compare exactly. A lost probe counts /// towards loss and is skipped (not a break) when measuring jitter. /// /// # Panics /// Panics on an empty list or a negative sample. pub fn summarise_latency(samples_us: &[Option]) -> LatencySummary { if samples_us.is_empty() { panic!("samplesUs must not be empty"); } let mut got: Vec = Vec::new(); for s in samples_us.iter().flatten() { if *s < 0 { panic!("samples must not be negative, received {}", s); } got.push(*s); } let sent = samples_us.len() as i64; let received = got.len() as i64; let lost = sent - received; let loss = round_div(lost * 10000, sent, "half-up"); if received == 0 { return LatencySummary { sent, received, lost, loss_basis_points: loss, min_us: None, avg_us: None, max_us: None, p95_us: None, jitter_us: None, }; } let jitter = if received >= 2 { let diffs: i64 = got.windows(2).map(|w| (w[1] - w[0]).abs()).sum(); Some(round_div(diffs, received - 1, "half-up")) } else { None }; // Nearest-rank returns a sample that was really seen, so it is a whole number. let floats: Vec = got.iter().map(|v| *v as f64).collect(); let p95 = percentile(&floats, 95.0, "nearest-rank", 0) as i64; LatencySummary { sent, received, lost, loss_basis_points: loss, min_us: got.iter().min().copied(), avg_us: Some(round_div(got.iter().sum(), received, "half-up")), max_us: got.iter().max().copied(), p95_us: Some(p95), jitter_us: jitter, } } fn opt(v: Option) -> Value { match v { Some(n) => Value::Int(n), None => Value::Null, } } pub fn latency_summary_to_value(s: &LatencySummary) -> Value { Value::obj(vec![ ("sent", Value::Int(s.sent)), ("received", Value::Int(s.received)), ("lost", Value::Int(s.lost)), ("lossBasisPoints", Value::Int(s.loss_basis_points)), ("minUs", opt(s.min_us)), ("avgUs", opt(s.avg_us)), ("maxUs", opt(s.max_us)), ("p95Us", opt(s.p95_us)), ("jitterUs", opt(s.jitter_us)), ]) } /// Read a LatencySummary back from JSON, for capabilities that take one. pub fn latency_summary_from_value(v: &Value) -> LatencySummary { let o = |k: &str| { let x = v.get(k); if x.is_null() { None } else { Some(x.as_i64()) } }; LatencySummary { sent: v.get("sent").as_i64(), received: v.get("received").as_i64(), lost: v.get("lost").as_i64(), loss_basis_points: v.get("lossBasisPoints").as_i64(), min_us: o("minUs"), avg_us: o("avgUs"), max_us: o("maxUs"), p95_us: o("p95Us"), jitter_us: o("jitterUs"), } } pub fn fune_vector(args: &[Value]) -> Value { let samples: Vec> = args[0] .as_arr() .iter() .map(|v| match v { Value::Null => None, Value::Int(n) => Some(*n), Value::Float(f) if f.fract() == 0.0 && f.is_finite() => Some(*f as i64), other => panic!( "each sample must be a whole number of microseconds or null, received {}", other ), }) .collect(); latency_summary_to_value(&summarise_latency(&samples)) }