From 0ad3d1845f10295aa723c0cead81cd4d8d002d0b Mon Sep 17 00:00:00 2001 From: xx Date: Thu, 16 Apr 2026 08:10:43 +0800 Subject: [PATCH 1/4] Add `acked` packet count to connection stats to make short-term loss rate more precisely --- apps/src/bin/quiche-server.rs | 3 ++- quiche/include/quiche.h | 3 +++ quiche/src/ffi.rs | 2 ++ quiche/src/lib.rs | 10 ++++++++ .../src/recovery/congestion/delivery_rate.rs | 1 + quiche/src/recovery/congestion/recovery.rs | 7 ++++++ quiche/src/recovery/gcongestion/recovery.rs | 8 ++++++ quiche/src/recovery/mod.rs | 25 +++++++++++++++++++ quiche/src/tests.rs | 20 +++++++++++++++ 9 files changed, 78 insertions(+), 1 deletion(-) diff --git a/apps/src/bin/quiche-server.rs b/apps/src/bin/quiche-server.rs index ca4e397b235..bb526eed02b 100644 --- a/apps/src/bin/quiche-server.rs +++ b/apps/src/bin/quiche-server.rs @@ -541,8 +541,9 @@ fn main() { continue_write = false; for client in clients.values_mut() { // Reduce max_send_burst by 25% if loss is increasing more than 0.1%. + let stats = client.conn.stats(); let loss_rate = - client.conn.stats().lost as f64 / client.conn.stats().sent as f64; + stats.lost as f64 / stats.acked as f64 + stats.lost as f64; if loss_rate > client.loss_rate + 0.001 { client.max_send_burst = client.max_send_burst / 4 * 3; // Minimum bound of 10xMSS. diff --git a/quiche/include/quiche.h b/quiche/include/quiche.h index f72f8fcf0bd..132497c6e60 100644 --- a/quiche/include/quiche.h +++ b/quiche/include/quiche.h @@ -572,6 +572,9 @@ typedef struct { // The number of QUIC packets sent on this connection. size_t sent; + // The number of QUIC packets that were acked. + size_t acked; + // The number of QUIC packets that were lost. size_t lost; diff --git a/quiche/src/ffi.rs b/quiche/src/ffi.rs index 827fb01b382..b49a92f09f1 100644 --- a/quiche/src/ffi.rs +++ b/quiche/src/ffi.rs @@ -1322,6 +1322,7 @@ pub extern "C" fn quiche_stream_iter_free(iter: *mut StreamIter) { pub struct Stats { recv: usize, sent: usize, + acked: usize, lost: usize, spurious_lost: usize, retrans: usize, @@ -1370,6 +1371,7 @@ pub extern "C" fn quiche_conn_stats(conn: &Connection, out: &mut Stats) { out.recv = stats.recv; out.sent = stats.sent; + out.acked = stats.acked; out.lost = stats.lost; out.spurious_lost = stats.spurious_lost; out.retrans = stats.retrans; diff --git a/quiche/src/lib.rs b/quiche/src/lib.rs index 5275629ceee..047d445c14b 100644 --- a/quiche/src/lib.rs +++ b/quiche/src/lib.rs @@ -1384,6 +1384,9 @@ where /// Total number of sent packets. sent_count: usize, + /// Total number of acked packets. + acked_count: usize, + /// Total number of lost packets. lost_count: usize, @@ -2110,6 +2113,7 @@ impl Connection { recv_count: 0, sent_count: 0, + acked_count: 0, lost_count: 0, spurious_lost_count: 0, retrans_count: 0, @@ -7785,6 +7789,7 @@ impl Connection { Stats { recv: self.recv_count, sent: self.sent_count, + acked: self.acked_count, lost: self.lost_count, spurious_lost: self.spurious_lost_count, retrans: self.retrans_count, @@ -8299,6 +8304,7 @@ impl Connection { let OnAckReceivedOutcome { lost_packets, lost_bytes, + acked_packets, acked_bytes, spurious_losses, } = p.recovery.on_ack_received( @@ -8327,6 +8333,7 @@ impl Connection { self.lost_count += lost_packets; self.lost_bytes += lost_bytes as u64; + self.acked_count += acked_packets; self.acked_bytes += acked_bytes as u64; self.spurious_lost_count += spurious_losses; } @@ -9355,6 +9362,9 @@ pub struct Stats { /// The number of QUIC packets sent. pub sent: usize, + /// The number of QUIC packets that were acked. + pub acked: usize, + /// The number of QUIC packets that were lost. pub lost: usize, diff --git a/quiche/src/recovery/congestion/delivery_rate.rs b/quiche/src/recovery/congestion/delivery_rate.rs index 6cab772ad7c..0f013cbbcd5 100644 --- a/quiche/src/recovery/congestion/delivery_rate.rs +++ b/quiche/src/recovery/congestion/delivery_rate.rs @@ -507,6 +507,7 @@ mod tests { assert_eq!(ack_outcome, OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: packet_count, acked_bytes: mss * packet_count, spurious_losses: 0, }); diff --git a/quiche/src/recovery/congestion/recovery.rs b/quiche/src/recovery/congestion/recovery.rs index bd519e4ce68..a0bfbcdeb9e 100644 --- a/quiche/src/recovery/congestion/recovery.rs +++ b/quiche/src/recovery/congestion/recovery.rs @@ -96,6 +96,7 @@ struct RecoveryEpoch { } struct AckedDetectionResult { + acked_packets: usize, acked_bytes: usize, spurious_losses: usize, spurious_pkt_thresh: Option, @@ -119,6 +120,7 @@ impl RecoveryEpoch { ) -> Result { newly_acked.clear(); + let mut acked_packets = 0; let mut acked_bytes = 0; let mut spurious_losses = 0; let mut spurious_pkt_thresh = None; @@ -176,6 +178,8 @@ impl RecoveryEpoch { has_in_flight_spurious_loss = true; } } else { + acked_packets += 1; + if unacked.in_flight { self.in_flight_count -= 1; acked_bytes += unacked.size; @@ -207,6 +211,7 @@ impl RecoveryEpoch { self.drain_acked_and_lost_packets(now - rtt_stats.rtt()); Ok(AckedDetectionResult { + acked_packets, acked_bytes, spurious_losses, spurious_pkt_thresh, @@ -638,6 +643,7 @@ impl RecoveryOps for LegacyRecovery { trace_id: &str, ) -> Result { let AckedDetectionResult { + acked_packets, acked_bytes, spurious_losses, spurious_pkt_thresh, @@ -715,6 +721,7 @@ impl RecoveryOps for LegacyRecovery { Ok(OnAckReceivedOutcome { lost_packets, lost_bytes, + acked_packets, acked_bytes, spurious_losses, }) diff --git a/quiche/src/recovery/gcongestion/recovery.rs b/quiche/src/recovery/gcongestion/recovery.rs index b30a13286c5..e98b337fdc1 100644 --- a/quiche/src/recovery/gcongestion/recovery.rs +++ b/quiche/src/recovery/gcongestion/recovery.rs @@ -113,6 +113,7 @@ struct RecoveryEpoch { } struct AckedDetectionResult { + acked_packets: usize, acked_bytes: usize, spurious_losses: usize, spurious_pkt_thresh: Option, @@ -170,6 +171,7 @@ impl RecoveryEpoch { ) -> Result { newly_acked.clear(); + let mut acked_packets = 0; let mut acked_bytes = 0; let mut spurious_losses = 0; let mut spurious_pkt_thresh = None; @@ -219,6 +221,8 @@ impl RecoveryEpoch { ack_eliciting, .. } => { + acked_packets += 1; + if in_flight { self.pkts_in_flight -= 1; acked_bytes += sent_bytes; @@ -252,6 +256,7 @@ impl RecoveryEpoch { self.drain_acked_and_lost_packets(); Ok(AckedDetectionResult { + acked_packets, acked_bytes, spurious_losses, spurious_pkt_thresh, @@ -777,6 +782,7 @@ impl RecoveryOps for GRecovery { let prior_in_flight = self.bytes_in_flight.get(); let AckedDetectionResult { + acked_packets, acked_bytes, spurious_losses, spurious_pkt_thresh, @@ -795,6 +801,7 @@ impl RecoveryOps for GRecovery { if self.newly_acked.is_empty() { return Ok(OnAckReceivedOutcome { + acked_packets, acked_bytes, spurious_losses, ..Default::default() @@ -851,6 +858,7 @@ impl RecoveryOps for GRecovery { Ok(OnAckReceivedOutcome { lost_packets, lost_bytes, + acked_packets, acked_bytes, spurious_losses, }) diff --git a/quiche/src/recovery/mod.rs b/quiche/src/recovery/mod.rs index 920f46cbe85..e7bbe5c6c0e 100644 --- a/quiche/src/recovery/mod.rs +++ b/quiche/src/recovery/mod.rs @@ -172,6 +172,7 @@ pub(crate) enum Recovery { pub struct OnAckReceivedOutcome { pub lost_packets: usize, pub lost_bytes: usize, + pub acked_packets: usize, pub acked_bytes: usize, pub spurious_losses: usize, } @@ -1027,6 +1028,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 2, acked_bytes: 2 * 1000, spurious_losses: 0, } @@ -1128,6 +1130,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 2, lost_bytes: 2000, + acked_packets: 2, acked_bytes: 2 * 1000, spurious_losses: 0, } @@ -1312,6 +1315,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 3, acked_bytes: 3 * 1000, spurious_losses: 0, } @@ -1404,6 +1408,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 1, lost_bytes: 1000, + acked_packets: 2, acked_bytes: 1000 * 2, spurious_losses: 0, } @@ -1434,6 +1439,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 1, } @@ -1551,6 +1557,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 1, lost_bytes: 1000, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 0, } @@ -1578,6 +1585,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 0, acked_bytes: 0, spurious_losses: 1, } @@ -1609,6 +1617,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 0, } @@ -1638,6 +1647,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 1, lost_bytes: 1000, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 0, } @@ -1726,6 +1736,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 1, lost_bytes: 1000, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 0, } @@ -1754,6 +1765,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 0, acked_bytes: 0, spurious_losses: 1, } @@ -1793,6 +1805,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 1, lost_bytes: 1000, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 0, } @@ -1825,6 +1838,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 0, acked_bytes: 0, spurious_losses: 1, } @@ -1926,6 +1940,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 10, acked_bytes: 12000, spurious_losses: 0, } @@ -2096,6 +2111,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 1, acked_bytes: 6000, spurious_losses: 0, } @@ -2125,6 +2141,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 1, acked_bytes: 6000, spurious_losses: 0, } @@ -2167,6 +2184,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 1, acked_bytes: 1000, spurious_losses: 0, } @@ -2275,6 +2293,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 2, acked_bytes: 2400, spurious_losses: 0, } @@ -2338,6 +2357,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 2, acked_bytes: 2 * 1000, spurious_losses: 0, } @@ -2366,6 +2386,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 2, acked_bytes: 2 * 1000, spurious_losses: 0, } @@ -2504,6 +2525,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 2, acked_bytes: 2 * 1000, spurious_losses: 0, } @@ -2593,6 +2615,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 10, acked_bytes: total_bytes_sent, spurious_losses: 0, } @@ -2672,6 +2695,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 3, acked_bytes: 3600, spurious_losses: 0, } @@ -2727,6 +2751,7 @@ mod tests { OnAckReceivedOutcome { lost_packets: 0, lost_bytes: 0, + acked_packets: 1, acked_bytes: 0, spurious_losses: 0, } diff --git a/quiche/src/tests.rs b/quiche/src/tests.rs index 7eec0653996..9bf04e2b2b3 100644 --- a/quiche/src/tests.rs +++ b/quiche/src/tests.rs @@ -2212,6 +2212,26 @@ fn stream_left_reset_bidi( assert_eq!(3, pipe.client.peer_streams_left_bidi()); } +#[rstest] +fn acked_packet_counts( + #[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str, +) { + let mut pipe = test_utils::Pipe::new(cc_algorithm_name).unwrap(); + assert_eq!(pipe.handshake(), Ok(())); + + let acked_before = pipe.client.stats().acked; + + assert_eq!(pipe.client.stream_send(0, b"a", false), Ok(1)); + + // Run twice to make sure both data and the corresponding ACK are processed. + pipe.advance().unwrap(); + pipe.advance().unwrap(); + + let stats = pipe.client.stats(); + assert!(stats.acked > acked_before); + assert!(stats.acked <= stats.sent); +} + #[rstest] fn stream_reset_counts( #[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str, From f529e0165fb5f7d12ae583c50354c2a0acaa4939 Mon Sep 17 00:00:00 2001 From: xx Date: Thu, 16 Apr 2026 08:36:34 +0800 Subject: [PATCH 2/4] fix calculation issue --- apps/src/bin/quiche-server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/bin/quiche-server.rs b/apps/src/bin/quiche-server.rs index bb526eed02b..499146f0da9 100644 --- a/apps/src/bin/quiche-server.rs +++ b/apps/src/bin/quiche-server.rs @@ -543,7 +543,7 @@ fn main() { // Reduce max_send_burst by 25% if loss is increasing more than 0.1%. let stats = client.conn.stats(); let loss_rate = - stats.lost as f64 / stats.acked as f64 + stats.lost as f64; + stats.lost as f64 / (stats.acked as f64 + stats.lost as f64); if loss_rate > client.loss_rate + 0.001 { client.max_send_burst = client.max_send_burst / 4 * 3; // Minimum bound of 10xMSS. From ecf837002ef9375c89456df4a19ec34897cdfd61 Mon Sep 17 00:00:00 2001 From: xx Date: Thu, 16 Apr 2026 08:43:32 +0800 Subject: [PATCH 3/4] apply denominator guard --- apps/src/bin/quiche-server.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/src/bin/quiche-server.rs b/apps/src/bin/quiche-server.rs index 499146f0da9..e9265816db3 100644 --- a/apps/src/bin/quiche-server.rs +++ b/apps/src/bin/quiche-server.rs @@ -542,8 +542,13 @@ fn main() { for client in clients.values_mut() { // Reduce max_send_burst by 25% if loss is increasing more than 0.1%. let stats = client.conn.stats(); - let loss_rate = - stats.lost as f64 / (stats.acked as f64 + stats.lost as f64); + let confirmed = stats.acked.saturating_add(stats.lost); + let loss_rate = if confirmed == 0 { + 0.0 + } else { + stats.lost as f64 / confirmed as f64 + }; + if loss_rate > client.loss_rate + 0.001 { client.max_send_burst = client.max_send_burst / 4 * 3; // Minimum bound of 10xMSS. From 13d97d0ddab8e0f74209e55408a13319c0309de7 Mon Sep 17 00:00:00 2001 From: xx Date: Thu, 16 Apr 2026 19:40:48 +0800 Subject: [PATCH 4/4] better variable name --- apps/src/bin/quiche-server.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/src/bin/quiche-server.rs b/apps/src/bin/quiche-server.rs index e9265816db3..38649cfe392 100644 --- a/apps/src/bin/quiche-server.rs +++ b/apps/src/bin/quiche-server.rs @@ -542,11 +542,11 @@ fn main() { for client in clients.values_mut() { // Reduce max_send_burst by 25% if loss is increasing more than 0.1%. let stats = client.conn.stats(); - let confirmed = stats.acked.saturating_add(stats.lost); - let loss_rate = if confirmed == 0 { + let finished = stats.acked.saturating_add(stats.lost); + let loss_rate = if finished == 0 { 0.0 } else { - stats.lost as f64 / confirmed as f64 + stats.lost as f64 / finished as f64 }; if loss_rate > client.loss_rate + 0.001 {