Skip to content

perf: non-blocking pacing in poll loop (recv always immediate) #118

Description

@Yanstart

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

  • ACK processing happens immediately on socket.readable(), not after pacing sleep
  • Pacing only gates outgoing packet sends, not the entire poll loop
  • 351+ tests pass
  • Proxmox benchmark: terrestrial upload >30 Mbps (currently 8.27)

Expected Impact

8 Mbps → 50+ Mbps terrestrial upload (pacing no longer delays ACK processing)

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceThroughput and latency optimizations

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions