ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
#!/usr/bin/env python3
"""Plot a pibench-sweep.csv into CPU-vs-pps curves for the three backends.

Usage: pibench-plot.py [pibench-sweep.csv] [out.png]
"""
import csv
import sys
from collections import defaultdict

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

csv_path = sys.argv[1] if len(sys.argv) > 1 else "pibench-sweep.csv"
out_path = sys.argv[2] if len(sys.argv) > 2 else "pibench-sweep.png"

# backend -> list of (pps, proc_cpu_pct, cpu_us_frame, drops)
data = defaultdict(list)
with open(csv_path) as f:
    for row in csv.DictReader(f):
        try:
            pps = float(row["pps"])
        except (ValueError, KeyError):
            continue
        drops = row["drops"]
        drops = float(drops) if drops not in ("n/a", "NA", "") else None
        data[row["backend"]].append((
            pps,
            float(row["proc_cpu_pct"]),
            float(row["cpu_us_frame"]),
            drops,
        ))

# Stable order/colour per backend; ring is the hero line.
STYLE = {
    "ring":  ("#1b9e77", "o", "ferranet ring (zero-copy mmap, V3)"),
    "basic": ("#d95f02", "s", "ferranet basic (recvfrom)"),
    "pnet":  ("#7570b3", "^", "libpnet (recvfrom)"),
    "pcap":  ("#e7298a", "D", "libpcap (PACKET_MMAP)"),
}
order = [b for b in ("ring", "basic", "pnet", "pcap") if b in data]

fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16, 5))

for be in order:
    pts = sorted(data[be])
    color, marker, label = STYLE[be]
    xs = [p[0] / 1000 for p in pts]                # kpps
    ax1.plot(xs, [p[1] for p in pts], marker=marker, color=color, label=label)
    ax2.plot(xs, [p[2] for p in pts], marker=marker, color=color, label=label)
    dx = [p[0] / 1000 for p in pts if p[3] is not None]
    dy = [p[3] for p in pts if p[3] is not None]
    if dx:
        ax3.plot(dx, dy, marker=marker, color=color, label=label)

ax1.set(title="Capture-process CPU vs offered pps",
        xlabel="received packets/sec (kpps)", ylabel="proc_cpu (% of one core)")
ax2.set(title="Per-frame CPU cost vs offered pps",
        xlabel="received packets/sec (kpps)", ylabel="CPU µs / frame")
ax3.set(title="Kernel drops vs offered pps",
        xlabel="received packets/sec (kpps)", ylabel="drops in window")
for ax in (ax1, ax2, ax3):
    ax.grid(True, alpha=0.3)
    ax.legend()
    ax.set_ylim(bottom=0)

fig.suptitle("ferranet capture backends on Raspberry Pi Zero 2 W\n"
             "(direct 100M link saturated; pps swept via frame size 1472B→18B, reps averaged)",
             fontsize=12)
fig.tight_layout(rect=(0, 0, 1, 0.96))
fig.savefig(out_path, dpi=120)
print(f"wrote {out_path}")