Skip to content

Commit 3673322

Browse files
committed
tcp: add netsim test.
This test simulates a network with a given latency and packet loss, and measures the throughput between two virtual smoltcp instances.
1 parent 7a248ae commit 3673322

File tree

3 files changed

+381
-0
lines changed

3 files changed

+381
-0
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ getopts = "0.2"
3535
rand = "0.8"
3636
url = "2.0"
3737
rstest = "0.17"
38+
insta = "1.41.1"
39+
rand_chacha = "0.3.1"
3840

3941
[features]
4042
std = ["managed/std", "alloc"]

tests/netsim.rs

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
use std::cell::RefCell;
2+
use std::collections::BinaryHeap;
3+
use std::fmt::Write as _;
4+
use std::io::Write as _;
5+
use std::sync::Mutex;
6+
7+
use rand::{Rng, SeedableRng};
8+
use rand_chacha::ChaCha20Rng;
9+
use smoltcp::iface::{Config, Interface, SocketSet};
10+
use smoltcp::phy::Tracer;
11+
use smoltcp::phy::{self, ChecksumCapabilities, Device, DeviceCapabilities, Medium};
12+
use smoltcp::socket::tcp;
13+
use smoltcp::time::{Duration, Instant};
14+
use smoltcp::wire::{EthernetAddress, HardwareAddress, IpAddress, IpCidr};
15+
16+
const MAC_A: HardwareAddress = HardwareAddress::Ethernet(EthernetAddress([2, 0, 0, 0, 0, 1]));
17+
const MAC_B: HardwareAddress = HardwareAddress::Ethernet(EthernetAddress([2, 0, 0, 0, 0, 2]));
18+
const IP_A: IpAddress = IpAddress::v4(10, 0, 0, 1);
19+
const IP_B: IpAddress = IpAddress::v4(10, 0, 0, 2);
20+
21+
const BYTES: usize = 10 * 1024 * 1024;
22+
23+
static CLOCK: Mutex<(Instant, char)> = Mutex::new((Instant::ZERO, ' '));
24+
25+
#[test]
26+
fn netsim() {
27+
setup_logging();
28+
29+
let buffers = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];
30+
let losses = [0.0, 0.001, 0.01, 0.02, 0.05, 0.10, 0.20, 0.30];
31+
32+
let mut s = String::new();
33+
34+
write!(&mut s, "buf\\loss").unwrap();
35+
for loss in losses {
36+
write!(&mut s, "{loss:9.3} ").unwrap();
37+
}
38+
writeln!(&mut s).unwrap();
39+
40+
for buffer in buffers {
41+
write!(&mut s, "{buffer:7}").unwrap();
42+
for loss in losses {
43+
let r = run_test(TestCase {
44+
rtt: Duration::from_millis(100),
45+
buffer,
46+
loss,
47+
});
48+
write!(&mut s, " {r:9.2}").unwrap();
49+
}
50+
writeln!(&mut s).unwrap();
51+
}
52+
53+
insta::assert_snapshot!(s);
54+
}
55+
56+
struct TestCase {
57+
rtt: Duration,
58+
loss: f64,
59+
buffer: usize,
60+
}
61+
62+
fn run_test(case: TestCase) -> f64 {
63+
let mut time = Instant::ZERO;
64+
65+
let params = QueueParams {
66+
latency: case.rtt / 2,
67+
loss: case.loss,
68+
};
69+
let queue_a_to_b = RefCell::new(PacketQueue::new(params.clone(), 0));
70+
let queue_b_to_a = RefCell::new(PacketQueue::new(params.clone(), 1));
71+
let device_a = QueueDevice::new(&queue_a_to_b, &queue_b_to_a, Medium::Ethernet);
72+
let device_b = QueueDevice::new(&queue_b_to_a, &queue_a_to_b, Medium::Ethernet);
73+
74+
let mut device_a = Tracer::new(device_a, |_timestamp, _printer| log::trace!("{}", _printer));
75+
let mut device_b = Tracer::new(device_b, |_timestamp, _printer| log::trace!("{}", _printer));
76+
77+
let mut iface_a = Interface::new(Config::new(MAC_A), &mut device_a, time);
78+
iface_a.update_ip_addrs(|a| a.push(IpCidr::new(IP_A, 8)).unwrap());
79+
let mut iface_b = Interface::new(Config::new(MAC_B), &mut device_b, time);
80+
iface_b.update_ip_addrs(|a| a.push(IpCidr::new(IP_B, 8)).unwrap());
81+
82+
// Create sockets
83+
let socket_a = {
84+
let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; case.buffer]);
85+
let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; case.buffer]);
86+
tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer)
87+
};
88+
89+
let socket_b = {
90+
let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; case.buffer]);
91+
let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; case.buffer]);
92+
tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer)
93+
};
94+
95+
let mut sockets_a: [_; 2] = Default::default();
96+
let mut sockets_a = SocketSet::new(&mut sockets_a[..]);
97+
let socket_a_handle = sockets_a.add(socket_a);
98+
99+
let mut sockets_b: [_; 2] = Default::default();
100+
let mut sockets_b = SocketSet::new(&mut sockets_b[..]);
101+
let socket_b_handle = sockets_b.add(socket_b);
102+
103+
let mut did_listen = false;
104+
let mut did_connect = false;
105+
let mut processed = 0;
106+
while processed < BYTES {
107+
*CLOCK.lock().unwrap() = (time, ' ');
108+
log::info!("loop");
109+
//println!("t = {}", time);
110+
111+
*CLOCK.lock().unwrap() = (time, 'A');
112+
113+
iface_a.poll(time, &mut device_a, &mut sockets_a);
114+
115+
let socket = sockets_a.get_mut::<tcp::Socket>(socket_a_handle);
116+
if !socket.is_active() && !socket.is_listening() && !did_listen {
117+
//println!("listening");
118+
socket.listen(1234).unwrap();
119+
did_listen = true;
120+
}
121+
122+
while socket.can_recv() {
123+
let received = socket.recv(|buffer| (buffer.len(), buffer.len())).unwrap();
124+
//println!("got {:?}", received,);
125+
processed += received;
126+
}
127+
128+
*CLOCK.lock().unwrap() = (time, 'B');
129+
iface_b.poll(time, &mut device_b, &mut sockets_b);
130+
let socket = sockets_b.get_mut::<tcp::Socket>(socket_b_handle);
131+
let cx = iface_b.context();
132+
if !socket.is_open() && !did_connect {
133+
//println!("connecting");
134+
socket.connect(cx, (IP_A, 1234), 65000).unwrap();
135+
did_connect = true;
136+
}
137+
138+
while socket.can_send() {
139+
//println!("sending");
140+
socket.send(|buffer| (buffer.len(), ())).unwrap();
141+
}
142+
143+
*CLOCK.lock().unwrap() = (time, ' ');
144+
145+
let mut next_time = queue_a_to_b.borrow_mut().next_expiration();
146+
next_time = next_time.min(queue_b_to_a.borrow_mut().next_expiration());
147+
if let Some(t) = iface_a.poll_at(time, &sockets_a) {
148+
next_time = next_time.min(t);
149+
}
150+
if let Some(t) = iface_b.poll_at(time, &sockets_b) {
151+
next_time = next_time.min(t);
152+
}
153+
assert!(next_time.total_micros() != i64::MAX);
154+
time = time.max(next_time);
155+
}
156+
157+
let duration = time - Instant::ZERO;
158+
processed as f64 / duration.total_micros() as f64 * 1e6
159+
}
160+
161+
struct Packet {
162+
timestamp: Instant,
163+
id: u64,
164+
data: Vec<u8>,
165+
}
166+
167+
impl PartialEq for Packet {
168+
fn eq(&self, other: &Self) -> bool {
169+
(other.timestamp, other.id) == (self.timestamp, self.id)
170+
}
171+
}
172+
173+
impl Eq for Packet {}
174+
175+
impl PartialOrd for Packet {
176+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
177+
(other.timestamp, other.id).partial_cmp(&(self.timestamp, self.id))
178+
}
179+
}
180+
181+
impl Ord for Packet {
182+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
183+
(other.timestamp, other.id).cmp(&(self.timestamp, self.id))
184+
}
185+
}
186+
187+
#[derive(Clone)]
188+
struct QueueParams {
189+
latency: Duration,
190+
loss: f64,
191+
}
192+
193+
struct PacketQueue {
194+
queue: BinaryHeap<Packet>,
195+
next_id: u64,
196+
params: QueueParams,
197+
rng: ChaCha20Rng,
198+
}
199+
200+
impl PacketQueue {
201+
pub fn new(params: QueueParams, seed: u64) -> Self {
202+
Self {
203+
queue: BinaryHeap::new(),
204+
next_id: 0,
205+
params,
206+
rng: ChaCha20Rng::seed_from_u64(seed),
207+
}
208+
}
209+
210+
pub fn next_expiration(&self) -> Instant {
211+
self.queue
212+
.peek()
213+
.map(|p| p.timestamp)
214+
.unwrap_or(Instant::from_micros(i64::MAX))
215+
}
216+
217+
pub fn push(&mut self, data: Vec<u8>, timestamp: Instant) {
218+
if self.rng.gen::<f64>() < self.params.loss {
219+
log::info!("PACKET LOST!");
220+
return;
221+
}
222+
223+
self.queue.push(Packet {
224+
data,
225+
id: self.next_id,
226+
timestamp: timestamp + self.params.latency,
227+
});
228+
self.next_id += 1;
229+
}
230+
231+
pub fn pop(&mut self, timestamp: Instant) -> Option<Vec<u8>> {
232+
let p = self.queue.peek()?;
233+
if p.timestamp > timestamp {
234+
return None;
235+
}
236+
Some(self.queue.pop().unwrap().data)
237+
}
238+
}
239+
240+
pub struct QueueDevice<'a> {
241+
tx_queue: &'a RefCell<PacketQueue>,
242+
rx_queue: &'a RefCell<PacketQueue>,
243+
medium: Medium,
244+
}
245+
246+
impl<'a> QueueDevice<'a> {
247+
fn new(
248+
tx_queue: &'a RefCell<PacketQueue>,
249+
rx_queue: &'a RefCell<PacketQueue>,
250+
medium: Medium,
251+
) -> Self {
252+
Self {
253+
tx_queue,
254+
rx_queue,
255+
medium,
256+
}
257+
}
258+
}
259+
260+
impl Device for QueueDevice<'_> {
261+
type RxToken<'a>
262+
= RxToken
263+
where
264+
Self: 'a;
265+
type TxToken<'a>
266+
= TxToken<'a>
267+
where
268+
Self: 'a;
269+
270+
fn capabilities(&self) -> DeviceCapabilities {
271+
let mut caps = DeviceCapabilities::default();
272+
caps.max_transmission_unit = 1514;
273+
caps.medium = self.medium;
274+
caps.checksum = ChecksumCapabilities::ignored();
275+
caps
276+
}
277+
278+
fn receive(&mut self, timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
279+
self.rx_queue
280+
.borrow_mut()
281+
.pop(timestamp)
282+
.map(move |buffer| {
283+
let rx = RxToken { buffer };
284+
let tx = TxToken {
285+
queue: &mut self.tx_queue,
286+
timestamp,
287+
};
288+
(rx, tx)
289+
})
290+
}
291+
292+
fn transmit(&mut self, timestamp: Instant) -> Option<Self::TxToken<'_>> {
293+
Some(TxToken {
294+
queue: &mut self.tx_queue,
295+
timestamp,
296+
})
297+
}
298+
}
299+
300+
pub struct RxToken {
301+
buffer: Vec<u8>,
302+
}
303+
304+
impl phy::RxToken for RxToken {
305+
fn consume<R, F>(self, f: F) -> R
306+
where
307+
F: FnOnce(&[u8]) -> R,
308+
{
309+
f(&self.buffer)
310+
}
311+
}
312+
313+
pub struct TxToken<'a> {
314+
queue: &'a RefCell<PacketQueue>,
315+
timestamp: Instant,
316+
}
317+
318+
impl<'a> phy::TxToken for TxToken<'a> {
319+
fn consume<R, F>(self, len: usize, f: F) -> R
320+
where
321+
F: FnOnce(&mut [u8]) -> R,
322+
{
323+
let mut buffer = vec![0; len];
324+
let result = f(&mut buffer);
325+
self.queue.borrow_mut().push(buffer, self.timestamp);
326+
result
327+
}
328+
}
329+
330+
pub fn setup_logging() {
331+
env_logger::Builder::new()
332+
.format(move |buf, record| {
333+
let (elapsed, side) = *CLOCK.lock().unwrap();
334+
335+
let timestamp = format!("[{elapsed} {side}]");
336+
if record.target().starts_with("smoltcp::") {
337+
writeln!(
338+
buf,
339+
"{} ({}): {}",
340+
timestamp,
341+
record.target().replace("smoltcp::", ""),
342+
record.args()
343+
)
344+
} else if record.level() == log::Level::Trace {
345+
let message = format!("{}", record.args());
346+
writeln!(
347+
buf,
348+
"{} {}",
349+
timestamp,
350+
message.replace('\n', "\n ")
351+
)
352+
} else {
353+
writeln!(
354+
buf,
355+
"{} ({}): {}",
356+
timestamp,
357+
record.target(),
358+
record.args()
359+
)
360+
}
361+
})
362+
.parse_env("RUST_LOG")
363+
.init();
364+
}

tests/snapshots/netsim__netsim.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
source: tests/netsim.rs
3+
expression: s
4+
snapshot_kind: text
5+
---
6+
buf\loss 0.000 0.001 0.010 0.020 0.050 0.100 0.200 0.300
7+
128 1279.98 1255.76 1054.15 886.36 538.66 227.84 33.99 7.18
8+
256 2559.91 2507.27 2100.03 1770.30 1070.71 468.24 66.71 14.35
9+
512 5119.63 5011.95 4172.36 3531.57 2098.73 942.38 144.73 29.45
10+
1024 10238.50 10023.19 8340.90 7084.25 4003.34 1869.94 290.74 60.92
11+
2048 17535.11 17171.82 14093.50 12063.90 7205.27 3379.12 824.76 131.54
12+
4096 35062.41 33852.31 27011.08 22073.09 13680.70 7631.11 1617.81 302.65
13+
8192 77374.28 72409.99 58428.68 48310.75 29123.30 14314.36 2880.39 551.60
14+
16384 161842.28 159448.56 141467.31 127073.06 78239.08 38637.20 7565.64 1112.31
15+
32768 322944.88 314313.90 266384.37 245985.29 138762.29 83162.99 10739.10 1951.95

0 commit comments

Comments
 (0)