Skip to content

Commit d8b45ee

Browse files
authored
Add end to end "send+recv" benchmarks (#497)
* Add end to end "send+recv" benchmarks * Add e2e required-features to manifest * Add byte throughput measurements * Reduce down to 8x bench variants * Use 127.0.0.1 for client & server ip * Use None max message/frame config
1 parent f20436c commit d8b45ee

4 files changed

Lines changed: 110 additions & 4 deletions

File tree

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ optional = true
6363
version = "0.26"
6464

6565
[dev-dependencies]
66-
criterion = "0.5.0"
66+
criterion = "0.6"
6767
env_logger = "0.11"
6868
input_buffer = "0.5.0"
6969
rand = "0.9.0"
@@ -84,6 +84,11 @@ harness = false
8484
name = "read"
8585
harness = false
8686

87+
[[bench]]
88+
name = "e2e"
89+
harness = false
90+
required-features = ["handshake"]
91+
8792
[[example]]
8893
name = "client"
8994
required-features = ["handshake"]

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ Testing
8080
Tungstenite is thoroughly tested and passes the [Autobahn Test Suite](https://github.com/crossbario/autobahn-testsuite) for
8181
WebSockets. It is also covered by internal unit tests as well as possible.
8282

83+
Benchmark
84+
---------
85+
Benches are in [./benches](./benches/).
86+
* Run all with `cargo bench --bench \* -- --quick --noplot`
87+
* Run a particular set with, say "e2e", with `cargo bench --bench e2e -- --quick --noplot`
88+
8389
Contributing
8490
------------
8591

benches/buffer.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
use std::io::{Cursor, Read, Result as IoResult};
1+
#![allow(clippy::incompatible_msrv)] // msrv doesn't apply to benches
22

33
use bytes::Buf;
4-
use criterion::*;
4+
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
55
use input_buffer::InputBuffer;
6-
6+
use std::{
7+
hint::black_box,
8+
io::{Cursor, Read, Result as IoResult},
9+
};
710
use tungstenite::buffer::ReadBuffer;
811

912
const CHUNK_SIZE: usize = 4096;

benches/e2e.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//! Benchmarks for end to end performance including real `Read` & `Write` impls.
2+
use bytes::Bytes;
3+
use criterion::{BatchSize, Criterion, Throughput};
4+
use rand::{
5+
distr::{Alphanumeric, SampleString},
6+
rngs::SmallRng,
7+
SeedableRng,
8+
};
9+
use std::net::TcpListener;
10+
use tungstenite::{accept_hdr_with_config, protocol::WebSocketConfig, Message};
11+
12+
/// Binary message meaning "stop".
13+
const B_STOP: Bytes = Bytes::from_static(b"stop");
14+
15+
fn benchmark(c: &mut Criterion) {
16+
/// Benchmark that starts a simple server and client then sends (writes+flush) a
17+
/// single text message client->server and reads a single response text message
18+
/// server->client. Both message will be of the given `msg_len` size.
19+
fn send_and_recv(msg_len: usize, b: &mut criterion::Bencher<'_>) {
20+
let socket = TcpListener::bind("127.0.0.1:0").unwrap();
21+
let port = socket.local_addr().unwrap().port();
22+
let conf = WebSocketConfig::default().max_message_size(None).max_frame_size(None);
23+
24+
let server_thread = std::thread::spawn(move || {
25+
// single thread / single client server
26+
let (stream, _) = socket.accept().unwrap();
27+
let mut websocket =
28+
accept_hdr_with_config(stream, |_: &_, res| Ok(res), Some(conf)).unwrap();
29+
loop {
30+
let uppercase_txt = match websocket.read().unwrap() {
31+
Message::Text(msg) => msg.to_ascii_uppercase(),
32+
Message::Binary(msg) if msg == B_STOP => return,
33+
msg => panic!("Unexpected msg: {msg:?}"),
34+
};
35+
websocket.send(Message::text(uppercase_txt)).unwrap();
36+
}
37+
});
38+
39+
let (mut client, _) = tungstenite::client::connect_with_config(
40+
format!("ws://127.0.0.1:{port}"),
41+
Some(conf),
42+
3,
43+
)
44+
.unwrap();
45+
let mut rng = SmallRng::seed_from_u64(123);
46+
47+
b.iter_batched(
48+
|| {
49+
let msg = Alphanumeric.sample_string(&mut rng, msg_len);
50+
let expected_response = msg.to_ascii_uppercase();
51+
(msg, expected_response)
52+
},
53+
|(txt, expected_response)| {
54+
client.send(Message::text(txt)).unwrap();
55+
let response = client.read().unwrap();
56+
match response {
57+
Message::Text(v) => assert_eq!(v, expected_response),
58+
msg => panic!("Unexpected response msg: {msg:?}"),
59+
};
60+
},
61+
BatchSize::PerIteration,
62+
);
63+
64+
// cleanup
65+
client.send(Message::binary(B_STOP)).unwrap();
66+
server_thread.join().unwrap();
67+
}
68+
69+
// bench sending & receiving various sizes 512B to 1GiB.
70+
for len in (0..8).map(|n| 512 * 8_usize.pow(n)) {
71+
let mut group = c.benchmark_group("send+recv");
72+
group
73+
.throughput(Throughput::Bytes(len as u64 * 2)) // *2 as we send and then recv it
74+
.bench_function(HumanLen(len).to_string(), |b| send_and_recv(len, b));
75+
}
76+
}
77+
78+
struct HumanLen(usize);
79+
80+
impl std::fmt::Display for HumanLen {
81+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82+
match self.0 {
83+
n if n < 1024 => write!(f, "{n} B"),
84+
n if n < 1024 * 1024 => write!(f, "{} KiB", n / 1024),
85+
n if n < 1024 * 1024 * 1024 => write!(f, "{} MiB", n / (1024 * 1024)),
86+
n => write!(f, "{} GiB", n / (1024 * 1024 * 1024)),
87+
}
88+
}
89+
}
90+
91+
criterion::criterion_group!(read_benches, benchmark);
92+
criterion::criterion_main!(read_benches);

0 commit comments

Comments
 (0)