//! Zero-dependency throughput benchmarks (std only, no criterion). //! //! Run: `cargo run --release --example bench` //! Reports req/s (higher is better) for decode/encode/filter/pools + UDP loopback. //! Old-vs-new: record numbers before optimization, re-run after; same machine, //! same build flags, median of 3 runs. use dhcp4r::{dhcpv6, options, packet, server}; use std::hint::black_box; use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket}; use std::time::{Duration, Instant}; fn bench_fn(name: &str, iters: u64, mut f: impl FnMut()) -> f64 { // warmup (not timed) for _ in 0..5_000 { f(); } let t = Instant::now(); for _ in 0..iters { f(); } let el = t.elapsed(); let rps = iters as f64 / el.as_secs_f64(); println!( "{:<28} {:>12.0} req/s ({:.1} ns/req)", name, rps, el.as_nanos() as f64 / iters as f64 ); rps } fn discover_packet() -> packet::Packet { packet::Packet { reply: false, hops: 0, xid: 0x1234_5678, secs: 0, broadcast: false, ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED, siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED, chaddr: [1, 2, 3, 4, 5, 6], options: vec![ options::DhcpOption::DhcpMessageType(options::MessageType::Discover), options::DhcpOption::ParameterRequestList(vec![1, 3, 6, 15]), options::DhcpOption::HostName("bench-host".to_string()), ], } } fn request_packet() -> packet::Packet { packet::Packet { reply: false, hops: 0, xid: 0x9abc_def0, secs: 0, broadcast: false, ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED, siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED, chaddr: [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff], options: vec![ options::DhcpOption::DhcpMessageType(options::MessageType::Request), options::DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 2, 10)), options::DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 2, 1)), options::DhcpOption::ParameterRequestList(vec![1, 3, 6, 51]), options::DhcpOption::HostName("bench-request-host".to_string()), options::DhcpOption::IpAddressLeaseTime(3600), ], } } fn offer_options() -> Vec { vec![ options::DhcpOption::IpAddressLeaseTime(86400), options::DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)), options::DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 2, 1)]), options::DhcpOption::DomainNameServer(vec![ Ipv4Addr::new(8, 8, 8, 8), Ipv4Addr::new(8, 8, 4, 4), ]), ] } /// reply assembly without sockets: mirrors Server::reply's Vec build /// (auto [Msg,SrvId] + extend + conditional PRL filter). Isolates the /// ~80-150ns assembly cost from the ~15µs UDP number. fn reply_assembly(offer: &[options::DhcpOption], prl: Option<&[u8]>) -> Vec { let mut opts = Vec::with_capacity(offer.len() + 2); opts.push(options::DhcpOption::DhcpMessageType( options::MessageType::Offer, )); opts.push(options::DhcpOption::ServerIdentifier( Ipv4Addr::new(192, 168, 2, 1), )); opts.extend(offer.iter().cloned()); if let Some(p) = prl { server::filter_options_by_req(&mut opts, p); } opts } fn bench_udp_roundtrip() -> f64 { // Minimal Offer handler over real loopback UDP, exercising // Server::reply -> filter_options_by_req -> Packet::encode -> send_to. // Offer options are cached once (like the real server) so the bench // measures reply/filter/encode/send, not option rebuilds. struct BenchHandler { server_ip: Ipv4Addr, offer: Vec, } impl server::Handler for BenchHandler { fn handle_request(&mut self, s: &server::Server, p: packet::Packet) { let _ = s.reply( options::MessageType::Offer, self.offer.clone(), Ipv4Addr::new(192, 168, 2, 10), p, ); } } let srv_sock = UdpSocket::bind("127.0.0.1:0").expect("bind srv"); let srv_addr = srv_sock.local_addr().unwrap(); srv_sock .set_read_timeout(Some(Duration::from_millis(5000))) .ok(); let server_ip = Ipv4Addr::new(127, 0, 0, 1); let broadcast_ip = Ipv4Addr::new(127, 0, 0, 1); let offer = offer_options(); std::thread::spawn(move || { let _ = server::Server::serve(srv_sock, server_ip, broadcast_ip, BenchHandler { server_ip, offer }); }); // Give server a moment to bind. std::thread::sleep(Duration::from_millis(100)); let cli = UdpSocket::bind("127.0.0.1:0").expect("bind cli"); cli.connect(srv_addr).expect("connect"); cli.set_read_timeout(Some(Duration::from_secs(5))).ok(); let mut buf = [0u8; dhcp4r::packet::WIRE_MAX]; let wire = { let mut tmp = [0u8; dhcp4r::packet::WIRE_MAX]; discover_packet().encode(&mut tmp).to_vec() }; // warmup let mut rx = [0u8; 1500]; for _ in 0..200 { cli.send(black_box(&wire)).unwrap(); let _ = cli.recv(&mut rx); } const N: u64 = 5_000; let t = Instant::now(); let mut ok = 0u64; for _ in 0..N { cli.send(black_box(&wire)).unwrap(); match cli.recv(&mut rx) { Ok(l) => { black_box(l); ok += 1; } Err(_) => break, } } let el = t.elapsed(); // Silence unused warning for buf black_box(&mut buf); let rps = ok as f64 / el.as_secs_f64(); println!( "{:<28} {:>12.0} req/s ({:.1} ns/req, {}/{{}} ok)", "udp/offer-roundtrip", rps, el.as_nanos() as f64 / (ok.max(1) as f64), ok, ); // Server thread is detached; process exit kills it. rps } fn main() { println!("=== dhcp4r bench (release, std Instant, black_box) ==="); println!( "sizeof Packet={} DhcpOption={} RawDhcpOption={} Server={}", std::mem::size_of::(), std::mem::size_of::(), std::mem::size_of::(), std::mem::size_of::(), ); // Wire ceiling is WIRE_MAX = 300 (BOOTP minimum: 240 header+cookie, // up to 59 bytes options + END, PAD to 300); the shipped // send path already uses [u8; WIRE_MAX], so the bench does too. let mut encode_buf = [0u8; dhcp4r::packet::WIRE_MAX]; let d = discover_packet(); let r = request_packet(); let d_wire = d.encode(&mut encode_buf).to_vec(); let r_wire = r.encode(&mut encode_buf).to_vec(); // sanity: roundtrip preserves options assert_eq!( packet::Packet::from(&d_wire).ok().expect("decode").options, d.options ); const N: u64 = 200_000; bench_fn("decode/discover", N, || { black_box(packet::Packet::from(black_box(&d_wire)).ok().expect("decode")); }); bench_fn("decode/request", N, || { black_box(packet::Packet::from(black_box(&r_wire)).ok().expect("decode")); }); bench_fn("encode/discover", N, || { let n = black_box(&d).encode(black_box(&mut encode_buf)).len(); black_box(n); }); bench_fn("encode/request", N, || { let n = black_box(&r).encode(black_box(&mut encode_buf)).len(); black_box(n); }); // filter_options_by_req: typical Offer(4 opts) filtered by PRL[1,3,6] bench_fn("filter/prl-3", N, || { let mut o = offer_options(); // prepend auto opts like Server::reply does o.insert(0, options::DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 2, 1))); o.insert( 0, options::DhcpOption::DhcpMessageType(options::MessageType::Offer), ); server::filter_options_by_req(black_box(&mut o), black_box(&[1u8, 3, 6])); black_box(o); }); // options::to_raw throughput (Router x2) for old-vs-new comparison let router2 = options::DhcpOption::Router(vec![ Ipv4Addr::new(192, 168, 2, 1), Ipv4Addr::new(192, 168, 2, 2), ]); bench_fn("options/to_raw-router2", N, || { black_box(black_box(&router2).to_raw()); }); // DHCPv6 codec let v6_solicit = dhcpv6::Packet { msg_type: dhcpv6::MsgType::Solicit, transaction_id: 0x112233, options: vec![ dhcpv6::Dhcpv6Option::ClientId(vec![0, 3, 0, 1, 2, 0, 0x5e, 0xaa, 0xbb, 0xcc]), dhcpv6::Dhcpv6Option::IaNa(dhcpv6::IaNa { iaid: 1, t1: 0, t2: 0, addrs: vec![], }), ], }; let v6_wire = v6_solicit.encode().unwrap(); bench_fn("v6/decode-solicit", N, || { black_box(dhcpv6::Packet::from(black_box(&v6_wire)).expect("v6 decode")); }); bench_fn("v6/encode-solicit", N, || { black_box(black_box(&v6_solicit).encode().unwrap()); }); let v6_reply = dhcpv6::Packet { msg_type: dhcpv6::MsgType::Reply, transaction_id: 0x445566, options: vec![ dhcpv6::Dhcpv6Option::ServerId(vec![0, 3, 0, 1, 2, 0, 0x5e, 0xaa, 0xbb, 0xcc]), dhcpv6::Dhcpv6Option::ClientId(vec![0, 3, 0, 1, 9, 9, 9, 9, 9, 9]), dhcpv6::Dhcpv6Option::IaNa(dhcpv6::IaNa { iaid: 7, t1: 43200, t2: 75600, addrs: vec![dhcpv6::IaAddr { addr: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x100), preferred: 86400, valid: 86400, }], }), dhcpv6::Dhcpv6Option::DnsServers(vec![Ipv6Addr::new( 0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888, )]), ], }; bench_fn("v6/encode-reply-1addr", N, || { black_box(black_box(&v6_reply).encode().unwrap()); }); // V6Pool: 1000-entry pool, hit vs miss { let mut pool = dhcpv6::V6Pool::new( Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x100), 1000, Duration::from_secs(86400), ); // prefill 1000 leases with distinct DUIDs for i in 0..1000u32 { let mut duid = vec![0, 3, 0, 1]; duid.extend_from_slice(&i.to_be_bytes()); let ip = Ipv6Addr::from(u128::from(pool.start_addr()) + i as u128); pool.insert(ip, duid, Some(Instant::now() + Duration::from_secs(3600))); } let owner_duid = { let mut d = vec![0, 3, 0, 1]; d.extend_from_slice(&42u32.to_be_bytes()); d }; bench_fn("v6pool/discover-hit-1000", N, || { black_box(pool.discover(black_box(&owner_duid))); }); let stranger: Vec = vec![9, 9, 9, 9, 9, 9, 9, 9]; // exhausted pool: stranger always misses after full scan bench_fn("v6pool/discover-miss-full", 5_000, || { black_box(pool.discover(black_box(&stranger))); }); } { // v4-style pool simulation via V6Pool with small count for comparison let mut pool = dhcpv6::V6Pool::new( Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x200), 252, Duration::from_secs(86400), ); bench_fn("v6pool/discover-miss-empty-252", 20_000, || { let mut duid = vec![1, 2, 3, 4]; duid.extend_from_slice(&black_box(12345u32).to_be_bytes()); black_box(pool.discover(black_box(&duid))); }); } // ---- Round-2 additions: Nak, hostname-63, large PRL, DNS4, combined ---- { let nak = packet::Packet { reply: true, hops: 0, xid: 0x1234_5678, secs: 0, broadcast: false, ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED, siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED, chaddr: [1, 2, 3, 4, 5, 6], options: vec![ options::DhcpOption::DhcpMessageType(options::MessageType::Nak), options::DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 2, 1)), options::DhcpOption::Message("Requested IP not available".to_string()), ], }; bench_fn("encode/nak", N, || { let n = black_box(&nak).encode(black_box(&mut encode_buf)).len(); black_box(n); }); let mut h63 = discover_packet(); h63.options .push(options::DhcpOption::HostName("h".repeat(63))); let h63_wire = h63.encode(&mut encode_buf).to_vec(); bench_fn("decode/hostname-63", 100_000, || { black_box( packet::Packet::from(black_box(&h63_wire)) .ok() .expect("decode"), ); }); bench_fn("encode/hostname-63", 100_000, || { let n = black_box(&h63).encode(black_box(&mut encode_buf)).len(); black_box(n); }); bench_fn("decode+msgtype/request", N, || { let p = packet::Packet::from(black_box(&r_wire)) .ok() .expect("decode"); black_box(p.message_type().ok()); black_box(p.option(50).is_some()); black_box(p.option(12).is_some()); }); } { const PRL10: &[u8] = &[1, 3, 6, 15, 51, 54, 58, 59, 42, 12]; bench_fn("filter/prl-10", N, || { let mut o = offer_options(); o.insert( 0, options::DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 2, 1)), ); o.insert( 0, options::DhcpOption::DhcpMessageType(options::MessageType::Offer), ); server::filter_options_by_req(black_box(&mut o), black_box(PRL10)); black_box(o); }); let dns4 = options::DhcpOption::DomainNameServer(vec![ Ipv4Addr::new(8, 8, 8, 8), Ipv4Addr::new(8, 8, 4, 4), Ipv4Addr::new(1, 1, 1, 1), Ipv4Addr::new(9, 9, 9, 9), ]); bench_fn("options/to_raw-dns4", N, || { black_box(black_box(&dns4).to_raw()); }); bench_fn("reply/asm-no-prl", N, || { let o = offer_options(); black_box(reply_assembly(black_box(&o), None)); }); bench_fn("reply/asm-prl-4", N, || { let o = offer_options(); black_box(reply_assembly(black_box(&o), Some(black_box(&[1u8, 3, 6, 51])))); }); } { let v6_big = dhcpv6::Packet { msg_type: dhcpv6::MsgType::Solicit, transaction_id: 0x112233, options: vec![ dhcpv6::Dhcpv6Option::ClientId(vec![ 0, 3, 0, 1, 2, 0, 0x5e, 0xaa, 0xbb, 0xcc, ]), dhcpv6::Dhcpv6Option::IaNa(dhcpv6::IaNa { iaid: 9, t1: 0, t2: 0, addrs: vec![ dhcpv6::IaAddr { addr: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x101), preferred: 100, valid: 200, }, dhcpv6::IaAddr { addr: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x102), preferred: 100, valid: 200, }, dhcpv6::IaAddr { addr: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x103), preferred: 100, valid: 200, }, dhcpv6::IaAddr { addr: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x104), preferred: 100, valid: 200, }, ], }), ], }; let big_wire = v6_big.encode().unwrap(); bench_fn("v6/decode-iana-4addr", N, || { black_box(dhcpv6::Packet::from(black_box(&big_wire)).expect("v6 decode")); }); bench_fn("v6/encode-iana-4addr", N, || { black_box(black_box(&v6_big).encode().unwrap()); }); let mut pool = dhcpv6::V6Pool::new( Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x300), 252, Duration::from_secs(86400), ); bench_fn("v6pool/release-hit", 20_000, || { let mut d = vec![8, 8, 8, 8]; d.extend_from_slice(&black_box(pool.len() as u32).to_be_bytes()); d.extend_from_slice(&black_box(999u32).to_be_bytes()); if let Some(ip) = pool.discover(&d) { let _ = pool.request(&d, ip); } black_box(pool.release(black_box(&d))); }); } // ---- Final-round diagnostics: hostile wire + v6 mix (not headlines) ---- { // 4096B / 1285-option bomb: proves Vec growth stays linear/bounded. let mut opts = vec![53u8, 1, 1]; for _ in 0..1284 { opts.extend_from_slice(&[200, 1, 7]); } opts.push(255); let mut raw = vec![0u8; 236]; raw[0] = 1; raw[1] = 1; raw[2] = 6; raw.extend_from_slice(&[99, 130, 83, 99]); raw.extend_from_slice(&opts); assert_eq!(raw.len(), 4096); bench_fn("diag/decode-4096b", 5_000, || { let p = packet::Packet::from(black_box(&raw)).ok().expect("decode"); black_box(p.options.len()); }); // Mixed reject rate: truncated, bad type, short, bad cookie, // non-UTF8 hostname, Router fast-fail. No-cliff proof only. let mut bad_cookie = vec![0u8; 236]; bad_cookie[0] = 1; bad_cookie[1] = 1; bad_cookie[2] = 6; bad_cookie.extend_from_slice(&[9, 9, 9, 9, 53, 1, 1, 255]); let cases: Vec> = vec![ { let mut v = vec![0u8; 236]; v[0] = 1; v[1] = 1; v[2] = 6; v.extend_from_slice(&[99, 130, 83, 99, 53, 1, 1, 200, 255, 7]); v }, { let mut v = vec![0u8; 236]; v[0] = 1; v[1] = 1; v[2] = 6; v.extend_from_slice(&[99, 130, 83, 99, 53, 1, 9]); v }, vec![0u8; 10], bad_cookie, ]; bench_fn("diag/reject-mixed", 20_000, || { for c in &cases { black_box(packet::Packet::from(black_box(c)).is_err()); } }); } { // v6 traffic mix: solicit-fresh / renew-hit / release, half-full pool. let mut pool = dhcpv6::V6Pool::new( Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x400), 252, Duration::from_secs(86400), ); for i in 0..126u32 { let mut duid = vec![0, 3, 0, 1]; duid.extend_from_slice(&i.to_be_bytes()); let ip = Ipv6Addr::from(u128::from(pool.start_addr()) + i as u128); pool.insert(ip, duid, Some(Instant::now() + Duration::from_secs(3600))); } let mut state: u64 = 0x1234_5678_9abc_def0; let mut next_rand = || { state ^= state << 13; state ^= state >> 7; state ^= state << 17; state }; bench_fn("mix/v6-solicit-renew-release", 50_000, || { let r = next_rand(); let mut duid = vec![0, 3, 0, 1]; duid.extend_from_slice(&((r & 0xFFFF) as u32).to_be_bytes()); match r % 3 { 0 => { black_box(pool.discover(black_box(&duid))); } 1 => { if let Some(ip) = pool.discover(&duid) { black_box(pool.request(black_box(&duid), black_box(ip)).ok()); } } _ => { black_box(pool.release(black_box(&duid))); } } }); } // Full UDP loopback (dominant for "requests per second it can do"). bench_udp_roundtrip(); if std::env::var("BENCH_SUSTAINED").is_ok() { bench_udp_sustained(); } println!("=== done (median of 3 runs; higher req/s is better) ==="); } fn bench_udp_sustained() { use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; struct CountHandler { n: Arc, offer: Vec, } impl server::Handler for CountHandler { fn handle_request(&mut self, s: &server::Server, p: packet::Packet) { self.n.fetch_add(1, Ordering::Relaxed); let _ = s.reply( options::MessageType::Offer, self.offer.clone(), Ipv4Addr::new(192, 168, 2, 10), p, ); } } let mut medians = vec![]; for _ in 0..1 { let srv = UdpSocket::bind("127.0.0.1:0").expect("bind srv"); let sa = srv.local_addr().unwrap(); let handled = Arc::new(AtomicU64::new(0)); let h = handled.clone(); let offer = offer_options(); std::thread::spawn(move || { let _ = server::Server::serve( srv, Ipv4Addr::new(127, 0, 0, 1), Ipv4Addr::new(127, 0, 0, 1), CountHandler { n: h, offer }, ); }); std::thread::sleep(Duration::from_millis(100)); let tx = UdpSocket::bind("127.0.0.1:0").expect("bind tx"); tx.connect(sa).unwrap(); let rx = tx.try_clone().unwrap(); rx.set_read_timeout(Some(Duration::from_millis(500))).ok(); let wire = { let mut t = [0u8; dhcp4r::packet::WIRE_MAX]; discover_packet().encode(&mut t).to_vec() }; let lats = Arc::new(Mutex::new(Vec::with_capacity(1 << 20))); let run = Arc::new(AtomicBool::new(true)); let rx_lats = lats.clone(); let rx_flag = run.clone(); let rxh = std::thread::spawn(move || { let mut b = [0u8; 1500]; let mut n = 0u64; while rx_flag.load(Ordering::Relaxed) { let t0 = Instant::now(); if rx.recv(&mut b).is_ok() { n += 1; rx_lats.lock().unwrap().push(t0.elapsed()); } } n }); let dur = Duration::from_secs(10); let t0 = Instant::now(); let mut sent = 0u64; while t0.elapsed() < dur { tx.send(black_box(&wire)).unwrap(); sent += 1; } std::thread::sleep(Duration::from_millis(500)); run.store(false, Ordering::Relaxed); let got = rxh.join().unwrap(); let hv = handled.load(Ordering::Relaxed); let mut v = lats.lock().unwrap().clone(); v.sort_unstable(); let pct = |q: f64| v.get((v.len() as f64 * q) as usize).copied().unwrap_or_default(); let loss = 100.0 * (sent.saturating_sub(got) as f64) / (sent.max(1) as f64); let rps = hv as f64 / dur.as_secs_f64(); println!( "{:<28} {:>12.0} req/s (sent={} handled={} rx={} loss={:.1}% p50={:?} p99={:?})", "udp/sustained-10s", rps, sent, hv, got, loss, pct(0.5), pct(0.99) ); medians.push(rps); } medians.sort_by(|a, b| a.partial_cmp(b).unwrap()); println!( "{:<28} {:>12.0} req/s (median)", "udp/sustained-median", medians[medians.len() / 2] ); }