Context
Root cause found by adversarial agent review. The upload bottleneck is NOT Mutex contention — it is the pacing sleep in the poll task blocking ACK processing.
The poll loop (client.rs run_poll_loop) does: recv packets → drain conn.send() → pacing sleep → loop. During the pacing sleep, NO ACKs are processed, NO writable notifications fire. BBR2 estimates the rate at ~8 Mbps and paces accordingly, creating a self-reinforcing ceiling.
Proof: Download uses the same Mutex and achieves 15 Mbps (2x upload). If Mutex were the bottleneck, both directions would be equally degraded.
Fix
Separate recv (always immediate) from send (gated by pacing):
loop {
let pacing_deadline = next_pacing_at.saturating_duration_since(Instant::now());
tokio::select! {
biased;
result = socket.readable() => {
// ALWAYS process ACKs immediately, even during pacing
drain_recv_packets();
update_flow_control();
notify_readable_streams();
}
_ = send_data_notify.notified() => {}
_ = tokio::time::sleep(pacing_deadline) => {}
}
// Send packets ONLY if pacing deadline passed
if Instant::now() >= next_pacing_at {
drain_and_send_batch();
next_pacing_at = send_info.at;
}
}
Acceptance Criteria
Expected Impact
8 Mbps → 50+ Mbps terrestrial upload (pacing no longer delays ACK processing)
Context
Root cause found by adversarial agent review. The upload bottleneck is NOT Mutex contention — it is the pacing sleep in the poll task blocking ACK processing.
The poll loop (client.rs run_poll_loop) does: recv packets → drain conn.send() → pacing sleep → loop. During the pacing sleep, NO ACKs are processed, NO writable notifications fire. BBR2 estimates the rate at ~8 Mbps and paces accordingly, creating a self-reinforcing ceiling.
Proof: Download uses the same Mutex and achieves 15 Mbps (2x upload). If Mutex were the bottleneck, both directions would be equally degraded.
Fix
Separate recv (always immediate) from send (gated by pacing):
Acceptance Criteria
Expected Impact
8 Mbps → 50+ Mbps terrestrial upload (pacing no longer delays ACK processing)