use std::{cmp, net::SocketAddr};
use tracing::trace;
use super::{
mtud::MtuDiscovery,
pacing::Pacer,
spaces::{PacketSpace, SentPacket},
};
use crate::{Duration, Instant, TIMER_GRANULARITY, TransportConfig, congestion, packet::SpaceId};
#[cfg(feature = "qlog")]
use qlog::events::{ExData, quic::RecoveryMetricsUpdated};
pub(super) struct PathData {
pub(super) remote: SocketAddr,
pub(super) rtt: RttEstimator,
pub(super) sending_ecn: bool,
pub(super) congestion: Box<dyn congestion::Controller>,
pub(super) pacing: Pacer,
pub(super) challenge: Option<u64>,
pub(super) challenge_pending: bool,
pub(super) validated: bool,
pub(super) total_sent: u64,
pub(super) total_recvd: u64,
pub(super) mtud: MtuDiscovery,
pub(super) first_packet_after_rtt_sample: Option<(SpaceId, u64)>,
pub(super) in_flight: InFlight,
first_packet: Option<u64>,
#[cfg(feature = "qlog")]
recovery_metrics: RecoveryMetrics,
generation: u64,
}
impl PathData {
pub(super) fn new(
remote: SocketAddr,
allow_mtud: bool,
peer_max_udp_payload_size: Option<u16>,
generation: u64,
now: Instant,
config: &TransportConfig,
initial_rtt: Duration,
) -> Self {
let congestion = config
.congestion_controller_factory
.clone()
.build(now, config.get_initial_mtu());
Self {
remote,
rtt: RttEstimator::new(initial_rtt),
sending_ecn: true,
pacing: Pacer::new(
initial_rtt,
congestion.initial_window(),
config.get_initial_mtu(),
config.max_outgoing_bytes_per_second,
now,
),
congestion,
challenge: None,
challenge_pending: false,
validated: false,
total_sent: 0,
total_recvd: 0,
mtud: config
.mtu_discovery_config
.as_ref()
.filter(|_| allow_mtud)
.map_or_else(
|| MtuDiscovery::disabled(config.get_initial_mtu(), config.min_mtu),
|mtud_config| {
MtuDiscovery::new(
config.get_initial_mtu(),
config.min_mtu,
peer_max_udp_payload_size,
mtud_config.clone(),
)
},
),
first_packet_after_rtt_sample: None,
in_flight: InFlight::new(),
first_packet: None,
#[cfg(feature = "qlog")]
recovery_metrics: RecoveryMetrics::default(),
generation,
}
}
pub(super) fn from_previous(
remote: SocketAddr,
prev: &Self,
generation: u64,
now: Instant,
) -> Self {
let congestion = prev.congestion.clone_box();
let smoothed_rtt = prev.rtt.get();
Self {
remote,
rtt: prev.rtt,
pacing: Pacer::new(
smoothed_rtt,
congestion.window(),
prev.current_mtu(),
prev.pacing.max_bytes_per_second(),
now,
),
sending_ecn: true,
congestion,
challenge: None,
challenge_pending: false,
validated: false,
total_sent: 0,
total_recvd: 0,
mtud: prev.mtud.clone(),
first_packet_after_rtt_sample: prev.first_packet_after_rtt_sample,
in_flight: InFlight::new(),
first_packet: None,
#[cfg(feature = "qlog")]
recovery_metrics: prev.recovery_metrics.clone(),
generation,
}
}
pub(super) fn reset(&mut self, now: Instant, config: &TransportConfig) {
self.rtt = RttEstimator::new(config.initial_rtt);
self.congestion = config
.congestion_controller_factory
.clone()
.build(now, config.get_initial_mtu());
self.mtud.reset(config.get_initial_mtu(), config.min_mtu);
self.pacing = Pacer::new(
self.rtt.get(),
self.congestion.initial_window(),
self.current_mtu(),
config.max_outgoing_bytes_per_second,
now,
);
}
pub(super) fn set_initial_rtt(&mut self, initial_rtt: Duration, now: Instant) {
if !self.rtt.set_initial_rtt(initial_rtt) {
return;
}
let window = self.congestion.window();
let mtu = self.current_mtu();
self.pacing = Pacer::new(
initial_rtt,
window,
mtu,
self.pacing.max_bytes_per_second(),
now,
);
}
pub(super) fn anti_amplification_blocked(&self, bytes_to_send: u64) -> bool {
!self.validated && self.total_recvd * 3 < self.total_sent + bytes_to_send
}
pub(super) fn current_mtu(&self) -> u16 {
self.mtud.current_mtu()
}
pub(super) fn sent(&mut self, pn: u64, packet: SentPacket, space: &mut PacketSpace) {
self.in_flight.insert(&packet);
if self.first_packet.is_none() {
self.first_packet = Some(pn);
}
if let Some(forgotten) = space.sent(pn, packet) {
self.remove_in_flight(&forgotten);
}
}
pub(super) fn remove_in_flight(&mut self, packet: &SentPacket) -> bool {
if packet.path_generation != self.generation {
return false;
}
self.in_flight.remove(packet);
true
}
#[cfg(feature = "qlog")]
pub(super) fn qlog_recovery_metrics(
&mut self,
pto_count: u32,
) -> Option<RecoveryMetricsUpdated> {
let controller_metrics = self.congestion.metrics();
let metrics = RecoveryMetrics {
min_rtt: Some(self.rtt.min),
smoothed_rtt: Some(self.rtt.get()),
latest_rtt: Some(self.rtt.latest),
rtt_variance: Some(self.rtt.var),
pto_count: Some(pto_count),
bytes_in_flight: Some(self.in_flight.bytes),
packets_in_flight: Some(self.in_flight.ack_eliciting),
congestion_window: Some(controller_metrics.congestion_window),
ssthresh: controller_metrics.ssthresh,
pacing_rate: controller_metrics.pacing_rate,
};
let event = metrics.to_qlog_event(&self.recovery_metrics);
self.recovery_metrics = metrics;
event
}
pub(super) fn generation(&self) -> u64 {
self.generation
}
}
#[cfg(feature = "qlog")]
#[derive(Default, Clone, PartialEq)]
#[non_exhaustive]
struct RecoveryMetrics {
pub min_rtt: Option<Duration>,
pub smoothed_rtt: Option<Duration>,
pub latest_rtt: Option<Duration>,
pub rtt_variance: Option<Duration>,
pub pto_count: Option<u32>,
pub bytes_in_flight: Option<u64>,
pub packets_in_flight: Option<u64>,
pub congestion_window: Option<u64>,
pub ssthresh: Option<u64>,
pub pacing_rate: Option<u64>,
}
#[cfg(feature = "qlog")]
impl RecoveryMetrics {
fn retain_updated(&self, previous: &Self) -> Self {
macro_rules! keep_if_changed {
($name:ident) => {
if previous.$name == self.$name {
None
} else {
self.$name
}
};
}
Self {
min_rtt: keep_if_changed!(min_rtt),
smoothed_rtt: keep_if_changed!(smoothed_rtt),
latest_rtt: keep_if_changed!(latest_rtt),
rtt_variance: keep_if_changed!(rtt_variance),
pto_count: keep_if_changed!(pto_count),
bytes_in_flight: keep_if_changed!(bytes_in_flight),
packets_in_flight: keep_if_changed!(packets_in_flight),
congestion_window: keep_if_changed!(congestion_window),
ssthresh: keep_if_changed!(ssthresh),
pacing_rate: keep_if_changed!(pacing_rate),
}
}
fn to_qlog_event(&self, previous: &Self) -> Option<RecoveryMetricsUpdated> {
let updated = self.retain_updated(previous);
if updated == Self::default() {
return None;
}
Some(RecoveryMetricsUpdated {
ex_data: ExData::default(),
min_rtt: updated.min_rtt.map(|rtt| rtt.as_micros() as f32 / 1000.0),
smoothed_rtt: updated
.smoothed_rtt
.map(|rtt| rtt.as_micros() as f32 / 1000.0),
latest_rtt: updated
.latest_rtt
.map(|rtt| rtt.as_micros() as f32 / 1000.0),
rtt_variance: updated
.rtt_variance
.map(|rtt| rtt.as_micros() as f32 / 1000.0),
pto_count: updated
.pto_count
.map(|count| count.try_into().unwrap_or(u16::MAX)),
bytes_in_flight: updated.bytes_in_flight,
packets_in_flight: updated.packets_in_flight,
congestion_window: updated.congestion_window,
ssthresh: updated.ssthresh,
pacing_rate: updated.pacing_rate,
})
}
}
#[derive(Copy, Clone)]
pub struct RttEstimator {
latest: Duration,
smoothed: Option<Duration>,
var: Duration,
min: Duration,
}
impl RttEstimator {
pub(crate) fn new(initial_rtt: Duration) -> Self {
Self {
latest: initial_rtt,
smoothed: None,
var: initial_rtt / 2,
min: initial_rtt,
}
}
pub fn get(&self) -> Duration {
self.smoothed.unwrap_or(self.latest)
}
pub(crate) fn smoothed(&self) -> Option<Duration> {
self.smoothed
}
fn set_initial_rtt(&mut self, initial_rtt: Duration) -> bool {
if self.smoothed.is_some() {
return false;
}
*self = Self::new(initial_rtt);
true
}
pub fn conservative(&self) -> Duration {
self.get().max(self.latest)
}
pub fn min(&self) -> Duration {
self.min
}
pub(crate) fn pto_base(&self) -> Duration {
self.get() + cmp::max(4 * self.var, TIMER_GRANULARITY)
}
pub(crate) fn update(&mut self, ack_delay: Duration, rtt: Duration) {
self.latest = rtt;
self.min = cmp::min(self.min, self.latest);
if let Some(smoothed) = self.smoothed {
let adjusted_rtt = if self.min + ack_delay <= self.latest {
self.latest - ack_delay
} else {
self.latest
};
let var_sample = smoothed.abs_diff(adjusted_rtt);
self.var = (3 * self.var + var_sample) / 4;
self.smoothed = Some((7 * smoothed + adjusted_rtt) / 8);
} else {
self.smoothed = Some(self.latest);
self.var = self.latest / 2;
self.min = self.latest;
}
}
}
#[cfg(test)]
mod rtt_estimator_tests {
use super::*;
#[test]
fn initial_rtt_only_changes_before_first_sample() {
let mut rtt = RttEstimator::new(Duration::from_millis(333));
assert!(rtt.set_initial_rtt(Duration::from_millis(20)));
assert_eq!(rtt.get(), Duration::from_millis(20));
assert_eq!(rtt.var, Duration::from_millis(10));
assert_eq!(rtt.min, Duration::from_millis(20));
rtt.update(Duration::ZERO, Duration::from_millis(30));
assert!(!rtt.set_initial_rtt(Duration::from_millis(40)));
assert_eq!(rtt.get(), Duration::from_millis(30));
assert_eq!(rtt.min, Duration::from_millis(30));
}
}
#[derive(Default)]
pub(crate) struct PathResponses {
pending: Vec<PathResponse>,
}
impl PathResponses {
pub(crate) fn push(&mut self, packet: u64, token: u64, remote: SocketAddr) {
const MAX_PATH_RESPONSES: usize = 16;
let response = PathResponse {
packet,
token,
remote,
};
let existing = self.pending.iter_mut().find(|x| x.remote == remote);
if let Some(existing) = existing {
if existing.packet <= packet {
*existing = response;
}
return;
}
if self.pending.len() < MAX_PATH_RESPONSES {
self.pending.push(response);
} else {
trace!("ignoring excessive PATH_CHALLENGE");
}
}
pub(crate) fn pop_off_path(&mut self, remote: SocketAddr) -> Option<(u64, SocketAddr)> {
let response = *self.pending.last()?;
if response.remote == remote {
return None;
}
self.pending.pop();
Some((response.token, response.remote))
}
pub(crate) fn pop_on_path(&mut self, remote: SocketAddr) -> Option<u64> {
let response = *self.pending.last()?;
if response.remote != remote {
return None;
}
self.pending.pop();
Some(response.token)
}
pub(crate) fn is_empty(&self) -> bool {
self.pending.is_empty()
}
}
#[derive(Copy, Clone)]
struct PathResponse {
packet: u64,
token: u64,
remote: SocketAddr,
}
pub(super) struct InFlight {
pub(super) bytes: u64,
pub(super) ack_eliciting: u64,
}
impl InFlight {
fn new() -> Self {
Self {
bytes: 0,
ack_eliciting: 0,
}
}
fn insert(&mut self, packet: &SentPacket) {
self.bytes += u64::from(packet.size);
self.ack_eliciting += u64::from(packet.ack_eliciting);
}
fn remove(&mut self, packet: &SentPacket) {
self.bytes -= u64::from(packet.size);
self.ack_eliciting -= u64::from(packet.ack_eliciting);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reset_refreshes_pacer_budget() {
let now = Instant::now();
let config = TransportConfig::default();
let remote = "203.0.113.1:4433".parse().unwrap();
let mut path = PathData::new(remote, true, None, 0, now, &config, config.initial_rtt);
let mtu = path.current_mtu();
let window = path.congestion.window();
for _ in 0..1000 {
if path
.pacing
.delay(path.rtt.get(), mtu.into(), mtu, window, now)
.is_some()
{
break;
}
path.pacing.on_transmit(mtu);
}
assert!(
path.pacing
.delay(path.rtt.get(), mtu.into(), mtu, window, now)
.is_some()
);
path.reset(now, &config);
assert_eq!(
path.pacing.delay(
path.rtt.get(),
path.current_mtu().into(),
path.current_mtu(),
path.congestion.window(),
now
),
None
);
}
}