vmsilo/bufferbloat-test/parse-results.py
Davíð Steinn Geirsson fa7c0693cb fix(bufferbloat-test): send SIGINT to ping and parse summary stats
Use kill -INT so ping prints its statistics summary (packets
transmitted/received, rtt mdev) before exiting. Parse these from the
output and include them in the latency results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:50:34 +00:00

122 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Parse iperf3 JSON and ping output, output combined results JSON."""
import json
import re
import sys
def parse_iperf(path):
"""Parse iperf3 JSON output for throughput stats."""
try:
with open(path) as f:
data = json.load(f)
# Average from end summary
avg_bps = data.get("end", {}).get("sum_sent", {}).get("bits_per_second", 0)
avg_mbps = avg_bps / 1_000_000
# Min/max from intervals
intervals = data.get("intervals", [])
if intervals:
rates = [i.get("sum", {}).get("bits_per_second", 0) for i in intervals]
min_mbps = min(rates) / 1_000_000
max_mbps = max(rates) / 1_000_000
else:
min_mbps = avg_mbps
max_mbps = avg_mbps
return {"avg": avg_mbps, "min": min_mbps, "max": max_mbps}
except Exception as e:
print(f"iperf parse error: {e}", file=sys.stderr)
return {"avg": 0, "min": 0, "max": 0}
def parse_ping(path):
"""Parse ping output for latency stats."""
try:
with open(path) as f:
content = f.read()
# Extract individual RTT values from "time=X.XX ms" patterns
# Handles both "time=1.23 ms" and "time=1.23ms" formats
rtt_pattern = re.compile(r"time[=<](\d+\.?\d*)\s*ms", re.IGNORECASE)
rtts = [float(m.group(1)) for m in rtt_pattern.finditer(content)]
print(f"Found {len(rtts)} ping samples", file=sys.stderr)
if rtts:
print(f"RTT samples: {rtts[:5]}{'...' if len(rtts) > 5 else ''}", file=sys.stderr)
# Parse summary statistics (from SIGINT termination)
# "6 packets transmitted, 6 received, 0% packet loss, time 5155ms"
pkt_match = re.search(r"(\d+) packets transmitted, (\d+) received", content)
packets_transmitted = int(pkt_match.group(1)) if pkt_match else 0
packets_received = int(pkt_match.group(2)) if pkt_match else 0
# "rtt min/avg/max/mdev = 0.238/0.331/0.381/0.048 ms"
mdev_match = re.search(
r"rtt min/avg/max/mdev\s*=\s*[\d.]+/[\d.]+/[\d.]+/([\d.]+)\s*ms", content
)
mdev = float(mdev_match.group(1)) if mdev_match else 0
if not rtts:
# Try parsing summary line as fallback
summary = re.search(
r"rtt min/avg/max/mdev\s*=\s*([\d.]+)/([\d.]+)/([\d.]+)", content
)
if summary:
print("Using summary line fallback", file=sys.stderr)
return {
"min": float(summary.group(1)),
"avg": float(summary.group(2)),
"max": float(summary.group(3)),
"p99": float(summary.group(3)), # Use max as p99 approximation
"mdev": mdev,
"packets_transmitted": packets_transmitted,
"packets_received": packets_received,
}
print("No ping data found", file=sys.stderr)
return {
"min": 0, "avg": 0, "max": 0, "p99": 0,
"mdev": 0, "packets_transmitted": 0, "packets_received": 0,
}
# Calculate stats from individual samples
rtts_sorted = sorted(rtts)
n = len(rtts_sorted)
p99_idx = int(n * 0.99)
if p99_idx >= n:
p99_idx = n - 1
return {
"min": min(rtts),
"avg": sum(rtts) / len(rtts),
"max": max(rtts),
"p99": rtts_sorted[p99_idx],
"mdev": mdev,
"packets_transmitted": packets_transmitted,
"packets_received": packets_received,
}
except Exception as e:
print(f"ping parse error: {e}", file=sys.stderr)
return {"min": 0, "avg": 0, "max": 0, "p99": 0}
def main():
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <iperf.json> <ping.txt>", file=sys.stderr)
sys.exit(1)
iperf_path = sys.argv[1]
ping_path = sys.argv[2]
throughput = parse_iperf(iperf_path)
latency = parse_ping(ping_path)
result = {"throughput_mbps": throughput, "latency_ms": latency}
print(json.dumps(result))
if __name__ == "__main__":
main()