refactor(client): use SignedDuration for effective RTT to support negative values
Simplify RttSample by merging signed and unsigned fields into a single SignedDuration for adjusted and effective RTT. This ensures accurate reporting and statistics when server processing time exceeds raw RTT.
This commit is contained in:
1 parent
37de7c96a1
commit
bec56d4114
14 files changed
+207
-86
No files matched your search
@@ -129,7 +129,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
while let Ok(event) = events.recv() {
|
while let Ok(event) = events.recv() {
|
||||||
match event {
|
match event {
|
||||||
ClientEvent::EchoReply { seq, rtt, .. } => {
|
ClientEvent::EchoReply { seq, rtt, .. } => {
|
||||||
println!("seq={seq} rtt_us={}", rtt.effective.as_micros());
|
println!("seq={seq} rtt_us={}", rtt.effective.ns / 1_000);
|
||||||
}
|
}
|
||||||
ClientEvent::SessionClosed { .. } => break,
|
ClientEvent::SessionClosed { .. } => break,
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
+78
-15
@@ -12,8 +12,8 @@ use std::{
|
|||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use irtt_client::{
|
use irtt_client::{
|
||||||
ClientConfig, ClientEvent, NegotiatedParams, NegotiationPolicy, OneWayDelaySample, PacketMeta,
|
ClientConfig, ClientEvent, NegotiatedParams, NegotiationPolicy, OneWayDelaySample, PacketMeta,
|
||||||
ReceivedStatsSample, RttSample, ServerTiming, SocketConfig, WarningKind, MAX_DSCP_CODEPOINT,
|
ReceivedStatsSample, RttSample, ServerTiming, SignedDuration, SocketConfig, WarningKind,
|
||||||
MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
|
MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
|
||||||
};
|
};
|
||||||
use irtt_proto::{Clock, ReceivedStats, StampAt};
|
use irtt_proto::{Clock, ReceivedStats, StampAt};
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ pub fn format_human_event_with_options(
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut out = format!("seq={seq}");
|
let mut out = format!("seq={seq}");
|
||||||
write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
|
write!(out, " rtt={}", format_signed_duration(rtt.effective)).unwrap();
|
||||||
write_human_one_way(&mut out, *one_way);
|
write_human_one_way(&mut out, *one_way);
|
||||||
write!(out, " ipdv={}", format_human_ipdv(stats, *seq)).unwrap();
|
write!(out, " ipdv={}", format_human_ipdv(stats, *seq)).unwrap();
|
||||||
if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
|
if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
|
||||||
@@ -425,7 +425,7 @@ pub fn format_human_event_with_options(
|
|||||||
} => {
|
} => {
|
||||||
let mut out = format!("late seq={seq} highest_seen={highest_seen} remote={remote}",);
|
let mut out = format!("late seq={seq} highest_seen={highest_seen} remote={remote}",);
|
||||||
if let Some(rtt) = rtt {
|
if let Some(rtt) = rtt {
|
||||||
write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
|
write!(out, " rtt={}", format_signed_duration(rtt.effective)).unwrap();
|
||||||
write_human_one_way(&mut out, *one_way);
|
write_human_one_way(&mut out, *one_way);
|
||||||
write!(out, " ipdv={}", format_human_ipdv(stats, *seq)).unwrap();
|
write!(out, " ipdv={}", format_human_ipdv(stats, *seq)).unwrap();
|
||||||
}
|
}
|
||||||
@@ -440,7 +440,7 @@ pub fn format_human_event_with_options(
|
|||||||
|
|
||||||
fn format_rtt_us(event: &ClientEvent) -> Option<String> {
|
fn format_rtt_us(event: &ClientEvent) -> Option<String> {
|
||||||
match event {
|
match event {
|
||||||
ClientEvent::EchoReply { rtt, .. } => Some(duration_us(rtt.effective).to_string()),
|
ClientEvent::EchoReply { rtt, .. } => Some(signed_duration_us(rtt.effective).to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,10 +585,10 @@ fn format_simple(event: &ClientEvent) -> String {
|
|||||||
} => {
|
} => {
|
||||||
let mut out = format!(
|
let mut out = format!(
|
||||||
"reply seq={seq} remote={remote} rtt_us={}",
|
"reply seq={seq} remote={remote} rtt_us={}",
|
||||||
duration_us(rtt.effective)
|
signed_duration_us(rtt.effective)
|
||||||
);
|
);
|
||||||
if let Some(raw) = rtt.adjusted.map(|_| rtt.raw) {
|
if rtt.adjusted.is_some() {
|
||||||
write!(out, " raw_rtt_us={}", duration_us(raw)).unwrap();
|
write!(out, " raw_rtt_us={}", duration_us(rtt.raw)).unwrap();
|
||||||
}
|
}
|
||||||
if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
|
if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
|
||||||
write!(out, " server_processing_us={}", duration_us(processing)).unwrap();
|
write!(out, " server_processing_us={}", duration_us(processing)).unwrap();
|
||||||
@@ -610,7 +610,7 @@ fn format_simple(event: &ClientEvent) -> String {
|
|||||||
} => {
|
} => {
|
||||||
let mut out = format!("late seq={seq} highest_seen={highest_seen} remote={remote}",);
|
let mut out = format!("late seq={seq} highest_seen={highest_seen} remote={remote}",);
|
||||||
if let Some(rtt) = rtt {
|
if let Some(rtt) = rtt {
|
||||||
write!(out, " rtt_us={}", duration_us(rtt.effective)).unwrap();
|
write!(out, " rtt_us={}", signed_duration_us(rtt.effective)).unwrap();
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -656,11 +656,11 @@ fn write_rtt(out: &mut String, rtt: &RttSample) {
|
|||||||
out,
|
out,
|
||||||
" raw_rtt_us={} effective_rtt_us={}",
|
" raw_rtt_us={} effective_rtt_us={}",
|
||||||
duration_us(rtt.raw),
|
duration_us(rtt.raw),
|
||||||
duration_us(rtt.effective)
|
signed_duration_us(rtt.effective)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if let Some(adjusted) = rtt.adjusted {
|
if let Some(adjusted) = rtt.adjusted {
|
||||||
write!(out, " adjusted_rtt_us={}", duration_us(adjusted)).unwrap();
|
write!(out, " adjusted_rtt_us={}", signed_duration_us(adjusted)).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +773,10 @@ fn duration_us(duration: Duration) -> u128 {
|
|||||||
duration.as_micros()
|
duration.as_micros()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn signed_duration_us(duration: SignedDuration) -> i128 {
|
||||||
|
duration.ns / 1_000
|
||||||
|
}
|
||||||
|
|
||||||
fn format_optional_duration(duration: Option<Duration>) -> String {
|
fn format_optional_duration(duration: Option<Duration>) -> String {
|
||||||
duration
|
duration
|
||||||
.map(format_duration)
|
.map(format_duration)
|
||||||
@@ -783,6 +787,18 @@ fn format_duration(duration: Duration) -> String {
|
|||||||
format_ns(duration.as_nanos() as f64)
|
format_ns(duration.as_nanos() as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_signed_duration(duration: SignedDuration) -> String {
|
||||||
|
format_signed_ns(duration.ns as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_signed_ns(ns: f64) -> String {
|
||||||
|
if ns < 0.0 {
|
||||||
|
format!("-{}", format_ns(-ns))
|
||||||
|
} else {
|
||||||
|
format_ns(ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_ns(ns: f64) -> String {
|
fn format_ns(ns: f64) -> String {
|
||||||
if ns < 1_000.0 {
|
if ns < 1_000.0 {
|
||||||
format!("{ns:.0}ns")
|
format!("{ns:.0}ns")
|
||||||
@@ -853,10 +869,8 @@ mod tests {
|
|||||||
received_at: test_timestamp(Duration::from_secs(1) + Duration::from_micros(1500)),
|
received_at: test_timestamp(Duration::from_secs(1) + Duration::from_micros(1500)),
|
||||||
rtt: RttSample {
|
rtt: RttSample {
|
||||||
raw: Duration::from_micros(1500),
|
raw: Duration::from_micros(1500),
|
||||||
adjusted: Some(Duration::from_micros(1200)),
|
adjusted: Some(SignedDuration { ns: 1_200_000 }),
|
||||||
effective: Duration::from_micros(1200),
|
effective: SignedDuration { ns: 1_200_000 },
|
||||||
adjusted_signed: Some(SignedDuration { ns: 1_200_000 }),
|
|
||||||
effective_signed: SignedDuration { ns: 1_200_000 },
|
|
||||||
},
|
},
|
||||||
server_timing: Some(ServerTiming {
|
server_timing: Some(ServerTiming {
|
||||||
receive_wall_ns: Some(1_000),
|
receive_wall_ns: Some(1_000),
|
||||||
@@ -880,6 +894,33 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn negative_adjusted_reply_event() -> ClientEvent {
|
||||||
|
ClientEvent::EchoReply {
|
||||||
|
seq: 8,
|
||||||
|
remote: test_remote(),
|
||||||
|
sent_at: test_timestamp(Duration::from_secs(1)),
|
||||||
|
received_at: test_timestamp(Duration::from_secs(1) + Duration::from_micros(500)),
|
||||||
|
rtt: RttSample {
|
||||||
|
raw: Duration::from_micros(500),
|
||||||
|
adjusted: Some(SignedDuration { ns: -1_500_000 }),
|
||||||
|
effective: SignedDuration { ns: -1_500_000 },
|
||||||
|
},
|
||||||
|
server_timing: Some(ServerTiming {
|
||||||
|
receive_wall_ns: Some(1_000),
|
||||||
|
receive_mono_ns: Some(2_000),
|
||||||
|
send_wall_ns: Some(2_001_000),
|
||||||
|
send_mono_ns: Some(2_002_000),
|
||||||
|
midpoint_wall_ns: None,
|
||||||
|
midpoint_mono_ns: None,
|
||||||
|
processing: Some(Duration::from_micros(2_000)),
|
||||||
|
}),
|
||||||
|
one_way: None,
|
||||||
|
received_stats: None,
|
||||||
|
bytes: 64,
|
||||||
|
packet_meta: PacketMeta::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn late_event_with_meta(packet_meta: PacketMeta) -> ClientEvent {
|
fn late_event_with_meta(packet_meta: PacketMeta) -> ClientEvent {
|
||||||
ClientEvent::LateReply {
|
ClientEvent::LateReply {
|
||||||
seq: 4,
|
seq: 4,
|
||||||
@@ -1157,6 +1198,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
fn streamed_and_summary_rtt_use_signed_effective_policy() {
|
||||||
|
let event = negative_adjusted_reply_event();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format_event(&event, OutputMode::RttUs),
|
||||||
|
Some("-1500".to_owned())
|
||||||
|
);
|
||||||
|
|
||||||
|
let machine = format_event(&event, OutputMode::Machine).unwrap();
|
||||||
|
assert!(machine.contains("raw_rtt_us=500"));
|
||||||
|
assert!(machine.contains("effective_rtt_us=-1500"));
|
||||||
|
assert!(machine.contains("adjusted_rtt_us=-1500"));
|
||||||
|
|
||||||
|
let mut collector = irtt_stats::StatsCollector::new(irtt_stats::StatsConfig::finite());
|
||||||
|
collector.process(&event);
|
||||||
|
let summary = collector.snapshot();
|
||||||
|
assert_eq!(summary.rtt.primary.min_ns, Some(-1_500_000));
|
||||||
|
assert_eq!(summary.rtt.adjusted.min_ns, Some(-1_500_000));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn machine_prints_stable_key_value_fields() {
|
fn machine_prints_stable_key_value_fields() {
|
||||||
let line = format_event(&reply_event(), OutputMode::Machine).unwrap();
|
let line = format_event(&reply_event(), OutputMode::Machine).unwrap();
|
||||||
|
|||||||
@@ -426,9 +426,7 @@ mod tests {
|
|||||||
rtt: RttSample {
|
rtt: RttSample {
|
||||||
raw: Duration::from_micros(rtt_us),
|
raw: Duration::from_micros(rtt_us),
|
||||||
adjusted: None,
|
adjusted: None,
|
||||||
effective: Duration::from_micros(rtt_us),
|
effective: SignedDuration {
|
||||||
adjusted_signed: None,
|
|
||||||
effective_signed: SignedDuration {
|
|
||||||
ns: i128::from(rtt_us) * 1_000,
|
ns: i128::from(rtt_us) * 1_000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -468,9 +466,7 @@ mod tests {
|
|||||||
rtt: RttSample {
|
rtt: RttSample {
|
||||||
raw: Duration::from_micros(1200),
|
raw: Duration::from_micros(1200),
|
||||||
adjusted: None,
|
adjusted: None,
|
||||||
effective: Duration::from_micros(1200),
|
effective: SignedDuration { ns: 1_200_000 },
|
||||||
adjusted_signed: None,
|
|
||||||
effective_signed: SignedDuration { ns: 1_200_000 },
|
|
||||||
},
|
},
|
||||||
server_timing: None,
|
server_timing: None,
|
||||||
one_way: None,
|
one_way: None,
|
||||||
@@ -583,9 +579,7 @@ mod tests {
|
|||||||
rtt: RttSample {
|
rtt: RttSample {
|
||||||
raw: Duration::from_micros(1),
|
raw: Duration::from_micros(1),
|
||||||
adjusted: None,
|
adjusted: None,
|
||||||
effective: Duration::from_micros(1),
|
effective: SignedDuration { ns: 1_000 },
|
||||||
adjusted_signed: None,
|
|
||||||
effective_signed: SignedDuration { ns: 1_000 },
|
|
||||||
},
|
},
|
||||||
server_timing: None,
|
server_timing: None,
|
||||||
one_way: None,
|
one_way: None,
|
||||||
|
|||||||
@@ -206,10 +206,8 @@ mod tests {
|
|||||||
received_at,
|
received_at,
|
||||||
rtt: RttSample {
|
rtt: RttSample {
|
||||||
raw: Duration::from_micros(1500),
|
raw: Duration::from_micros(1500),
|
||||||
adjusted: Some(Duration::from_micros(1200)),
|
adjusted: Some(SignedDuration { ns: 1_200_000 }),
|
||||||
effective: Duration::from_micros(1200),
|
effective: SignedDuration { ns: 1_200_000 },
|
||||||
adjusted_signed: Some(SignedDuration { ns: 1_200_000 }),
|
|
||||||
effective_signed: SignedDuration { ns: 1_200_000 },
|
|
||||||
},
|
},
|
||||||
server_timing: Some(ServerTiming {
|
server_timing: Some(ServerTiming {
|
||||||
receive_wall_ns: None,
|
receive_wall_ns: None,
|
||||||
@@ -254,10 +252,8 @@ mod tests {
|
|||||||
received_at,
|
received_at,
|
||||||
rtt: RttSample {
|
rtt: RttSample {
|
||||||
raw: Duration::from_micros(1500),
|
raw: Duration::from_micros(1500),
|
||||||
adjusted: Some(Duration::from_micros(1200)),
|
adjusted: Some(SignedDuration { ns: 1_200_000 }),
|
||||||
effective: Duration::from_micros(1200),
|
effective: SignedDuration { ns: 1_200_000 },
|
||||||
adjusted_signed: Some(SignedDuration { ns: 1_200_000 }),
|
|
||||||
effective_signed: SignedDuration { ns: 1_200_000 },
|
|
||||||
},
|
},
|
||||||
server_timing: None,
|
server_timing: None,
|
||||||
one_way: None,
|
one_way: None,
|
||||||
|
|||||||
@@ -727,22 +727,15 @@ fn compute_rtt(
|
|||||||
|
|
||||||
let server_processing = compute_server_processing(ts);
|
let server_processing = compute_server_processing(ts);
|
||||||
|
|
||||||
let adjusted = server_processing.and_then(|sp| raw.checked_sub(sp));
|
let adjusted = server_processing.map(|sp| SignedDuration {
|
||||||
|
|
||||||
let effective = adjusted.unwrap_or(raw);
|
|
||||||
let adjusted_signed = server_processing.map(|sp| SignedDuration {
|
|
||||||
ns: duration_ns_i128(raw) - duration_ns_i128(sp),
|
ns: duration_ns_i128(raw) - duration_ns_i128(sp),
|
||||||
});
|
});
|
||||||
let effective_signed = adjusted_signed.unwrap_or(SignedDuration {
|
let effective = adjusted.unwrap_or_else(|| SignedDuration::from_duration(raw));
|
||||||
ns: duration_ns_i128(raw),
|
|
||||||
});
|
|
||||||
|
|
||||||
RttSample {
|
RttSample {
|
||||||
raw,
|
raw,
|
||||||
adjusted,
|
adjusted,
|
||||||
effective,
|
effective,
|
||||||
adjusted_signed,
|
|
||||||
effective_signed,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ fn recv_once_decodes_echo_reply_and_emits_event() {
|
|||||||
assert_eq!(*seq, 0);
|
assert_eq!(*seq, 0);
|
||||||
assert_eq!(*bytes, echo_packet_len(false, ¶ms));
|
assert_eq!(*bytes, echo_packet_len(false, ¶ms));
|
||||||
assert!(rtt.raw > Duration::ZERO);
|
assert!(rtt.raw > Duration::ZERO);
|
||||||
assert_eq!(rtt.effective, rtt.adjusted.unwrap_or(rtt.raw));
|
assert_eq!(
|
||||||
|
rtt.effective,
|
||||||
|
rtt.adjusted
|
||||||
|
.unwrap_or_else(|| SignedDuration::from_duration(rtt.raw))
|
||||||
|
);
|
||||||
assert!(received_stats.is_some());
|
assert!(received_stats.is_some());
|
||||||
let stats = received_stats.as_ref().unwrap();
|
let stats = received_stats.as_ref().unwrap();
|
||||||
assert_eq!(stats.count, Some(42));
|
assert_eq!(stats.count, Some(42));
|
||||||
|
|||||||
@@ -18,13 +18,27 @@ fn server_processing_greater_than_raw_does_not_underflow() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert!(rtt.adjusted.is_none());
|
assert_eq!(rtt.adjusted, Some(SignedDuration { ns: -999_999_999 }));
|
||||||
assert_eq!(rtt.effective, rtt.raw);
|
assert_eq!(rtt.effective, SignedDuration { ns: -999_999_999 });
|
||||||
assert_eq!(
|
}
|
||||||
rtt.adjusted_signed,
|
|
||||||
Some(SignedDuration { ns: -999_999_999 })
|
#[test]
|
||||||
|
fn effective_rtt_uses_raw_when_adjusted_rtt_is_unavailable() {
|
||||||
|
let base = Instant::now();
|
||||||
|
let rtt = compute_rtt(
|
||||||
|
&ClientTimestamp {
|
||||||
|
mono: base,
|
||||||
|
wall: SystemTime::now(),
|
||||||
|
},
|
||||||
|
&ClientTimestamp {
|
||||||
|
mono: base + Duration::from_nanos(42),
|
||||||
|
wall: SystemTime::now(),
|
||||||
|
},
|
||||||
|
&TimestampFields::default(),
|
||||||
);
|
);
|
||||||
assert_eq!(rtt.effective_signed, SignedDuration { ns: -999_999_999 });
|
|
||||||
|
assert_eq!(rtt.adjusted, None);
|
||||||
|
assert_eq!(rtt.effective, SignedDuration::from_duration(rtt.raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -106,11 +106,15 @@ pub enum WarningKind {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct RttSample {
|
pub struct RttSample {
|
||||||
|
/// Client-observed RTT from send to receive.
|
||||||
pub raw: Duration,
|
pub raw: Duration,
|
||||||
pub adjusted: Option<Duration>,
|
/// Signed RTT adjusted for server processing when server processing is available.
|
||||||
pub effective: Duration,
|
pub adjusted: Option<SignedDuration>,
|
||||||
pub adjusted_signed: Option<SignedDuration>,
|
/// Signed effective RTT used by stats and CLI reporting.
|
||||||
pub effective_signed: SignedDuration,
|
///
|
||||||
|
/// This is `adjusted` when server processing is available, otherwise `raw`
|
||||||
|
/// converted to a signed duration.
|
||||||
|
pub effective: SignedDuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -118,6 +122,20 @@ pub struct SignedDuration {
|
|||||||
pub ns: i128,
|
pub ns: i128,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SignedDuration {
|
||||||
|
pub fn from_duration(duration: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
ns: i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Duration> for SignedDuration {
|
||||||
|
fn from(value: Duration) -> Self {
|
||||||
|
Self::from_duration(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ServerTiming {
|
pub struct ServerTiming {
|
||||||
pub receive_wall_ns: Option<i64>,
|
pub receive_wall_ns: Option<i64>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use irtt_client::{
|
use irtt_client::{
|
||||||
Client, ClientConfig, ClientError, ClientEvent, OneWayDelaySample, ReceivedStatsSample,
|
Client, ClientConfig, ClientError, ClientEvent, OneWayDelaySample, ReceivedStatsSample,
|
||||||
RttSample, ServerTiming,
|
RttSample, ServerTiming, SignedDuration,
|
||||||
};
|
};
|
||||||
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, TimestampFields};
|
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, TimestampFields};
|
||||||
|
|
||||||
@@ -69,7 +69,10 @@ fn timestamp_modes_drive_reply_event_timing_fields() {
|
|||||||
assert!(reply.server_timing.is_none());
|
assert!(reply.server_timing.is_none());
|
||||||
assert!(reply.one_way.is_none());
|
assert!(reply.one_way.is_none());
|
||||||
assert!(reply.rtt.adjusted.is_none());
|
assert!(reply.rtt.adjusted.is_none());
|
||||||
assert_eq!(reply.rtt.effective, reply.rtt.raw);
|
assert_eq!(
|
||||||
|
reply.rtt.effective,
|
||||||
|
SignedDuration::from_duration(reply.rtt.raw)
|
||||||
|
);
|
||||||
assert!(reply.rtt.raw > Duration::ZERO);
|
assert!(reply.rtt.raw > Duration::ZERO);
|
||||||
}
|
}
|
||||||
StampAt::Send => {
|
StampAt::Send => {
|
||||||
@@ -349,7 +352,7 @@ fn assert_processing_subtracted(reply: ReplyView<'_>) {
|
|||||||
assert_eq!(processing, Duration::from_nanos(1));
|
assert_eq!(processing, Duration::from_nanos(1));
|
||||||
let adjusted = reply.rtt.adjusted.unwrap();
|
let adjusted = reply.rtt.adjusted.unwrap();
|
||||||
assert_eq!(reply.rtt.effective, adjusted);
|
assert_eq!(reply.rtt.effective, adjusted);
|
||||||
assert!(adjusted < reply.rtt.raw);
|
assert!(adjusted.ns < SignedDuration::from_duration(reply.rtt.raw).ns);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_one_way_presence(
|
fn assert_one_way_presence(
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ impl CoreStats {
|
|||||||
StatsEvent::UniqueReply {
|
StatsEvent::UniqueReply {
|
||||||
is_late, sample, ..
|
is_late, sample, ..
|
||||||
} => self.apply_unique_reply(is_late, *sample),
|
} => self.apply_unique_reply(is_late, *sample),
|
||||||
StatsEvent::DuplicateReply { .. } => {
|
StatsEvent::DuplicateReply { bytes, .. } => {
|
||||||
self.apply_duplicate_reply();
|
self.apply_duplicate_reply(bytes);
|
||||||
EventStatsUpdate::default()
|
EventStatsUpdate::default()
|
||||||
}
|
}
|
||||||
StatsEvent::Loss { .. } => {
|
StatsEvent::Loss { .. } => {
|
||||||
@@ -81,8 +81,8 @@ impl CoreStats {
|
|||||||
self.apply_warning();
|
self.apply_warning();
|
||||||
EventStatsUpdate::default()
|
EventStatsUpdate::default()
|
||||||
}
|
}
|
||||||
StatsEvent::UntrackedLate { .. } => {
|
StatsEvent::UntrackedLate { bytes, .. } => {
|
||||||
self.apply_untracked_late();
|
self.apply_untracked_late(bytes);
|
||||||
EventStatsUpdate::default()
|
EventStatsUpdate::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,12 @@ impl CoreStats {
|
|||||||
.saturating_add(sample.bytes as u64);
|
.saturating_add(sample.bytes as u64);
|
||||||
|
|
||||||
if let Some(count) = sample.received_count {
|
if let Some(count) = sample.received_count {
|
||||||
self.packets.server_packets_received = Some(u64::from(count));
|
let count = u64::from(count);
|
||||||
|
self.packets.server_packets_received = Some(
|
||||||
|
self.packets
|
||||||
|
.server_packets_received
|
||||||
|
.map_or(count, |current| current.max(count)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(window) = sample.received_window {
|
if let Some(window) = sample.received_window {
|
||||||
self.packets.server_received_window = Some(window);
|
self.packets.server_received_window = Some(window);
|
||||||
@@ -181,10 +186,11 @@ impl CoreStats {
|
|||||||
updates
|
updates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_duplicate_reply(&mut self) {
|
fn apply_duplicate_reply(&mut self, bytes: usize) {
|
||||||
self.events.duplicate_replies += 1;
|
self.events.duplicate_replies += 1;
|
||||||
self.packets.packets_received += 1;
|
self.packets.packets_received += 1;
|
||||||
self.packets.duplicates += 1;
|
self.packets.duplicates += 1;
|
||||||
|
self.packets.bytes_received = self.packets.bytes_received.saturating_add(bytes as u64);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_loss(&mut self) {
|
fn apply_loss(&mut self) {
|
||||||
@@ -195,8 +201,11 @@ impl CoreStats {
|
|||||||
self.events.warning_events += 1;
|
self.events.warning_events += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_untracked_late(&mut self) {
|
fn apply_untracked_late(&mut self, bytes: usize) {
|
||||||
self.events.untracked_late_replies += 1;
|
self.events.untracked_late_replies += 1;
|
||||||
|
self.packets.packets_received += 1;
|
||||||
|
self.packets.late_packets += 1;
|
||||||
|
self.packets.bytes_received = self.packets.bytes_received.saturating_add(bytes as u64);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn snapshot(&self) -> Snapshot {
|
pub(crate) fn snapshot(&self) -> Snapshot {
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ pub struct EventCounts {
|
|||||||
/// Warning events processed.
|
/// Warning events processed.
|
||||||
pub warning_events: u64,
|
pub warning_events: u64,
|
||||||
/// Late replies that could not be matched to retained in-flight state.
|
/// Late replies that could not be matched to retained in-flight state.
|
||||||
|
///
|
||||||
|
/// These replies are observed on the socket, but cannot contribute RTT,
|
||||||
|
/// one-way delay, server-processing, or IPDV samples.
|
||||||
pub untracked_late_replies: u64,
|
pub untracked_late_replies: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,19 +209,26 @@ pub struct EventCounts {
|
|||||||
pub struct PacketCounts {
|
pub struct PacketCounts {
|
||||||
/// Probe packets sent by the local client.
|
/// Probe packets sent by the local client.
|
||||||
pub packets_sent: u64,
|
pub packets_sent: u64,
|
||||||
/// Reply packets received by the local client, including duplicates and late packets.
|
/// Reply packets observed by the local client.
|
||||||
|
///
|
||||||
|
/// This includes unique measurable replies, duplicate replies, and late
|
||||||
|
/// replies that cannot be matched to retained in-flight state.
|
||||||
pub packets_received: u64,
|
pub packets_received: u64,
|
||||||
/// Unique replies received by the local client.
|
/// Unique replies that can contribute timing measurements.
|
||||||
pub unique_replies: u64,
|
pub unique_replies: u64,
|
||||||
/// Duplicate replies received by the local client.
|
/// Duplicate replies received by the local client.
|
||||||
pub duplicates: u64,
|
pub duplicates: u64,
|
||||||
/// Late reply packets received by the local client.
|
/// Late reply packets received by the local client, including untracked-late replies.
|
||||||
pub late_packets: u64,
|
pub late_packets: u64,
|
||||||
/// Probe bytes sent by the local client.
|
/// Probe bytes sent by the local client.
|
||||||
pub bytes_sent: u64,
|
pub bytes_sent: u64,
|
||||||
/// Reply bytes received by the local client.
|
/// Reply bytes observed by the local client.
|
||||||
|
///
|
||||||
|
/// This includes bytes from unique measurable replies, duplicate replies,
|
||||||
|
/// and untracked-late replies. Per-category byte counts are not currently
|
||||||
|
/// exposed separately.
|
||||||
pub bytes_received: u64,
|
pub bytes_received: u64,
|
||||||
/// Server-reported packets received, when available.
|
/// Highest cumulative server-reported packets received, when available.
|
||||||
pub server_packets_received: Option<u64>,
|
pub server_packets_received: Option<u64>,
|
||||||
/// Server-reported receive window, when available.
|
/// Server-reported receive window, when available.
|
||||||
pub server_received_window: Option<u64>,
|
pub server_received_window: Option<u64>,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub(crate) enum StatsEvent {
|
|||||||
},
|
},
|
||||||
DuplicateReply {
|
DuplicateReply {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
|
bytes: usize,
|
||||||
},
|
},
|
||||||
Loss {
|
Loss {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
@@ -31,6 +32,7 @@ pub(crate) enum StatsEvent {
|
|||||||
},
|
},
|
||||||
UntrackedLate {
|
UntrackedLate {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
|
bytes: usize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +98,17 @@ pub(crate) fn normalize_event(event: &ClientEvent) -> Option<StatsEvent> {
|
|||||||
*bytes,
|
*bytes,
|
||||||
)),
|
)),
|
||||||
}),
|
}),
|
||||||
ClientEvent::LateReply { received_at, .. } => Some(StatsEvent::UntrackedLate {
|
ClientEvent::LateReply {
|
||||||
|
received_at, bytes, ..
|
||||||
|
} => Some(StatsEvent::UntrackedLate {
|
||||||
at: received_at.mono,
|
at: received_at.mono,
|
||||||
|
bytes: *bytes,
|
||||||
}),
|
}),
|
||||||
ClientEvent::DuplicateReply { received_at, .. } => Some(StatsEvent::DuplicateReply {
|
ClientEvent::DuplicateReply {
|
||||||
|
received_at, bytes, ..
|
||||||
|
} => Some(StatsEvent::DuplicateReply {
|
||||||
at: received_at.mono,
|
at: received_at.mono,
|
||||||
|
bytes: *bytes,
|
||||||
}),
|
}),
|
||||||
ClientEvent::EchoLoss { timeout_at, .. } => Some(StatsEvent::Loss { at: *timeout_at }),
|
ClientEvent::EchoLoss { timeout_at, .. } => Some(StatsEvent::Loss { at: *timeout_at }),
|
||||||
ClientEvent::Warning { at, .. } => Some(StatsEvent::Warning { at: at.mono }),
|
ClientEvent::Warning { at, .. } => Some(StatsEvent::Warning { at: at.mono }),
|
||||||
@@ -115,10 +123,10 @@ impl StatsEvent {
|
|||||||
match self {
|
match self {
|
||||||
Self::Sent { at, .. }
|
Self::Sent { at, .. }
|
||||||
| Self::UniqueReply { at, .. }
|
| Self::UniqueReply { at, .. }
|
||||||
| Self::DuplicateReply { at }
|
| Self::DuplicateReply { at, .. }
|
||||||
| Self::Loss { at }
|
| Self::Loss { at }
|
||||||
| Self::Warning { at }
|
| Self::Warning { at }
|
||||||
| Self::UntrackedLate { at } => *at,
|
| Self::UntrackedLate { at, .. } => *at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,11 +156,11 @@ impl ReplySample {
|
|||||||
received_stats: Option<ReceivedStatsSample>,
|
received_stats: Option<ReceivedStatsSample>,
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let rtt_primary_ns = signed_duration_ns(rtt.effective_signed);
|
let rtt_primary_ns = signed_duration_ns(rtt.effective);
|
||||||
Self {
|
Self {
|
||||||
bytes,
|
bytes,
|
||||||
rtt_raw_ns: duration_ns_i128(rtt.raw),
|
rtt_raw_ns: duration_ns_i128(rtt.raw),
|
||||||
rtt_adjusted_ns: rtt.adjusted_signed.map(signed_duration_ns),
|
rtt_adjusted_ns: rtt.adjusted.map(signed_duration_ns),
|
||||||
send_delay_ns: one_way
|
send_delay_ns: one_way
|
||||||
.and_then(|sample| sample.client_to_server)
|
.and_then(|sample| sample.client_to_server)
|
||||||
.map(duration_ns_i128),
|
.map(duration_ns_i128),
|
||||||
|
|||||||
@@ -13,19 +13,13 @@ pub fn ts(ms: u64) -> ClientTimestamp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn rtt(raw_ms: u64, effective_ms: i128) -> RttSample {
|
pub fn rtt(raw_ms: u64, effective_ms: i128) -> RttSample {
|
||||||
|
let effective = SignedDuration {
|
||||||
|
ns: effective_ms * 1_000_000,
|
||||||
|
};
|
||||||
RttSample {
|
RttSample {
|
||||||
raw: Duration::from_millis(raw_ms),
|
raw: Duration::from_millis(raw_ms),
|
||||||
adjusted: u64::try_from(effective_ms).ok().map(Duration::from_millis),
|
adjusted: Some(effective),
|
||||||
effective: u64::try_from(effective_ms)
|
effective,
|
||||||
.ok()
|
|
||||||
.map(Duration::from_millis)
|
|
||||||
.unwrap_or_else(|| Duration::from_millis(raw_ms)),
|
|
||||||
adjusted_signed: Some(SignedDuration {
|
|
||||||
ns: effective_ms * 1_000_000,
|
|
||||||
}),
|
|
||||||
effective_signed: SignedDuration {
|
|
||||||
ns: effective_ms * 1_000_000,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fn cumulative_rtt_uses_signed_effective_and_tracks_raw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn late_unique_counts_and_duplicates_do_not_update_measurements() {
|
fn late_unique_counts_and_duplicates_do_not_update_duplicate_measurements() {
|
||||||
let mut collector = StatsCollector::new(StatsConfig::finite());
|
let mut collector = StatsCollector::new(StatsConfig::finite());
|
||||||
collector.process(&sent(0, ts(0)));
|
collector.process(&sent(0, ts(0)));
|
||||||
collector.process(&sent(1, ts(10)));
|
collector.process(&sent(1, ts(10)));
|
||||||
@@ -68,6 +68,7 @@ fn late_unique_counts_and_duplicates_do_not_update_measurements() {
|
|||||||
assert_eq!(snapshot.packets.unique_replies, 2);
|
assert_eq!(snapshot.packets.unique_replies, 2);
|
||||||
assert_eq!(snapshot.packets.duplicates, 1);
|
assert_eq!(snapshot.packets.duplicates, 1);
|
||||||
assert_eq!(snapshot.packets.late_packets, 1);
|
assert_eq!(snapshot.packets.late_packets, 1);
|
||||||
|
assert_eq!(snapshot.packets.bytes_received, 64 + 64 + 64);
|
||||||
assert_eq!(snapshot.rtt.primary.count, 2);
|
assert_eq!(snapshot.rtt.primary.count, 2);
|
||||||
assert_eq!(snapshot.loss.lost_packets, 0);
|
assert_eq!(snapshot.loss.lost_packets, 0);
|
||||||
assert_eq!(snapshot.loss.duplicate_percent, 100.0 / 3.0);
|
assert_eq!(snapshot.loss.duplicate_percent, 100.0 / 3.0);
|
||||||
@@ -210,6 +211,20 @@ fn server_processing_and_one_way_require_available_samples() {
|
|||||||
assert_eq!(snapshot.server_processing.processing.count, 1);
|
assert_eq!(snapshot.server_processing.processing.count, 1);
|
||||||
assert_eq!(snapshot.one_way_delay.send_delay.count, 1);
|
assert_eq!(snapshot.one_way_delay.send_delay.count, 1);
|
||||||
assert_eq!(snapshot.events.untracked_late_replies, 1);
|
assert_eq!(snapshot.events.untracked_late_replies, 1);
|
||||||
|
assert_eq!(snapshot.packets.packets_received, 2);
|
||||||
|
assert_eq!(snapshot.packets.unique_replies, 1);
|
||||||
|
assert_eq!(snapshot.packets.late_packets, 1);
|
||||||
|
assert_eq!(snapshot.packets.bytes_received, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn older_cumulative_server_receive_count_does_not_regress() {
|
||||||
|
let mut collector = StatsCollector::new(StatsConfig::finite());
|
||||||
|
collector.process(&reply(1, 10, 10));
|
||||||
|
collector.process(&late_reply(0, 10, 10));
|
||||||
|
|
||||||
|
let snapshot = collector.snapshot();
|
||||||
|
assert_eq!(snapshot.packets.server_packets_received, Some(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user