use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use criterion::{BenchmarkId, Criterion, Throughput};
use ferranet::{Channel, FanoutMode, MacAddr, Receiver, RingConfig, Sender};
use pnet_datalink::{Channel as PnetChannel, Config as PnetConfig, DataLinkReceiver, DataLinkSender};
const BATCH: usize = 64;
const FLOODERS: usize = 4;
const UNIT: u64 = 50_000; const SIZES: &[usize] = &[64, 1500];
fn iface_a() -> &'static str {
static A: OnceLock<String> = OnceLock::new();
A.get_or_init(|| std::env::var("FERRANET_BENCH_IFACE_A").unwrap_or_else(|_| "fnbencha".into()))
}
fn iface_b() -> &'static str {
static B: OnceLock<String> = OnceLock::new();
B.get_or_init(|| std::env::var("FERRANET_BENCH_IFACE_B").unwrap_or_else(|_| "fnbenchb".into()))
}
fn real_nics() -> bool {
std::env::var_os("FERRANET_BENCH_IFACE_A").is_some()
&& std::env::var_os("FERRANET_BENCH_IFACE_B").is_some()
}
fn main() {
let real = real_nics();
if real {
eprintln!("throughput bench: using real interfaces {} -> {}", iface_a(), iface_b());
} else if !setup_veth() {
eprintln!(
"throughput bench: could not create a veth pair.\n\
Run inside a namespace: `unshare --user --map-root-user --net cargo bench --bench throughput`,\n\
or point it at real NICs with FERRANET_BENCH_IFACE_A / FERRANET_BENCH_IFACE_B.\n\
Skipping."
);
return;
}
let _guard = (!real).then(|| VethGuard);
let mut c = Criterion::default()
.configure_from_args()
.sample_size(10)
.warm_up_time(Duration::from_secs(1))
.measurement_time(Duration::from_secs(3));
bench_tx(&mut c);
bench_rx(&mut c);
bench_fanout(&mut c);
c.final_summary();
}
fn bench_tx(c: &mut Criterion) {
let mut group = c.benchmark_group("tx_throughput");
group.throughput(Throughput::Elements(UNIT));
for &size in SIZES {
let frames: Vec<Vec<u8>> = (0..BATCH).map(|_| make_frame(size)).collect();
let batch: Vec<&[u8]> = frames.iter().map(Vec::as_slice).collect();
for &basic in &[false, true] {
let (mut tx, _rx) = build(iface_a(), basic);
group.bench_with_input(BenchmarkId::new(mode(basic), size), &size, |b, _| {
b.iter_custom(|iters| {
let target = iters * UNIT;
let mut sent = 0u64;
let start = Instant::now();
while sent < target {
sent += tx.send_batch(&batch).unwrap_or(0) as u64;
}
start.elapsed()
});
});
}
let (mut tx, _rx) = pnet_channel(iface_a());
let frame = make_frame(size);
group.bench_with_input(BenchmarkId::new("libpnet", size), &size, |b, _| {
b.iter_custom(|iters| {
let target = iters * UNIT;
let mut sent = 0u64;
let start = Instant::now();
while sent < target {
if let Some(Ok(())) = tx.send_to(&frame, None) {
sent += 1;
}
}
start.elapsed()
});
});
}
group.finish();
}
fn bench_rx(c: &mut Criterion) {
let mut group = c.benchmark_group("rx_throughput");
group.throughput(Throughput::Elements(UNIT));
for &size in SIZES {
let stop = Arc::new(AtomicBool::new(false));
let flooders = spawn_flooders(size, &stop);
thread::sleep(Duration::from_millis(150));
for &basic in &[false, true] {
let (_tx, mut rx) = build_rx(iface_b(), basic);
group.bench_with_input(BenchmarkId::new(mode(basic), size), &size, |b, _| {
b.iter_custom(|iters| {
let target = iters * UNIT;
let mut got = 0u64;
let start = Instant::now();
while got < target {
match rx.recv_block() {
Ok(block) => got += block.frames().count() as u64,
Err(_) => break,
}
}
start.elapsed()
});
});
}
let (_tx, mut rx) = pnet_channel(iface_b());
group.bench_with_input(BenchmarkId::new("libpnet", size), &size, |b, _| {
b.iter_custom(|iters| {
let target = iters * UNIT;
let mut got = 0u64;
let start = Instant::now();
while got < target {
match rx.next() {
Ok(_) => got += 1,
Err(_) => break,
}
}
start.elapsed()
});
});
stop.store(true, Ordering::Relaxed);
join_all(flooders);
}
group.finish();
}
fn bench_fanout(c: &mut Criterion) {
let mut group = c.benchmark_group("fanout_rx_64B");
group.throughput(Throughput::Elements(UNIT));
for &n in &[1usize, 2, 4] {
let counter = Arc::new(AtomicU64::new(0));
let recv_stop = Arc::new(AtomicBool::new(false));
let receivers = Channel::builder(iface_b())
.fanout(FanoutMode::LoadBalance)
.rx_ring(rx_cfg())
.build_fanout_rx(n)
.expect("open fanout group");
let workers: Vec<_> = receivers
.into_iter()
.map(|mut rx| {
let counter = counter.clone();
let stop = recv_stop.clone();
thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
match rx.recv_block() {
Ok(block) => {
counter.fetch_add(block.frames().count() as u64, Ordering::Relaxed);
}
Err(_) => break,
}
}
})
})
.collect();
let flood_stop = Arc::new(AtomicBool::new(false));
let flooders = spawn_flooders(64, &flood_stop);
thread::sleep(Duration::from_millis(150));
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| {
b.iter_custom(|iters| {
let target = iters * UNIT;
let start_count = counter.load(Ordering::Relaxed);
let start = Instant::now();
while counter.load(Ordering::Relaxed).wrapping_sub(start_count) < target {
std::hint::spin_loop();
}
start.elapsed()
});
});
flood_stop.store(true, Ordering::Relaxed);
join_all(flooders);
recv_stop.store(true, Ordering::Relaxed);
join_all(workers);
}
group.finish();
}
fn spawn_flooders(size: usize, stop: &Arc<AtomicBool>) -> Vec<JoinHandle<()>> {
(0..FLOODERS)
.map(|_| {
let stop = stop.clone();
thread::spawn(move || {
let (mut tx, _rx) = build(iface_a(), false);
let frames: Vec<Vec<u8>> = (0..BATCH).map(|_| make_frame(size)).collect();
let batch: Vec<&[u8]> = frames.iter().map(Vec::as_slice).collect();
while !stop.load(Ordering::Relaxed) {
let _ = tx.send_batch(&batch);
}
})
})
.collect()
}
fn join_all(handles: Vec<JoinHandle<()>>) {
for h in handles {
let _ = h.join();
}
}
fn mode(basic: bool) -> &'static str {
if basic { "basic" } else { "ring" }
}
fn rx_cfg() -> RingConfig {
RingConfig { retire_blk_tov_ms: 10, ..RingConfig::rx_default() }
}
fn build(iface: &str, basic: bool) -> (Sender, Receiver) {
let mut b = Channel::builder(iface);
if basic {
b = b.basic();
}
b.build_sync().expect("open channel")
}
fn build_rx(iface: &str, basic: bool) -> (Sender, Receiver) {
let mut b = Channel::builder(iface).rx_ring(rx_cfg());
if basic {
b = b.basic();
}
b.build_sync().expect("open channel")
}
fn pnet_channel(iface: &str) -> (Box<dyn DataLinkSender>, Box<dyn DataLinkReceiver>) {
let interface = pnet_datalink::interfaces()
.into_iter()
.find(|i| i.name == iface)
.unwrap_or_else(|| panic!("libpnet could not find interface {iface}"));
let config = PnetConfig { read_timeout: None, ..PnetConfig::default() };
match pnet_datalink::channel(&interface, config) {
Ok(PnetChannel::Ethernet(tx, rx)) => (tx, rx),
Ok(_) => panic!("unexpected libpnet channel type"),
Err(e) => panic!("libpnet channel error on {iface}: {e}"),
}
}
fn make_frame(size: usize) -> Vec<u8> {
let mut frame = Vec::with_capacity(size);
frame.extend_from_slice(&MacAddr::BROADCAST.octets());
frame.extend_from_slice(&[0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
frame.extend_from_slice(&[0x88, 0xb5]);
frame.resize(size.max(14), 0x5a);
frame
}
fn setup_veth() -> bool {
let _ = run("ip", &["link", "del", iface_a()]);
run("ip", &["link", "add", iface_a(), "type", "veth", "peer", "name", iface_b()])
&& run("ip", &["link", "set", iface_a(), "up"])
&& run("ip", &["link", "set", iface_b(), "up"])
}
fn run(cmd: &str, args: &[&str]) -> bool {
Command::new(cmd)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
struct VethGuard;
impl Drop for VethGuard {
fn drop(&mut self) {
let _ = run("ip", &["link", "del", iface_a()]);
}
}