use std::{
collections::{HashMap, VecDeque},
future::Future,
hint::black_box,
io,
pin::{Pin, pin},
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU32, Ordering},
},
task::{Context, Poll, Waker},
thread,
time::Duration,
};
use cpus::Cpus;
use criterion::{
BenchmarkGroup, Criterion, Throughput, async_executor::AsyncExecutor, criterion_group,
criterion_main, measurement::Measurement,
};
use futures::future::{self, try_join_all};
use zruntime::{Event, Shared, SharedRuntime, Task, lock};
#[path = "common/cpus.rs"]
mod cpus;
struct ZruntimeExecutor;
impl AsyncExecutor for ZruntimeExecutor {
fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
zruntime::block_on(future)
}
}
const METHOD_TIMEOUT: Duration = Duration::from_secs(30);
const METHOD_PING: u8 = 0;
const METHOD_ECHO: u8 = 1;
fn dispatch(body: &[u8]) -> Vec<u8> {
match body.first().copied() {
Some(METHOD_PING) => body[1..5].to_vec(),
Some(METHOD_ECHO) => body[1..].to_vec(),
_ => Vec::new(),
}
}
type Handler = fn(&[u8]) -> Vec<u8>;
async fn ping(connection: &Connection, value: u32) -> io::Result<u32> {
let mut body = Vec::with_capacity(5);
body.push(METHOD_PING);
body.extend_from_slice(&value.to_le_bytes());
let reply = connection.call(body).await?;
Ok(u32::from_le_bytes(reply.body[..4].try_into().unwrap()))
}
#[cfg(unix)]
async fn echo(connection: &Connection, payload: &[u8]) -> io::Result<Vec<u8>> {
let mut body = Vec::with_capacity(1 + payload.len());
body.push(METHOD_ECHO);
body.extend_from_slice(payload);
Ok(connection.call(body).await?.body)
}
const CONCURRENT_METHOD_CALLS: usize = 1000;
async fn call_ping_concurrently(client: &Connection) {
let replies =
try_join_all((0..CONCURRENT_METHOD_CALLS as u32).map(|value| ping(client, value)))
.await
.unwrap();
black_box(replies);
}
type BuildFuture = Pin<Box<dyn Future<Output = (BoxedReader, BoxedWriter)>>>;
fn run_burst_bench<M>(
group: &mut BenchmarkGroup<'_, M>,
id: &str,
build_server: impl FnOnce(&SharedRuntime) -> BuildFuture + Send + 'static,
build_client: impl FnOnce(&SharedRuntime) -> BuildFuture,
) where
M: Measurement,
{
let cpus = Cpus::allowed();
let server_cpu = cpus.nth(1);
let _pinned = cpus.nth(0).pin();
let server_thread = thread::spawn(move || {
let _pinned = server_cpu.pin();
zruntime::block_on(async move {
let runtime = SharedRuntime::current().expect("a runtime for this thread");
let (reader, writer) = build_server(&runtime).await;
let connection = build_connection(reader, writer, &runtime, Some(dispatch));
connection.closed().await;
})
});
let client = zruntime::block_on(async {
let runtime = SharedRuntime::current().expect("a runtime for this thread");
let (reader, writer) = build_client(&runtime).await;
build_connection(reader, writer, &runtime, None)
});
group.bench_function(id, |b| {
b.to_async(ZruntimeExecutor)
.iter(|| call_ping_concurrently(&client));
});
drop(client);
server_thread.join().unwrap();
}
const CHANNEL_CAPACITY: usize = 32;
fn concurrent_channel_bench(c: &mut Criterion) {
let mut group = c.benchmark_group("method-call");
group.sample_size(10);
group.throughput(Throughput::Elements(CONCURRENT_METHOD_CALLS as u64));
let client_to_server = Arc::new(Queue::bounded(CHANNEL_CAPACITY));
let server_to_client = Arc::new(Queue::bounded(CHANNEL_CAPACITY));
let (server_reads, server_writes) = (client_to_server.clone(), server_to_client.clone());
run_burst_bench(
&mut group,
"1000-concurrent-p2p",
move |_runtime| {
let reader: BoxedReader = Box::new(ChannelReader(server_reads));
let writer: BoxedWriter = Box::new(ChannelWriter(server_writes));
Box::pin(std::future::ready((reader, writer)))
},
move |_runtime| {
let reader: BoxedReader = Box::new(ChannelReader(server_to_client));
let writer: BoxedWriter = Box::new(ChannelWriter(client_to_server));
Box::pin(std::future::ready((reader, writer)))
},
);
group.finish();
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FrameKind {
Call,
Reply,
#[cfg(unix)]
Signal,
}
#[derive(Clone, Debug)]
struct Frame {
kind: FrameKind,
serial: u32,
reply_serial: u32,
body: Vec<u8>,
}
trait FrameReader: Send + 'static {
fn read_frame(&self) -> Pin<Box<dyn Future<Output = Option<Frame>> + Send + '_>>;
}
trait FrameWriter: Send + 'static {
fn write_frame<'a>(
&'a self,
frame: &'a Frame,
) -> Pin<Box<dyn Future<Output = io::Result<()>> + Send + 'a>>;
}
type BoxedReader = Box<dyn FrameReader>;
type BoxedWriter = Box<dyn FrameWriter>;
struct Queue<T> {
items: Mutex<VecDeque<T>>,
capacity: Option<usize>,
closed: AtomicBool,
not_empty: Event,
not_full: Event,
}
impl<T> Queue<T> {
fn unbounded() -> Self {
Self::new(None)
}
fn bounded(capacity: usize) -> Self {
Self::new(Some(capacity))
}
fn new(capacity: Option<usize>) -> Self {
Self {
items: Mutex::new(VecDeque::new()),
capacity,
closed: AtomicBool::new(false),
not_empty: Event::new(),
not_full: Event::new(),
}
}
async fn push(&self, item: T) {
let mut item = Some(item);
loop {
if self.try_push(&mut item) {
return;
}
let listener = self.not_full.listen();
if self.try_push(&mut item) {
return;
}
listener.await;
}
}
fn try_push(&self, item: &mut Option<T>) -> bool {
let mut items = self.items.lock().unwrap();
if self
.capacity
.is_some_and(|capacity| items.len() >= capacity)
{
return false;
}
items.push_back(item.take().expect("pushed twice"));
drop(items);
self.not_empty.notify(1);
true
}
async fn pop(&self) -> Option<T> {
loop {
if let Some(outcome) = self.try_pop() {
return outcome;
}
let listener = self.not_empty.listen();
if let Some(outcome) = self.try_pop() {
return outcome;
}
listener.await;
}
}
fn try_pop(&self) -> Option<Option<T>> {
let mut items = self.items.lock().unwrap();
if let Some(item) = items.pop_front() {
drop(items);
self.not_full.notify(1);
return Some(Some(item));
}
self.closed.load(Ordering::Acquire).then_some(None)
}
fn close(&self) {
self.closed.store(true, Ordering::Release);
self.not_empty.notify(usize::MAX);
self.not_full.notify(usize::MAX);
}
}
struct ChannelWriter(Arc<Queue<Frame>>);
impl FrameWriter for ChannelWriter {
fn write_frame<'a>(
&'a self,
frame: &'a Frame,
) -> Pin<Box<dyn Future<Output = io::Result<()>> + Send + 'a>> {
Box::pin(async move {
self.0.push(frame.clone()).await;
Ok(())
})
}
}
impl Drop for ChannelWriter {
fn drop(&mut self) {
self.0.close();
}
}
struct ChannelReader(Arc<Queue<Frame>>);
impl FrameReader for ChannelReader {
fn read_frame(&self) -> Pin<Box<dyn Future<Output = Option<Frame>> + Send + '_>> {
Box::pin(async move { self.0.pop().await })
}
}
#[derive(Default)]
struct PendingCalls {
calls: Mutex<HashMap<u32, PendingSlot>>,
closed: AtomicBool,
}
#[derive(Default)]
struct PendingSlot {
reply: Option<Frame>,
waker: Option<Waker>,
}
impl PendingCalls {
fn register(self: &Arc<Self>, serial: u32) -> PendingCall {
self.calls
.lock()
.unwrap()
.insert(serial, PendingSlot::default());
PendingCall {
serial,
pending: self.clone(),
}
}
fn complete(&self, serial: u32, reply: Frame) {
let mut calls = self.calls.lock().unwrap();
let Some(slot) = calls.get_mut(&serial) else {
return;
};
slot.reply = Some(reply);
let waker = slot.waker.take();
drop(calls);
if let Some(waker) = waker {
waker.wake();
}
}
fn fail_all(&self) {
self.closed.store(true, Ordering::Release);
let wakers: Vec<_> = self
.calls
.lock()
.unwrap()
.values_mut()
.filter_map(|slot| slot.waker.take())
.collect();
for waker in wakers {
waker.wake();
}
}
fn remove(&self, serial: u32) {
self.calls.lock().unwrap().remove(&serial);
}
}
struct PendingCall {
serial: u32,
pending: Arc<PendingCalls>,
}
impl Future for PendingCall {
type Output = Option<Frame>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut calls = self.pending.calls.lock().unwrap();
let slot = calls.get_mut(&self.serial).expect("this call's own slot");
if let Some(reply) = slot.reply.take() {
calls.remove(&self.serial);
return Poll::Ready(Some(reply));
}
if self.pending.closed.load(Ordering::Acquire) {
calls.remove(&self.serial);
return Poll::Ready(None);
}
slot.waker = Some(cx.waker().clone());
Poll::Pending
}
}
impl Drop for PendingCall {
fn drop(&mut self) {
self.pending.remove(self.serial);
}
}
struct Connection {
write: Arc<lock::Mutex<BoxedWriter>>,
pending: Arc<PendingCalls>,
#[cfg(unix)]
signals: Arc<Queue<Frame>>,
next_serial: AtomicU32,
closed: Arc<AtomicBool>,
closed_event: Arc<Event>,
runtime: SharedRuntime,
_reader_task: Task<(), Shared>,
_object_server_task: Option<Task<(), Shared>>,
drop_event: Arc<Event>,
}
impl Connection {
async fn call(&self, body: Vec<u8>) -> io::Result<Frame> {
let serial = self.next_serial.fetch_add(1, Ordering::Relaxed);
let pending = self.pending.register(serial);
let frame = Frame {
kind: FrameKind::Call,
serial,
reply_serial: 0,
body,
};
self.write.lock().await.write_frame(&frame).await?;
enum Outcome {
Replied(Option<Frame>),
TimedOut,
}
let outcome = future::select(
pin!(async { Outcome::Replied(pending.await) }),
pin!(async {
self.runtime.sleep(METHOD_TIMEOUT).await;
Outcome::TimedOut
}),
)
.await
.factor_first()
.0;
match outcome {
Outcome::Replied(Some(frame)) => Ok(frame),
Outcome::Replied(None) => Err(io::Error::other(
"the connection closed before a reply arrived",
)),
Outcome::TimedOut => Err(io::Error::new(
io::ErrorKind::TimedOut,
"method call timed out",
)),
}
}
#[cfg(unix)]
async fn emit_signal(&self, body: Vec<u8>) {
let frame = Frame {
kind: FrameKind::Signal,
serial: 0,
reply_serial: 0,
body,
};
self.write
.lock()
.await
.write_frame(&frame)
.await
.expect("emit a signal");
}
#[cfg(unix)]
async fn next_signal(&self) -> Frame {
self.signals
.pop()
.await
.expect("the connection closed while waiting for a signal")
}
async fn closed(&self) {
loop {
if self.closed.load(Ordering::Acquire) {
return;
}
let listener = self.closed_event.listen();
if self.closed.load(Ordering::Acquire) {
return;
}
listener.await;
}
}
#[cfg(unix)]
async fn graceful_shutdown(self) {
let listener = self.drop_event.listen();
drop(self);
listener.await;
}
}
impl Drop for Connection {
fn drop(&mut self) {
self.drop_event.notify(usize::MAX);
}
}
fn build_connection(
reader: BoxedReader,
writer: BoxedWriter,
runtime: &SharedRuntime,
handler: Option<Handler>,
) -> Connection {
let write = Arc::new(lock::Mutex::new(writer));
let pending = Arc::new(PendingCalls::default());
#[cfg(unix)]
let signals = Arc::new(Queue::unbounded());
let closed = Arc::new(AtomicBool::new(false));
let closed_event = Arc::new(Event::new());
let calls = handler.map(|_| Arc::new(Queue::unbounded()));
let reader_task = runtime.spawn(
"bench socket reader",
run_reader(
reader,
pending.clone(),
#[cfg(unix)]
signals.clone(),
calls.clone(),
closed.clone(),
closed_event.clone(),
),
);
let object_server_task = match (calls, handler) {
(Some(calls), Some(handler)) => Some(runtime.spawn(
"bench object server",
run_object_server(calls, write.clone(), runtime.clone(), handler),
)),
_ => None,
};
Connection {
write,
pending,
#[cfg(unix)]
signals,
next_serial: AtomicU32::new(1),
closed,
closed_event,
runtime: runtime.clone(),
_reader_task: reader_task,
_object_server_task: object_server_task,
drop_event: Arc::new(Event::new()),
}
}
async fn run_reader(
reader: BoxedReader,
pending: Arc<PendingCalls>,
#[cfg(unix)] signals: Arc<Queue<Frame>>,
calls: Option<Arc<Queue<Frame>>>,
closed: Arc<AtomicBool>,
closed_event: Arc<Event>,
) {
while let Some(frame) = reader.read_frame().await {
match frame.kind {
FrameKind::Reply => pending.complete(frame.reply_serial, frame),
#[cfg(unix)]
FrameKind::Signal => signals.push(frame).await,
FrameKind::Call => {
if let Some(calls) = &calls {
calls.push(frame).await;
}
}
}
}
closed.store(true, Ordering::Release);
closed_event.notify(usize::MAX);
pending.fail_all();
#[cfg(unix)]
signals.close();
if let Some(calls) = &calls {
calls.close();
}
}
async fn run_object_server(
calls: Arc<Queue<Frame>>,
write: Arc<lock::Mutex<BoxedWriter>>,
runtime: SharedRuntime,
handler: Handler,
) {
while let Some(call) = calls.pop().await {
let write = write.clone();
runtime
.spawn("bench call dispatcher", async move {
let reply_body = handler(&call.body);
let reply = Frame {
kind: FrameKind::Reply,
serial: 0,
reply_serial: call.serial,
body: reply_body,
};
let _ = write.lock().await.write_frame(&reply).await;
})
.detach();
}
}
#[cfg(unix)]
mod unix {
use std::{
hint::black_box,
io::{Read, Write},
os::unix::net::UnixStream,
sync::Arc,
time::{Duration, Instant},
};
use criterion::{Criterion, Throughput};
use futures::future;
use zruntime::{Interest, Registration, Shared, SharedRuntime};
use super::{
BoxedReader, BoxedWriter, Connection, Frame, FrameKind, ZruntimeExecutor, build_connection,
dispatch, echo, ping, run_burst_bench,
};
const BIG: usize = 1024 * 1024;
fn runtime_handle() -> SharedRuntime {
zruntime::block_on(async { SharedRuntime::current().expect("a runtime for this thread") })
}
const HEADER_LEN: usize = 1 + 4 + 4 + 4;
impl FrameKind {
fn to_byte(self) -> u8 {
match self {
FrameKind::Call => 0,
FrameKind::Reply => 1,
FrameKind::Signal => 2,
}
}
fn from_byte(byte: u8) -> Self {
match byte {
0 => FrameKind::Call,
1 => FrameKind::Reply,
2 => FrameKind::Signal,
_ => panic!("not a frame kind: {byte}"),
}
}
}
impl Frame {
fn encode(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(HEADER_LEN + self.body.len());
buf.push(self.kind.to_byte());
buf.extend_from_slice(&self.serial.to_le_bytes());
buf.extend_from_slice(&self.reply_serial.to_le_bytes());
buf.extend_from_slice(&(self.body.len() as u32).to_le_bytes());
buf.extend_from_slice(&self.body);
buf
}
}
fn decode_header(header: &[u8; HEADER_LEN]) -> (FrameKind, u32, u32, usize) {
let kind = FrameKind::from_byte(header[0]);
let serial = u32::from_le_bytes(header[1..5].try_into().unwrap());
let reply_serial = u32::from_le_bytes(header[5..9].try_into().unwrap());
let body_len = u32::from_le_bytes(header[9..13].try_into().unwrap()) as usize;
(kind, serial, reply_serial, body_len)
}
pub(super) fn connection_benches(c: &mut Criterion) {
let runtime = runtime_handle();
let mut group = c.benchmark_group("connection");
group.sample_size(20);
group.bench_function("build-and-drop", |b| {
let runtime = &runtime;
b.to_async(ZruntimeExecutor)
.iter(|| async { drop(black_box(pair(runtime).await)) });
});
group.bench_function("graceful-shutdown", |b| {
b.iter_custom(|iters| {
zruntime::block_on(async {
let mut total = Duration::ZERO;
for _ in 0..iters {
let (server, client) = pair(&runtime).await;
let started = Instant::now();
future::join(server.graceful_shutdown(), client.graceful_shutdown()).await;
total += started.elapsed();
}
total
})
});
});
group.finish();
}
pub(super) fn method_call_benches(c: &mut Criterion) {
let runtime = runtime_handle();
let mut group = c.benchmark_group("method-call");
{
let (_server, client) = zruntime::block_on(pair(&runtime));
group.bench_function("roundtrip", |b| {
b.to_async(ZruntimeExecutor)
.iter(|| async { black_box(ping(&client, 1).await.unwrap()) });
});
}
group.sample_size(10);
group.throughput(Throughput::Bytes(BIG as u64));
{
let (_server, client) = zruntime::block_on(pair(&runtime));
let body = vec![7u8; BIG];
group.bench_function("1MiB-body", |b| {
b.to_async(ZruntimeExecutor)
.iter(|| async { black_box(echo(&client, &body).await.unwrap()) });
});
}
group.finish();
}
pub(super) fn signal_benches(c: &mut Criterion) {
let runtime = runtime_handle();
let mut group = c.benchmark_group("signal");
let (server, client) = zruntime::block_on(pair(&runtime));
group.bench_function("emit-receive", |b| {
b.to_async(ZruntimeExecutor).iter(|| async {
server.emit_signal(Vec::new()).await;
black_box(client.next_signal().await);
});
});
group.finish();
}
pub(super) fn concurrent_socket_bench(c: &mut Criterion) {
let mut group = c.benchmark_group("method-call");
group.sample_size(10);
group.throughput(Throughput::Elements(super::CONCURRENT_METHOD_CALLS as u64));
let (server_stream, client_stream) = UnixStream::pair().unwrap();
run_burst_bench(
&mut group,
"1000-concurrent-p2p-socket",
move |runtime: &SharedRuntime| {
let end = Arc::new(UnixEnd::register(runtime, server_stream));
Box::pin(async move {
server_handshake(&end).await;
let reader: BoxedReader = Box::new(end.clone());
let writer: BoxedWriter = Box::new(end);
(reader, writer)
})
},
move |runtime: &SharedRuntime| {
let end = Arc::new(UnixEnd::register(runtime, client_stream));
Box::pin(async move {
client_handshake(&end).await;
let reader: BoxedReader = Box::new(end.clone());
let writer: BoxedWriter = Box::new(end);
(reader, writer)
})
},
);
group.finish();
}
async fn pair(runtime: &SharedRuntime) -> (Connection, Connection) {
let (server_stream, client_stream) = UnixStream::pair().unwrap();
let server_end = Arc::new(UnixEnd::register(runtime, server_stream));
let client_end = Arc::new(UnixEnd::register(runtime, client_stream));
future::join(server_handshake(&server_end), client_handshake(&client_end)).await;
let server = build_connection(
Box::new(server_end.clone()),
Box::new(server_end),
runtime,
Some(dispatch),
);
let client = build_connection(
Box::new(client_end.clone()),
Box::new(client_end),
runtime,
None,
);
(server, client)
}
struct UnixEnd {
registration: Registration<Shared>,
stream: UnixStream,
}
impl UnixEnd {
fn register(runtime: &SharedRuntime, stream: UnixStream) -> Self {
stream.set_nonblocking(true).expect("nonblocking");
let registration = runtime
.register(
stream
.try_clone()
.expect("clone the fd for the registration"),
)
.expect("register the socket");
Self {
registration,
stream,
}
}
async fn read_exact(&self, mut buf: &mut [u8]) -> Option<()> {
while !buf.is_empty() {
let read = std::future::poll_fn(|cx| {
self.registration
.poll_io(cx, Interest::Readable, || (&self.stream).read(buf))
})
.await
.expect("read from the bench socket");
if read == 0 {
return None;
}
buf = &mut buf[read..];
}
Some(())
}
async fn write_all(&self, mut buf: &[u8]) {
while !buf.is_empty() {
let written = std::future::poll_fn(|cx| {
self.registration
.poll_io(cx, Interest::Writable, || (&self.stream).write(buf))
})
.await
.expect("write to the bench socket");
buf = &buf[written..];
}
}
}
impl super::FrameReader for Arc<UnixEnd> {
fn read_frame(
&self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<Frame>> + Send + '_>>
{
Box::pin(async move {
let mut header = [0u8; HEADER_LEN];
self.read_exact(&mut header).await?;
let (kind, serial, reply_serial, body_len) = decode_header(&header);
let mut body = vec![0u8; body_len];
self.read_exact(&mut body).await?;
Some(Frame {
kind,
serial,
reply_serial,
body,
})
})
}
}
impl super::FrameWriter for Arc<UnixEnd> {
fn write_frame<'a>(
&'a self,
frame: &'a Frame,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Send + 'a>>
{
Box::pin(async move {
self.write_all(&frame.encode()).await;
Ok(())
})
}
}
const HANDSHAKE_LINE: &[u8] = b"AUTH EXTERNAL 0\r\n";
const HANDSHAKE_ROUND_TRIPS: usize = 2;
async fn client_handshake(end: &UnixEnd) {
for _ in 0..HANDSHAKE_ROUND_TRIPS {
end.write_all(HANDSHAKE_LINE).await;
let mut reply = [0u8; HANDSHAKE_LINE.len()];
end.read_exact(&mut reply)
.await
.expect("the server closed the connection during the handshake");
}
}
async fn server_handshake(end: &UnixEnd) {
for _ in 0..HANDSHAKE_ROUND_TRIPS {
let mut request = [0u8; HANDSHAKE_LINE.len()];
end.read_exact(&mut request)
.await
.expect("the client closed the connection during the handshake");
end.write_all(HANDSHAKE_LINE).await;
}
}
}
#[cfg(unix)]
criterion_group!(
benches,
unix::connection_benches,
unix::method_call_benches,
unix::signal_benches,
concurrent_channel_bench,
unix::concurrent_socket_bench,
);
#[cfg(not(unix))]
criterion_group!(benches, concurrent_channel_bench);
criterion_main!(benches);