xref: /linux/tools/testing/selftests/drivers/net/lib/py/load.py (revision db6b35cffe59c619ea3772b21d7c7c8a7b885dc1)
1# SPDX-License-Identifier: GPL-2.0
2
3import re
4import time
5import json
6
7from lib.py import ksft_pr, cmd, ip, rand_port, wait_port_listen
8
9
10class Iperf3Runner:
11    """
12    Sets up and runs iperf3 traffic.
13    """
14    def __init__(self, env, port=None, server_ip=None, client_ip=None):
15        env.require_cmd("iperf3", local=True, remote=True)
16        self.env = env
17        self.port = rand_port() if port is None else port
18        self.server_ip = server_ip
19        self.client_ip = client_ip
20
21    def _build_server(self):
22        cmdline = f"iperf3 -s -1 -p {self.port}"
23        if self.server_ip:
24            cmdline += f" -B {self.server_ip}"
25        return cmdline
26
27    def _build_client(self, streams, duration, reverse):
28        host = self.env.addr if self.server_ip is None else self.server_ip
29        cmdline = f"iperf3 -c {host} -p {self.port} -P {streams} -t {duration} -J"
30        if self.client_ip:
31            cmdline += f" -B {self.client_ip}"
32        if reverse:
33            cmdline += " --reverse"
34        return cmdline
35
36    def start_server(self):
37        """
38        Starts an iperf3 server with optional bind IP.
39        """
40        cmdline = self._build_server()
41        proc = cmd(cmdline, background=True)
42        wait_port_listen(self.port)
43        time.sleep(0.1)
44        return proc
45
46    def start_client(self, background=False, streams=1, duration=10, reverse=False):
47        """
48        Starts the iperf3 client with the configured options.
49        """
50        cmdline = self._build_client(streams, duration, reverse)
51        return cmd(cmdline, background=background, host=self.env.remote)
52
53    def measure_bandwidth(self, reverse=False):
54        """
55        Runs an iperf3 measurement and returns the average bandwidth (Gbps).
56        Discards the first and last few reporting intervals and uses only the
57        middle part of the run where throughput is typically stable.
58        """
59        self.start_server()
60        result = self.start_client(duration=10, reverse=reverse)
61
62        if result.ret != 0:
63            raise RuntimeError("iperf3 failed to run successfully")
64        try:
65            out = json.loads(result.stdout)
66        except json.JSONDecodeError as exc:
67            raise ValueError("Failed to parse iperf3 JSON output") from exc
68
69        intervals = out.get("intervals", [])
70        samples = [i["sum"]["bits_per_second"] / 1e9 for i in intervals]
71        if len(samples) < 10:
72            raise ValueError(f"iperf3 returned too few intervals: {len(samples)}")
73        # Discard potentially unstable first and last 3 seconds.
74        stable = samples[3:-3]
75
76        avg = sum(stable) / len(stable)
77
78        return avg
79
80
81class GenerateTraffic:
82    def __init__(self, env, port=None):
83        self.env = env
84        self.runner = Iperf3Runner(env, port)
85
86        self._iperf_server = self.runner.start_server()
87        self._iperf_client = self.runner.start_client(background=True, streams=16, duration=86400)
88
89        # Wait for traffic to ramp up
90        if not self._wait_pkts(pps=1000):
91            self.stop(verbose=True)
92            raise Exception("iperf3 traffic did not ramp up")
93
94    def _wait_pkts(self, pkt_cnt=None, pps=None):
95        """
96        Wait until we've seen pkt_cnt or until traffic ramps up to pps.
97        Only one of pkt_cnt or pss can be specified.
98        """
99        pkt_start = ip("-s link show dev " + self.env.ifname, json=True)[0]["stats64"]["rx"]["packets"]
100        for _ in range(50):
101            time.sleep(0.1)
102            pkt_now = ip("-s link show dev " + self.env.ifname, json=True)[0]["stats64"]["rx"]["packets"]
103            if pps:
104                if pkt_now - pkt_start > pps / 10:
105                    return True
106                pkt_start = pkt_now
107            elif pkt_cnt:
108                if pkt_now - pkt_start > pkt_cnt:
109                    return True
110        return False
111
112    def wait_pkts_and_stop(self, pkt_cnt):
113        failed = not self._wait_pkts(pkt_cnt=pkt_cnt)
114        self.stop(verbose=failed)
115
116    def stop(self, verbose=None):
117        self._iperf_client.process(terminate=True)
118        if verbose:
119            ksft_pr(">> Client:")
120            ksft_pr(self._iperf_client.stdout)
121            ksft_pr(self._iperf_client.stderr)
122        self._iperf_server.process(terminate=True)
123        if verbose:
124            ksft_pr(">> Server:")
125            ksft_pr(self._iperf_server.stdout)
126            ksft_pr(self._iperf_server.stderr)
127        self._wait_client_stopped()
128
129    def _wait_client_stopped(self, sleep=0.005, timeout=5):
130        end = time.monotonic() + timeout
131
132        live_port_pattern = re.compile(fr":{self.runner.port:04X} 0[^6] ")
133
134        while time.monotonic() < end:
135            data = cmd("cat /proc/net/tcp*", host=self.env.remote).stdout
136            if not live_port_pattern.search(data):
137                return
138            time.sleep(sleep)
139        raise Exception(f"Waiting for client to stop timed out after {timeout}s")
140