use std::io;
use std::net::SocketAddr;
use std::rc::Rc;
use std::time::{Duration, Instant};
use hiss::noise::P256;
use slither::config::Config;
use slither::constants::MAX_DATAGRAM_PAYLOAD;
use slither::identity::{Identity, PublicKeyOf};
use slither::packet::{Handshake, ReferenceSuite};
use slither::shell::wire::Wire;
use slither::shell::{Connection, Endpoint, RecvStream, SendStream};
use slither::testutil::{CountingIdentity, FlakyPolicy, FlakyWire, Network, Pair, addr_a, addr_b};
use tokio::runtime::Runtime;
use tokio::task::LocalSet;
mod aes_suite {
use slither::prelude::{AesGcm, Blake2b, P256};
slither::channel! {
pub BenchAesSuite<P256, AesGcm, Blake2b>;
}
}
use aes_suite::BenchAesSuite;
trait BenchSuite: Handshake<Curve = P256> + 'static {}
impl<S: Handshake<Curve = P256> + 'static> BenchSuite for S {}
fn protocol_name<S: slither::packet::Channel>() -> &'static str {
<S as slither::packet::Channel>::PROTOCOL_NAME
}
const SAMPLE_BYTES: usize = 4 << 20;
const CHUNK: usize = 64 << 10;
const SAMPLES: usize = 5;
const DATAGRAM_COUNT: usize = 4096;
const MESSAGE_BYTES: usize = 16 << 10;
const MESSAGE_COUNT: usize = 256;
const HANDSHAKES: usize = 16;
const ONE_WAY_DELAY: Duration = Duration::from_millis(10);
const RTT_SAMPLE_BYTES: usize = 2 << 20;
const RTT_SAMPLES: usize = 3;
const PARALLEL_STREAMS: usize = 4;
const DATAGRAM_IDLE: Duration = Duration::from_millis(250);
struct Report {
name: &'static str,
bytes_per_sample: u64,
ops_per_sample: Option<(u64, &'static str)>,
samples: Vec<Duration>,
note: Option<String>,
}
impl Report {
fn sorted(&self) -> Vec<Duration> {
let mut s = self.samples.clone();
s.sort_unstable();
s
}
fn mib_per_sec(&self, d: Duration) -> f64 {
let secs = d.as_secs_f64();
if secs <= 0.0 {
return f64::INFINITY;
}
(self.bytes_per_sample as f64 / (1024.0 * 1024.0)) / secs
}
fn ops_per_sec(&self, d: Duration) -> Option<f64> {
let (ops, _) = self.ops_per_sample?;
let secs = d.as_secs_f64();
(secs > 0.0).then(|| ops as f64 / secs)
}
fn print(&self) {
let s = self.sorted();
let (best, mid, worst) = (s[0], s[s.len() / 2], s[s.len() - 1]);
println!(" {}", self.name);
if let Some((_, unit)) = self.ops_per_sample {
let rate = |d: Duration| self.ops_per_sec(d).unwrap_or(f64::INFINITY);
let each = |d: Duration| {
d.as_secs_f64() * 1e3 / self.ops_per_sample.expect("checked above").0 as f64
};
println!(
" {:>9.0} {unit}/s (median {:.0}, worst {:.0})",
rate(best),
rate(mid),
rate(worst),
);
println!(
" {:>9.3} ms each (median {:.3}, worst {:.3})",
each(best),
each(mid),
each(worst),
);
} else {
println!(
" {:>9.1} MiB/s (median {:.1}, worst {:.1})",
self.mib_per_sec(best),
self.mib_per_sec(mid),
self.mib_per_sec(worst),
);
println!(
" {:>9.1} Mbit/s over {} samples of {:.1} MiB",
self.mib_per_sec(best) * 8.0 * 1.048_576,
self.samples.len(),
self.bytes_per_sample as f64 / (1024.0 * 1024.0),
);
}
if let Some(note) = &self.note {
println!(" note: {note}");
}
println!();
}
}
async fn write_all<S: Handshake>(tx: &mut SendStream<S>, buf: &[u8]) {
let mut off = 0;
while off < buf.len() {
let n = tx.write(&buf[off..]).await.expect("stream write");
assert!(n > 0, "write returned 0 for a non-empty buffer");
off += n;
}
}
async fn read_exactly<S: Handshake>(
rx: &mut RecvStream<S>,
want: usize,
scratch: &mut [u8],
) -> usize {
let mut got = 0;
let mut reads = 0;
while got < want {
let cap = scratch.len().min(want - got);
reads += 1;
match rx.read(&mut scratch[..cap]).await.expect("stream read") {
Some(n) => got += n,
None => panic!("end of stream after {got} of {want} bytes — the sample is short"),
}
}
reads
}
fn payload(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
type BenchIdentity<S> = CountingIdentity<S>;
type BenchEndpoint<S> = Endpoint<BenchIdentity<S>>;
#[derive(Clone)]
struct BenchWire(Rc<FlakyWire>);
impl Wire for BenchWire {
async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> io::Result<usize> {
self.0.send_to(buf, addr).await
}
async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
self.0.recv_from(buf).await
}
}
struct BenchPair<S: BenchSuite> {
net: Network,
a: BenchEndpoint<S>,
b: BenchEndpoint<S>,
addr_b: SocketAddr,
pk_b: PublicKeyOf<BenchIdentity<S>>,
}
fn bench_seed(seed: u64, salt: u8) -> [u8; 32] {
let mut out = [salt; 32];
out[..8].copy_from_slice(&seed.to_le_bytes());
out
}
impl<S: BenchSuite> BenchPair<S> {
fn seeded(seed: u64) -> Self {
let net = Network::seeded(seed);
let (a, _) = Self::spawn(&net, addr_a(), seed, 0xA1);
let (b, pk_b) = Self::spawn(&net, addr_b(), seed, 0xB2);
BenchPair {
net,
a,
b,
addr_b: addr_b(),
pk_b,
}
}
fn spawn(
net: &Network,
addr: SocketAddr,
seed: u64,
salt: u8,
) -> (BenchEndpoint<S>, PublicKeyOf<BenchIdentity<S>>) {
let wire = BenchWire(Rc::new(net.endpoint(addr)));
let identity: BenchIdentity<S> = CountingIdentity::seeded(bench_seed(seed, salt));
let public_static = *Identity::public_static(&identity);
let endpoint = Endpoint::builder()
.identity(identity)
.wire(wire)
.config(Config::new())
.rng_seed(bench_seed(seed, salt ^ 0xFF))
.build();
(endpoint, public_static)
}
async fn establish(&self) -> (Connection<S>, Connection<S>) {
let dial = async {
self.a
.connect(self.addr_b, self.pk_b)
.expect("connect")
.await
.expect("the dial completed")
};
let accept = async {
let intro = self.b.accept().await.expect("an introduction");
let claimed = intro.read_identity().await.expect("read_identity");
let proven = claimed.authenticate().await.expect("authenticate");
proven.accept().await.expect("accept")
};
tokio::join!(dial, accept)
}
}
async fn loopback_pair<S: BenchSuite>() -> (
BenchEndpoint<S>,
BenchEndpoint<S>,
Connection<S>,
Connection<S>,
) {
let sock_a = tokio::net::UdpSocket::bind("127.0.0.1:0")
.await
.expect("bind a");
let sock_b = tokio::net::UdpSocket::bind("127.0.0.1:0")
.await
.expect("bind b");
let addr_b = sock_b.local_addr().expect("local_addr b");
let id_a: BenchIdentity<S> = CountingIdentity::seeded([0xA1; 32]);
let id_b: BenchIdentity<S> = CountingIdentity::seeded([0xB2; 32]);
let pk_b: PublicKeyOf<BenchIdentity<S>> = *Identity::public_static(&id_b);
let ep_a: BenchEndpoint<S> = Endpoint::builder()
.identity(id_a)
.wire(sock_a)
.rng_seed([0x11; 32])
.build();
let ep_b: BenchEndpoint<S> = Endpoint::builder()
.identity(id_b)
.wire(sock_b)
.rng_seed([0x22; 32])
.build();
let dial = async {
ep_a.connect(addr_b, pk_b)
.expect("connect")
.await
.expect("the dial completed")
};
let accept = async {
let intro = ep_b.accept().await.expect("an introduction");
let claimed = intro.read_identity().await.expect("read_identity");
let proven = claimed.authenticate().await.expect("authenticate");
proven.accept().await.expect("accept")
};
let (ca, cb) = tokio::join!(dial, accept);
(ep_a, ep_b, ca, cb)
}
async fn one_way_stream<S: Handshake>(
ca: &Connection<S>,
cb: &Connection<S>,
) -> (SendStream<S>, RecvStream<S>) {
let bi_a = ca.open_bi().await.expect("open_bi");
let (mut tx, _unused_rx) = bi_a.split();
tx.write(b"\0").await.expect("priming write");
let bi_b = cb.accept_bi().await.expect("accept_bi");
let (_unused_tx, mut rx) = bi_b.split();
let mut prime = [0u8; 1];
read_exactly(&mut rx, 1, &mut prime).await;
(tx, rx)
}
async fn stream_sample<S: Handshake>(
tx: &mut SendStream<S>,
rx: &mut RecvStream<S>,
chunk: &[u8],
bytes: usize,
) -> (Duration, usize) {
let chunks = bytes / CHUNK;
let start = Instant::now();
let write = async {
for _ in 0..chunks {
write_all(tx, chunk).await;
}
};
let read = async {
let mut scratch = vec![0u8; CHUNK];
read_exactly(rx, chunks * CHUNK, &mut scratch).await
};
let (_, reads) = tokio::join!(write, read);
(start.elapsed(), reads)
}
fn bench_loopback_stream<S: BenchSuite>(rt: &Runtime, name: &'static str) -> Report {
let ls = LocalSet::new();
let (samples, reads) = ls.block_on(rt, async {
let (_ep_a, _ep_b, ca, cb) = loopback_pair::<S>().await;
let (mut tx, mut rx) = one_way_stream(&ca, &cb).await;
let chunk = payload(CHUNK);
stream_sample(&mut tx, &mut rx, &chunk, SAMPLE_BYTES).await;
let mut out = Vec::with_capacity(SAMPLES);
let mut reads = 0;
for _ in 0..SAMPLES {
let (d, r) = stream_sample(&mut tx, &mut rx, &chunk, SAMPLE_BYTES).await;
out.push(d);
reads += r;
}
(out, reads)
});
Report {
name,
bytes_per_sample: SAMPLE_BYTES as u64,
ops_per_sample: None,
samples,
note: Some(format!(
"{}; mean read fill {:.0} B over {reads} read calls; both endpoints share one thread",
protocol_name::<S>(),
(SAMPLE_BYTES * SAMPLES) as f64 / reads as f64,
)),
}
}
fn bench_inmem_stream<S: BenchSuite>(rt: &Runtime, name: &'static str) -> Report {
let ls = LocalSet::new();
let (samples, reads) = ls.block_on(rt, async {
let pair = BenchPair::<S>::seeded(0x5117_4E00);
let tap = pair.net.tap();
let (ca, cb) = pair.establish().await;
let (mut tx, mut rx) = one_way_stream(&ca, &cb).await;
let chunk = payload(CHUNK);
stream_sample(&mut tx, &mut rx, &chunk, SAMPLE_BYTES).await;
let mut out = Vec::with_capacity(SAMPLES);
let mut reads = 0;
for _ in 0..SAMPLES {
tap.drain();
let (d, r) = stream_sample(&mut tx, &mut rx, &chunk, SAMPLE_BYTES).await;
out.push(d);
reads += r;
}
(out, reads)
});
Report {
name,
bytes_per_sample: SAMPLE_BYTES as u64,
ops_per_sample: None,
samples,
note: Some(format!(
"{}; mean read fill {:.0} B; includes two Vec allocs + copies per datagram the fixture adds",
protocol_name::<S>(),
(SAMPLE_BYTES * SAMPLES) as f64 / reads as f64,
)),
}
}
fn bench_inmem_streams_parallel(rt: &Runtime) -> Report {
let ls = LocalSet::new();
let samples = ls.block_on(rt, async {
let pair = Pair::seeded(0x0A_9A11E1);
let tap = pair.net.tap();
let (ca, cb) = pair.establish().await;
let (mut tx0, mut rx0) = one_way_stream(&ca, &cb).await;
let (mut tx1, mut rx1) = one_way_stream(&ca, &cb).await;
let (mut tx2, mut rx2) = one_way_stream(&ca, &cb).await;
let (mut tx3, mut rx3) = one_way_stream(&ca, &cb).await;
let chunk = payload(CHUNK);
let per = SAMPLE_BYTES / PARALLEL_STREAMS;
let mut out = Vec::with_capacity(SAMPLES);
for sample in 0..=SAMPLES {
tap.drain();
let start = Instant::now();
let _ = tokio::join!(
stream_sample(&mut tx0, &mut rx0, &chunk, per),
stream_sample(&mut tx1, &mut rx1, &chunk, per),
stream_sample(&mut tx2, &mut rx2, &chunk, per),
stream_sample(&mut tx3, &mut rx3, &chunk, per),
);
if sample > 0 {
out.push(start.elapsed());
}
}
out
});
Report {
name: "inmem/streams×4[chacha] four bi streams, same total bytes",
bytes_per_sample: SAMPLE_BYTES as u64,
ops_per_sample: None,
samples,
note: Some("compare against inmem/stream: equal means the connection is the limit, faster means the stream was".into()),
}
}
fn bench_inmem_stream_rtt(rt: &Runtime) -> Report {
let ls = LocalSet::new();
let samples = ls.block_on(rt, async {
let pair = Pair::seeded(0x00BD_9000);
let tap = pair.net.tap();
let (ca, cb) = pair.establish().await;
let (mut tx, mut rx) = one_way_stream(&ca, &cb).await;
let slow = FlakyPolicy::perfect().with_delay(ONE_WAY_DELAY, Duration::ZERO);
pair.a.wire.set_policy(slow.clone());
pair.b.wire.set_policy(slow);
let chunk = payload(CHUNK);
let mut out = Vec::with_capacity(RTT_SAMPLES);
for _ in 0..RTT_SAMPLES {
tap.drain();
let (d, _) = stream_sample(&mut tx, &mut rx, &chunk, RTT_SAMPLE_BYTES).await;
out.push(d);
}
out
});
let rtt = ONE_WAY_DELAY.as_secs_f64() * 2.0;
let predicted = (slither::constants::INITIAL_MAX_STREAM_DATA as f64 / (1024.0 * 1024.0)) / rtt;
let best = samples.iter().copied().min().unwrap_or_default();
let measured = (RTT_SAMPLE_BYTES as f64 / (1024.0 * 1024.0)) / best.as_secs_f64();
Report {
name: "inmem/stream@rtt[chacha] one bi stream, 20 ms injected RTT",
bytes_per_sample: RTT_SAMPLE_BYTES as u64,
ops_per_sample: None,
samples,
note: Some(format!(
"window/RTT predicts {predicted:.1} MiB/s, measured {measured:.1} — \
the 256 KiB stream window is static, so a real path caps at window/RTT"
)),
}
}
fn bench_inmem_datagram<S: BenchSuite>(rt: &Runtime, name: &'static str) -> Report {
let ls = LocalSet::new();
let (samples, delivered) = ls.block_on(rt, async {
let pair = BenchPair::<S>::seeded(0x0A7A_6100);
let tap = pair.net.tap();
let (ca, cb) = pair.establish().await;
let msg = payload(MAX_DATAGRAM_PAYLOAD);
let mut out = Vec::with_capacity(SAMPLES);
let mut got_total = 0u64;
for sample in 0..=SAMPLES {
tap.drain();
let start = Instant::now();
let send = async {
for _ in 0..DATAGRAM_COUNT {
if ca.send_datagram(&msg).is_err() {
break;
}
tokio::task::yield_now().await;
}
};
let recv = async {
let mut got = 0u64;
while got < DATAGRAM_COUNT as u64 {
match tokio::time::timeout(DATAGRAM_IDLE, cb.recv_datagram()).await {
Ok(Ok(_)) => got += 1,
Err(_) => break,
Ok(Err(e)) => panic!("connection lost mid-benchmark: {e}"),
}
}
got
};
let (_, got) = tokio::join!(send, recv);
let elapsed = start.elapsed();
if sample > 0 {
out.push(if got < DATAGRAM_COUNT as u64 {
elapsed.saturating_sub(DATAGRAM_IDLE)
} else {
elapsed
});
got_total += got;
}
}
(out, got_total)
});
let attempted = (DATAGRAM_COUNT * SAMPLES) as u64;
let ratio = delivered as f64 / attempted as f64 * 100.0;
Report {
name,
bytes_per_sample: (delivered / SAMPLES.max(1) as u64) * MAX_DATAGRAM_PAYLOAD as u64,
ops_per_sample: None,
samples,
note: Some(format!(
"{}; {delivered}/{attempted} delivered ({ratio:.1}%) — throughput counts arrivals, not sends",
protocol_name::<S>(),
)),
}
}
fn bench_inmem_message(rt: &Runtime) -> Report {
let ls = LocalSet::new();
let samples = ls.block_on(rt, async {
let pair = Pair::seeded(0x0E55_A6E0);
let tap = pair.net.tap();
let (ca, cb) = pair.establish().await;
let msg = payload(MESSAGE_BYTES);
let mut out = Vec::with_capacity(SAMPLES);
for sample in 0..=SAMPLES {
tap.drain();
let start = Instant::now();
let send = async {
for _ in 0..MESSAGE_COUNT {
ca.send_message(&msg).await.expect("send_message");
}
};
let recv = async {
for _ in 0..MESSAGE_COUNT {
let got = cb.recv_message().await.expect("recv_message");
assert_eq!(got.len(), MESSAGE_BYTES, "a message arrived truncated");
}
};
tokio::join!(send, recv);
if sample > 0 {
out.push(start.elapsed());
}
}
out
});
Report {
name: "inmem/message[chacha] single-shot, 16 KiB each",
bytes_per_sample: (MESSAGE_COUNT * MESSAGE_BYTES) as u64,
ops_per_sample: None,
samples,
note: None,
}
}
fn bench_inmem_handshake<S: BenchSuite>(rt: &Runtime, name: &'static str) -> Report {
let ls = LocalSet::new();
let samples = ls.block_on(rt, async {
let mut out = Vec::with_capacity(SAMPLES);
for sample in 0..=SAMPLES {
let start = Instant::now();
for i in 0..HANDSHAKES {
let pair = BenchPair::<S>::seeded(0x4144_0000 + (sample * HANDSHAKES + i) as u64);
let (ca, cb) = pair.establish().await;
drop((ca, cb, pair));
}
if sample > 0 {
out.push(start.elapsed());
}
}
out
});
Report {
name,
bytes_per_sample: 0,
ops_per_sample: Some((HANDSHAKES as u64, "conn")),
samples,
note: Some(format!(
"{}; includes two P-256 static keypair derivations per connection",
protocol_name::<S>(),
)),
}
}
macro_rules! arm {
($wrapper:ident = $bench:ident::<$suite:ty>($label:expr)) => {
fn $wrapper(rt: &Runtime) -> Report {
$bench::<$suite>(rt, $label)
}
};
}
arm!(
loopback_stream_chacha = bench_loopback_stream::<ReferenceSuite>(
"loopback/stream[chacha] one bi stream, real UDP syscalls"
)
);
arm!(
loopback_stream_aesgcm = bench_loopback_stream::<BenchAesSuite>(
"loopback/stream[aesgcm] one bi stream, real UDP syscalls"
)
);
arm!(
inmem_stream_chacha =
bench_inmem_stream::<ReferenceSuite>("inmem/stream[chacha] one bi stream, no kernel")
);
arm!(
inmem_stream_aesgcm =
bench_inmem_stream::<BenchAesSuite>("inmem/stream[aesgcm] one bi stream, no kernel")
);
arm!(
inmem_datagram_chacha = bench_inmem_datagram::<ReferenceSuite>(
"inmem/datagram[chacha] unreliable, 1169-byte payloads"
)
);
arm!(
inmem_datagram_aesgcm = bench_inmem_datagram::<BenchAesSuite>(
"inmem/datagram[aesgcm] unreliable, 1169-byte payloads"
)
);
arm!(
inmem_handshake_chacha = bench_inmem_handshake::<ReferenceSuite>(
"inmem/handshake[chacha] endpoint setup + full 4-DH ladder"
)
);
arm!(
inmem_handshake_aesgcm = bench_inmem_handshake::<BenchAesSuite>(
"inmem/handshake[aesgcm] endpoint setup + full 4-DH ladder"
)
);
fn main() {
let filter = std::env::args()
.skip(1)
.find(|a| !a.starts_with('-'))
.unwrap_or_default();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime");
#[allow(clippy::type_complexity)]
let all: Vec<(&str, fn(&Runtime) -> Report)> = vec![
("loopback/stream[chacha]", loopback_stream_chacha),
("loopback/stream[aesgcm]", loopback_stream_aesgcm),
("inmem/stream[chacha]", inmem_stream_chacha),
("inmem/stream[aesgcm]", inmem_stream_aesgcm),
(
"inmem/streams-parallel[chacha]",
bench_inmem_streams_parallel,
),
("inmem/stream@rtt[chacha]", bench_inmem_stream_rtt),
("inmem/datagram[chacha]", inmem_datagram_chacha),
("inmem/datagram[aesgcm]", inmem_datagram_aesgcm),
("inmem/message[chacha]", bench_inmem_message),
("inmem/handshake[chacha]", inmem_handshake_chacha),
("inmem/handshake[aesgcm]", inmem_handshake_aesgcm),
];
let selected: Vec<_> = all
.into_iter()
.filter(|(name, _)| filter.is_empty() || name.contains(&filter))
.collect();
if selected.is_empty() {
eprintln!("no benchmark matches {filter:?}");
std::process::exit(1);
}
println!();
println!("slither throughput — one thread carries both endpoints");
println!("{}", "─".repeat(72));
println!();
for (_, run) in selected {
run(&rt).print();
}
}