Skip to content

Commit 81402f6

Browse files
committed
feat: Testing examples & some fixes
1 parent 21dd3b4 commit 81402f6

File tree

12 files changed

+747
-73
lines changed

12 files changed

+747
-73
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/target
22
Cargo.lock
33
*.pcap
4+
/scripts
5+
/*.md
6+
*.log

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interfac
326326
name = "benchmark"
327327
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-raw", "socket-udp"]
328328

329+
[[example]]
330+
name = "perf_server"
331+
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-tcp", "socket-tcp-cubic", "socket-tcp-bbr"]
332+
333+
[[example]]
334+
name = "perf_client"
335+
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-tcp", "socket-tcp-cubic", "socket-tcp-bbr"]
336+
329337
[[example]]
330338
name = "dhcp_client"
331339
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "proto-dhcpv4", "socket-raw"]

examples/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ fn main() {
4444
.push(IpCidr::new(IpAddress::v4(192, 168, 69, 2), 24))
4545
.unwrap();
4646
ip_addrs
47-
.push(IpCidr::new(IpAddress::v6(0xfdaa, 0, 0, 0, 0, 0, 0, 1), 64))
47+
.push(IpCidr::new(IpAddress::v6(0xfdaa, 0, 0, 0, 0, 0, 0, 2), 64))
4848
.unwrap();
4949
ip_addrs
50-
.push(IpCidr::new(IpAddress::v6(0xfe80, 0, 0, 0, 0, 0, 0, 1), 64))
50+
.push(IpCidr::new(IpAddress::v6(0xfe80, 0, 0, 0, 0, 0, 0, 2), 64))
5151
.unwrap();
5252
});
5353
iface

examples/perf_client.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
mod utils;
2+
3+
use log::info;
4+
use std::os::unix::io::AsRawFd;
5+
use std::str::FromStr;
6+
7+
use smoltcp::iface::{Config, Interface, SocketSet};
8+
use smoltcp::phy::{Device, Medium, wait as phy_wait};
9+
use smoltcp::socket::tcp::{self, CongestionControl, State};
10+
use smoltcp::time::Instant;
11+
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
12+
13+
const BUFFER_SIZE: usize = 6 * 1024 * 1024;
14+
15+
struct LatencyStats {
16+
samples: Vec<i64>,
17+
sum: i64,
18+
count: usize,
19+
min: i64,
20+
max: i64,
21+
}
22+
23+
impl LatencyStats {
24+
fn new() -> Self {
25+
LatencyStats {
26+
samples: Vec::new(),
27+
sum: 0,
28+
count: 0,
29+
min: i64::MAX,
30+
max: 0,
31+
}
32+
}
33+
34+
fn add_sample(&mut self, latency_us: i64) {
35+
self.samples.push(latency_us);
36+
self.sum += latency_us;
37+
self.count += 1;
38+
self.min = self.min.min(latency_us);
39+
self.max = self.max.max(latency_us);
40+
}
41+
42+
fn mean(&self) -> f64 {
43+
if self.count == 0 {
44+
0.0
45+
} else {
46+
self.sum as f64 / self.count as f64
47+
}
48+
}
49+
50+
fn percentile(&mut self, p: f64) -> i64 {
51+
if self.samples.is_empty() {
52+
return 0;
53+
}
54+
self.samples.sort_unstable();
55+
let idx = ((p / 100.0) * self.samples.len() as f64) as usize;
56+
self.samples[idx.min(self.samples.len() - 1)]
57+
}
58+
59+
fn print_summary(&mut self) {
60+
if self.count == 0 {
61+
info!("No latency samples collected");
62+
return;
63+
}
64+
65+
info!("");
66+
info!("Latency Statistics:");
67+
info!(" Samples: {}", self.count);
68+
info!(" Min: {:.3} ms", self.min as f64 / 1000.0);
69+
info!(" Mean: {:.3} ms", self.mean() / 1000.0);
70+
info!(" p50: {:.3} ms", self.percentile(50.0) as f64 / 1000.0);
71+
info!(" p95: {:.3} ms", self.percentile(95.0) as f64 / 1000.0);
72+
info!(" p99: {:.3} ms", self.percentile(99.0) as f64 / 1000.0);
73+
info!(" Max: {:.3} ms", self.max as f64 / 1000.0);
74+
}
75+
}
76+
77+
fn parse_congestion_control(s: &str) -> CongestionControl {
78+
match s.to_lowercase().as_str() {
79+
"none" => CongestionControl::None,
80+
#[cfg(feature = "socket-tcp-reno")]
81+
"reno" => CongestionControl::Reno,
82+
#[cfg(feature = "socket-tcp-cubic")]
83+
"cubic" => CongestionControl::Cubic,
84+
#[cfg(feature = "socket-tcp-bbr")]
85+
"bbr" => CongestionControl::Bbr,
86+
_ => {
87+
eprintln!("Unknown congestion control algorithm: {}", s);
88+
eprintln!("Available options:");
89+
eprintln!(" none");
90+
#[cfg(feature = "socket-tcp-reno")]
91+
eprintln!(" reno");
92+
#[cfg(feature = "socket-tcp-cubic")]
93+
eprintln!(" cubic");
94+
#[cfg(feature = "socket-tcp-bbr")]
95+
eprintln!(" bbr");
96+
std::process::exit(1);
97+
}
98+
}
99+
}
100+
101+
fn main() {
102+
utils::setup_logging("info");
103+
104+
let (mut opts, mut free) = utils::create_options();
105+
utils::add_tuntap_options(&mut opts, &mut free);
106+
utils::add_middleware_options(&mut opts, &mut free);
107+
108+
opts.optopt("c", "congestion", "Congestion control algorithm (none/reno/cubic/bbr)", "ALGO");
109+
opts.optopt("s", "server", "Server address", "ADDRESS");
110+
opts.optopt("p", "port", "Server port", "PORT");
111+
112+
let mut matches = utils::parse_options(&opts, vec![]);
113+
114+
let cc_algo = parse_congestion_control(
115+
&matches.opt_str("c").unwrap_or_else(|| {
116+
#[cfg(feature = "socket-tcp-bbr")]
117+
{ "bbr".to_string() }
118+
#[cfg(not(feature = "socket-tcp-bbr"))]
119+
#[cfg(feature = "socket-tcp-cubic")]
120+
{ "cubic".to_string() }
121+
#[cfg(not(any(feature = "socket-tcp-bbr", feature = "socket-tcp-cubic")))]
122+
#[cfg(feature = "socket-tcp-reno")]
123+
{ "reno".to_string() }
124+
#[cfg(not(any(feature = "socket-tcp-bbr", feature = "socket-tcp-cubic", feature = "socket-tcp-reno")))]
125+
{ "none".to_string() }
126+
})
127+
);
128+
129+
let server_addr = IpAddress::from_str(
130+
&matches.opt_str("s").unwrap_or_else(|| "192.168.69.1".to_string())
131+
).expect("invalid server address");
132+
133+
let server_port = matches.opt_str("p")
134+
.and_then(|s| u16::from_str(&s).ok())
135+
.unwrap_or(8000);
136+
137+
let device = utils::parse_tuntap_options(&mut matches);
138+
let fd = device.as_raw_fd();
139+
let mut device = utils::parse_middleware_options(&mut matches, device, false);
140+
141+
// Create interface
142+
let mut config = match device.capabilities().medium {
143+
Medium::Ethernet => {
144+
Config::new(EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]).into())
145+
}
146+
Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip),
147+
Medium::Ieee802154 => todo!(),
148+
};
149+
config.random_seed = rand::random();
150+
151+
let mut iface = Interface::new(config, &mut device, Instant::now());
152+
iface.update_ip_addrs(|ip_addrs| {
153+
ip_addrs
154+
.push(IpCidr::new(IpAddress::v4(192, 168, 69, 2), 24))
155+
.unwrap();
156+
});
157+
158+
// Create TCP socket with large buffers
159+
let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; BUFFER_SIZE]);
160+
let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; BUFFER_SIZE]);
161+
let mut tcp_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer);
162+
163+
// Set congestion control algorithm
164+
tcp_socket.set_congestion_control(cc_algo);
165+
166+
let mut sockets = SocketSet::new(vec![]);
167+
let tcp_handle = sockets.add(tcp_socket);
168+
169+
info!("Performance Client");
170+
info!("==================");
171+
info!("Congestion Control: {:?}", cc_algo);
172+
info!("Connecting to: {}:{}", server_addr, server_port);
173+
info!("Buffer size: {} bytes", BUFFER_SIZE);
174+
info!("");
175+
176+
// Connect to server
177+
let socket = sockets.get_mut::<tcp::Socket>(tcp_handle);
178+
socket.connect(iface.context(), (server_addr, server_port), 49500).unwrap();
179+
180+
let mut bytes_received = 0usize;
181+
let mut start_time: Option<Instant> = None;
182+
let mut last_report = Instant::now();
183+
let mut tcp_active = false;
184+
let mut latency_stats = LatencyStats::new();
185+
let mut sample_interval = 0; // Sample every Nth chunk
186+
187+
loop {
188+
let timestamp = Instant::now();
189+
iface.poll(timestamp, &mut device, &mut sockets);
190+
191+
let socket = sockets.get_mut::<tcp::Socket>(tcp_handle);
192+
193+
// Track connection state
194+
if socket.is_active() && !tcp_active {
195+
info!("Connected to server");
196+
start_time = Some(timestamp);
197+
bytes_received = 0;
198+
last_report = timestamp;
199+
latency_stats = LatencyStats::new();
200+
} else if !socket.is_active() && tcp_active {
201+
if let Some(start) = start_time {
202+
let elapsed = (timestamp - start).total_millis() as f64 / 1000.0;
203+
let throughput_gbps = (bytes_received as f64 * 8.0) / elapsed / 1e9;
204+
info!("");
205+
info!("Connection closed");
206+
info!("==================");
207+
info!("Total received: {:.2} MB", bytes_received as f64 / 1e6);
208+
info!("Time: {:.2} seconds", elapsed);
209+
info!("Throughput: {:.3} Gbps", throughput_gbps);
210+
latency_stats.print_summary();
211+
}
212+
break;
213+
}
214+
tcp_active = socket.is_active();
215+
216+
// Check if server has closed (received FIN)
217+
if socket.state() == State::CloseWait {
218+
// Server has closed its side, close our side too
219+
socket.close();
220+
}
221+
222+
// Receive data
223+
if socket.may_recv() {
224+
let recv_result = socket.recv(|buffer| {
225+
let len = buffer.len();
226+
227+
// Sample latency from timestamps in the data
228+
// Extract timestamp from every 100000th 8-byte chunk to minimize overhead
229+
if len >= 8 {
230+
for chunk in buffer.chunks_exact(8) {
231+
sample_interval += 1;
232+
if sample_interval >= 100000 {
233+
sample_interval = 0;
234+
235+
let mut ts_bytes = [0u8; 8];
236+
ts_bytes.copy_from_slice(chunk);
237+
let sent_time_us = i64::from_le_bytes(ts_bytes);
238+
let now_us = timestamp.total_micros();
239+
240+
// Calculate one-way delay (approximation)
241+
let latency_us = now_us - sent_time_us;
242+
243+
// Only record reasonable latency values (< 10 seconds)
244+
if latency_us > 0 && latency_us < 10_000_000 {
245+
latency_stats.add_sample(latency_us);
246+
}
247+
break; // Only sample once per recv
248+
}
249+
}
250+
}
251+
252+
(len, len)
253+
}).unwrap();
254+
255+
bytes_received += recv_result;
256+
257+
// Report progress every 5 seconds
258+
if (timestamp - last_report).total_millis() >= 5000 {
259+
if let Some(start) = start_time {
260+
let elapsed = (timestamp - start).total_millis() as f64 / 1000.0;
261+
if elapsed > 0.0 {
262+
let throughput_gbps = (bytes_received as f64 * 8.0) / elapsed / 1e9;
263+
let avg_latency = latency_stats.mean() / 1000.0; // Convert to ms
264+
info!("{:.2} MB received | {:.3} Gbps | Avg Latency: {:.3} ms",
265+
bytes_received as f64 / 1e6, throughput_gbps, avg_latency);
266+
}
267+
}
268+
last_report = timestamp;
269+
}
270+
}
271+
272+
phy_wait(fd, iface.poll_delay(timestamp, &sockets)).expect("wait error");
273+
}
274+
}

0 commit comments

Comments
 (0)