prt_core/core/
bandwidth.rs1use std::time::Instant;
10
11#[derive(Debug, Clone, Copy)]
13pub struct BandwidthRate {
14 pub rx_bytes_per_sec: f64,
15 pub tx_bytes_per_sec: f64,
16}
17
18#[derive(Debug)]
20pub struct BandwidthTracker {
21 prev_sample: Option<(u64, u64, Instant)>, pub current_rate: Option<BandwidthRate>,
23}
24
25impl Default for BandwidthTracker {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl BandwidthTracker {
32 pub fn new() -> Self {
33 Self {
34 prev_sample: None,
35 current_rate: None,
36 }
37 }
38
39 pub fn sample(&mut self) {
41 let (rx, tx) = match read_counters() {
42 Some(v) => v,
43 None => return,
44 };
45 let now = Instant::now();
46
47 if let Some((prev_rx, prev_tx, prev_time)) = self.prev_sample {
48 let elapsed = now.duration_since(prev_time).as_secs_f64();
49 if elapsed > 0.0 {
50 let rx_delta = rx.saturating_sub(prev_rx) as f64;
51 let tx_delta = tx.saturating_sub(prev_tx) as f64;
52 self.current_rate = Some(BandwidthRate {
53 rx_bytes_per_sec: rx_delta / elapsed,
54 tx_bytes_per_sec: tx_delta / elapsed,
55 });
56 }
57 }
58
59 self.prev_sample = Some((rx, tx, now));
60 }
61}
62
63pub fn format_rate(bytes_per_sec: f64) -> String {
65 if bytes_per_sec < 1024.0 {
66 format!("{:.0} B/s", bytes_per_sec)
67 } else if bytes_per_sec < 1024.0 * 1024.0 {
68 format!("{:.1} KB/s", bytes_per_sec / 1024.0)
69 } else if bytes_per_sec < 1024.0 * 1024.0 * 1024.0 {
70 format!("{:.1} MB/s", bytes_per_sec / (1024.0 * 1024.0))
71 } else {
72 format!("{:.1} GB/s", bytes_per_sec / (1024.0 * 1024.0 * 1024.0))
73 }
74}
75
76fn read_counters() -> Option<(u64, u64)> {
78 #[cfg(target_os = "linux")]
79 {
80 read_counters_linux()
81 }
82 #[cfg(target_os = "macos")]
83 {
84 read_counters_macos()
85 }
86 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
87 {
88 None
89 }
90}
91
92#[cfg(target_os = "linux")]
94fn read_counters_linux() -> Option<(u64, u64)> {
95 let content = std::fs::read_to_string("/proc/net/dev").ok()?;
96 parse_proc_net_dev(&content)
97}
98
99#[cfg(target_os = "macos")]
101fn read_counters_macos() -> Option<(u64, u64)> {
102 let output = std::process::Command::new("netstat")
103 .args(["-ib"])
104 .output()
105 .ok()?;
106 let content = String::from_utf8(output.stdout).ok()?;
107 parse_netstat_ib(&content)
108}
109
110#[allow(dead_code)]
120fn parse_proc_net_dev(content: &str) -> Option<(u64, u64)> {
121 let mut total_rx = 0u64;
122 let mut total_tx = 0u64;
123 let mut found = false;
124
125 for line in content.lines().skip(2) {
126 let line = line.trim();
128 if line.is_empty() {
129 continue;
130 }
131 let parts: Vec<&str> = line.split_whitespace().collect();
132 if parts.len() < 10 {
133 continue;
134 }
135 if parts[0].starts_with("lo:") || parts[0] == "lo:" {
137 continue;
138 }
139 if let (Ok(rx), Ok(tx)) = (parts[1].parse::<u64>(), parts[9].parse::<u64>()) {
140 total_rx += rx;
141 total_tx += tx;
142 found = true;
143 }
144 }
145
146 if found {
147 Some((total_rx, total_tx))
148 } else {
149 None
150 }
151}
152
153#[allow(dead_code)]
162fn parse_netstat_ib(content: &str) -> Option<(u64, u64)> {
163 let mut total_rx = 0u64;
164 let mut total_tx = 0u64;
165 let mut found = false;
166
167 for line in content.lines().skip(1) {
168 let parts: Vec<&str> = line.split_whitespace().collect();
169 if parts.len() < 11 {
170 continue;
171 }
172 if parts[0].starts_with("lo") {
174 continue;
175 }
176 if !parts[2].starts_with("<Link#") {
178 continue;
179 }
180 if let (Ok(rx), Ok(tx)) = (parts[6].parse::<u64>(), parts[9].parse::<u64>()) {
181 total_rx += rx;
182 total_tx += tx;
183 found = true;
184 }
185 }
186
187 if found {
188 Some((total_rx, total_tx))
189 } else {
190 None
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn format_rate_bytes() {
200 assert_eq!(format_rate(0.0), "0 B/s");
201 assert_eq!(format_rate(500.0), "500 B/s");
202 assert_eq!(format_rate(1023.0), "1023 B/s");
203 }
204
205 #[test]
206 fn format_rate_kilobytes() {
207 assert_eq!(format_rate(1024.0), "1.0 KB/s");
208 assert_eq!(format_rate(1536.0), "1.5 KB/s");
209 assert_eq!(format_rate(500_000.0), "488.3 KB/s");
210 }
211
212 #[test]
213 fn format_rate_megabytes() {
214 assert_eq!(format_rate(1_048_576.0), "1.0 MB/s");
215 assert_eq!(format_rate(10_000_000.0), "9.5 MB/s");
216 }
217
218 #[test]
219 fn format_rate_gigabytes() {
220 assert_eq!(format_rate(1_073_741_824.0), "1.0 GB/s");
221 }
222
223 #[test]
224 fn tracker_first_sample_no_rate() {
225 let mut t = BandwidthTracker::new();
226 assert!(t.current_rate.is_none());
227 t.sample();
228 }
231
232 #[test]
233 fn parse_proc_net_dev_valid() {
234 let content = "\
235Inter-| Receive | Transmit
236 face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
237 lo: 100 10 0 0 0 0 0 0 100 10 0 0 0 0 0 0
238 eth0: 5000 50 0 0 0 0 0 0 3000 30 0 0 0 0 0 0
239 eth1: 2000 20 0 0 0 0 0 0 1000 10 0 0 0 0 0 0
240";
241 let (rx, tx) = parse_proc_net_dev(content).unwrap();
242 assert_eq!(rx, 7000); assert_eq!(tx, 4000);
244 }
245
246 #[test]
247 fn parse_proc_net_dev_empty() {
248 let content = "\
249Inter-| Receive | Transmit
250 face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
251";
252 assert!(parse_proc_net_dev(content).is_none());
253 }
254
255 #[test]
256 fn parse_netstat_ib_valid() {
257 let content = "\
258Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll
259lo0 16384 <Link#1> 1000 0 500000 1000 0 500000 0
260en0 1500 <Link#6> aa:bb:cc:dd:ee:ff 5000 0 3000000 4000 0 2000000 0
261en0 1500 192.168.1 192.168.1.100 5000 0 3000000 4000 0 2000000 0
262";
263 let (rx, tx) = parse_netstat_ib(content).unwrap();
264 assert_eq!(rx, 3_000_000); assert_eq!(tx, 2_000_000);
266 }
267
268 #[test]
269 fn parse_netstat_ib_empty() {
270 let content = "Name Mtu Network Address\n";
271 assert!(parse_netstat_ib(content).is_none());
272 }
273}