Merge pull request #72 from Lochnair/audit/recv-window-downstream-loss

docs(stats): clarify server receive-window semantics
This commit is contained in:
Nils Andreas Svee committed 2026-08-11 23:22:03 +07:00
commit db0bc282f3
5 files changed
+67 -3

No files matched your search

+4 -1
View File
@@ -361,7 +361,10 @@ pub struct OneWayDelaySample {
pub struct ReceivedStatsSample {
/// Server-reported count of received packets, when available.
pub count: Option<u32>,
/// Server-reported receive window value, when available.
/// Raw server-reported receive window bitmap, when available.
///
/// The bitmap is preserved exactly as it appeared on the wire; the client
/// does not interpret its bits.
pub window: Option<u64>,
}
+13 -1
View File
@@ -229,8 +229,20 @@ pub struct PacketCounts {
/// exposed separately.
pub bytes_received: u64,
/// Highest cumulative server-reported packets received, when available.
///
/// This is the only server-reported input to the directional loss estimates
/// in [`LossStats`].
pub server_packets_received: Option<u64>,
/// Server-reported receive window, when available.
/// Raw server-reported receive window from the most recently processed
/// reply that carried one, when available.
///
/// The value is stored exactly as the server reported it and is not
/// interpreted by this crate. It is a bounded bitmap of recent server
/// receive history rather than a fixed 64-packet loss mask, so a value of
/// `0x1` means no earlier history is represented and not that the previous
/// 63 packets were lost. Replies are aggregated in local arrival order, so
/// a late reply can leave an older window here than an already-processed
/// reply carried.
pub server_received_window: Option<u64>,
}
+5
View File
@@ -6,6 +6,11 @@ use crate::PacketCounts;
/// Percentage fields are percentages from `0.0` to `100.0` in normal cases.
/// Directional upstream and downstream loss can be negative when
/// server-reported counts exceed local expectations.
///
/// The directional fields are derived from
/// [`PacketCounts::server_packets_received`] and are `None` when the server
/// reported no receive count. [`PacketCounts::server_received_window`] is never
/// used to derive loss.
pub struct LossStats {
/// Locally inferred total lost packets.
pub lost_packets: u64,
+16
View File
@@ -84,6 +84,22 @@ fn reply_with_rtt(seq: u32, raw_ms: u64, rtt: RttSample) -> ClientEvent {
}
}
/// Builds a normal reply carrying exactly the supplied server received stats.
pub fn reply_with_received_stats(
seq: u32,
raw_ms: u64,
count: Option<u32>,
window: Option<u64>,
) -> ClientEvent {
let mut event = unadjusted_reply(seq, raw_ms);
let ClientEvent::EchoReply { received_stats, .. } = &mut event else {
unreachable!();
};
*received_stats =
(count.is_some() || window.is_some()).then_some(ReceivedStatsSample { count, window });
event
}
pub fn unadjusted_late_reply(seq: u32, raw_ms: u64) -> ClientEvent {
late_reply_from(unadjusted_reply(seq, raw_ms))
}
+29 -1
View File
@@ -2,7 +2,9 @@ mod common;
use std::time::{Duration, Instant, UNIX_EPOCH};
use common::{adjusted_reply, sent, ts, unadjusted_late_reply, unadjusted_reply};
use common::{
adjusted_reply, reply_with_received_stats, sent, ts, unadjusted_late_reply, unadjusted_reply,
};
use irtt_client::{
ClientEvent, ClientTimestamp, OneWayDelaySample, PacketMeta, RttSample, SignedDuration,
};
@@ -378,6 +380,32 @@ fn empty_and_all_lost_edges_are_defined() {
assert_eq!(all_lost.loss.packet_loss_percent, 100.0);
}
#[test]
fn receive_window_is_stored_raw_and_never_drives_directional_loss() {
// Window-only negotiation: the server reports a bitmap but no receive count.
let mut collector = StatsCollector::new(StatsConfig::finite());
collector.process(&sent(0, ts(0)));
collector.process(&reply_with_received_stats(0, 10, None, Some(0x1)));
// `0x1` represents no earlier history. It must be preserved verbatim and
// must not be read as 63 lost packets or stand in for a receive count.
let snapshot = collector.snapshot();
assert_eq!(snapshot.packets.server_received_window, Some(0x1));
assert_eq!(snapshot.packets.server_packets_received, None);
assert_eq!(snapshot.loss.upstream_loss_packets, None);
assert_eq!(snapshot.loss.downstream_loss_packets, None);
assert_eq!(snapshot.loss.upstream_loss_percent, 0.0);
assert_eq!(snapshot.loss.downstream_loss_percent, 0.0);
// An advancing bitmap is stored verbatim across the full 64-bit range.
collector.process(&sent(1, ts(10)));
collector.process(&reply_with_received_stats(1, 10, None, Some(u64::MAX)));
assert_eq!(
collector.snapshot().packets.server_received_window,
Some(u64::MAX)
);
}
#[test]
fn directional_loss_uses_server_received_count_when_available() {
let mut collector = StatsCollector::new(StatsConfig::finite());