netem_trace/
mahimahi.rs

1//! This module can generate traces in mahimahi format for struct implementing [`BwTrace`] and
2//! load traces in mahimahi format to [`RepeatedBwPatternConfig`].
3//!
4//! Enable `mahimahi` feature to use this module.
5//!
6//! ## Examples
7//!
8//! ```
9//! # use netem_trace::{Mahimahi, MahimahiExt};
10//! # use netem_trace::model::StaticBwConfig;
11//! # use netem_trace::{Bandwidth, Duration};
12//! let mut static_bw = StaticBwConfig::new()
13//!     .bw(Bandwidth::from_mbps(24))
14//!     .duration(Duration::from_secs(1))
15//!     .build();
16//! assert_eq!(static_bw.mahimahi(&Duration::from_millis(5)), [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]);
17//! let mut static_bw = StaticBwConfig::new()
18//!     .bw(Bandwidth::from_mbps(12))
19//!     .duration(Duration::from_secs(1))
20//!     .build();
21//! assert_eq!(static_bw.mahimahi_to_string(&Duration::from_millis(5)), "1\n2\n3\n4\n5");
22//! ```
23
24use crate::{
25    model::{BwTraceConfig, RepeatedBwPatternConfig, StaticBwConfig},
26    Bandwidth, BwTrace, Duration,
27};
28
29const MTU_IN_BYTES: u64 = 1500;
30const MTU_IN_BITS: u64 = MTU_IN_BYTES * 8;
31const MTU_PER_MILLIS: Bandwidth = Bandwidth::from_kbps(MTU_IN_BITS);
32const MAHIMAHI_TS_BIN: Duration = Duration::from_millis(1);
33
34macro_rules! saturating_duration_as_millis_u64 {
35    ($duration:expr) => {
36        $duration
37            .as_secs()
38            .saturating_mul(1_000)
39            .saturating_add($duration.subsec_millis() as u64)
40    };
41}
42
43/// The `Mahimahi` trait provides a method to generate a trace in mahimahi format.
44///
45/// The trace is a sequence of timestamps, each timestamp represents an opportunity
46/// of sending a packet at that timestamp.
47///
48/// This trait is automatically implemented for all types that implement `BwTrace`.
49///
50/// This trait is often used with [`MahimahiExt`] trait. [`MahimahiExt`] provides
51/// methods that generates trace and writes it to a string or a file.
52pub trait Mahimahi: BwTrace {
53    /// Generate a timestamp sequence in mahimahi format.
54    ///
55    /// Each timestamp represents an opportunity of sending a packet at that timestamp (in milliseconds).
56    ///
57    /// For example, if the bandwidth is 12Mbps (one packet per millisecond), then the sequence can be:
58    /// \[1, 2, 3, 4, 5\]
59    fn mahimahi(&mut self, total_dur: &Duration) -> Vec<u64> {
60        let mut timestamp = MAHIMAHI_TS_BIN;
61        let mut v = Vec::new();
62        let mut transfer = Bandwidth::from_bps(0);
63        let mut bin_rem = MAHIMAHI_TS_BIN;
64        while let Some((bw, mut dur)) = self.next_bw() {
65            if timestamp > *total_dur {
66                break;
67            }
68            while (timestamp <= *total_dur) && !dur.is_zero() {
69                let bin = bin_rem.min(dur);
70                bin_rem -= bin;
71                dur -= bin;
72                let bin_factor = bin.as_secs_f64() / MAHIMAHI_TS_BIN.as_secs_f64();
73                transfer += bw.mul_f64(bin_factor);
74                while transfer >= MTU_PER_MILLIS {
75                    v.push(saturating_duration_as_millis_u64!(timestamp));
76                    transfer -= MTU_PER_MILLIS;
77                }
78                if bin_rem.is_zero() {
79                    bin_rem = MAHIMAHI_TS_BIN;
80                    timestamp += MAHIMAHI_TS_BIN;
81                }
82            }
83        }
84        v
85    }
86}
87
88impl<T: BwTrace + ?Sized> Mahimahi for T {}
89
90/// The `MahimahiExt` trait provides some convenient methods to generate a trace in mahimahi format.
91pub trait MahimahiExt: Mahimahi {
92    /// Join the mahimahi timestamp sequence to a string.
93    fn mahimahi_to_string(&mut self, total_dur: &Duration) -> String {
94        let ts = self.mahimahi(total_dur);
95        itertools::join(ts, "\n")
96    }
97
98    /// Write the mahimahi timestamp sequence to a file.
99    fn mahimahi_to_file<P: AsRef<std::path::Path>>(&mut self, total_dur: &Duration, path: P) {
100        let content = self.mahimahi_to_string(total_dur);
101        std::fs::write(path, content).unwrap();
102    }
103}
104
105impl<T: Mahimahi + ?Sized> MahimahiExt for T {}
106
107/// Load a mahimahi trace to a [`RepeatedBwPatternConfig`].
108///
109/// The `trace` is a sequence of timestamps, each timestamp represents an opportunity
110/// of sending a packet at that timestamp.
111///
112/// The `count` is the number of times the trace repeats. If `count` is `None` or `Some(0)`,
113/// then the trace will repeat forever.
114///
115/// Each timestamp will be converted into a 12Mbps bandwidth lasting for 1 millisecond,
116/// and then accumulated. For example, if the trace is \[1, 1, 5, 6\] and count is `Some(1)`,
117/// then the bandwidth pattern will be \[24Mbps for 1ms, 0Mbps for 3ms, 12Mbps for 2ms\].
118///
119/// **Warning:** In some cases, this trace may slightly deviate from the behavior of mahimahi.
120///
121/// Returns `Err` string if the mahimahi trace is invalid.
122pub fn load_mahimahi_trace(
123    trace: Vec<u64>,
124    count: Option<usize>,
125) -> Result<RepeatedBwPatternConfig, &'static str> {
126    let mut pattern: Vec<StaticBwConfig> = vec![];
127    // The closure inserts a bandwidth config into the pattern.
128    let mut insert_into_pattern = |config| {
129        if pattern.is_empty() {
130            pattern.push(config);
131        } else {
132            let last_config = pattern.last_mut().unwrap();
133            if last_config.bw.unwrap() == config.bw.unwrap() {
134                last_config.duration =
135                    Some(last_config.duration.unwrap() + config.duration.unwrap());
136            } else {
137                pattern.push(config);
138            }
139        }
140    };
141
142    let mut zeor_ts_cnt = 0; // count of zero timestamps
143    let mut last_ts = 0; // last non-zero timestamp
144    let mut last_cnt = 0; // count of last non-zero timestamp
145    for ts in trace {
146        // count zero timestamps
147        if ts == 0 {
148            zeor_ts_cnt += 1;
149            continue;
150        }
151        // non-zero timestamps
152        match ts.cmp(&last_ts) {
153            std::cmp::Ordering::Less => {
154                return Err("timestamps must be monotonically nondecreasing");
155            }
156            std::cmp::Ordering::Equal => {
157                last_cnt += 1;
158            }
159            std::cmp::Ordering::Greater => {
160                if last_ts > 0 {
161                    // insert new bandwidth config
162                    insert_into_pattern(
163                        StaticBwConfig::new()
164                            .bw(MTU_PER_MILLIS * last_cnt)
165                            .duration(MAHIMAHI_TS_BIN),
166                    );
167                }
168                if ts - last_ts > 1 {
169                    // insert zero bandwidth
170                    insert_into_pattern(
171                        StaticBwConfig::new()
172                            .bw(Bandwidth::ZERO)
173                            .duration(MAHIMAHI_TS_BIN * ((ts - last_ts - 1) as u32)),
174                    );
175                }
176                last_cnt = 1;
177                last_ts = ts;
178            }
179        }
180    }
181    if last_cnt == 0 {
182        // no non-zero timestamps
183        return Err("trace must last for a nonzero amount of time");
184    } else {
185        // merge final timestamps and zero timestamps
186        insert_into_pattern(
187            StaticBwConfig::new()
188                .bw(MTU_PER_MILLIS * (last_cnt + zeor_ts_cnt))
189                .duration(MAHIMAHI_TS_BIN),
190        );
191    }
192    Ok(RepeatedBwPatternConfig::new()
193        .count(count.unwrap_or(0))
194        .pattern(
195            pattern
196                .drain(..)
197                .map(|config| Box::new(config) as Box<dyn BwTraceConfig>)
198                .collect(),
199        ))
200}
201
202#[cfg(test)]
203mod test {
204    use super::*;
205    use crate::model::StaticBwConfig;
206    use crate::Bandwidth;
207
208    #[test]
209    fn test_trait() {
210        let mut static_bw = StaticBwConfig::new()
211            .bw(Bandwidth::from_mbps(24))
212            .duration(Duration::from_secs(1))
213            .build();
214        assert_eq!(
215            static_bw.mahimahi(&Duration::from_millis(5)),
216            [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]
217        );
218        let mut static_bw = StaticBwConfig::new()
219            .bw(Bandwidth::from_mbps(12))
220            .duration(Duration::from_secs(1))
221            .build();
222        assert_eq!(
223            static_bw.mahimahi_to_string(&Duration::from_millis(5)),
224            "1\n2\n3\n4\n5"
225        );
226        let a = vec![
227            Box::new(
228                StaticBwConfig::new()
229                    .bw(Bandwidth::from_mbps(12))
230                    .duration(Duration::from_millis(2)),
231            ) as Box<dyn BwTraceConfig>,
232            Box::new(
233                StaticBwConfig::new()
234                    .bw(Bandwidth::from_mbps(24))
235                    .duration(Duration::from_millis(2)),
236            ) as Box<dyn BwTraceConfig>,
237        ];
238        let mut c = Box::new(RepeatedBwPatternConfig::new().pattern(a).count(2)).into_model();
239        assert_eq!(
240            c.mahimahi(&Duration::MAX),
241            [1, 2, 3, 3, 4, 4, 5, 6, 7, 7, 8, 8]
242        );
243    }
244
245    #[test]
246    fn test_load() {
247        assert!(matches!(
248            load_mahimahi_trace(vec![0, 2, 4, 3], None),
249            Err("timestamps must be monotonically nondecreasing")
250        ));
251        assert!(matches!(
252            load_mahimahi_trace(vec![0, 0, 0], None),
253            Err("trace must last for a nonzero amount of time")
254        ));
255
256        let trace = vec![1, 1, 5, 6];
257        let mut bw = load_mahimahi_trace(trace, None).unwrap().build();
258        // first cycle
259        assert_eq!(
260            bw.next_bw(),
261            Some((Bandwidth::from_mbps(24), Duration::from_millis(1)))
262        );
263        assert_eq!(
264            bw.next_bw(),
265            Some((Bandwidth::from_mbps(0), Duration::from_millis(3)))
266        );
267        assert_eq!(
268            bw.next_bw(),
269            Some((Bandwidth::from_mbps(12), Duration::from_millis(2)))
270        );
271        // second cycle
272        assert_eq!(
273            bw.next_bw(),
274            Some((Bandwidth::from_mbps(24), Duration::from_millis(1)))
275        );
276
277        let trace = vec![0, 0, 2, 2, 3, 3, 6, 6];
278        let mut bw = load_mahimahi_trace(trace, Some(0)).unwrap().build();
279        // first cycle
280        assert_eq!(
281            bw.next_bw(),
282            Some((Bandwidth::from_mbps(0), Duration::from_millis(1)))
283        );
284        assert_eq!(
285            bw.next_bw(),
286            Some((Bandwidth::from_mbps(24), Duration::from_millis(2)))
287        );
288        assert_eq!(
289            bw.next_bw(),
290            Some((Bandwidth::from_mbps(0), Duration::from_millis(2)))
291        );
292        assert_eq!(
293            bw.next_bw(),
294            Some((Bandwidth::from_mbps(48), Duration::from_millis(1)))
295        );
296        // second cycle
297        assert_eq!(
298            bw.next_bw(),
299            Some((Bandwidth::from_mbps(0), Duration::from_millis(1)))
300        );
301        assert_eq!(
302            bw.next_bw(),
303            Some((Bandwidth::from_mbps(24), Duration::from_millis(2)))
304        );
305
306        let mut bw = RepeatedBwPatternConfig::new()
307            .count(2)
308            .pattern(vec![
309                Box::new(load_mahimahi_trace(vec![1, 1, 2, 2, 3, 3], Some(1)).unwrap())
310                    as Box<dyn BwTraceConfig>,
311                Box::new(load_mahimahi_trace(vec![1, 2], Some(2)).unwrap())
312                    as Box<dyn BwTraceConfig>,
313            ])
314            .build();
315        assert_eq!(
316            bw.next_bw(),
317            Some((Bandwidth::from_mbps(24), Duration::from_millis(3)))
318        );
319        assert_eq!(
320            bw.next_bw(),
321            Some((Bandwidth::from_mbps(12), Duration::from_millis(2)))
322        );
323        assert_eq!(
324            bw.next_bw(),
325            Some((Bandwidth::from_mbps(12), Duration::from_millis(2)))
326        );
327        assert_eq!(
328            bw.next_bw(),
329            Some((Bandwidth::from_mbps(24), Duration::from_millis(3)))
330        );
331        assert_eq!(
332            bw.next_bw(),
333            Some((Bandwidth::from_mbps(12), Duration::from_millis(2)))
334        );
335        assert_eq!(
336            bw.next_bw(),
337            Some((Bandwidth::from_mbps(12), Duration::from_millis(2)))
338        );
339        assert_eq!(bw.next_bw(), None);
340    }
341
342    #[test]
343    fn test_interoperability() {
344        // this check only works on non-zero timestamps trace, which has full interoperability
345        let check = |trace: Vec<u64>| {
346            let mut bw = load_mahimahi_trace(trace.clone(), None).unwrap().build();
347            assert_eq!(
348                bw.mahimahi(&Duration::from_millis(*trace.last().unwrap())),
349                trace
350            );
351        };
352        check(vec![1, 1, 5, 6]);
353        check(vec![2, 2, 3, 3, 4, 4, 5, 5, 8, 9]);
354
355        let mut bw = load_mahimahi_trace(vec![0, 0, 2, 2, 3, 3, 6, 6], None)
356            .unwrap()
357            .build();
358        assert_eq!(
359            bw.mahimahi(&Duration::from_millis(12)),
360            vec![2, 2, 3, 3, 6, 6, 6, 6, 8, 8, 9, 9, 12, 12, 12, 12]
361        );
362
363        let mut bw = RepeatedBwPatternConfig::new()
364            .count(2)
365            .pattern(vec![
366                Box::new(load_mahimahi_trace(vec![1, 1, 2, 2, 3, 3], Some(1)).unwrap())
367                    as Box<dyn BwTraceConfig>,
368                Box::new(load_mahimahi_trace(vec![1, 2], Some(2)).unwrap())
369                    as Box<dyn BwTraceConfig>,
370            ])
371            .build();
372        assert_eq!(
373            bw.mahimahi(&Duration::MAX),
374            vec![1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 12, 13, 14]
375        );
376    }
377}