Functional Weave
Code in Python

net.latency-summary@1.0.0

impl/rust.rs

3,833 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::stats_percentile::percentile;  ← from stats.percentile ^2.0.0 · built alongside by fune

/// 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<i64>]) -> LatencySummary {
    if samples_us.is_empty() {
        panic!("samplesUs must not be empty");
    }
    let mut got: Vec<i64> = 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<f64> = 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<i64>) -> 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<Option<i64>> = 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))
}