Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ until 0.1.0.
Introduced `tai64n_sub_duration`, rejected pre-boot replayed Inits on cold start, initialized rendezvous sequence numbers with wall-clock epoch ms, and rejected post-TTL `RegisterSigned` records.

### Performance
- **Per-core UMEM slab allocation & lock-free ring recycling for AF_XDP (`crates/yip-io`, `bin/yipd`, `crates/yip-bench`, #172).**
Eliminated centralized free-list contention and cross-core MESI cache bouncing in AF_XDP kernel-bypass networking. Partitioned `UmemPool` into isolated, non-overlapping `UmemSlab`s (`UmemPool::new_slabs` / `create_slabs`) allowing worker threads to allocate and free chunks locally with zero atomic coordination. Added lock-free batch recycling (`UmemSlab::recycle_from_rings`) and batch population (`populate_fill_ring`), and vectorized `produce_batch` / `consume_batch` on all circular descriptor rings (`FillRing`, `CompletionRing`, `RxRing`, `TxRing`) using power-of-two mask and slice copying. Boosted circular ring batch throughput from 1,438 Mops/s to **4,057 Mops/s (0.25 ns/op)** and scaled 8-worker throughput to **19.60 Gbps (1.914 Mpps)** with 0 packet drops and 0 reordering.
- **Zero-allocation FEC reassembler hot path, in-place RS repair & flat `ObjectTable` (`crates/yip-transport`, `crates/yip-bench`, #171, PR #176).**
Eliminated all per-packet heap allocations, `HashMap` lookups, and intermediate `Vec<Vec<u8>>` cloning from `FecReassembler::push()`. Pre-allocated contiguous object buffers with a 256-bit bitset (`seen_mask: [u64; 4]`), delivering incoming systematic source shards directly into pre-allocated memory slices (`payload[idx * shard_len ..]`) and extracting completed objects via `std::mem::take` and `truncate` with zero dynamic allocations. Introduced in-place Reed-Solomon packet loss repair (`reconstruct_missing_sources` & `decode_source_into` in `crates/yip-transport/src/rs.rs`), solving missing source shards directly into the pre-allocated payload buffer using a single scratch slice and eliminating the `Vec<Vec<u8>>` heap matrix. Replaced standard library `HashMap<u64, ObjState>` with `ObjectTable`, a flat contiguous open-addressing table featuring SplitMix64 fast hashing and backward-shift deletion with zero tombstones and zero runtime allocations during packet ingress and eviction. Added Criterion microbenchmark suite in `crates/yip-bench/benches/fec_reassemble.rs`, verifying a 3.1x speedup on clean $K=1$ packets (117.11 ns vs 361.98 ns, -67.6% latency), 4.0x speedup on clean $K=3$ objects (173.12 ns vs 690.73 ns, -74.9% latency), and 1.65x speedup on loss recovery under $K=3, R=1$ (631.83 ns vs 1.0401 µs, -39.3% latency).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Because throughput is inversely proportional to the square root of packet loss p
- **TAI64N Anti-Replay Freshness Gating**: Sub-nanosecond monotonic timestamps enforce strict certificate freshness and reject stale or replayed topology announcements.

### 3. Kernel-Bypass Performance
- **Zero-Copy AF_XDP (XSK)**: Direct UMEM memory ring driver bypasses the Linux network stack, sustaining 40.27 Gbps and 3.93 Mpps across 8 CPU cores.
- **Zero-Copy AF_XDP (XSK)**: Direct UMEM memory ring driver with per-core slab partitioning and lock-free ring recycling bypasses the Linux network stack, sustaining 40.27 Gbps and 3.93 Mpps across 8 CPU cores.
- **Asynchronous `io_uring`**: High-performance multishot ring polling drives high packet rates without thread contention or syscall overhead.
- **Multi-Queue TUN Sharding**: `IFF_MULTI_QUEUE` worker isolation maps packet flows to dedicated CPU cores via lock-free SPSC ring buffers.
- **Vectorized Socket I/O**: Batched `recvmmsg` and `sendmmsg` with `UDP_SEGMENT` Generic Segmentation Offload (GSO) optimize standard POSIX network paths.
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ maintainer.
- **Data-plane windowed loss, 8-contender harness & bench polish (#11)** — exact per-class windowed loss fractions ($L_{\text{class}} = M_{\text{class}} / S_{\text{class}}$ in $(H_{\text{prev}}..H_{\text{curr}}]$), 4096-entry synchronized loss detection window matching sent-log capacity, bounded spurious NACK drop invariants, 8-contender comparison matrix and netns runners (yip, WireGuard, AmneziaWG, OpenVPN, n2n, ZeroTier, Tailscale, NetBird), dual TCP & UDP-under-loss evaluation, and configurable 1000-ping variance tightening.
- **Traffic-analysis defense (DAITA & AmneziaWG recipes, #11 / Sub-project #4)** — DAITA discrete packet size bucketing (`round_to_bucket`), stochastic decoy bursts with zero-allocation wire generation (`obf_scratch`), bounded egress micro-jitter queue (up to 1,024 packets with flush-oldest overrun protection), and event-loop poll timeout clamping (`next_scheduled_egress_delay_ms`).
- **Zero-friction distribution & packaging (#147)** — standalone, statically linked musl binaries (`x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl`) with zero dynamic runtime dependencies, multi-arch GitHub Actions and Forgejo automated release pipelines generating tarballs with detached `SHA256SUMS`, production multi-stage Dockerfile (`ghcr.io/femboyisp/yipd` and `yip-rendezvous`), turnkey POSIX `install.sh` with architecture detection, checksum verification, and init auto-detection, hardened multi-init service suite in `contrib/` (systemd with `LimitMEMLOCK=infinity`, capability bounding, and strict sandboxing; OpenRC; and runit), and `cargo-binstall` package metadata integration.
- **Per-core UMEM slab allocation & AF_XDP multi-worker scaling (#172)** — isolated `UmemSlab` partitions eliminating centralized free-list contention, lock-free batch chunk recycling between Completion and Fill rings, vectorized ring slice copying (4,057 Mops/s), and linear multi-worker scaling reaching 19.60 Gbps on 8 cores.
- **Zero-allocation FEC reassembler hot path (#171, PR #176)** — eliminated per-packet heap allocations, `HashMap` lookups, and intermediate `Vec<Vec<u8>>` cloning in `FecReassembler::push()`. Pre-allocated contiguous object buffers with a 256-bit bitset (`seen_mask: [u64; 4]`), zero-alloc in-place systematic delivery via buffer slicing (`mem::take` & `truncate`), in-place Cauchy Reed-Solomon packet loss repair (`reconstruct_missing_sources` & `decode_source_into`) avoiding `Vec<Vec<u8>>` heap matrix allocations, and bounded flat open-addressing slot table (`ObjectTable`) with SplitMix64 fast hashing and backward-shift deletion with zero tombstones. Achieved 3.1x faster reassembly on clean $K=1$ (117 ns), 4.0x on clean $K=3$ (173 ns), and 1.65x on packet loss recovery (632 ns).
- **Comprehensive documentation overhaul & landing page architecture (#170, PR #175)** — transformed repository landing page into an accessible, structured guide with clear "What is yip?", "When to use / when not to use", simplified quickstart, audience-specific pathways, terminal telemetry demo GIF, sanitized production-grade zero-trust CA key management, standardized default port 443 (with 8443 unprivileged fallback), best-of-3 AWS WAN benchmark measurements, and 6 standalone guides in `docs/` (`01-getting-started.md`, `02-configuration.md`, `03-architecture.md`, `04-security-and-threat-model.md`, `05-benchmarking-and-performance.md`, `06-troubleshooting.md`, plus `motivation.md`).

Expand Down
43 changes: 27 additions & 16 deletions bin/yipd/src/sharding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ use std::net::ToSocketAddrs;
use std::os::fd::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};

use yip_io::af_xdp::{FillRing, UmemPool, XskBindMode, XskDesc, XskSocket, UMEM_RING_SIZE};
use yip_io::af_xdp::{
FillRing, UmemPool, UmemSlab, XskBindMode, XskDesc, XskSocket, UMEM_RING_SIZE,
};
use yip_io::poll::Dispatch;

use crate::config::Config;
Expand Down Expand Up @@ -643,6 +645,15 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
}
}

// Allocate single shared UMEM pool partitioned into per-worker slabs for AF_XDP
let mut slabs: Option<Vec<UmemSlab>> = if std::env::var("YIP_XDP_IFNAME").is_ok() {
UmemPool::new_slabs(2048 * num_shards, 4096, num_shards)
.ok()
.map(|(_, s)| s)
} else {
None
};

let mut handles = Vec::with_capacity(num_shards);

for shard_id in 0..num_shards {
Expand All @@ -652,6 +663,13 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
let rx_channels = std::mem::take(&mut rx_matrix[shard_id]);
let peers = all_peers.clone();
let cfg = config.clone();
let mut slab = slabs.as_mut().and_then(|s| {
if !s.is_empty() {
Some(s.remove(0))
} else {
None
}
});

let handle = std::thread::Builder::new()
.name(format!("yipd-shard-{shard_id}"))
Expand All @@ -672,10 +690,9 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
let tun_fd = tun_dev.as_raw_fd();

// Probe opportunistic AF_XDP socket with 3-tier fallback
let mut umem_pool = UmemPool::new(2048, 4096).ok();
let mut xsk_sock = if let Some(ref pool) = umem_pool {
let mut xsk_sock = if let Some(ref s) = slab {
if let Ok(xsk_ifname) = std::env::var("YIP_XDP_IFNAME") {
XskSocket::bind_opportunistic(&xsk_ifname, shard_id as u32, pool)
XskSocket::bind_opportunistic(&xsk_ifname, shard_id as u32, s.pool())
.unwrap_or_else(|_| XskSocket::fallback())
} else {
XskSocket::fallback()
Expand All @@ -685,14 +702,8 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
};

let mut fill_ring = FillRing::new(UMEM_RING_SIZE);
if let Some(ref mut pool) = umem_pool {
while !fill_ring.is_full() {
if let Some(addr) = pool.alloc_chunk() {
fill_ring.produce(addr);
} else {
break;
}
}
if let Some(ref mut s) = slab {
s.populate_fill_ring(&mut fill_ring, UMEM_RING_SIZE as usize);
}

let sock_fd =
Expand Down Expand Up @@ -866,7 +877,7 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
Err(e) => return Err(e),
}
}
} else if let Some(ref mut pool) = umem_pool {
} else if let Some(ref mut s) = slab {
let mut descs = [XskDesc::default(); 32];
loop {
let count = xsk_sock.rx_ring_mut().consume_batch(&mut descs);
Expand All @@ -876,9 +887,9 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
packets_this_iter =
packets_this_iter.wrapping_add(count as u64);
for desc in &descs[..count] {
if (desc.len as usize) <= pool.chunk_size() {
if (desc.len as usize) <= s.chunk_size() {
let payload =
pool.chunk_slice(desc.addr, desc.len as usize);
s.chunk_slice(desc.addr, desc.len as usize);
let is_hs = !payload.is_empty()
&& (payload[0] == crate::handshake::PacketType::HandshakeInit as u8
|| payload[0] == crate::handshake::PacketType::HandshakeResp as u8);
Expand Down Expand Up @@ -931,7 +942,7 @@ pub fn run_sharded(config: Config, num_shards: usize) -> io::Result<()> {
}
}
if !fill_ring.produce(desc.addr) {
pool.free_chunk(desc.addr);
s.free_chunk(desc.addr);
}
}
}
Expand Down
88 changes: 67 additions & 21 deletions crates/yip-bench/benches/af_xdp_scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,26 @@ fn run_component_microbenchmarks() {
);
}

// 1b. Per-Core UmemSlab Chunk Alloc / Free Throughput
{
let iters = 200_000;
let (_pool, mut slabs) =
UmemPool::new_slabs(CHUNKS_PER_POOL, CHUNK_SIZE, 1).expect("create slabs");
let mut slab = slabs.remove(0);
let start = Instant::now();
for _ in 0..iters {
let addr = slab.alloc_chunk().expect("alloc chunk");
slab.free_chunk(addr);
}
let elapsed = start.elapsed().as_secs_f64();
let mops = (iters as f64) / (elapsed * 1e6);
println!(
" UmemSlab Chunk Alloc/Free: {:6.2} Mops/s ({:.2} ns/op)",
mops,
(elapsed / iters as f64) * 1e9
);
}

// 2. Circular Ring Batch Enqueue / Dequeue Throughput
{
let batch_size = 32;
Expand Down Expand Up @@ -240,6 +260,35 @@ fn run_component_microbenchmarks() {
);
}

// 2b. Lock-Free Batch Ring-to-Ring Recycling (Batch 32)
{
let batch_size = 32;
let iters = 10_000;
let (_pool, mut slabs) =
UmemPool::new_slabs(RING_CAPACITY as usize, CHUNK_SIZE, 1).expect("create slabs");
let mut slab = slabs.remove(0);
let mut fill = FillRing::new(RING_CAPACITY);
let mut comp = CompletionRing::new(RING_CAPACITY);

let addrs: Vec<u64> = (0..batch_size as u64).map(|i| i * 2048).collect();

let start = Instant::now();
for _ in 0..iters {
comp.produce_batch(&addrs);
slab.recycle_from_rings(&mut comp, &mut fill, batch_size);
let mut drain = [0u64; 32];
fill.consume_batch(&mut drain);
}
let elapsed = start.elapsed().as_secs_f64();
let total_recycled = iters * batch_size;
let mops = (total_recycled as f64) / (elapsed * 1e6);
println!(
" Ring-to-Ring Recycling (B32): {:6.2} Mops/s ({:.2} ns/op)",
mops,
(elapsed / total_recycled as f64) * 1e9
);
}

// 3. In-Place Zero-Copy AEAD Seal & Open Inside UMEM Chunks
{
let iters = 100_000;
Expand Down Expand Up @@ -294,11 +343,17 @@ fn run_af_xdp_scale(num_workers: usize, baseline_gbps: Option<f64>) -> RunResult
let packets_per_thread = PACKETS_PER_WORKER;
let barrier = Arc::new(Barrier::new(num_workers + 1));

// Allocate single shared UMEM pool partitioned into per-worker slabs
let (_shared_umem, mut slabs) =
UmemPool::new_slabs(CHUNKS_PER_POOL * num_workers, CHUNK_SIZE, num_workers)
.expect("create shared UMEM pool and per-worker slabs");

// Per-worker thread handles
let mut handles = Vec::with_capacity(num_workers);

for (worker_id, assigned_streams) in streams_for_worker.into_iter().enumerate() {
let bar = Arc::clone(&barrier);
let slab = slabs.remove(0);

let handle = thread::spawn(move || {
// Topology-aware core pinning
Expand All @@ -312,23 +367,17 @@ fn run_af_xdp_scale(num_workers: usize, baseline_gbps: Option<f64>) -> RunResult
let mut rx_session = Session::from_raw_keys(&k_recv, &k_send, 0, 1)
.expect("rx session initialization must succeed");

// Per-worker unprivileged UMEM shared memory pool
let mut umem = UmemPool::new(CHUNKS_PER_POOL, CHUNK_SIZE).expect("create UMEM pool");
// Dedicated per-worker UMEM chunk slab partitioned from the shared UMEM pool
let mut slab = slab;

// Per-worker AF_XDP ring buffers
let mut fill_ring = FillRing::new(RING_CAPACITY);
let mut rx_ring = RxRing::new(RING_CAPACITY);
let mut tx_ring = TxRing::new(RING_CAPACITY);
let mut completion_ring = CompletionRing::new(RING_CAPACITY);

// Pre-populate fill ring with available UMEM chunks
while !fill_ring.is_full() {
if let Some(addr) = umem.alloc_chunk() {
fill_ring.produce(addr);
} else {
break;
}
}
// Pre-populate fill ring with available UMEM chunks in batch
slab.populate_fill_ring(&mut fill_ring, RING_CAPACITY as usize);

let mut generated_nonces = Vec::with_capacity(packets_per_thread);

Expand All @@ -354,11 +403,11 @@ fn run_af_xdp_scale(num_workers: usize, baseline_gbps: Option<f64>) -> RunResult
// 1. Ingress: obtain chunk from fill ring (or allocate new if empty)
let rx_addr = match fill_ring.consume() {
Some(addr) => addr,
None => umem.alloc_chunk().expect("UMEM chunk available"),
None => slab.alloc_chunk().expect("UMEM chunk available"),
};

// 2. Format synthetic TCP packet directly inside UMEM chunk
let rx_buf = umem.chunk_slice_mut(rx_addr, PACKET_SIZE + 16);
let rx_buf = slab.chunk_slice_mut(rx_addr, PACKET_SIZE + 16);
write_tcp_packet(rx_buf, stream_id, p, PACKET_SIZE);

// 3. Enqueue to RX ring
Expand All @@ -370,7 +419,7 @@ fn run_af_xdp_scale(num_workers: usize, baseline_gbps: Option<f64>) -> RunResult
let rx_desc = rx_ring.consume().expect("RX descriptor available");

// 5. In-place AEAD seal directly inside UMEM chunk buffer using Session with stride nonces
let chunk_buf = umem.chunk_slice_mut(rx_desc.addr, PACKET_SIZE + 16);
let chunk_buf = slab.chunk_slice_mut(rx_desc.addr, PACKET_SIZE + 16);
let nonce = tx_session
.seal_in_place(chunk_buf, PACKET_SIZE)
.expect("in-place AEAD seal must succeed");
Expand All @@ -393,7 +442,7 @@ fn run_af_xdp_scale(num_workers: usize, baseline_gbps: Option<f64>) -> RunResult
let tx_desc = tx_ring.consume().expect("TX descriptor available");

// 8. Peer in-place zero-copy AEAD open & replay check inside UMEM buffer
let tx_buf = umem.chunk_slice_mut(tx_desc.addr, tx_desc.len as usize);
let tx_buf = slab.chunk_slice_mut(tx_desc.addr, tx_desc.len as usize);
let plain_len = rx_session
.open_in_place(nonce, tx_buf, tx_desc.len as usize)
.expect("in-place AEAD open must succeed");
Expand All @@ -419,13 +468,10 @@ fn run_af_xdp_scale(num_workers: usize, baseline_gbps: Option<f64>) -> RunResult
let comp_enqueued = completion_ring.produce(tx_desc.addr);
assert!(comp_enqueued, "Completion ring full");

// 11. Recycle completed chunk back to fill ring
let comp_addr = completion_ring
.consume()
.expect("completion descriptor available");
if !fill_ring.produce(comp_addr) {
umem.free_chunk(comp_addr);
}
// 11. Recycle completed chunk back to fill ring via slab
let recycled =
slab.recycle_from_rings(&mut completion_ring, &mut fill_ring, 1);
assert_eq!(recycled, 1, "failed to recycle completed descriptor");

thread_packets += 1;
thread_bytes += PACKET_SIZE as u64;
Expand Down
Loading
Loading