Skip to content

Commit 947d5e9

Browse files
committed
feat(trace): support extension to convert to series
1 parent 2b986db commit 947d5e9

File tree

6 files changed

+1141
-12
lines changed

6 files changed

+1141
-12
lines changed

Cargo.toml

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ homepage = "https://github.com/stack-rs/netem-trace"
99
repository = "https://github.com/stack-rs/netem-trace"
1010
keywords = ["emulation", "trace", "network", "utility", "model"]
1111
documentation = "https://docs.rs/netem-trace"
12-
categories = ["network-programming", "config", "development-tools", "simulation"]
12+
categories = [
13+
"network-programming",
14+
"config",
15+
"development-tools",
16+
"simulation",
17+
]
1318
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1419

1520
[dependencies]
@@ -21,6 +26,7 @@ itertools = { version = "0.14.0", optional = true }
2126
rand = { version = "0.9.1", optional = true }
2227
rand_distr = { version = "0.5.1", optional = true }
2328
serde = { version = "1.0", features = ["derive"], optional = true }
29+
serde_json = { version = "1.0", optional = true }
2430
statrs = { version = "0.18.0", optional = true }
2531
typetag = { version = "0.2.5", optional = true }
2632

@@ -32,11 +38,11 @@ serde_json = "1.0"
3238
[features]
3339
default = ["model"]
3440
model = [
35-
"bw-model",
36-
"delay-model",
37-
"delay-per-packet-model",
38-
"loss-model",
39-
"duplicate-model"
41+
"bw-model",
42+
"delay-model",
43+
"delay-per-packet-model",
44+
"loss-model",
45+
"duplicate-model",
4046
]
4147
bw-model = ["dep:rand", "dep:rand_distr", "dep:dyn-clone"]
4248
delay-model = ["dep:dyn-clone"]
@@ -46,13 +52,18 @@ duplicate-model = ["dep:dyn-clone"]
4652
serde = ["dep:serde", "dep:typetag", "bandwidth/serde"]
4753
mahimahi = ["dep:itertools"]
4854
human = [
49-
"serde",
50-
"dep:humantime-serde",
51-
"dep:human-bandwidth",
52-
"human-bandwidth/serde"
55+
"serde",
56+
"dep:humantime-serde",
57+
"dep:human-bandwidth",
58+
"human-bandwidth/serde",
5359
]
54-
full = ["model", "mahimahi", "human", "truncated-normal"]
60+
trace-ext = ["serde", "dep:serde_json"]
61+
full = ["model", "mahimahi", "human", "truncated-normal", "trace-ext"]
5562
truncated-normal = ["statrs"]
5663

64+
[[example]]
65+
name = "plot_traces"
66+
required-features = ["trace-ext", "model"]
67+
5768
[package.metadata.docs.rs]
5869
all-features = true

README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![github-repo](https://img.shields.io/badge/github-stack--rs/netem--trace-f5dc23?logo=github)](https://github.com/stack-rs/netem-trace)
44
[![crates.io](https://img.shields.io/crates/v/netem-trace.svg?logo=rust)](https://crates.io/crates/netem-trace)
5-
[![docs.rs](https://img.shields.io/badge/docs.rs-netem--trace-blue?logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/netem-trace)
5+
[![docs.rs](https://img.shields.io/badge/docs.rs-netem--trace-blue?logo=docsdotrs)](https://docs.rs/netem-trace)
66
[![LICENSE Apache-2.0](https://img.shields.io/github/license/stack-rs/netem-trace?logo=Apache)](https://github.com/stack-rs/netem-trace/blob/main/LICENSE)
77

88
A library for generating network emulation trace. Now only supported [mahimahi](https://github.com/ravinet/mahimahi).
@@ -158,6 +158,82 @@ assert_eq!(
158158
);
159159
```
160160

161+
## Plotting and Visualization
162+
163+
The `series` module (requires `trace-ext` feature) allows you to expand trace models into time series data for plotting and analysis.
164+
165+
### Quick Example
166+
167+
```rust
168+
use netem_trace::model::StaticBwConfig;
169+
use netem_trace::series::{expand_bw_trace, write_bw_series_json, write_bw_series_csv};
170+
use netem_trace::{Bandwidth, Duration};
171+
172+
// Create and expand a trace
173+
let mut trace = StaticBwConfig::new()
174+
.bw(Bandwidth::from_mbps(10))
175+
.duration(Duration::from_secs(5))
176+
.build();
177+
178+
let series = expand_bw_trace(&mut trace, Duration::from_secs(0), Duration::from_secs(5));
179+
180+
// Export to JSON or CSV
181+
write_bw_series_json(&series, "trace.json").unwrap();
182+
write_bw_series_csv(&series, "trace.csv").unwrap();
183+
```
184+
185+
### Supported Trace Types
186+
187+
- **Bandwidth traces**: `expand_bw_trace()``BwSeriesPoint` (start_time, bandwidth, duration)
188+
- **Delay traces**: `expand_delay_trace()``DelaySeriesPoint` (start_time, delay, duration)
189+
- **Per-packet delay**: `expand_delay_per_packet_trace()``DelayPerPacketSeriesPoint` (packet_index, delay)
190+
- **Loss/Duplicate traces**: `expand_loss_trace()`, `expand_duplicate_trace()`
191+
192+
### Export Formats
193+
194+
**JSON**: Full structure with nested fields
195+
196+
```json
197+
[{"start_time": 0.0, "value": {"gbps": 0, "bps": 10000000}, "duration": 1.0}, ...]
198+
199+
```
200+
201+
**CSV**: Flat format for easy plotting
202+
203+
```csv
204+
start_time_secs,bandwidth_bps,duration_secs
205+
0.0,10000000,1.0
206+
```
207+
208+
### Plotting in Python
209+
210+
```python
211+
import json
212+
import matplotlib.pyplot as plt
213+
214+
with open('trace.json') as f:
215+
data = json.load(f)
216+
217+
times, bandwidths = [], []
218+
for point in data:
219+
start = point['start_time']
220+
duration = point['duration']
221+
bw = point['value']['bps'] / 1_000_000
222+
times.extend([start, start + duration])
223+
bandwidths.extend([bw, bw])
224+
225+
plt.plot(times, bandwidths)
226+
plt.xlabel('Time (s)')
227+
plt.ylabel('Bandwidth (Mbps)')
228+
plt.show()
229+
```
230+
231+
See `examples/plot_traces.rs` and `examples/plot_example.py` for complete examples. Run with:
232+
233+
```bash
234+
cargo run --example plot_traces --features trace-ext
235+
```
236+
161237
## Maintainer
162238

163239
[@BobAnkh](https://github.com/BobAnkh)

examples/plot_example.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example Python script demonstrating how to plot trace series exported from Rust.
4+
5+
This script reads the JSON/CSV files generated by the plot_traces example
6+
and creates visualizations using matplotlib.
7+
8+
Requirements:
9+
pip install matplotlib polars
10+
11+
Usage:
12+
python examples/plot_example.py
13+
"""
14+
15+
import json
16+
import polars as pl
17+
import matplotlib.pyplot as plt
18+
19+
20+
def plot_bw_trace_from_json(filename, title):
21+
"""Plot bandwidth trace from JSON file."""
22+
with open(filename, "r") as f:
23+
data = json.load(f)
24+
25+
# Extract data points
26+
times = []
27+
bandwidths = []
28+
29+
for point in data:
30+
start_time = point["start_time"]
31+
duration = point["duration"]
32+
bw_mbps = point["value"]["bps"] / 1_000_000
33+
34+
# Create step plot by adding points at start and end of each segment
35+
times.append(start_time)
36+
bandwidths.append(bw_mbps)
37+
times.append(start_time + duration)
38+
bandwidths.append(bw_mbps)
39+
40+
plt.figure(figsize=(10, 5))
41+
plt.plot(times, bandwidths, linewidth=2)
42+
plt.xlabel("Time (seconds)")
43+
plt.ylabel("Bandwidth (Mbps)")
44+
plt.title(title)
45+
plt.grid(True, alpha=0.3)
46+
plt.tight_layout()
47+
return plt
48+
49+
50+
def plot_bw_trace_from_csv(filename, title):
51+
"""Plot bandwidth trace from CSV file."""
52+
df = pl.read_csv(filename)
53+
54+
# Create step plot
55+
times = []
56+
bandwidths = []
57+
58+
for row in df.iter_rows(named=True):
59+
start_time = row["start_time_secs"]
60+
duration = row["duration_secs"]
61+
bw_mbps = row["bandwidth_bps"] / 1_000_000
62+
63+
times.append(start_time)
64+
bandwidths.append(bw_mbps)
65+
times.append(start_time + duration)
66+
bandwidths.append(bw_mbps)
67+
68+
plt.figure(figsize=(10, 5))
69+
plt.plot(times, bandwidths, linewidth=2)
70+
plt.xlabel("Time (seconds)")
71+
plt.ylabel("Bandwidth (Mbps)")
72+
plt.title(title)
73+
plt.grid(True, alpha=0.3)
74+
plt.tight_layout()
75+
return plt
76+
77+
78+
def plot_delay_per_packet_from_csv(filename, title):
79+
"""Plot per-packet delay from CSV file."""
80+
df = pl.read_csv(filename)
81+
82+
plt.figure(figsize=(10, 5))
83+
plt.plot(
84+
df["packet_index"].to_list(),
85+
(df["delay_secs"] * 1000).to_list(),
86+
"o-",
87+
linewidth=2,
88+
)
89+
plt.xlabel("Packet Index")
90+
plt.ylabel("Delay (ms)")
91+
plt.title(title)
92+
plt.grid(True, alpha=0.3)
93+
plt.tight_layout()
94+
return plt
95+
96+
97+
def create_multi_plot():
98+
"""Create a multi-panel plot showing different trace types."""
99+
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
100+
101+
# Plot 1: Static bandwidth
102+
with open("static_bw.json", "r") as f:
103+
data = json.load(f)
104+
times, bandwidths = [], []
105+
for point in data:
106+
start = point["start_time"]
107+
dur = point["duration"]
108+
bw = point["value"]["bps"] / 1_000_000
109+
times.extend([start, start + dur])
110+
bandwidths.extend([bw, bw])
111+
axes[0, 0].plot(times, bandwidths, linewidth=2)
112+
axes[0, 0].set_xlabel("Time (s)")
113+
axes[0, 0].set_ylabel("Bandwidth (Mbps)")
114+
axes[0, 0].set_title("Static Bandwidth Trace")
115+
axes[0, 0].grid(True, alpha=0.3)
116+
117+
# Plot 2: Sawtooth bandwidth
118+
df = pl.read_csv("sawtooth_bw.csv")
119+
times, bandwidths = [], []
120+
for row in df.iter_rows(named=True):
121+
start = row["start_time_secs"]
122+
dur = row["duration_secs"]
123+
bw = row["bandwidth_bps"] / 1_000_000
124+
times.extend([start, start + dur])
125+
bandwidths.extend([bw, bw])
126+
axes[0, 1].plot(times, bandwidths, linewidth=2, color="orange")
127+
axes[0, 1].set_xlabel("Time (s)")
128+
axes[0, 1].set_ylabel("Bandwidth (Mbps)")
129+
axes[0, 1].set_title("Sawtooth Bandwidth Trace")
130+
axes[0, 1].grid(True, alpha=0.3)
131+
132+
# Plot 3: Normal bandwidth
133+
df = pl.read_csv("normal_bw.csv")
134+
times, bandwidths = [], []
135+
for row in df.iter_rows(named=True):
136+
start = row["start_time_secs"]
137+
dur = row["duration_secs"]
138+
bw = row["bandwidth_bps"] / 1_000_000
139+
times.extend([start, start + dur])
140+
bandwidths.extend([bw, bw])
141+
axes[1, 0].plot(times, bandwidths, linewidth=2, color="green")
142+
axes[1, 0].set_xlabel("Time (s)")
143+
axes[1, 0].set_ylabel("Bandwidth (Mbps)")
144+
axes[1, 0].set_title("Normalized Bandwidth Trace")
145+
axes[1, 0].grid(True, alpha=0.3)
146+
147+
# Plot 4: Repeated pattern
148+
df = pl.read_csv("repeated_bw.csv")
149+
times, bandwidths = [], []
150+
for row in df.iter_rows(named=True):
151+
start = row["start_time_secs"]
152+
dur = row["duration_secs"]
153+
bw = row["bandwidth_bps"] / 1_000_000
154+
times.extend([start, start + dur])
155+
bandwidths.extend([bw, bw])
156+
axes[1, 1].plot(times, bandwidths, linewidth=2, color="red")
157+
axes[1, 1].set_xlabel("Time (s)")
158+
axes[1, 1].set_ylabel("Bandwidth (Mbps)")
159+
axes[1, 1].set_title("Repeated Pattern Trace")
160+
axes[1, 1].grid(True, alpha=0.3)
161+
162+
plt.tight_layout()
163+
return plt
164+
165+
166+
if __name__ == "__main__":
167+
print("Creating plots from exported trace data...")
168+
169+
# Create individual plots
170+
print("1. Plotting static bandwidth trace from JSON...")
171+
plt1 = plot_bw_trace_from_json("static_bw.json", "Static Bandwidth Trace")
172+
plt1.savefig("plot_static_bw.png", dpi=150)
173+
print(" Saved to plot_static_bw.png")
174+
175+
print("2. Plotting sawtooth bandwidth trace from CSV...")
176+
plt2 = plot_bw_trace_from_csv("sawtooth_bw.csv", "Sawtooth Bandwidth Trace")
177+
plt2.savefig("plot_sawtooth_bw.png", dpi=150)
178+
print(" Saved to plot_sawtooth_bw.png")
179+
180+
print("3. Plotting per-packet delay from CSV...")
181+
plt3 = plot_delay_per_packet_from_csv("per_packet_delay.csv", "Per-Packet Delay")
182+
plt3.savefig("plot_per_packet_delay.png", dpi=150)
183+
print(" Saved to plot_per_packet_delay.png")
184+
185+
print("4. Creating multi-panel plot...")
186+
plt4 = create_multi_plot()
187+
plt4.savefig("plot_multi.png", dpi=150)
188+
print(" Saved to plot_multi.png")
189+
190+
print("\nAll plots created successfully!")
191+
print("You can view the plots by opening the PNG files in your image viewer.")

0 commit comments

Comments
 (0)