fix(quality): use windowed loss instead of cumulative for codec adaptation
Quinn's cumulative loss_pct (lost / sent since connection start) was biased forever by handshake-era losses. Even ~5 lost-out-of-100 early packets pinned us at "Degraded" (5% threshold) and Codec2_1200 was just a few more drops away. The metric only diluted as thousands more clean packets accumulated — by which time the call was over. LossWindow tracks prev (sent, lost) and reports delta loss per ~25- packet window. The cumulative value is the fallback when the window hasn't accumulated enough samples (< 20 packets). All 6 sites converted (DRED tuner + QualityReport on both send tasks, self-observation on both recv tasks). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,32 @@ const QUALITY_REPORT_INTERVAL: u32 = 50;
|
||||
/// Profile index mapping for the AtomicU8 adaptive-quality bridge.
|
||||
const PROFILE_NO_CHANGE: u8 = 0xFF;
|
||||
|
||||
/// Tracks Quinn's cumulative sent/lost counters so callers can compute
|
||||
/// loss over a sliding window instead of since-connection-start. The
|
||||
/// cumulative percentage is monotonically biased by handshake-era losses
|
||||
/// and never recovers; the windowed percentage reflects current health.
|
||||
#[derive(Default)]
|
||||
struct LossWindow {
|
||||
prev_sent: u64,
|
||||
prev_lost: u64,
|
||||
}
|
||||
|
||||
impl LossWindow {
|
||||
/// Returns the loss percentage observed since the last call. Falls back
|
||||
/// to the cumulative value while we don't yet have a delta to compare.
|
||||
fn observe(&mut self, sent_packets: u64, lost_packets: u64, cumulative_pct: f32) -> f32 {
|
||||
let d_sent = sent_packets.saturating_sub(self.prev_sent);
|
||||
let d_lost = lost_packets.saturating_sub(self.prev_lost);
|
||||
self.prev_sent = sent_packets;
|
||||
self.prev_lost = lost_packets;
|
||||
if d_sent >= 20 {
|
||||
(d_lost as f32 / d_sent as f32) * 100.0
|
||||
} else {
|
||||
cumulative_pct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_to_index(p: &QualityProfile) -> u8 {
|
||||
match p.codec {
|
||||
CodecId::Opus64k => 0,
|
||||
@@ -843,6 +869,7 @@ impl CallEngine {
|
||||
let mut dred_tuner = wzp_proto::DredTuner::new(config.profile.codec);
|
||||
let mut frames_since_dred_poll: u32 = 0;
|
||||
let mut frames_since_quality_report: u32 = 0;
|
||||
let mut send_loss_window = LossWindow::default();
|
||||
|
||||
let mut heartbeat = std::time::Instant::now();
|
||||
let mut last_rms: u32;
|
||||
@@ -983,8 +1010,13 @@ impl CallEngine {
|
||||
frames_since_dred_poll = 0;
|
||||
let snap = quinn_t.quinn_path_stats();
|
||||
let pq = send_t.path_quality();
|
||||
let win_loss = send_loss_window.observe(
|
||||
snap.sent_packets,
|
||||
snap.lost_packets,
|
||||
snap.loss_pct,
|
||||
);
|
||||
if let Some(tuning) =
|
||||
dred_tuner.update(snap.loss_pct, snap.rtt_ms, pq.jitter_ms)
|
||||
dred_tuner.update(win_loss, snap.rtt_ms, pq.jitter_ms)
|
||||
{
|
||||
encoder.apply_dred_tuning(tuning);
|
||||
if wzp_codec::dred_verbose_logs() {
|
||||
@@ -992,7 +1024,7 @@ impl CallEngine {
|
||||
dred_frames = tuning.dred_frames,
|
||||
dred_ms = tuning.dred_frames as u32 * 10,
|
||||
expected_loss = tuning.expected_loss_pct,
|
||||
quinn_loss = format!("{:.1}", snap.loss_pct),
|
||||
quinn_loss = format!("{:.1}", win_loss),
|
||||
quinn_rtt = snap.rtt_ms,
|
||||
jitter = pq.jitter_ms,
|
||||
spike = dred_tuner.spike_boost_active(),
|
||||
@@ -1009,8 +1041,13 @@ impl CallEngine {
|
||||
frames_since_quality_report = 0;
|
||||
let snap = quinn_t.quinn_path_stats();
|
||||
let pq = send_t.path_quality();
|
||||
let report = wzp_proto::QualityReport::from_path_stats(
|
||||
let win_loss = send_loss_window.observe(
|
||||
snap.sent_packets,
|
||||
snap.lost_packets,
|
||||
snap.loss_pct,
|
||||
);
|
||||
let report = wzp_proto::QualityReport::from_path_stats(
|
||||
win_loss,
|
||||
snap.rtt_ms,
|
||||
pq.jitter_ms,
|
||||
);
|
||||
@@ -1080,6 +1117,7 @@ impl CallEngine {
|
||||
let mut dred_recv = DredRecvState::new();
|
||||
let mut quality_ctrl = AdaptiveQualityController::new();
|
||||
let mut recv_quality_counter: u32 = 0;
|
||||
let mut recv_loss_window = LossWindow::default();
|
||||
info!(codec = ?current_codec, t_ms = recv_t0.elapsed().as_millis(), "first-join diag: recv task spawned (android/oboe)");
|
||||
// First-join diagnostic latches — see send task above for the
|
||||
// sibling capture milestones.
|
||||
@@ -1300,8 +1338,13 @@ impl CallEngine {
|
||||
recv_quality_counter = 0;
|
||||
let snap = quinn_t.quinn_path_stats();
|
||||
let pq = recv_t.path_quality();
|
||||
let local_report = wzp_proto::QualityReport::from_path_stats(
|
||||
let win_loss = recv_loss_window.observe(
|
||||
snap.sent_packets,
|
||||
snap.lost_packets,
|
||||
snap.loss_pct,
|
||||
);
|
||||
let local_report = wzp_proto::QualityReport::from_path_stats(
|
||||
win_loss,
|
||||
snap.rtt_ms,
|
||||
pq.jitter_ms,
|
||||
);
|
||||
@@ -1876,6 +1919,7 @@ impl CallEngine {
|
||||
let mut dred_tuner = wzp_proto::DredTuner::new(config.profile.codec);
|
||||
let mut frames_since_dred_poll: u32 = 0;
|
||||
let mut frames_since_quality_report: u32 = 0;
|
||||
let mut send_loss_window = LossWindow::default();
|
||||
let mut heartbeat = std::time::Instant::now();
|
||||
let mut last_rms: u32;
|
||||
let mut last_pkt_bytes: usize = 0;
|
||||
@@ -1973,8 +2017,13 @@ impl CallEngine {
|
||||
frames_since_dred_poll = 0;
|
||||
let snap = quinn_t.quinn_path_stats();
|
||||
let pq = send_t.path_quality();
|
||||
let win_loss = send_loss_window.observe(
|
||||
snap.sent_packets,
|
||||
snap.lost_packets,
|
||||
snap.loss_pct,
|
||||
);
|
||||
if let Some(tuning) =
|
||||
dred_tuner.update(snap.loss_pct, snap.rtt_ms, pq.jitter_ms)
|
||||
dred_tuner.update(win_loss, snap.rtt_ms, pq.jitter_ms)
|
||||
{
|
||||
encoder.apply_dred_tuning(tuning);
|
||||
}
|
||||
@@ -1987,8 +2036,13 @@ impl CallEngine {
|
||||
frames_since_quality_report = 0;
|
||||
let snap = quinn_t.quinn_path_stats();
|
||||
let pq = send_t.path_quality();
|
||||
let report = wzp_proto::QualityReport::from_path_stats(
|
||||
let win_loss = send_loss_window.observe(
|
||||
snap.sent_packets,
|
||||
snap.lost_packets,
|
||||
snap.loss_pct,
|
||||
);
|
||||
let report = wzp_proto::QualityReport::from_path_stats(
|
||||
win_loss,
|
||||
snap.rtt_ms,
|
||||
pq.jitter_ms,
|
||||
);
|
||||
@@ -2039,6 +2093,7 @@ impl CallEngine {
|
||||
let mut dred_recv = DredRecvState::new();
|
||||
let mut quality_ctrl = AdaptiveQualityController::new();
|
||||
let mut recv_quality_counter: u32 = 0;
|
||||
let mut recv_loss_window = LossWindow::default();
|
||||
let mut heartbeat = std::time::Instant::now();
|
||||
let mut first_packet_logged = false;
|
||||
let mut video_reassembler = wzp_video::transport::VideoReassembler::new();
|
||||
@@ -2203,8 +2258,13 @@ impl CallEngine {
|
||||
recv_quality_counter = 0;
|
||||
let snap = quinn_t.quinn_path_stats();
|
||||
let pq = recv_t.path_quality();
|
||||
let local_report = wzp_proto::QualityReport::from_path_stats(
|
||||
let win_loss = recv_loss_window.observe(
|
||||
snap.sent_packets,
|
||||
snap.lost_packets,
|
||||
snap.loss_pct,
|
||||
);
|
||||
let local_report = wzp_proto::QualityReport::from_path_stats(
|
||||
win_loss,
|
||||
snap.rtt_ms,
|
||||
pq.jitter_ms,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user