ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
#!/usr/bin/env python3
"""Plot etherparse (--verify) overhead from a pibench sweep CSV.

Pairs each backend with its ":verify" variant (ring vs ring_verify, pcap vs pcap_verify), matched
by frame size, and plots absolute per-frame CPU (capture alone vs capture+parse) and the isolated
etherparse cost (verify minus plain). Feed it the merged plain+verify sweep CSV.

Usage: pibench-plot-verify.py [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-verify.png"

# backend -> { frame_len: (pps, cpu_us_frame) }
data = defaultdict(dict)
with open(csv_path) as f:
    for row in csv.DictReader(f):
        try:
            data[row["backend"]][int(row["len"])] = (float(row["pps"]), float(row["cpu_us_frame"]))
        except (ValueError, KeyError):
            continue

# (plain backend, verify backend, colour, label)
PAIRS = [
    ("ring", "ring_verify", "#1b9e77", "ferranet ring"),
    ("pcap", "pcap_verify", "#e7298a", "libpcap"),
]

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))

for plain, verify, color, label in PAIRS:
    if plain not in data or verify not in data:
        continue
    lens = sorted(set(data[plain]) & set(data[verify]))
    xs = [data[plain][l][0] / 1000 for l in lens]                       # kpps (plain run)
    ax1.plot(xs, [data[plain][l][1] for l in lens],
             marker="o", color=color, label=f"{label} (capture only)")
    ax1.plot(xs, [data[verify][l][1] for l in lens],
             marker="D", ls="--", color=color, label=f"{label} + etherparse")
    ax2.plot(xs, [data[verify][l][1] - data[plain][l][1] for l in lens],
             marker="s", color=color, label=label)

ax1.set(title="Per-frame CPU: capture vs capture + etherparse",
        xlabel="received packets/sec (kpps)", ylabel="CPU µs / frame")
ax2.set(title="Isolated etherparse overhead (verify − plain)",
        xlabel="received packets/sec (kpps)", ylabel="added CPU µs / frame")
for ax in (ax1, ax2):
    ax.grid(True, alpha=0.3)
    ax.legend()
    ax.set_ylim(bottom=0)

fig.suptitle("etherparse parse + classify + IPv4-checksum cost per frame (Raspberry Pi Zero 2 W)",
             fontsize=12)
fig.tight_layout(rect=(0, 0, 1, 0.95))
fig.savefig(out_path, dpi=120)
print(f"wrote {out_path}")