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 loaded_rtt = args.last().is_some_and(|arg| arg == "--loaded-rtt");
36    if loaded_rtt {
37        args.pop();
38    }
39    let tcp = args.get(1).is_some_and(|arg| arg == "--tcp");
40    if tcp {
41        args.remove(1);
42    }
43    let batch = args.get(1).is_some_and(|arg| arg == "--batch");
44    if batch {
45        args.remove(1);
46    }
47    if batch && tcp {
48        return Err("--batch is only supported for RakNet".into());
49    }
50    if args.len() != 5 {
51        return Err(
52            "usage: concurrency_benchmark [--tcp | --batch] ADDRESS CONNECTIONS MESSAGES PAYLOAD [--loaded-rtt]"
53                .into(),
54        );
55    }
56    let address = args[1].parse()?;
57    let count: usize = args[2].parse()?;
58    let messages: usize = args[3].parse()?;
59    let size: usize = args[4].parse()?;
60    if count == 0 || messages == 0 || size < 17 {
61        return Err("invalid counts".into());
62    }
63    if loaded_rtt {
64        tokio::time::timeout(
65            Duration::from_secs(180),
66            run::<true>(address, count, messages, size, tcp, batch),
67        )
68        .await??;
69    } else {
70        tokio::time::timeout(
71            Duration::from_secs(180),
72            run::<false>(address, count, messages, size, tcp, batch),
73        )
74        .await??;
75    }
76    Ok(())
77}
78
79async fn run<const LOADED: bool>(
80    address: std::net::SocketAddr,
81    count: usize,
82    messages: usize,
83    size: usize,
84    tcp: bool,
85    batch: bool,
86) -> Result<(), BenchError> {
87    println!(
88        "Sending API: {}",
89        if tcp {
90            "TCP complete-record writes"
91        } else if batch {
92            "send_bytes_batch (ready messages only)"
93        } else {
94            "send (no batch API)"
95        }
96    );
97    let setup = Instant::now();
98    let dialing = Arc::new(Semaphore::new(8));
99    let mut tasks = JoinSet::new();
100    for id in 0..count {
101        let dialing = dialing.clone();
102        tasks.spawn(async move {
103            let _permit = dialing.acquire().await.unwrap();
104            let socket = EchoConnection::connect(address, tcp).await?;
105            Ok::<_, BenchError>((id, socket))
106        });
107    }
108    let mut sockets = Vec::with_capacity(count);
109    while let Some(result) = tasks.join_next().await {
110        sockets.push(result??);
111    }
112    let setup_seconds = setup.elapsed().as_secs_f64();
113    let barrier = Arc::new(Barrier::new(count + 1));
114    let start_gate = Arc::new(Barrier::new(count + 1));
115    let mut exchanges = JoinSet::new();
116    for (id, mut socket) in sockets {
117        let barrier = barrier.clone();
118        let start_gate = start_gate.clone();
119        exchanges.spawn(async move {
120            let mut rtts = Vec::with_capacity(if LOADED { messages.max(20) } else { 20 });
121            let mut stamps = if LOADED {
122                vec![Instant::now(); 16]
123            } else {
124                Vec::new()
125            };
126            let mut expected = payload(id, 0, size);
127            let mut outgoing = expected.clone();
128            let mut actual = Vec::with_capacity(size);
129            for message in 0..20 {
130                expected[9..17].copy_from_slice(&(message as u64).to_le_bytes());
131                let start = Instant::now();
132                socket.send(&expected).await?;
133                socket.recv(size, &mut actual).await?;
134                if actual != expected {
135                    return Err::<_, BenchError>("echo payload or order mismatch".into());
136                }
137                rtts.push(start.elapsed().as_nanos());
138            }
139            if LOADED {
140                rtts.clear();
141            }
142            barrier.wait().await;
143            start_gate.wait().await;
144            // Match the C reference's sliding application window. Refill a slot
145            // as each echo arrives instead of waiting for a whole batch.
146            if batch {
147                let EchoConnection::RakNet(ref connection) = socket else {
148                    unreachable!()
149                };
150                let mut sent = 0;
151                let mut received = 0;
152                let mut incoming = Vec::with_capacity(16);
153                let mut pending = Vec::with_capacity(16);
154                for message in 0..messages.min(16) {
155                    pending.push(rust_raknet::Bytes::from(payload(id, message + 20, size)));
156                    if LOADED {
157                        stamps[message % 16] = Instant::now();
158                    }
159                    sent += 1;
160                }
161                connection
162                    .send_bytes_batch(&pending, Reliability::ReliableOrdered)
163                    .await?;
164                while received < messages {
165                    connection.recv_bytes_batch(&mut incoming, 16).await?;
166                    for actual in &incoming {
167                        expected[9..17].copy_from_slice(&((received + 20) as u64).to_le_bytes());
168                        if actual.as_ref() != expected {
169                            return Err::<_, BenchError>("echo payload or order mismatch".into());
170                        }
171                        if LOADED {
172                            rtts.push(stamps[received % 16].elapsed().as_nanos());
173                        }
174                        received += 1;
175                    }
176                    pending.clear();
177                    for _ in 0..incoming.len().min(messages - sent) {
178                        pending.push(rust_raknet::Bytes::from(payload(id, sent + 20, size)));
179                        if LOADED {
180                            stamps[sent % 16] = Instant::now();
181                        }
182                        sent += 1;
183                    }
184                    if !pending.is_empty() {
185                        connection
186                            .send_bytes_batch(&pending, Reliability::ReliableOrdered)
187                            .await?;
188                    }
189                }
190            } else {
191                let mut sent = 0;
192                while sent < messages.min(16) {
193                    outgoing[9..17].copy_from_slice(&((sent + 20) as u64).to_le_bytes());
194                    if LOADED {
195                        stamps[sent % 16] = Instant::now();
196                    }
197                    socket.send(&outgoing).await?;
198                    sent += 1;
199                }
200                for received in 0..messages {
201                    expected[9..17].copy_from_slice(&((received + 20) as u64).to_le_bytes());
202                    socket.recv(size, &mut actual).await?;
203                    if actual != expected {
204                        return Err::<_, BenchError>("echo payload or order mismatch".into());
205                    }
206                    if LOADED {
207                        rtts.push(stamps[received % 16].elapsed().as_nanos());
208                    }
209                    if sent < messages {
210                        outgoing[9..17].copy_from_slice(&((sent + 20) as u64).to_le_bytes());
211                        if LOADED {
212                            stamps[sent % 16] = Instant::now();
213                        }
214                        socket.send(&outgoing).await?;
215                        sent += 1;
216                    }
217                }
218            }
219            // Keep every connection open until all peers finish the measured burst.
220            Ok::<_, BenchError>((socket, rtts))
221        });
222    }
223    barrier.wait().await;
224    let started = Instant::now();
225    start_gate.wait().await;
226    let mut retained = Vec::with_capacity(count);
227    let mut rtts = Vec::with_capacity(count * if LOADED { messages } else { 20 });
228    while let Some(result) = exchanges.join_next().await {
229        let (socket, values) = result??;
230        retained.push(socket);
231        rtts.extend(values);
232    }
233    let elapsed = started.elapsed().as_secs_f64();
234    println!("RTT samples: {}", rtts.len());
235    rtts.sort_unstable();
236    println!(
237        "Connections: {count}\nMessages per connection: {messages}\nPayload size: {size} bytes\nSetup: {setup_seconds:.6} s\nElapsed: {elapsed:.6} s"
238    );
239    println!(
240        "Echo payload throughput (per direction): {:.2} MiB/s",
241        count as f64 * messages as f64 * size as f64 / 1048576.0 / elapsed
242    );
243    println!(
244        "RTT p50: {:.1} us\nRTT p95: {:.1} us\nRTT p99: {:.1} us",
245        percentile(&rtts, 50),
246        percentile(&rtts, 95),
247        percentile(&rtts, 99)
248    );
249    println!(
250        "RTT measurement: {}",
251        if LOADED {
252            "per-message throughout measured burst"
253        } else {
254            "sequential warmup before burst"
255        }
256    );
257    println!("Verified ordered echoes: {}", count * (messages + 20));
258    drop(retained);
259    Ok(())
260}
261
262// TCP records use a four-byte length prefix and one complete-record write.
263// Both buffers are retained across messages, just like the payload templates.
264enum EchoConnection {
265    RakNet(RaknetSocket),
266    Tcp { stream: TcpStream, record: Vec<u8> },
267}
268
269impl EchoConnection {
270    async fn connect(address: std::net::SocketAddr, tcp: bool) -> Result<Self, BenchError> {
271        if tcp {
272            let stream = TcpStream::connect(address).await?;
273            stream.set_nodelay(true)?;
274            Ok(Self::Tcp {
275                stream,
276                record: Vec::new(),
277            })
278        } else {
279            Ok(Self::RakNet(
280                RaknetSocket::connect_with_version(&address, 11).await?,
281            ))
282        }
283    }
284
285    async fn send(&mut self, payload: &[u8]) -> Result<(), BenchError> {
286        match self {
287            Self::RakNet(socket) => socket.send(payload, Reliability::ReliableOrdered).await?,
288            Self::Tcp { stream, record, .. } => {
289                let length = u32::try_from(payload.len())?;
290                record.clear();
291                record.extend_from_slice(&length.to_le_bytes());
292                record.extend_from_slice(payload);
293                stream.write_all(record).await?;
294            }
295        }
296        Ok(())
297    }
298
299    async fn recv(&mut self, size: usize, incoming: &mut Vec<u8>) -> Result<(), BenchError> {
300        match self {
301            Self::RakNet(socket) => *incoming = socket.recv().await?,
302            Self::Tcp { stream, .. } => {
303                let length = stream.read_u32_le().await? as usize;
304                if length != size {
305                    return Err("unexpected TCP record length".into());
306                }
307                incoming.resize(length, 0);
308                stream.read_exact(incoming).await?;
309            }
310        }
311        Ok(())
312    }
313}
314
315async fn echo_tcp(mut stream: TcpStream) -> Result<(), BenchError> {
316    stream.set_nodelay(true)?;
317    let mut record = Vec::new();
318    loop {
319        let mut header = [0; 4];
320        match stream.read_exact(&mut header).await {
321            Ok(_) => {}
322            Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(()),
323            Err(error) => return Err(error.into()),
324        }
325        let length = u32::from_le_bytes(header) as usize;
326        if length > 65536 {
327            return Err("TCP record exceeds benchmark limit".into());
328        }
329        record.resize(length + 4, 0);
330        record[..4].copy_from_slice(&header);
331        stream.read_exact(&mut record[4..]).await?;
332        stream.write_all(&record).await?;
333    }
334}
335
336#[cfg(target_os = "linux")]
337async fn tcp_server(address: std::net::SocketAddr) -> Result<(), BenchError> {
338    use socket2::{Domain, Protocol, Socket, Type};
339    let mut listeners = Vec::with_capacity(4);
340    for _ in 0..4 {
341        let socket = Socket::new(
342            Domain::for_address(address),
343            Type::STREAM,
344            Some(Protocol::TCP),
345        )?;
346        socket.set_reuse_address(true)?;
347        socket.set_reuse_port(true)?;
348        socket.set_nonblocking(true)?;
349        socket.bind(&address.into())?;
350        socket.listen(4096)?;
351        listeners.push(TcpListener::from_std(socket.into())?);
352    }
353    println!("TCP echo listening on {address} with four receive sockets");
354    let mut tasks = JoinSet::new();
355    for listener in listeners {
356        tasks.spawn(async move {
357            loop {
358                let (stream, _) = listener.accept().await?;
359                tokio::spawn(async move {
360                    if let Err(error) = echo_tcp(stream).await {
361                        eprintln!("TCP echo failed: {error}");
362                    }
363                });
364            }
365            #[allow(unreachable_code)]
366            Ok::<(), BenchError>(())
367        });
368    }
369    while let Some(result) = tasks.join_next().await {
370        result??;
371    }
372    Ok(())
373}
374
375#[cfg(not(target_os = "linux"))]
376async fn tcp_server(_address: std::net::SocketAddr) -> Result<(), BenchError> {
377    Err("The four-socket TCP benchmark server requires Linux".into())
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[tokio::test]
385    async fn tcp_records_preserve_payload_order_and_reuse_buffers() -> Result<(), BenchError> {
386        let listener = TcpListener::bind("127.0.0.1:0").await?;
387        let address = listener.local_addr()?;
388        let server = tokio::spawn(async move {
389            let (stream, _) = listener.accept().await?;
390            echo_tcp(stream).await
391        });
392        let mut connection = EchoConnection::connect(address, true).await?;
393        let mut received = Vec::new();
394        for size in [17, 64, 800, 4096, 64] {
395            for message in 0..32 {
396                let expected = payload(3, message, size);
397                connection.send(&expected).await?;
398                connection.recv(size, &mut received).await?;
399                assert_eq!(received, expected);
400            }
401        }
402        let capacity = received.capacity();
403        connection.send(&payload(3, 40, 64)).await?;
404        connection.recv(64, &mut received).await?;
405        assert_eq!(received.capacity(), capacity);
406        drop(connection);
407        server.await??;
408        Ok(())
409    }
410}