mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-05 20:19:45 +00:00
perf(mpsc): batch writev flush (threshold=8) for TCP — +7% pps
Add configurable batch flush threshold to SpinSink. When threshold > 1, MpscTunnelSender::send accumulates packets in FramedWriter's BufList without flushing. After N packets, poll_flush triggers a single writev() syscall instead of N individual write() syscalls. Implementation: - SpinSink: pending_count + batch_threshold atomics - MpscTunnelSender::send: flush every N packets via writev - Default threshold=1 (per-packet flush, safe for handshake/control) - Settable via set_batch_threshold() through PeerConn → Peer → PeerManager - Bench: HOTPATH_BATCH env var, set after convergence Batch threshold must be 1 during handshake (control packets are request-response, can't be delayed). Bench sets threshold=8 only after routes converge. Benchmark (no hotpath, 3 runs avg): TCP batch=1: 985K pps TCP batch=8: 1,053K pps (+7%) Ring: unchanged (flush is no-op for RingSink) UDP: unchanged (flush is no-op for RingSink) MpscTunnelSender::send avg: 343ns → 213ns (-38%, with hotpath) — writev writes 8 Bytes in one syscall vs 8 write() calls. All 210 peers tests pass. 6 netns tests fail (require root, unchanged).
This commit is contained in:
@@ -126,6 +126,17 @@ async fn main() {
|
||||
let pm = inst_a.get_peer_manager();
|
||||
let send_pkt = make_data_packet(src, "10.144.144.2", pkt_size);
|
||||
|
||||
let batch_threshold: u32 = std::env::var("HOTPATH_BATCH")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
// After convergence, enable batch flush for writev optimization
|
||||
if converged && batch_threshold > 1 {
|
||||
pm.set_peer_conn_batch_threshold(batch_threshold);
|
||||
println!("cpu_hotspot_ring: batch_threshold={}", batch_threshold);
|
||||
}
|
||||
|
||||
let pipeline_depth: usize = std::env::var("HOTPATH_PIPELINE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
|
||||
@@ -268,6 +268,12 @@ impl Peer {
|
||||
self.default_conn_id.load()
|
||||
}
|
||||
|
||||
pub fn set_batch_threshold(&self, n: u32) {
|
||||
for conn in self.conns.iter() {
|
||||
conn.value().set_batch_threshold(n);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_peer_identity_type(&self) -> Option<PeerIdentityType> {
|
||||
self.peer_identity_type.load()
|
||||
}
|
||||
|
||||
@@ -451,6 +451,10 @@ impl PeerConn {
|
||||
self.conn_id
|
||||
}
|
||||
|
||||
pub fn set_batch_threshold(&self, n: u32) {
|
||||
self.sink.set_batch_threshold(n);
|
||||
}
|
||||
|
||||
pub fn set_is_hole_punched(&mut self, is_hole_punched: bool) {
|
||||
self.is_hole_punched = is_hole_punched;
|
||||
}
|
||||
|
||||
@@ -1994,6 +1994,18 @@ impl PeerManager {
|
||||
self.peers.clone()
|
||||
}
|
||||
|
||||
pub fn set_peer_conn_batch_threshold(&self, n: u32) {
|
||||
let peers = self.peers.clone();
|
||||
tokio::spawn(async move {
|
||||
let peer_ids = peers.list_peers();
|
||||
for peer_id in peer_ids {
|
||||
if let Some(peer) = peers.get_peer_by_id(peer_id) {
|
||||
peer.set_batch_threshold(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_relay_peer_map(&self) -> Arc<RelayPeerMap> {
|
||||
self.relay_peer_map.clone()
|
||||
}
|
||||
|
||||
+27
-11
@@ -4,7 +4,7 @@ use std::{
|
||||
cell::UnsafeCell,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
task::Poll,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -26,6 +26,8 @@ use futures::SinkExt;
|
||||
struct SpinSink {
|
||||
locked: AtomicBool,
|
||||
sink: UnsafeCell<Pin<Box<dyn ZCPacketSink>>>,
|
||||
pending_count: AtomicU32,
|
||||
batch_threshold: AtomicU32,
|
||||
}
|
||||
|
||||
// SAFETY: access is serialized by the spinlock.
|
||||
@@ -55,9 +57,15 @@ impl SpinSink {
|
||||
Self {
|
||||
locked: AtomicBool::new(false),
|
||||
sink: UnsafeCell::new(sink),
|
||||
pending_count: AtomicU32::new(0),
|
||||
batch_threshold: AtomicU32::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_batch_threshold(&self, n: u32) {
|
||||
self.batch_threshold.store(n, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn try_lock(&self) -> Option<SpinGuard<'_>> {
|
||||
if self
|
||||
.locked
|
||||
@@ -89,17 +97,19 @@ impl MpscTunnelSender {
|
||||
match guard.as_mut().poll_ready(&mut cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
guard.as_mut().start_send(item)?;
|
||||
if self.direct_batch_flush {
|
||||
// RingSink: flush is no-op, data already in ring buffer.
|
||||
// Skip to allow poll_ready batching at max_buffer_count.
|
||||
return Ok(());
|
||||
}
|
||||
// FramedWriter (TCP): must flush per-packet, otherwise
|
||||
// noop_waker can't wake when socket is full.
|
||||
match guard.as_mut().poll_flush(&mut cx) {
|
||||
Poll::Ready(Err(e)) => return Err(e),
|
||||
_ => return Ok(()),
|
||||
let count = sink.pending_count.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let threshold = sink.batch_threshold.load(Ordering::Relaxed);
|
||||
if count >= threshold {
|
||||
sink.pending_count.store(0, Ordering::Relaxed);
|
||||
// Batch flush: writev all accumulated BufList entries.
|
||||
// RingSink: no-op. FramedWriter: single writev syscall.
|
||||
match guard.as_mut().poll_flush(&mut cx) {
|
||||
Poll::Ready(Err(e)) => return Err(e),
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
// Accumulate in BufList, no flush yet
|
||||
return Ok(());
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Err(e),
|
||||
Poll::Pending => return Err(TunnelError::BufferFull),
|
||||
@@ -120,6 +130,12 @@ impl MpscTunnelSender {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_batch_threshold(&self, n: u32) {
|
||||
if let Some(sink) = &self.direct_sink {
|
||||
sink.set_batch_threshold(n);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_async(&self, item: ZCPacket) -> Result<(), TunnelError> {
|
||||
let tx = self.channel_tx.as_ref().ok_or(TunnelError::Shutdown)?;
|
||||
match tx.try_send(item) {
|
||||
|
||||
Reference in New Issue
Block a user