use crate::nq_latency::LatencyResult;
use crate::nq_rpm::ResponsivenessResult;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::util::{pretty_ms, pretty_secs_to_ms};
#[derive(Serialize, Deserialize)]
pub struct Report {
unloaded_latency_ms: f64,
jitter_ms: f64,
download: RpmReport,
upload: RpmReport,
}
impl Report {
pub fn from_rtt_and_rpm_results(
rtt_result: &LatencyResult,
download_rpm_result: &ResponsivenessResult,
upload_rpm_result: &ResponsivenessResult,
) -> anyhow::Result<Self> {
let unloaded_latency_ms = rtt_result
.median()
.map(pretty_secs_to_ms)
.context("no unloaded latency measurements")?;
let jitter_ms = rtt_result.jitter().map(pretty_secs_to_ms).unwrap_or(0.0);
let download =
RpmReport::from_rpm_result(download_rpm_result).context("building download report")?;
let upload =
RpmReport::from_rpm_result(upload_rpm_result).context("building upload report")?;
Ok(Report {
unloaded_latency_ms,
jitter_ms,
download,
upload,
})
}
}
#[derive(Serialize, Deserialize)]
struct RpmReport {
throughput: usize,
loaded_latency_ms: f64,
rpm: usize,
#[serde(skip_serializing_if = "is_zero")]
failed_connections: usize,
}
fn is_zero(n: &usize) -> bool {
*n == 0
}
impl RpmReport {
pub fn from_rpm_result(result: &ResponsivenessResult) -> anyhow::Result<RpmReport> {
Ok(RpmReport {
throughput: result.throughput().context("no throughputs available")?,
loaded_latency_ms: result
.self_probe_latencies
.quantile(0.5)
.map(pretty_ms)
.context("no loaded latency measurements")?,
rpm: result
.rpm
.context("no rpm measurements: no interval produced a responsiveness sample")?
as usize,
failed_connections: result.failed_connections,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nq_core::Timestamp;
fn result_with_rpm(rpm: Option<f64>) -> ResponsivenessResult {
let mut result = ResponsivenessResult {
rpm,
..Default::default()
};
let at = Timestamp::now();
result.average_goodput_series.add(at, 104_000_000.0);
result.self_probe_latencies.add(at, 0.231);
result
}
#[test]
fn absent_rpm_fails_the_report_instead_of_serializing_zero() {
let err = match RpmReport::from_rpm_result(&result_with_rpm(None)) {
Ok(_) => panic!("absent rpm must not produce a report"),
Err(err) => err,
};
assert!(
format!("{err:#}").contains("no rpm measurements"),
"unexpected error: {err:#}"
);
}
#[test]
fn present_rpm_is_reported_unchanged() {
let report = RpmReport::from_rpm_result(&result_with_rpm(Some(347.9)))
.expect("a measured rpm should report");
let json = serde_json::to_string(&report).expect("serializing report");
assert!(json.contains("\"rpm\":347"), "got {json}");
}
}