use std::{
io,
mem::ManuallyDrop,
net::{SocketAddr, TcpListener as StdListener},
process::{Child, Command, Stdio},
sync::{
Arc, Barrier,
atomic::{AtomicU64, Ordering::Relaxed},
},
time::{Duration, Instant},
};
use compio::{
buf::BufResult,
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use compio_pool::{Config, Manage, Metrics, Pool, SlotMeta};
struct Params {
conns: usize,
threads: usize,
per_shard: usize,
rounds: usize,
payload: usize,
nopool_rounds: usize,
}
impl Params {
fn from_env() -> Self {
let conns = env_usize("CONNS", 45);
let threads = env_usize("THREADS", 5);
assert!(
conns.is_multiple_of(threads),
"CONNS ({conns}) must divide evenly into THREADS ({threads}); \
max_size is per shard, so the per-thread budget has to be a whole number"
);
Self {
conns,
threads,
per_shard: conns / threads,
rounds: env_usize("ROUNDS", 2_000),
payload: env_usize("PAYLOAD", 64),
nopool_rounds: env_usize("NOPOOL_ROUNDS", 100),
}
}
}
fn env_usize(key: &str, default: usize) -> usize {
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
#[cfg(unix)]
fn with_std<T>(conn: &TcpStream, f: impl FnOnce(&std::net::TcpStream) -> T) -> T {
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
let raw = conn.as_fd().as_raw_fd();
let borrowed = ManuallyDrop::new(unsafe { std::net::TcpStream::from_raw_fd(raw) });
f(&borrowed)
}
#[cfg(windows)]
fn with_std<T>(conn: &TcpStream, f: impl FnOnce(&std::net::TcpStream) -> T) -> T {
use std::os::windows::io::{AsRawSocket, FromRawSocket};
let raw = conn.as_raw_socket();
let borrowed = ManuallyDrop::new(unsafe { std::net::TcpStream::from_raw_socket(raw) });
f(&borrowed)
}
#[derive(Default)]
struct Instrumentation {
dials: AtomicU64,
dial_nanos: AtomicU64,
recycles: AtomicU64,
recycle_nanos: AtomicU64,
}
struct NcatManager {
addr: SocketAddr,
stats: Arc<Instrumentation>,
}
impl Manage for NcatManager {
type Connection = TcpStream;
type Error = io::Error;
async fn connect(&self) -> io::Result<TcpStream> {
let start = Instant::now();
let conn = TcpStream::connect(self.addr).await?;
conn.set_nodelay(true)?;
self.stats.dials.fetch_add(1, Relaxed);
self.stats
.dial_nanos
.fetch_add(start.elapsed().as_nanos() as u64, Relaxed);
Ok(conn)
}
async fn recycle(&self, conn: &mut TcpStream, _meta: &SlotMeta) -> io::Result<()> {
let start = Instant::now();
let result = match with_std(conn, |s| s.take_error())? {
Some(err) => Err(err),
None => Ok(()),
};
self.stats.recycles.fetch_add(1, Relaxed);
self.stats
.recycle_nanos
.fetch_add(start.elapsed().as_nanos() as u64, Relaxed);
result
}
}
async fn round_trip(
conn: &mut TcpStream,
req: Vec<u8>,
resp: Vec<u8>,
tag: u64,
round: u64,
) -> (io::Result<()>, Vec<u8>, Vec<u8>) {
let mut req = req;
req[..8].copy_from_slice(&tag.to_le_bytes());
req[8..16].copy_from_slice(&round.to_le_bytes());
let BufResult(written, req) = conn.write_all(req).await;
if let Err(e) = written {
return (Err(e), req, resp);
}
let BufResult(read, resp) = conn.read_exact(resp).await;
if let Err(e) = read {
return (Err(e), req, resp);
}
if resp[..16] != req[..16] {
let e = io::Error::new(io::ErrorKind::InvalidData, "echo did not match request");
return (Err(e), req, resp);
}
(Ok(()), req, resp)
}
#[derive(Default)]
struct Samples {
rtt: Vec<u64>,
acquire: Vec<u64>,
connect_err: u64,
io_err: u64,
first_err: Option<String>,
}
impl Samples {
fn with_capacity(n: usize) -> Self {
Self {
rtt: Vec::with_capacity(n),
acquire: Vec::with_capacity(n),
..Self::default()
}
}
fn record_err(&mut self, e: &dyn std::fmt::Display) {
if self.first_err.is_none() {
self.first_err = Some(e.to_string());
}
}
fn errors(&self) -> u64 {
self.connect_err + self.io_err
}
fn merge(&mut self, other: Samples) {
self.rtt.extend_from_slice(&other.rtt);
self.acquire.extend_from_slice(&other.acquire);
self.connect_err += other.connect_err;
self.io_err += other.io_err;
if self.first_err.is_none() {
self.first_err = other.first_err;
}
}
}
async fn pooled_task(pool: Pool<NcatManager>, rounds: usize, payload: usize, tag: u64) -> Samples {
let mut s = Samples::with_capacity(rounds);
let mut req = vec![0u8; payload];
let mut resp = vec![0u8; payload];
for round in 0..rounds as u64 {
let t_acquire = Instant::now();
let mut conn = match pool.acquire().await {
Ok(c) => c,
Err(e) => {
s.connect_err += 1;
s.record_err(&e);
continue;
}
};
let acquired = t_acquire.elapsed();
let t_rtt = Instant::now();
let op = conn.begin_op();
let (result, r, p) = round_trip(&mut conn, req, resp, tag, round).await;
req = r;
resp = p;
match result {
Ok(()) => {
op.complete_op();
s.rtt.push(t_rtt.elapsed().as_nanos() as u64);
s.acquire.push(acquired.as_nanos() as u64);
}
Err(e) => {
s.io_err += 1;
s.record_err(&e);
}
}
}
s
}
async fn unpooled_task(addr: SocketAddr, rounds: usize, payload: usize, tag: u64) -> Samples {
let mut s = Samples::with_capacity(rounds);
let mut req = vec![0u8; payload];
let mut resp = vec![0u8; payload];
for round in 0..rounds as u64 {
let t_acquire = Instant::now();
let mut conn = match TcpStream::connect(addr).await {
Ok(c) => c,
Err(e) => {
s.connect_err += 1;
s.record_err(&e);
continue;
}
};
let _ = conn.set_nodelay(true);
let acquired = t_acquire.elapsed();
let t_rtt = Instant::now();
let (result, r, p) = round_trip(&mut conn, req, resp, tag, round).await;
req = r;
resp = p;
match result {
Ok(()) => {
s.rtt.push(t_rtt.elapsed().as_nanos() as u64);
s.acquire.push(acquired.as_nanos() as u64);
}
Err(e) => {
s.io_err += 1;
s.record_err(&e);
}
}
}
s
}
struct Phase {
name: &'static str,
tasks: usize,
rounds: usize,
pooled: bool,
}
struct PhaseResult {
samples: Samples,
elapsed: Duration,
delta: Metrics,
after: Metrics,
}
fn delta(after: &Metrics, before: &Metrics) -> Metrics {
Metrics {
live: 0,
idle: 0,
parked: 0,
created: after.created - before.created,
closed: after.closed - before.closed,
acquires: after.acquires - before.acquires,
waits: after.waits - before.waits,
timeouts: after.timeouts - before.timeouts,
poisoned: after.poisoned - before.poisoned,
recycle_failures: after.recycle_failures - before.recycle_failures,
unparked: after.unparked - before.unparked,
}
}
fn run(
params: &Params,
addr: SocketAddr,
pool: &Pool<NcatManager>,
phases: &'static [Phase],
) -> (Vec<PhaseResult>, Metrics) {
const WARMUP_ROUNDS: usize = 50;
let barrier = Arc::new(Barrier::new(params.threads));
let snapshots: Arc<std::sync::Mutex<Vec<Metrics>>> = Arc::default();
let payload = params.payload;
let params_per_shard = params.per_shard;
let workers: Vec<_> = (0..params.threads)
.map(|thread| {
let pool = pool.clone();
let barrier = barrier.clone();
let snapshots = snapshots.clone();
std::thread::spawn(move || {
compio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
pool.warm().await.expect("warm");
let warmup: Vec<_> = (0..params_per_shard)
.map(|i| {
compio::runtime::spawn(pooled_task(
pool.clone(),
WARMUP_ROUNDS,
payload,
(thread * 1_000 + i) as u64,
))
})
.collect();
for h in warmup {
h.await.expect("warmup task panicked");
}
let mut out = Vec::new();
for phase in phases {
barrier.wait();
if thread == 0 {
snapshots.lock().unwrap().push(pool.metrics());
}
barrier.wait();
let start = Instant::now();
let handles: Vec<_> = (0..phase.tasks)
.map(|i| {
let tag = (thread * 1_000 + i) as u64;
let pool = pool.clone();
if phase.pooled {
compio::runtime::spawn(pooled_task(
pool,
phase.rounds,
payload,
tag,
))
} else {
compio::runtime::spawn(unpooled_task(
addr,
phase.rounds,
payload,
tag,
))
}
})
.collect();
let mut samples = Samples::default();
for h in handles {
samples.merge(h.await.expect("task panicked"));
}
out.push((samples, start.elapsed()));
}
barrier.wait();
if thread == 0 {
snapshots.lock().unwrap().push(pool.metrics());
}
barrier.wait();
out
})
})
})
.collect();
let per_thread: Vec<_> = workers.into_iter().map(|w| w.join().unwrap()).collect();
let snapshots = snapshots.lock().unwrap().clone();
let results = (0..phases.len())
.map(|i| {
let mut samples = Samples::default();
let mut elapsed = Duration::ZERO;
for thread in &per_thread {
let (s, e) = &thread[i];
samples.rtt.extend_from_slice(&s.rtt);
samples.acquire.extend_from_slice(&s.acquire);
samples.connect_err += s.connect_err;
samples.io_err += s.io_err;
if samples.first_err.is_none() {
samples.first_err = s.first_err.clone();
}
elapsed = elapsed.max(*e);
}
PhaseResult {
samples,
elapsed,
delta: delta(&snapshots[i + 1], &snapshots[i]),
after: snapshots[i + 1],
}
})
.collect();
(results, *snapshots.last().unwrap())
}
fn percentile(sorted: &[u64], p: f64) -> u64 {
if sorted.is_empty() {
return 0;
}
let idx = ((sorted.len() - 1) as f64 * p).round() as usize;
sorted[idx]
}
fn us(nanos: u64) -> String {
format!("{:.1}", nanos as f64 / 1_000.0)
}
fn latency_row(label: &str, samples: &mut [u64]) {
samples.sort_unstable();
let mean = if samples.is_empty() {
0
} else {
samples.iter().sum::<u64>() / samples.len() as u64
};
println!(
" {label:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
us(mean),
us(percentile(samples, 0.50)),
us(percentile(samples, 0.90)),
us(percentile(samples, 0.99)),
us(percentile(samples, 0.999)),
us(percentile(samples, 1.0)),
);
}
fn report_phase(phase: &Phase, result: &mut PhaseResult, params: &Params) {
let ops = result.samples.rtt.len() as u64;
let secs = result.elapsed.as_secs_f64();
let in_flight = phase.tasks * params.threads;
println!(
"\n[{}] {in_flight} tasks over {} threads, {} requests each",
phase.name, params.threads, phase.rounds,
);
println!(
" {ops} ok / {} err in {:.3}s -> {:.0} req/s ({:.0} req/s per connection)",
result.samples.errors(),
secs,
ops as f64 / secs,
ops as f64 / secs / params.conns as f64,
);
println!(
" {:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
"latency (us)", "mean", "p50", "p90", "p99", "p99.9", "max"
);
latency_row(
if phase.pooled {
"acquire"
} else {
"connect (no pool)"
},
&mut result.samples.acquire,
);
latency_row("round trip", &mut result.samples.rtt);
if result.samples.errors() > 0 {
println!(
" errors: {} getting a connection, {} on an open one; first was: {}",
result.samples.connect_err,
result.samples.io_err,
result.samples.first_err.as_deref().unwrap_or("-"),
);
}
let m = &result.delta;
println!(
" pool: acquires={} waits={} timeouts={} created={} closed={} poisoned={} recycle_failures={} | live at end={}",
m.acquires,
m.waits,
m.timeouts,
m.created,
m.closed,
m.poisoned,
m.recycle_failures,
result.after.live,
);
}
struct Server(Option<Child>);
impl Drop for Server {
fn drop(&mut self) {
if let Some(child) = self.0.as_mut() {
let _ = child.kill();
let _ = child.wait();
}
}
}
fn spawn_ncat(port: u16, max_conns: usize) -> io::Result<Server> {
let child = Command::new("ncat")
.args([
"--listen",
"127.0.0.1",
&port.to_string(),
"--keep-open",
"--exec",
"/bin/cat",
"--max-conns",
&max_conns.to_string(),
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| {
io::Error::new(
e.kind(),
format!(
"could not start `ncat` ({e}). Install it (brew install nmap / \
apt install ncat), or point the example at a server you are \
already running with NCAT_ADDR=host:port"
),
)
})?;
Ok(Server(Some(child)))
}
fn free_port() -> io::Result<u16> {
Ok(StdListener::bind("127.0.0.1:0")?.local_addr()?.port())
}
fn wait_ready(addr: SocketAddr, limit: Duration) -> io::Result<()> {
let deadline = Instant::now() + limit;
loop {
match std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(200)) {
Ok(_) => return Ok(()),
Err(e) if Instant::now() >= deadline => return Err(e),
Err(_) => std::thread::sleep(Duration::from_millis(20)),
}
}
}
fn main() -> io::Result<()> {
let params = Params::from_env();
let phases: &'static [Phase] = Box::leak(Box::new([
Phase {
name: "A steady",
tasks: params.per_shard,
rounds: params.rounds,
pooled: true,
},
Phase {
name: "B oversubscribed",
tasks: params.per_shard * 2,
rounds: params.rounds / 2,
pooled: true,
},
Phase {
name: "C no pool",
tasks: params.per_shard,
rounds: params.nopool_rounds,
pooled: false,
},
]));
let (addr, _server) = match std::env::var("NCAT_ADDR") {
Ok(s) => {
let addr: SocketAddr = s.parse().expect("NCAT_ADDR must be host:port");
wait_ready(addr, Duration::from_secs(2))?;
println!("using the server already listening on {addr}");
(addr, None)
}
Err(_) => {
let port = free_port()?;
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let server = spawn_ncat(port, params.conns * 4)?;
wait_ready(addr, Duration::from_secs(5))?;
println!("started `ncat --listen 127.0.0.1 {port} --keep-open --exec /bin/cat`");
(addr, Some(server))
}
};
let stats = Arc::new(Instrumentation::default());
let pool = Pool::new(
NcatManager {
addr,
stats: stats.clone(),
},
Config::new()
.max_size(params.per_shard)
.min_idle(params.per_shard)
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(None)
.max_lifetime(None)
.reap_interval(Duration::from_secs(60)),
);
println!(
"\n{} connections = {} compio threads x {} per shard, {}-byte payload\n",
params.conns, params.threads, params.per_shard, params.payload,
);
let warm_start = Instant::now();
let (mut results, final_metrics) = run(¶ms, addr, &pool, phases);
let wall = warm_start.elapsed();
for (phase, result) in phases.iter().zip(results.iter_mut()) {
report_phase(phase, result, ¶ms);
}
let pooled_ops: u64 = phases
.iter()
.zip(results.iter())
.filter(|(p, _)| p.pooled)
.map(|(_, r)| r.samples.rtt.len() as u64)
.sum();
let dials = stats.dials.load(Relaxed);
let dial_nanos = stats.dial_nanos.load(Relaxed);
let recycles = stats.recycles.load(Relaxed);
let recycle_nanos = stats.recycle_nanos.load(Relaxed);
println!("\n--- pool counters (cumulative, all phases) ---");
println!("{final_metrics:#?}");
println!("\n--- what the numbers say ---");
println!(
" live connections held {} (expected {})",
final_metrics.live, params.conns
);
println!(
" dials {dials} ({} pooled requests served per dial)",
pooled_ops / dials.max(1)
);
println!(
" mean dial cost {} us (paid once per connection, not per request)",
us(dial_nanos / dials.max(1))
);
println!(
" mean recycle cost {} us ({recycles} checkouts validated; this is on the acquire hot path)",
us(recycle_nanos / recycles.max(1))
);
println!(
" checkouts that had to wait {} of {} ({:.2}%)",
final_metrics.waits,
final_metrics.acquires,
100.0 * final_metrics.waits as f64 / final_metrics.acquires.max(1) as f64,
);
println!(
" timeouts / poisoned / recycle failures {} / {} / {}",
final_metrics.timeouts, final_metrics.poisoned, final_metrics.recycle_failures,
);
println!(" total wall time {:.2}s", wall.as_secs_f64());
assert_eq!(
final_metrics.live as usize, params.conns,
"the pool should still be holding exactly {} connections",
params.conns
);
assert_eq!(
final_metrics.timeouts, 0,
"no checkout should have timed out"
);
pool.close();
Ok(())
}