[RFC net-next 5/6] selftests: drv-net: add a kperf runner
From: Stanislav Fomichev <hidden>
Date: 2026-09-16 19:04:22
Subsystem:
kernel selftest framework, networking drivers, the rest · Maintainers:
Shuah Khan, Shuah Khan, Andrew Lunn, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
Add a helper to manage kperf servers on both endpoints. Run the client and report aggregate throughput and retransmits. Use a unique PID file for cleanup and keep the external kperf tools optional. Signed-off-by: Stanislav Fomichev <sdf@fomichev.me> --- .../testing/selftests/drivers/net/README.rst | 8 + .../drivers/net/hw/lib/py/__init__.py | 5 +- .../selftests/drivers/net/lib/py/__init__.py | 4 +- .../selftests/drivers/net/lib/py/kperf.py | 148 ++++++++++++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 tools/testing/selftests/drivers/net/lib/py/kperf.py
diff --git a/tools/testing/selftests/drivers/net/README.rst b/tools/testing/selftests/drivers/net/README.rst
index 03373d6cecb0..36a79148c0b5 100644
--- a/tools/testing/selftests/drivers/net/README.rst
+++ b/tools/testing/selftests/drivers/net/README.rst@@ -141,6 +141,14 @@ Arguments used to construct the communication channel. for netns - name of the "remote" namespace for ssh - name/address of the remote host +kperf +~~~~~ + +Performance tests which use kperf expect ``kperf-client`` in ``PATH`` on the +local host and ``kperf-server`` in ``PATH`` on both endpoints. Kperf is an +optional dependency; tests which require it are skipped when it is not +installed. + Example =======
diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index 857bd016e5e0..5502969bfee4 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py@@ -34,7 +34,7 @@ KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve() from net.lib.py import ksft_eq, ksft_ge, ksft_in, ksft_is, ksft_lt, \ ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none from drivers.net.lib.py import GenerateTraffic, Remote, SystemMonitor, \ - Iperf3Runner + Iperf3Runner, KperfResult, KperfRunner from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
@@ -55,7 +55,8 @@ KSFT_DIR = (Path(__file__).parent / "../../../../..").resolve() "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt", "ksft_not_none", "ksft_not_none", "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic", - "Remote", "Iperf3Runner", "SystemMonitor"] + "Remote", "Iperf3Runner", "SystemMonitor", "KperfResult", + "KperfRunner"] except ModuleNotFoundError as e: print("Failed importing `net` library from kernel sources") print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index e418837f0b00..5f75ce61fb65 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py@@ -53,12 +53,14 @@ KSFT_DIR = (Path(__file__).parent / "../../../..").resolve() "ksft_not_none", "ksft_not_none"] from .env import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv + from .kperf import KperfResult, KperfRunner from .load import GenerateTraffic, Iperf3Runner from .system_monitor import SystemMonitor from .remote import Remote __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic", - "Remote", "Iperf3Runner", "SystemMonitor"] + "Remote", "Iperf3Runner", "SystemMonitor", "KperfResult", + "KperfRunner"] except ModuleNotFoundError as e: print("Failed importing `net` library from kernel sources") print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/kperf.py b/tools/testing/selftests/drivers/net/lib/py/kperf.py
new file mode 100644
index 000000000000..9f909dd2507f
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/lib/py/kperf.py@@ -0,0 +1,148 @@ +# SPDX-License-Identifier: GPL-2.0 + +"""Run and parse kperf network performance measurements.""" + +import re +import shlex +import subprocess +import uuid +from dataclasses import dataclass + +from lib.py import cmd, ksft_metric, ksft_metric_aggregate, \ + ksft_metric_reporting_enabled, ksft_pr, wait_port_listen + + +KPERF_PORT = 18323 + +_SECTION_RE = re.compile(r"==\s+(Source|Target)\b") +_RATE_RE = re.compile(r"\b(Tx|Rx)\s*([0-9]+(?:\.[0-9]+)?)\s+Gbps\b") +_TCP_HEADER = "TCP retrans reord rtt rttvar d_ce snd_wnd cwnd" +_TCP_ROW_RE = re.compile(r"^\s*(?:[^:\s]+:\s*)?(\d+)(?:\s+\d+){6}\s*$") + + +@dataclass +class KperfResult: + source_throughput: float + target_throughput: float + source_retransmits: int + target_retransmits: int + + +class KperfRunner: + """Manage kperf servers and run measurements between test endpoints.""" + + def __init__(self, env): + self.env = env + self._pid_file = f"/tmp/ksft-kperf-{uuid.uuid4().hex[:8]}.pid" + self._started = [] + env.require_cmd("kperf-client", local=True) + env.require_cmd("kperf-server", local=True, remote=True) + + def _server(self, remote, kill=False): + host = self.env.remote if remote else None + address = self.env.remote_addr if remote else self.env.addr + args = ["kperf-server", "--pid-file", self._pid_file] + args += ["--kill"] if kill else ["--addr", address] + return cmd(shlex.join(args) if remote else args, host=host, + fail=not kill) + + def __enter__(self): + try: + for remote in (False, True): + self._server(remote) + self._started.append(remote) + wait_port_listen(KPERF_PORT) + wait_port_listen(KPERF_PORT, host=self.env.remote) + except Exception: + self.close() + raise + return self + + def close(self): + while self._started: + self._server(self._started.pop(), kill=True) + + def __exit__(self, _exc_type, _exc_value, _exc_tb): + self.close() + + def run(self, client_args=()): + """Run kperf and report results when a SystemMonitor is active.""" + argv = ["kperf-client", "--src", self.env.remote_addr, + "--dst", self.env.addr] + argv += [str(arg) for arg in client_args] + command = cmd(argv, background=True) + try: + command.process(terminate=False, fail=False, timeout=300) + except subprocess.TimeoutExpired as error: + command.proc.kill() + command.process(terminate=False, fail=False) + raise RuntimeError(f"kperf client timed out\n{command!r}") \ + from error + if command.ret: + raise RuntimeError( + f"kperf client exited with status {command.ret}\n{command!r}") + + try: + result = self.parse(command.stdout + "\n" + command.stderr) + except ValueError: + ksft_pr(command) + raise + + if ksft_metric_reporting_enabled(): + ksft_metric_aggregate( + "throughput", summarize="distribution", + regression={"compare": "p50", "better": "higher", + "relative_tolerance": 0.05}) + ksft_metric_aggregate("tcp.retransmits", summarize="max") + for host, throughput, retransmits in ( + ("remote", result.source_throughput, + result.source_retransmits), + ("local", result.target_throughput, + result.target_retransmits)): + ksft_metric("throughput", throughput, shape="scalar", + host=host) + ksft_metric("tcp.retransmits", retransmits, shape="scalar", + host=host) + return result + + @staticmethod + def parse(output): + """Parse human-readable kperf client output.""" + throughput = {"Source": [], "Target": []} + retransmits = {"Source": [], "Target": []} + section = None + in_tcp = False + + for line in output.splitlines(): + match = _SECTION_RE.search(line) + if match: + section = match.group(1) + in_tcp = False + continue + if section is None: + continue + + match = _RATE_RE.search(line) + if match: + direction, value = match.groups() + if (section, direction) in (("Source", "Tx"), + ("Target", "Rx")): + throughput[section].append(float(value)) + continue + + if _TCP_HEADER in line: + in_tcp = True + continue + if in_tcp: + match = _TCP_ROW_RE.match(line) + if match: + retransmits[section].append(int(match.group(1))) + + values = (*throughput.values(), *retransmits.values()) + if not all(values): + raise ValueError("incomplete kperf result") + + return KperfResult(sum(throughput["Source"]), + sum(throughput["Target"]), + sum(retransmits["Source"]), + sum(retransmits["Target"]))
--
2.53.0-Meta