netem_trace/
series.rs

1//! This module provides functionality to expand trace models into time series data
2//! that can be used for plotting and analysis in other programming languages.
3//!
4//! **Note:** This module is only available when the `trace-ext` feature is enabled.
5//!
6//! ## Overview
7//!
8//! Trace models generate values by calling `next_xxx()` methods. This module provides
9//! functions to expand these traces into a complete series within a specified time range,
10//! making them suitable for visualization and export.
11//!
12//! ## Examples
13//!
14//! ```
15//! # use netem_trace::model::StaticBwConfig;
16//! # use netem_trace::{Bandwidth, Duration, BwTrace};
17//! # use netem_trace::series::expand_bw_trace;
18//! let mut static_bw = StaticBwConfig::new()
19//!     .bw(Bandwidth::from_mbps(24))
20//!     .duration(Duration::from_secs(2))
21//!     .build();
22//!
23//! let series = expand_bw_trace(
24//!     &mut static_bw,
25//!     Duration::from_secs(0),
26//!     Duration::from_secs(2)
27//! );
28//!
29//! assert_eq!(series.len(), 1);
30//! assert_eq!(series[0].start_time, Duration::from_secs(0));
31//! assert_eq!(series[0].value, Bandwidth::from_mbps(24));
32//! assert_eq!(series[0].duration, Duration::from_secs(2));
33//! ```
34
35use crate::{
36    Bandwidth, BwTrace, Delay, DelayPerPacketTrace, DelayTrace, DuplicatePattern, DuplicateTrace,
37    Duration, LossPattern, LossTrace,
38};
39use std::io::Write;
40
41#[cfg(feature = "serde")]
42use serde::{Deserialize, Serialize};
43
44/// A single point in a bandwidth trace series.
45#[derive(Debug, Clone, PartialEq)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47pub struct BwSeriesPoint {
48    /// The time when this bandwidth value starts (relative to trace start)
49    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
50    pub start_time: Duration,
51    /// The bandwidth value
52    pub value: Bandwidth,
53    /// How long this bandwidth value lasts
54    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
55    pub duration: Duration,
56}
57
58/// A single point in a delay trace series.
59#[derive(Debug, Clone, PartialEq)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61pub struct DelaySeriesPoint {
62    /// The time when this delay value starts (relative to trace start)
63    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
64    pub start_time: Duration,
65    /// The delay value
66    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
67    pub value: Delay,
68    /// How long this delay value lasts
69    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
70    pub duration: Duration,
71}
72
73/// A single point in a per-packet delay trace series.
74#[derive(Debug, Clone, PartialEq)]
75#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
76pub struct DelayPerPacketSeriesPoint {
77    /// The packet index (0-based)
78    pub packet_index: usize,
79    /// The delay value for this packet
80    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
81    pub value: Delay,
82}
83
84/// A single point in a loss trace series.
85#[derive(Debug, Clone, PartialEq)]
86#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
87pub struct LossSeriesPoint {
88    /// The time when this loss pattern starts (relative to trace start)
89    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
90    pub start_time: Duration,
91    /// The loss pattern
92    pub value: LossPattern,
93    /// How long this loss pattern lasts
94    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
95    pub duration: Duration,
96}
97
98/// A single point in a duplicate trace series.
99#[derive(Debug, Clone, PartialEq)]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101pub struct DuplicateSeriesPoint {
102    /// The time when this duplicate pattern starts (relative to trace start)
103    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
104    pub start_time: Duration,
105    /// The duplicate pattern
106    pub value: DuplicatePattern,
107    /// How long this duplicate pattern lasts
108    #[cfg_attr(feature = "serde", serde(with = "duration_serde"))]
109    pub duration: Duration,
110}
111
112#[cfg(feature = "serde")]
113mod duration_serde {
114    use serde::{Deserialize, Deserializer, Serializer};
115    use std::time::Duration;
116
117    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
118    where
119        S: Serializer,
120    {
121        let secs = duration.as_secs_f64();
122        serializer.serialize_f64(secs)
123    }
124
125    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
126    where
127        D: Deserializer<'de>,
128    {
129        let secs = f64::deserialize(deserializer)?;
130        Ok(Duration::from_secs_f64(secs))
131    }
132}
133
134/// Expands a bandwidth trace into a series within the specified time range.
135///
136/// This function repeatedly calls `next_bw()` on the trace to build a complete
137/// series, cutting the trace to fit within `start_time` to `end_time`.
138///
139/// # Arguments
140///
141/// * `trace` - The bandwidth trace to expand
142/// * `start_time` - The start time of the desired range
143/// * `end_time` - The end time of the desired range
144///
145/// # Returns
146///
147/// A vector of `BwSeriesPoint` representing the trace within the time range.
148///
149/// # Examples
150///
151/// ```
152/// # use netem_trace::model::StaticBwConfig;
153/// # use netem_trace::{Bandwidth, Duration, BwTrace};
154/// # use netem_trace::series::expand_bw_trace;
155/// let mut trace = StaticBwConfig::new()
156///     .bw(Bandwidth::from_mbps(10))
157///     .duration(Duration::from_secs(5))
158///     .build();
159///
160/// let series = expand_bw_trace(
161///     &mut trace,
162///     Duration::from_secs(1),
163///     Duration::from_secs(4)
164/// );
165///
166/// // The series will be cut to fit [1s, 4s], normalized to start at 0
167/// assert_eq!(series[0].start_time, Duration::from_secs(0));
168/// assert_eq!(series[0].duration, Duration::from_secs(3));
169/// ```
170pub fn expand_bw_trace(
171    trace: &mut dyn BwTrace,
172    start_time: Duration,
173    end_time: Duration,
174) -> Vec<BwSeriesPoint> {
175    let mut series = Vec::new();
176    let mut current_time = Duration::ZERO;
177
178    while let Some((value, duration)) = trace.next_bw() {
179        let segment_end = current_time + duration;
180
181        // Skip segments that end before start_time
182        if segment_end <= start_time {
183            current_time = segment_end;
184            continue;
185        }
186
187        // Stop if we've passed end_time
188        if current_time >= end_time {
189            break;
190        }
191
192        // Calculate the actual start and duration for this segment
193        let actual_start = current_time.max(start_time);
194        let actual_end = segment_end.min(end_time);
195        let actual_duration = actual_end.saturating_sub(actual_start);
196
197        if !actual_duration.is_zero() {
198            series.push(BwSeriesPoint {
199                start_time: actual_start - start_time, // Normalize to start at 0
200                value,
201                duration: actual_duration,
202            });
203        }
204
205        current_time = segment_end;
206
207        // Stop if we've reached end_time
208        if current_time >= end_time {
209            break;
210        }
211    }
212
213    series
214}
215
216/// Expands a delay trace into a series within the specified time range.
217///
218/// This function repeatedly calls `next_delay()` on the trace to build a complete
219/// series, cutting the trace to fit within `start_time` to `end_time`.
220pub fn expand_delay_trace(
221    trace: &mut dyn DelayTrace,
222    start_time: Duration,
223    end_time: Duration,
224) -> Vec<DelaySeriesPoint> {
225    let mut series = Vec::new();
226    let mut current_time = Duration::ZERO;
227
228    while let Some((value, duration)) = trace.next_delay() {
229        let segment_end = current_time + duration;
230
231        if segment_end <= start_time {
232            current_time = segment_end;
233            continue;
234        }
235
236        if current_time >= end_time {
237            break;
238        }
239
240        let actual_start = current_time.max(start_time);
241        let actual_end = segment_end.min(end_time);
242        let actual_duration = actual_end.saturating_sub(actual_start);
243
244        if !actual_duration.is_zero() {
245            series.push(DelaySeriesPoint {
246                start_time: actual_start - start_time,
247                value,
248                duration: actual_duration,
249            });
250        }
251
252        current_time = segment_end;
253
254        if current_time >= end_time {
255            break;
256        }
257    }
258
259    series
260}
261
262/// Expands a per-packet delay trace into a series.
263///
264/// Since per-packet delays don't have time durations, this function collects
265/// delays for a specified number of packets or until the trace ends.
266///
267/// # Arguments
268///
269/// * `trace` - The per-packet delay trace to expand
270/// * `max_packets` - Maximum number of packets to collect (None for unlimited)
271///
272/// # Returns
273///
274/// A vector of `DelayPerPacketSeriesPoint` representing the delays.
275pub fn expand_delay_per_packet_trace(
276    trace: &mut dyn DelayPerPacketTrace,
277    max_packets: Option<usize>,
278) -> Vec<DelayPerPacketSeriesPoint> {
279    let mut series = Vec::new();
280    let mut packet_index = 0;
281
282    while let Some(value) = trace.next_delay() {
283        series.push(DelayPerPacketSeriesPoint {
284            packet_index,
285            value,
286        });
287
288        packet_index += 1;
289
290        if let Some(max) = max_packets {
291            if packet_index >= max {
292                break;
293            }
294        }
295    }
296
297    series
298}
299
300/// Expands a loss trace into a series within the specified time range.
301pub fn expand_loss_trace(
302    trace: &mut dyn LossTrace,
303    start_time: Duration,
304    end_time: Duration,
305) -> Vec<LossSeriesPoint> {
306    let mut series = Vec::new();
307    let mut current_time = Duration::ZERO;
308
309    while let Some((value, duration)) = trace.next_loss() {
310        let segment_end = current_time + duration;
311
312        if segment_end <= start_time {
313            current_time = segment_end;
314            continue;
315        }
316
317        if current_time >= end_time {
318            break;
319        }
320
321        let actual_start = current_time.max(start_time);
322        let actual_end = segment_end.min(end_time);
323        let actual_duration = actual_end.saturating_sub(actual_start);
324
325        if !actual_duration.is_zero() {
326            series.push(LossSeriesPoint {
327                start_time: actual_start - start_time,
328                value,
329                duration: actual_duration,
330            });
331        }
332
333        current_time = segment_end;
334
335        if current_time >= end_time {
336            break;
337        }
338    }
339
340    series
341}
342
343/// Expands a duplicate trace into a series within the specified time range.
344pub fn expand_duplicate_trace(
345    trace: &mut dyn DuplicateTrace,
346    start_time: Duration,
347    end_time: Duration,
348) -> Vec<DuplicateSeriesPoint> {
349    let mut series = Vec::new();
350    let mut current_time = Duration::ZERO;
351
352    while let Some((value, duration)) = trace.next_duplicate() {
353        let segment_end = current_time + duration;
354
355        if segment_end <= start_time {
356            current_time = segment_end;
357            continue;
358        }
359
360        if current_time >= end_time {
361            break;
362        }
363
364        let actual_start = current_time.max(start_time);
365        let actual_end = segment_end.min(end_time);
366        let actual_duration = actual_end.saturating_sub(actual_start);
367
368        if !actual_duration.is_zero() {
369            series.push(DuplicateSeriesPoint {
370                start_time: actual_start - start_time,
371                value,
372                duration: actual_duration,
373            });
374        }
375
376        current_time = segment_end;
377
378        if current_time >= end_time {
379            break;
380        }
381    }
382
383    series
384}
385
386/// Writes a bandwidth series to a file in JSON format.
387///
388/// # Arguments
389///
390/// * `series` - The bandwidth series to write
391/// * `path` - The file path to write to
392///
393/// # Errors
394///
395/// Returns an error if the file cannot be created or written to, or if
396/// serialization fails.
397#[cfg(feature = "serde")]
398pub fn write_bw_series_json<P: AsRef<std::path::Path>>(
399    series: &[BwSeriesPoint],
400    path: P,
401) -> std::io::Result<()> {
402    let json = serde_json::to_string_pretty(series).map_err(std::io::Error::other)?;
403    std::fs::write(path, json)
404}
405
406/// Writes a bandwidth series to a file in CSV format.
407///
408/// The CSV will have columns: start_time_secs, bandwidth_bps, duration_secs
409///
410/// # Arguments
411///
412/// * `series` - The bandwidth series to write
413/// * `path` - The file path to write to
414pub fn write_bw_series_csv<P: AsRef<std::path::Path>>(
415    series: &[BwSeriesPoint],
416    path: P,
417) -> std::io::Result<()> {
418    let mut file = std::fs::File::create(path)?;
419    writeln!(file, "start_time_secs,bandwidth_bps,duration_secs")?;
420
421    for point in series {
422        writeln!(
423            file,
424            "{},{},{}",
425            point.start_time.as_secs_f64(),
426            point.value.as_bps(),
427            point.duration.as_secs_f64()
428        )?;
429    }
430
431    Ok(())
432}
433
434/// Writes a delay series to a file in JSON format.
435#[cfg(feature = "serde")]
436pub fn write_delay_series_json<P: AsRef<std::path::Path>>(
437    series: &[DelaySeriesPoint],
438    path: P,
439) -> std::io::Result<()> {
440    let json = serde_json::to_string_pretty(series).map_err(std::io::Error::other)?;
441    std::fs::write(path, json)
442}
443
444/// Writes a delay series to a file in CSV format.
445///
446/// The CSV will have columns: start_time_secs, delay_secs, duration_secs
447pub fn write_delay_series_csv<P: AsRef<std::path::Path>>(
448    series: &[DelaySeriesPoint],
449    path: P,
450) -> std::io::Result<()> {
451    let mut file = std::fs::File::create(path)?;
452    writeln!(file, "start_time_secs,delay_secs,duration_secs")?;
453
454    for point in series {
455        writeln!(
456            file,
457            "{},{},{}",
458            point.start_time.as_secs_f64(),
459            point.value.as_secs_f64(),
460            point.duration.as_secs_f64()
461        )?;
462    }
463
464    Ok(())
465}
466
467/// Writes a per-packet delay series to a file in JSON format.
468#[cfg(feature = "serde")]
469pub fn write_delay_per_packet_series_json<P: AsRef<std::path::Path>>(
470    series: &[DelayPerPacketSeriesPoint],
471    path: P,
472) -> std::io::Result<()> {
473    let json = serde_json::to_string_pretty(series).map_err(std::io::Error::other)?;
474    std::fs::write(path, json)
475}
476
477/// Writes a per-packet delay series to a file in CSV format.
478///
479/// The CSV will have columns: packet_index, delay_secs
480pub fn write_delay_per_packet_series_csv<P: AsRef<std::path::Path>>(
481    series: &[DelayPerPacketSeriesPoint],
482    path: P,
483) -> std::io::Result<()> {
484    let mut file = std::fs::File::create(path)?;
485    writeln!(file, "packet_index,delay_secs")?;
486
487    for point in series {
488        writeln!(file, "{},{}", point.packet_index, point.value.as_secs_f64())?;
489    }
490
491    Ok(())
492}
493
494/// Writes a loss series to a file in JSON format.
495#[cfg(feature = "serde")]
496pub fn write_loss_series_json<P: AsRef<std::path::Path>>(
497    series: &[LossSeriesPoint],
498    path: P,
499) -> std::io::Result<()> {
500    let json = serde_json::to_string_pretty(series).map_err(std::io::Error::other)?;
501    std::fs::write(path, json)
502}
503
504/// Writes a loss series to a file in CSV format.
505///
506/// The CSV will have columns: start_time_secs, loss_pattern, duration_secs
507/// where loss_pattern is a semicolon-separated list of probabilities.
508pub fn write_loss_series_csv<P: AsRef<std::path::Path>>(
509    series: &[LossSeriesPoint],
510    path: P,
511) -> std::io::Result<()> {
512    let mut file = std::fs::File::create(path)?;
513    writeln!(file, "start_time_secs,loss_pattern,duration_secs")?;
514
515    for point in series {
516        let pattern_str = point
517            .value
518            .iter()
519            .map(|p| p.to_string())
520            .collect::<Vec<_>>()
521            .join(";");
522
523        writeln!(
524            file,
525            "{},{},{}",
526            point.start_time.as_secs_f64(),
527            pattern_str,
528            point.duration.as_secs_f64()
529        )?;
530    }
531
532    Ok(())
533}
534
535/// Writes a duplicate series to a file in JSON format.
536#[cfg(feature = "serde")]
537pub fn write_duplicate_series_json<P: AsRef<std::path::Path>>(
538    series: &[DuplicateSeriesPoint],
539    path: P,
540) -> std::io::Result<()> {
541    let json = serde_json::to_string_pretty(series).map_err(std::io::Error::other)?;
542    std::fs::write(path, json)
543}
544
545/// Writes a duplicate series to a file in CSV format.
546///
547/// The CSV will have columns: start_time_secs, duplicate_pattern, duration_secs
548/// where duplicate_pattern is a semicolon-separated list of probabilities.
549pub fn write_duplicate_series_csv<P: AsRef<std::path::Path>>(
550    series: &[DuplicateSeriesPoint],
551    path: P,
552) -> std::io::Result<()> {
553    let mut file = std::fs::File::create(path)?;
554    writeln!(file, "start_time_secs,duplicate_pattern,duration_secs")?;
555
556    for point in series {
557        let pattern_str = point
558            .value
559            .iter()
560            .map(|p| p.to_string())
561            .collect::<Vec<_>>()
562            .join(";");
563
564        writeln!(
565            file,
566            "{},{},{}",
567            point.start_time.as_secs_f64(),
568            pattern_str,
569            point.duration.as_secs_f64()
570        )?;
571    }
572
573    Ok(())
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use crate::model::StaticBwConfig;
580
581    #[test]
582    fn test_expand_bw_trace_basic() {
583        let mut trace = StaticBwConfig::new()
584            .bw(Bandwidth::from_mbps(10))
585            .duration(Duration::from_secs(5))
586            .build();
587
588        let series = expand_bw_trace(&mut trace, Duration::from_secs(0), Duration::from_secs(5));
589
590        assert_eq!(series.len(), 1);
591        assert_eq!(series[0].start_time, Duration::from_secs(0));
592        assert_eq!(series[0].value, Bandwidth::from_mbps(10));
593        assert_eq!(series[0].duration, Duration::from_secs(5));
594    }
595
596    #[test]
597    fn test_expand_bw_trace_with_cutting() {
598        let mut trace = StaticBwConfig::new()
599            .bw(Bandwidth::from_mbps(10))
600            .duration(Duration::from_secs(10))
601            .build();
602
603        let series = expand_bw_trace(&mut trace, Duration::from_secs(2), Duration::from_secs(7));
604
605        assert_eq!(series.len(), 1);
606        assert_eq!(series[0].start_time, Duration::from_secs(0)); // Normalized to start at 0
607        assert_eq!(series[0].value, Bandwidth::from_mbps(10));
608        assert_eq!(series[0].duration, Duration::from_secs(5)); // 7 - 2 = 5
609    }
610}