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>
122 lines
4.1 KiB
Python
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()
|