Commit 2a60ce94 authored by Carolina Jubran's avatar Carolina Jubran Committed by Jakub Kicinski
Browse files

selftests: drv-net: introduce Iperf3Runner for measurement use cases



GenerateTraffic was added to spin up long-running iperf3 load, mainly
to drive high PPS background traffic. It was never meant to provide
stable throughput numbers, and trying to repurpose it for measurement
does not make sense.

Introduce Iperf3Runner to allow tests to split out server/client
configuration, control start/stop, and collect JSON output for
analysis. This makes it possible to measure bandwidth directly when
validating egress shaping.

GenerateTraffic stays as the background load generator, reusing the
common iperf3 helpers under the hood.

Signed-off-by: default avatarCarolina Jubran <cjubran@nvidia.com>
Reviewed-by: default avatarCosmin Ratiu <cratiu@nvidia.com>
Reviewed-by: default avatarNimrod Oren <noren@nvidia.com>
Link: https://patch.msgid.link/20251130091938.4109055-3-cjubran@nvidia.com


Signed-off-by: default avatarJakub Kicinski <kuba@kernel.org>
parent a8658f7b
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ try:
        ksft_setup, ksft_variants, KsftNamedVariant
    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
    from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
    from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv

    __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev",
@@ -44,7 +44,8 @@ try:
               "ksft_eq", "ksft_ge", "ksft_in", "ksft_is", "ksft_lt",
               "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
               "ksft_not_none", "ksft_not_none",
               "NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote"]
               "NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote",
               "Iperf3Runner"]
except ModuleNotFoundError as e:
    print("Failed importing `net` library from kernel sources")
    print(str(e))
+3 −2
Original line number Diff line number Diff line
@@ -44,10 +44,11 @@ try:
               "ksft_not_none", "ksft_not_none"]

    from .env import NetDrvEnv, NetDrvEpEnv
    from .load import GenerateTraffic
    from .load import GenerateTraffic, Iperf3Runner
    from .remote import Remote

    __all__ += ["NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote"]
    __all__ += ["NetDrvEnv", "NetDrvEpEnv", "GenerateTraffic", "Remote",
                "Iperf3Runner"]
except ModuleNotFoundError as e:
    print("Failed importing `net` library from kernel sources")
    print(str(e))
+76 −8
Original line number Diff line number Diff line
@@ -2,21 +2,89 @@

import re
import time
import json

from lib.py import ksft_pr, cmd, ip, rand_port, wait_port_listen

class GenerateTraffic:
    def __init__(self, env, port=None):
        env.require_cmd("iperf3", local=True, remote=True)

class Iperf3Runner:
    """
    Sets up and runs iperf3 traffic.
    """
    def __init__(self, env, port=None, server_ip=None, client_ip=None):
        env.require_cmd("iperf3", local=True, remote=True)
        self.env = env

        self.port = rand_port() if port is None else port
        self._iperf_server = cmd(f"iperf3 -s -1 -p {self.port}", background=True)
        self.server_ip = server_ip
        self.client_ip = client_ip

    def _build_server(self):
        cmdline = f"iperf3 -s -1 -p {self.port}"
        if self.server_ip:
            cmdline += f" -B {self.server_ip}"
        return cmdline

    def _build_client(self, streams, duration, reverse):
        host = self.env.addr if self.server_ip is None else self.server_ip
        cmdline = f"iperf3 -c {host} -p {self.port} -P {streams} -t {duration} -J"
        if self.client_ip:
            cmdline += f" -B {self.client_ip}"
        if reverse:
            cmdline += " --reverse"
        return cmdline

    def start_server(self):
        """
        Starts an iperf3 server with optional bind IP.
        """
        cmdline = self._build_server()
        proc = cmd(cmdline, background=True)
        wait_port_listen(self.port)
        time.sleep(0.1)
        self._iperf_client = cmd(f"iperf3 -c {env.addr} -P 16 -p {self.port} -t 86400",
                                 background=True, host=env.remote)
        return proc

    def start_client(self, background=False, streams=1, duration=10, reverse=False):
        """
        Starts the iperf3 client with the configured options.
        """
        cmdline = self._build_client(streams, duration, reverse)
        return cmd(cmdline, background=background, host=self.env.remote)

    def measure_bandwidth(self, reverse=False):
        """
        Runs an iperf3 measurement and returns the average bandwidth (Gbps).
        Discards the first and last few reporting intervals and uses only the
        middle part of the run where throughput is typically stable.
        """
        self.start_server()
        result = self.start_client(duration=10, reverse=reverse)

        if result.ret != 0:
            raise RuntimeError("iperf3 failed to run successfully")
        try:
            out = json.loads(result.stdout)
        except json.JSONDecodeError as exc:
            raise ValueError("Failed to parse iperf3 JSON output") from exc

        intervals = out.get("intervals", [])
        samples = [i["sum"]["bits_per_second"] / 1e9 for i in intervals]
        if len(samples) < 10:
            raise ValueError(f"iperf3 returned too few intervals: {len(samples)}")
        # Discard potentially unstable first and last 3 seconds.
        stable = samples[3:-3]

        avg = sum(stable) / len(stable)

        return avg


class GenerateTraffic:
    def __init__(self, env, port=None):
        self.env = env
        self.runner = Iperf3Runner(env, port)

        self._iperf_server = self.runner.start_server()
        self._iperf_client = self.runner.start_client(background=True, streams=16, duration=86400)

        # Wait for traffic to ramp up
        if not self._wait_pkts(pps=1000):
@@ -61,7 +129,7 @@ class GenerateTraffic:
    def _wait_client_stopped(self, sleep=0.005, timeout=5):
        end = time.monotonic() + timeout

        live_port_pattern = re.compile(fr":{self.port:04X} 0[^6] ")
        live_port_pattern = re.compile(fr":{self.runner.port:04X} 0[^6] ")

        while time.monotonic() < end:
            data = cmd("cat /proc/net/tcp*", host=self.env.remote).stdout