use std::net::Ipv4Addr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use gossan_engine::schedule::{BlackrockPermutation, ScanSchedule};
use netforge::engine::{EngineError, EngineStats, PacketEngine, RxPacket};
use netforge::seq::SeqEncoder;
use netforge::{packet, RawPacket};
struct CountingEngine {
sent: AtomicU64,
}
impl CountingEngine {
fn new() -> Self {
Self {
sent: AtomicU64::new(0),
}
}
}
impl PacketEngine for CountingEngine {
fn tx_batch(&self, packets: &[RawPacket]) -> Result<usize, EngineError> {
self.sent.fetch_add(packets.len() as u64, Ordering::Relaxed);
Ok(packets.len())
}
fn rx_batch(&self, _buf: &mut [RxPacket]) -> Result<usize, EngineError> {
Ok(0)
}
fn stats(&self) -> EngineStats {
EngineStats {
tx_packets: self.sent.load(Ordering::Relaxed),
..EngineStats::default()
}
}
fn name(&self) -> &'static str {
"counting-stub"
}
}
fn run_tx_hot_loop(num_targets: u64, num_ports: u64, batch_size: usize) -> u64 {
let source_ip = Ipv4Addr::new(10, 0, 0, 1);
let source_port: u16 = 53000;
let template = packet::build_syn_template(source_ip, source_port);
let encoder = SeqEncoder::new();
let backend = CountingEngine::new();
let schedule = ScanSchedule::new(num_targets, num_ports, 0);
let mut batch: Vec<RawPacket> = (0..batch_size).map(|_| template.clone()).collect();
let mut batch_len = 0usize;
let mut total_sent = 0u64;
for (ip_idx, port_idx) in schedule {
let target_ip = Ipv4Addr::new(
10,
(ip_idx >> 16) as u8 & 0xff,
(ip_idx >> 8) as u8 & 0xff,
(ip_idx & 0xff) as u8,
);
let port = (port_idx as u16).wrapping_add(1);
let slot = &mut batch[batch_len];
let seq = encoder.encode(target_ip, port, source_port, 0);
packet::stamp_syn(slot, target_ip, port, seq);
batch_len += 1;
if batch_len == batch_size {
let sent = backend.tx_batch(&batch[..batch_len]).unwrap_or(0);
total_sent += sent as u64;
batch_len = 0;
}
}
if batch_len > 0 {
let sent = backend.tx_batch(&batch[..batch_len]).unwrap_or(0);
total_sent += sent as u64;
}
total_sent
}
fn run_tx_hot_loop_parallel(
num_targets: u64,
num_ports: u64,
batch_size: usize,
num_threads: usize,
) -> u64 {
let source_ip = Ipv4Addr::new(10, 0, 0, 1);
let source_port_base: u16 = 53000;
let template = packet::build_syn_template(source_ip, source_port_base);
let total_probes = num_targets.saturating_mul(num_ports);
let total_sent = Arc::new(AtomicU64::new(0));
let permutation_seed: u64 = 0;
let mut handles = Vec::with_capacity(num_threads);
for thread_id in 0..num_threads {
let template = template.clone();
let total_sent = Arc::clone(&total_sent);
handles.push(std::thread::spawn(move || -> u64 {
#[cfg(target_os = "linux")]
unsafe {
let cpu_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let target_cpu = thread_id % cpu_count;
let mut cpuset: libc::cpu_set_t = std::mem::zeroed();
libc::CPU_SET(target_cpu, &mut cpuset);
let _ = libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &cpuset);
}
let encoder = SeqEncoder::new();
let backend = CountingEngine::new();
let mut batch: Vec<RawPacket> = (0..batch_size).map(|_| template.clone()).collect();
let mut batch_len = 0usize;
let mut local_sent = 0u64;
let perm = BlackrockPermutation::new(total_probes.max(1), permutation_seed);
let stride = num_threads as u64;
let mut idx = thread_id as u64;
while idx < total_probes {
let permuted = perm.shuffle(idx);
let ip_idx = permuted / num_ports;
let port_idx = permuted % num_ports;
let target_ip = Ipv4Addr::new(
10,
(ip_idx >> 16) as u8 & 0xff,
(ip_idx >> 8) as u8 & 0xff,
(ip_idx & 0xff) as u8,
);
let port = (port_idx as u16).wrapping_add(1);
let my_source_port = source_port_base + thread_id as u16;
let slot = &mut batch[batch_len];
let seq = encoder.encode(target_ip, port, my_source_port, 0);
packet::stamp_syn(slot, target_ip, port, seq);
batch_len += 1;
if batch_len == batch_size {
let sent = backend.tx_batch(&batch[..batch_len]).unwrap_or(0);
local_sent += sent as u64;
batch_len = 0;
}
idx += stride;
}
if batch_len > 0 {
let sent = backend.tx_batch(&batch[..batch_len]).unwrap_or(0);
local_sent += sent as u64;
}
total_sent.fetch_add(local_sent, Ordering::Relaxed);
local_sent
}));
}
for h in handles {
let _ = h.join();
}
total_sent.load(Ordering::Relaxed)
}
#[test]
#[cfg(not(debug_assertions))]
fn engine_tx_hot_loop_1_thread_meets_gate() {
let num_targets: u64 = 65_536;
let num_ports: u64 = 100;
let total_probes = num_targets * num_ports;
let start = std::time::Instant::now();
let sent = run_tx_hot_loop(num_targets, num_ports, 1024);
let elapsed = start.elapsed();
let mpps = (sent as f64 / elapsed.as_secs_f64()) / 1_000_000.0;
eprintln!(
"engine_tx_hot_loop_1_thread: {} probes / {:?} = {:.2} Mpps",
total_probes, elapsed, mpps
);
assert!(
mpps >= 5.0,
"engine 1-thread TX hot loop: {mpps:.2} Mpps is below the 5 Mpps regression gate"
);
}
#[test]
#[cfg(not(debug_assertions))]
fn engine_tx_hot_loop_4_threads_scales() {
let num_targets: u64 = 65_536;
let num_ports: u64 = 100;
let cpu_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
if cpu_count < 4 {
eprintln!("skipping: needs ≥4 CPUs, have {cpu_count}");
return;
}
let start = std::time::Instant::now();
let sent = run_tx_hot_loop_parallel(num_targets, num_ports, 1024, 4);
let elapsed = start.elapsed();
let mpps = (sent as f64 / elapsed.as_secs_f64()) / 1_000_000.0;
eprintln!(
"engine_tx_hot_loop_4_threads: {} probes / {:?} = {:.2} Mpps",
sent, elapsed, mpps
);
assert!(
mpps >= 15.0,
"engine 4-thread TX hot loop: {mpps:.2} Mpps is below the 15 Mpps regression gate"
);
}
#[test]
#[cfg(not(debug_assertions))]
fn engine_tx_hot_loop_2_threads_meets_gate() {
let num_targets: u64 = 65_536;
let num_ports: u64 = 100;
if std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
< 2
{
return;
}
let start = std::time::Instant::now();
let sent = run_tx_hot_loop_parallel(num_targets, num_ports, 1024, 2);
let elapsed = start.elapsed();
let mpps = (sent as f64 / elapsed.as_secs_f64()) / 1_000_000.0;
eprintln!("engine_tx_hot_loop_2_threads: {sent} probes / {elapsed:?} = {mpps:.2} Mpps");
assert!(
mpps >= 8.0,
"engine 2-thread TX hot loop: {mpps:.2} Mpps below 8 Mpps regression gate"
);
}
#[test]
#[cfg(not(debug_assertions))]
fn engine_tx_hot_loop_8_threads_scales() {
let num_targets: u64 = 65_536;
let num_ports: u64 = 100;
let cpu_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
if cpu_count < 8 {
eprintln!("skipping engine_tx_hot_loop_8_threads_scales: needs ≥8 CPUs, have {cpu_count}");
return;
}
let start = std::time::Instant::now();
let sent = run_tx_hot_loop_parallel(num_targets, num_ports, 1024, 8);
let elapsed = start.elapsed();
let mpps = (sent as f64 / elapsed.as_secs_f64()) / 1_000_000.0;
eprintln!("engine_tx_hot_loop_8_threads: {sent} probes / {elapsed:?} = {mpps:.2} Mpps");
assert!(
mpps >= 30.0,
"engine 8-thread TX hot loop: {mpps:.2} Mpps below 30 Mpps regression gate"
);
}
#[test]
#[cfg(not(debug_assertions))]
fn engine_tx_hot_loop_16_threads_documented() {
let num_targets: u64 = 65_536;
let num_ports: u64 = 100;
let cpu_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
if cpu_count < 16 {
eprintln!(
"skipping engine_tx_hot_loop_16_threads_documented: needs ≥16 CPUs, have {cpu_count}"
);
return;
}
let start = std::time::Instant::now();
let sent = run_tx_hot_loop_parallel(num_targets, num_ports, 1024, 16);
let elapsed = start.elapsed();
let mpps = (sent as f64 / elapsed.as_secs_f64()) / 1_000_000.0;
eprintln!("engine_tx_hot_loop_16_threads: {sent} probes / {elapsed:?} = {mpps:.2} Mpps");
assert!(
mpps >= 50.0,
"engine 16-thread TX hot loop: {mpps:.2} Mpps below 50 Mpps regression gate"
);
}
#[test]
fn engine_tx_hot_loop_perf_gate_is_release_only() {
let _ = run_tx_hot_loop(1024, 4, 256);
}