Skip to main content

concurrency_benchmark/
concurrency_benchmark.rs

1//! Concurrent ordered echo benchmark. Run in an isolated network namespace.
2use rust_raknet::{RaknetSocket, Reliability};
3use std::{
4    error::Error,
5    sync::Arc,
6    time::{Duration, Instant},
7};
8type BenchError = Box<dyn Error + Send + Sync>;
9
10use tokio::{
11    io::{AsyncReadExt, AsyncWriteExt},
12    net::{TcpListener, TcpStream},
13    sync::{Barrier, Semaphore},
14    task::JoinSet,
15};
16
17fn payload(connection: usize, message: usize, size: usize) -> Vec<u8> {
18    let mut data = vec![0xfe; size];
19    data[1..9].copy_from_slice(&(connection as u64).to_le_bytes());
20    data[9..17].copy_from_slice(&(message as u64).to_le_bytes());
21    data
22}
23fn percentile(values: &[u128], percentile: usize) -> f64 {
24    values[(values.len() * percentile).div_ceil(100) - 1] as f64 / 1000.0
25}
26#[tokio::main(worker_threads = 4)]
27async fn main() -> Result<(), BenchError> {
28    let mut args: Vec<_> = std::env::args().collect();
29    if args.get(1).is_some_and(|arg| arg == "--tcp-server") {
30        if args.len() != 3 {
31            return Err("usage: concurrency_benchmark --tcp-server ADDRESS".into());
32        }
33        return tcp_server(args[2].parse()?).await;
34    }
35    let tcp = args.get(1).is_some_and(|arg| arg == "--tcp");
36    if tcp {
37        args.remove(1);
38    }
39    if args.len() != 5 {
40        return Err(
41            "usage: concurrency_benchmark [--tcp] ADDRESS CONNECTIONS MESSAGES PAYLOAD".into(),
42        );
43    }
44    let address = args[1].parse()?;
45    let count: usize = args[2].parse()?;
46    let messages: usize = args[3].parse()?;
47    let size: usize = args[4].parse()?;
48    if count == 0 || messages == 0 || size < 17 {
49        return Err("invalid counts".into());
50    }
51    tokio::time::timeout(
52        Duration::from_secs(180),
53        run(address, count, messages, size, tcp),
54    )
55    .await??;
56    Ok(())
57}
58
59async fn run(
60    address: std::net::SocketAddr,
61    count: usize,
62    messages: usize,
63    size: usize,
64    tcp: bool,
65) -> Result<(), BenchError> {
66    let setup = Instant::now();
67    let dialing = Arc::new(Semaphore::new(8));
68    let mut tasks = JoinSet::new();
69    for id in 0..count {
70        let dialing = dialing.clone();
71        tasks.spawn(async move {
72            let _permit = dialing.acquire().await.unwrap();
73            let socket = EchoConnection::connect(address, tcp).await?;
74            Ok::<_, BenchError>((id, socket))
75        });
76    }
77    let mut sockets = Vec::with_capacity(count);
78    while let Some(result) = tasks.join_next().await {
79        sockets.push(result??);
80    }
81    let setup_seconds = setup.elapsed().as_secs_f64();
82    let barrier = Arc::new(Barrier::new(count + 1));
83    let start_gate = Arc::new(Barrier::new(count + 1));
84    let mut exchanges = JoinSet::new();
85    for (id, mut socket) in sockets {
86        let barrier = barrier.clone();
87        let start_gate = start_gate.clone();
88        exchanges.spawn(async move {
89            let mut rtts = Vec::with_capacity(20);
90            let mut expected = payload(id, 0, size);
91            let mut outgoing = expected.clone();
92            let mut actual = Vec::with_capacity(size);
93            for message in 0..20 {
94                expected[9..17].copy_from_slice(&(message as u64).to_le_bytes());
95                let start = Instant::now();
96                socket.send(&expected).await?;
97                socket.recv(size, &mut actual).await?;
98                if actual != expected {
99                    return Err::<_, BenchError>("echo payload or order mismatch".into());
100                }
101                rtts.push(start.elapsed().as_nanos());
102            }
103            barrier.wait().await;
104            start_gate.wait().await;
105            // Match the C reference's sliding application window. Refill a slot
106            // as each echo arrives instead of waiting for a whole batch.
107            let mut sent = 0;
108            while sent < messages.min(16) {
109                outgoing[9..17].copy_from_slice(&((sent + 20) as u64).to_le_bytes());
110                socket.send(&outgoing).await?;
111                sent += 1;
112            }
113            for received in 0..messages {
114                expected[9..17].copy_from_slice(&((received + 20) as u64).to_le_bytes());
115                socket.recv(size, &mut actual).await?;
116                if actual != expected {
117                    return Err::<_, BenchError>("echo payload or order mismatch".into());
118                }
119                if sent < messages {
120                    outgoing[9..17].copy_from_slice(&((sent + 20) as u64).to_le_bytes());
121                    socket.send(&outgoing).await?;
122                    sent += 1;
123                }
124            }
125            // Keep every connection open until all peers finish the measured burst.
126            Ok::<_, BenchError>((socket, rtts))
127        });
128    }
129    barrier.wait().await;
130    let started = Instant::now();
131    start_gate.wait().await;
132    let mut retained = Vec::with_capacity(count);
133    let mut rtts = Vec::with_capacity(count * 20);
134    while let Some(result) = exchanges.join_next().await {
135        let (socket, values) = result??;
136        retained.push(socket);
137        rtts.extend(values);
138    }
139    let elapsed = started.elapsed().as_secs_f64();
140    rtts.sort_unstable();
141    println!(
142        "Connections: {count}\nMessages per connection: {messages}\nPayload size: {size} bytes\nSetup: {setup_seconds:.6} s\nElapsed: {elapsed:.6} s"
143    );
144    println!(
145        "Echo payload throughput (per direction): {:.2} MiB/s",
146        count as f64 * messages as f64 * size as f64 / 1048576.0 / elapsed
147    );
148    println!(
149        "RTT p50: {:.1} us\nRTT p95: {:.1} us\nRTT p99: {:.1} us",
150        percentile(&rtts, 50),
151        percentile(&rtts, 95),
152        percentile(&rtts, 99)
153    );
154    println!("Verified ordered echoes: {}", count * (messages + 20));
155    drop(retained);
156    Ok(())
157}
158
159// TCP records use a four-byte length prefix and one complete-record write.
160// Both buffers are retained across messages, just like the payload templates.
161enum EchoConnection {
162    RakNet(RaknetSocket),
163    Tcp { stream: TcpStream, record: Vec<u8> },
164}
165
166impl EchoConnection {
167    async fn connect(address: std::net::SocketAddr, tcp: bool) -> Result<Self, BenchError> {
168        if tcp {
169            let stream = TcpStream::connect(address).await?;
170            stream.set_nodelay(true)?;
171            Ok(Self::Tcp {
172                stream,
173                record: Vec::new(),
174            })
175        } else {
176            Ok(Self::RakNet(
177                RaknetSocket::connect_with_version(&address, 11).await?,
178            ))
179        }
180    }
181
182    async fn send(&mut self, payload: &[u8]) -> Result<(), BenchError> {
183        match self {
184            Self::RakNet(socket) => socket.send(payload, Reliability::ReliableOrdered).await?,
185            Self::Tcp { stream, record, .. } => {
186                let length = u32::try_from(payload.len())?;
187                record.clear();
188                record.extend_from_slice(&length.to_le_bytes());
189                record.extend_from_slice(payload);
190                stream.write_all(record).await?;
191            }
192        }
193        Ok(())
194    }
195
196    async fn recv(&mut self, size: usize, incoming: &mut Vec<u8>) -> Result<(), BenchError> {
197        match self {
198            Self::RakNet(socket) => *incoming = socket.recv().await?,
199            Self::Tcp { stream, .. } => {
200                let length = stream.read_u32_le().await? as usize;
201                if length != size {
202                    return Err("unexpected TCP record length".into());
203                }
204                incoming.resize(length, 0);
205                stream.read_exact(incoming).await?;
206            }
207        }
208        Ok(())
209    }
210}
211
212async fn echo_tcp(mut stream: TcpStream) -> Result<(), BenchError> {
213    stream.set_nodelay(true)?;
214    let mut record = Vec::new();
215    loop {
216        let mut header = [0; 4];
217        match stream.read_exact(&mut header).await {
218            Ok(_) => {}
219            Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(()),
220            Err(error) => return Err(error.into()),
221        }
222        let length = u32::from_le_bytes(header) as usize;
223        if length > 65536 {
224            return Err("TCP record exceeds benchmark limit".into());
225        }
226        record.resize(length + 4, 0);
227        record[..4].copy_from_slice(&header);
228        stream.read_exact(&mut record[4..]).await?;
229        stream.write_all(&record).await?;
230    }
231}
232
233#[cfg(target_os = "linux")]
234async fn tcp_server(address: std::net::SocketAddr) -> Result<(), BenchError> {
235    use socket2::{Domain, Protocol, Socket, Type};
236    let mut listeners = Vec::with_capacity(4);
237    for _ in 0..4 {
238        let socket = Socket::new(
239            Domain::for_address(address),
240            Type::STREAM,
241            Some(Protocol::TCP),
242        )?;
243        socket.set_reuse_address(true)?;
244        socket.set_reuse_port(true)?;
245        socket.set_nonblocking(true)?;
246        socket.bind(&address.into())?;
247        socket.listen(4096)?;
248        listeners.push(TcpListener::from_std(socket.into())?);
249    }
250    println!("TCP echo listening on {address} with four receive sockets");
251    let mut tasks = JoinSet::new();
252    for listener in listeners {
253        tasks.spawn(async move {
254            loop {
255                let (stream, _) = listener.accept().await?;
256                tokio::spawn(async move {
257                    if let Err(error) = echo_tcp(stream).await {
258                        eprintln!("TCP echo failed: {error}");
259                    }
260                });
261            }
262            #[allow(unreachable_code)]
263            Ok::<(), BenchError>(())
264        });
265    }
266    while let Some(result) = tasks.join_next().await {
267        result??;
268    }
269    Ok(())
270}
271
272#[cfg(not(target_os = "linux"))]
273async fn tcp_server(_address: std::net::SocketAddr) -> Result<(), BenchError> {
274    Err("The four-socket TCP benchmark server requires Linux".into())
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[tokio::test]
282    async fn tcp_records_preserve_payload_order_and_reuse_buffers() -> Result<(), BenchError> {
283        let listener = TcpListener::bind("127.0.0.1:0").await?;
284        let address = listener.local_addr()?;
285        let server = tokio::spawn(async move {
286            let (stream, _) = listener.accept().await?;
287            echo_tcp(stream).await
288        });
289        let mut connection = EchoConnection::connect(address, true).await?;
290        let mut received = Vec::new();
291        for size in [17, 64, 800, 4096, 64] {
292            for message in 0..32 {
293                let expected = payload(3, message, size);
294                connection.send(&expected).await?;
295                connection.recv(size, &mut received).await?;
296                assert_eq!(received, expected);
297            }
298        }
299        let capacity = received.capacity();
300        connection.send(&payload(3, 40, 64)).await?;
301        connection.recv(64, &mut received).await?;
302        assert_eq!(received.capacity(), capacity);
303        drop(connection);
304        server.await??;
305        Ok(())
306    }
307}