use bytes::Bytes;
use monocoque::rt::{LocalRuntime, TcpListener, TcpStream};
use monocoque::zmq::{DealerSocket, PullSocket, PushSocket, SocketOptions};
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
use std::thread;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static COUNTING: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if COUNTING.load(Ordering::Relaxed) != 0 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
}
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if COUNTING.load(Ordering::Relaxed) != 0 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
}
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
const ADDR: &str = "127.0.0.1:0";
const PAYLOAD: usize = 64;
const WARM: usize = 4_000;
const MEAS: usize = 200_000;
const MAX_WINDOW_ALLOCS: usize = MEAS / 50;
#[test]
fn single_frame_hot_path_makes_no_per_message_allocation() {
let (port_tx, port_rx) = mpsc::channel::<u16>();
let sender = thread::spawn(move || {
let rt = LocalRuntime::new().unwrap();
rt.block_on(async move {
let port = port_rx.recv().unwrap();
let mut push = PushSocket::connect_with_options(
("127.0.0.1", port),
SocketOptions::default()
.with_buffer_sizes(16384, 16384)
.with_write_coalescing(true),
)
.await
.unwrap();
let payload = Bytes::from(vec![0u8; PAYLOAD]);
for _ in 0..(WARM + MEAS) {
push.send_one(payload.clone()).await.unwrap();
}
push.flush().await.unwrap();
});
});
let rt = LocalRuntime::new().unwrap();
rt.block_on(async move {
let listener = TcpListener::bind(ADDR).await.unwrap();
port_tx.send(listener.local_addr().unwrap().port()).unwrap();
let (stream, _) = listener.accept().await.unwrap();
let mut pull = PullSocket::from_tcp_with_options(
stream,
SocketOptions::default().with_buffer_sizes(16384, 16384),
)
.await
.unwrap();
let mut buf: Vec<Bytes> = Vec::with_capacity(4);
let mut warmed = 0usize;
while warmed < WARM {
if !pull.recv_into(&mut buf).await.unwrap() {
break;
}
warmed += 1;
while warmed < WARM && pull.try_recv_into(&mut buf).unwrap() {
warmed += 1;
}
}
COUNTING.store(1, Ordering::Relaxed);
let mut got = 0usize;
while got < MEAS {
if !pull.recv_into(&mut buf).await.unwrap() {
break;
}
got += 1;
while got < MEAS && pull.try_recv_into(&mut buf).unwrap() {
got += 1;
}
}
COUNTING.store(0, Ordering::Relaxed);
let allocs = ALLOCS.load(Ordering::Relaxed);
assert_eq!(got, MEAS, "received {got} of {MEAS} messages");
assert!(
allocs <= MAX_WINDOW_ALLOCS,
"single-frame hot path allocated {allocs} times over {MEAS} messages \
(ceiling {MAX_WINDOW_ALLOCS}). That is roughly one allocation per \
{} messages; a per-message allocation on the send or receive path \
would show ~{MEAS}. Something reintroduced per-message allocation.",
MEAS / allocs.max(1),
);
});
sender.join().unwrap();
}
#[test]
fn dealer_recv_into_avoids_the_per_message_vec_that_recv_allocates() {
const N: usize = 256; let rt = LocalRuntime::new().unwrap();
async fn dealer_pair() -> (DealerSocket<TcpStream>, DealerSocket<TcpStream>) {
let listener = TcpListener::bind(ADDR).await.unwrap();
let addr = listener.local_addr().unwrap();
let endpoint = format!("tcp://{addr}");
let accept = async {
let (stream, _) = listener.accept().await.unwrap();
DealerSocket::from_tcp(stream).await.unwrap()
};
let connect = DealerSocket::connect(&endpoint);
let (recv_side, send_side) = futures::future::join(accept, connect).await;
(send_side.unwrap(), recv_side)
}
let (recv_allocs, recv_into_allocs) = rt.block_on(async {
let payload = Bytes::from(vec![0u8; PAYLOAD]);
let (mut tx, mut rx) = dealer_pair().await;
for _ in 0..N {
tx.send(vec![payload.clone()]).await.unwrap();
}
ALLOCS.store(0, Ordering::Relaxed);
COUNTING.store(1, Ordering::Relaxed);
for _ in 0..N {
rx.recv().await.unwrap().unwrap();
}
COUNTING.store(0, Ordering::Relaxed);
let recv_allocs = ALLOCS.load(Ordering::Relaxed);
let (mut tx, mut rx) = dealer_pair().await;
for _ in 0..N {
tx.send(vec![payload.clone()]).await.unwrap();
}
let mut buf: Vec<Bytes> = Vec::with_capacity(4);
rx.recv_into(&mut buf).await.unwrap();
ALLOCS.store(0, Ordering::Relaxed);
COUNTING.store(1, Ordering::Relaxed);
for _ in 1..N {
rx.recv_into(&mut buf).await.unwrap();
}
COUNTING.store(0, Ordering::Relaxed);
let recv_into_allocs = ALLOCS.load(Ordering::Relaxed);
(recv_allocs, recv_into_allocs)
});
assert!(
recv_allocs >= N,
"recv() over {N} messages allocated only {recv_allocs}; expected >= {N} \
(one Vec per message)"
);
assert!(
recv_into_allocs * 8 < recv_allocs,
"recv_into() allocated {recv_into_allocs} over {N} messages vs recv()'s \
{recv_allocs}; expected recv_into to be far below (buffer reused)"
);
}