Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests: drv-net: rss_ctx: add tests for RSS configuration and contexts

Add tests focusing on indirection table configuration and
creating extra RSS contexts in drivers which support it.

$ export NETIF=eth0 REMOTE_...
$ ./drivers/net/hw/rss_ctx.py
KTAP version 1
1..8
ok 1 rss_ctx.test_rss_key_indir
ok 2 rss_ctx.test_rss_context
ok 3 rss_ctx.test_rss_context4
# Increasing queue count 44 -> 66
# Failed to create context 32, trying to test what we got
ok 4 rss_ctx.test_rss_context32 # SKIP Tested only 31 contexts, wanted 32
ok 5 rss_ctx.test_rss_context_overlap
ok 6 rss_ctx.test_rss_context_overlap2
# .. sprays traffic like a headless chicken ..
not ok 7 rss_ctx.test_rss_context_out_of_order
ok 8 rss_ctx.test_rss_context4_create_with_cfg
# Totals: pass:6 fail:1 xfail:0 xpass:0 skip:1 error:0

Note that rss_ctx.test_rss_context_out_of_order fails with the device
I tested with, but it seems to be a device / driver bug.

Reviewed-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Willem de Bruijn <willemb@google.com>
Link: https://patch.msgid.link/20240626012456.2326192-5-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>

+399 -5
+1
tools/testing/selftests/drivers/net/hw/Makefile
··· 11 11 hw_stats_l3_gre.sh \ 12 12 loopback.sh \ 13 13 pp_alloc_fail.py \ 14 + rss_ctx.py \ 14 15 # 15 16 16 17 TEST_FILES := \
+383
tools/testing/selftests/drivers/net/hw/rss_ctx.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + import datetime 5 + import random 6 + from lib.py import ksft_run, ksft_pr, ksft_exit, ksft_eq, ksft_ge, ksft_lt 7 + from lib.py import NetDrvEpEnv 8 + from lib.py import NetdevFamily 9 + from lib.py import KsftSkipEx 10 + from lib.py import rand_port 11 + from lib.py import ethtool, ip, GenerateTraffic, CmdExitFailure 12 + 13 + 14 + def _rss_key_str(key): 15 + return ":".join(["{:02x}".format(x) for x in key]) 16 + 17 + 18 + def _rss_key_rand(length): 19 + return [random.randint(0, 255) for _ in range(length)] 20 + 21 + 22 + def get_rss(cfg, context=0): 23 + return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0] 24 + 25 + 26 + def get_drop_err_sum(cfg): 27 + stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0] 28 + cnt = 0 29 + for key in ['errors', 'dropped', 'over_errors', 'fifo_errors', 30 + 'length_errors', 'crc_errors', 'missed_errors', 31 + 'frame_errors']: 32 + cnt += stats["stats64"]["rx"][key] 33 + return cnt, stats["stats64"]["tx"]["carrier_changes"] 34 + 35 + 36 + def ethtool_create(cfg, act, opts): 37 + output = ethtool(f"{act} {cfg.ifname} {opts}").stdout 38 + # Output will be something like: "New RSS context is 1" or 39 + # "Added rule with ID 7", we want the integer from the end 40 + return int(output.split()[-1]) 41 + 42 + 43 + def require_ntuple(cfg): 44 + features = ethtool(f"-k {cfg.ifname}", json=True)[0] 45 + if not features["ntuple-filters"]["active"]: 46 + # ntuple is more of a capability than a config knob, don't bother 47 + # trying to enable it (until some driver actually needs it). 48 + raise KsftSkipEx("Ntuple filters not enabled on the device: " + str(features["ntuple-filters"])) 49 + 50 + 51 + # Get Rx packet counts for all queues, as a simple list of integers 52 + # if @prev is specified the prev counts will be subtracted 53 + def _get_rx_cnts(cfg, prev=None): 54 + cfg.wait_hw_stats_settle() 55 + data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True) 56 + data = [x for x in data if x['queue-type'] == "rx"] 57 + max_q = max([x["queue-id"] for x in data]) 58 + queue_stats = [0] * (max_q + 1) 59 + for q in data: 60 + queue_stats[q["queue-id"]] = q["rx-packets"] 61 + if prev and q["queue-id"] < len(prev): 62 + queue_stats[q["queue-id"]] -= prev[q["queue-id"]] 63 + return queue_stats 64 + 65 + 66 + def test_rss_key_indir(cfg): 67 + """ 68 + Test basics like updating the main RSS key and indirection table. 69 + """ 70 + if len(_get_rx_cnts(cfg)) < 2: 71 + KsftSkipEx("Device has only one queue (or doesn't support queue stats)") 72 + 73 + data = get_rss(cfg) 74 + want_keys = ['rss-hash-key', 'rss-hash-function', 'rss-indirection-table'] 75 + for k in want_keys: 76 + if k not in data: 77 + raise KsftFailEx("ethtool results missing key: " + k) 78 + if not data[k]: 79 + raise KsftFailEx(f"ethtool results empty for '{k}': {data[k]}") 80 + 81 + key_len = len(data['rss-hash-key']) 82 + 83 + # Set the key 84 + key = _rss_key_rand(key_len) 85 + ethtool(f"-X {cfg.ifname} hkey " + _rss_key_str(key)) 86 + 87 + data = get_rss(cfg) 88 + ksft_eq(key, data['rss-hash-key']) 89 + 90 + # Set the indirection table 91 + ethtool(f"-X {cfg.ifname} equal 2") 92 + data = get_rss(cfg) 93 + ksft_eq(0, min(data['rss-indirection-table'])) 94 + ksft_eq(1, max(data['rss-indirection-table'])) 95 + 96 + # Check we only get traffic on the first 2 queues 97 + cnts = _get_rx_cnts(cfg) 98 + GenerateTraffic(cfg).wait_pkts_and_stop(20000) 99 + cnts = _get_rx_cnts(cfg, prev=cnts) 100 + # 2 queues, 20k packets, must be at least 5k per queue 101 + ksft_ge(cnts[0], 5000, "traffic on main context (1/2): " + str(cnts)) 102 + ksft_ge(cnts[1], 5000, "traffic on main context (2/2): " + str(cnts)) 103 + # The other queues should be unused 104 + ksft_eq(sum(cnts[2:]), 0, "traffic on unused queues: " + str(cnts)) 105 + 106 + # Restore, and check traffic gets spread again 107 + ethtool(f"-X {cfg.ifname} default") 108 + 109 + cnts = _get_rx_cnts(cfg) 110 + GenerateTraffic(cfg).wait_pkts_and_stop(20000) 111 + cnts = _get_rx_cnts(cfg, prev=cnts) 112 + # First two queues get less traffic than all the rest 113 + ksft_lt(sum(cnts[:2]), sum(cnts[2:]), "traffic distributed: " + str(cnts)) 114 + 115 + 116 + def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None): 117 + """ 118 + Test separating traffic into RSS contexts. 119 + The queues will be allocated 2 for each context: 120 + ctx0 ctx1 ctx2 ctx3 121 + [0 1] [2 3] [4 5] [6 7] ... 122 + """ 123 + 124 + require_ntuple(cfg) 125 + 126 + requested_ctx_cnt = ctx_cnt 127 + 128 + # Try to allocate more queues when necessary 129 + qcnt = len(_get_rx_cnts(cfg)) 130 + if qcnt >= 2 + 2 * ctx_cnt: 131 + qcnt = None 132 + else: 133 + try: 134 + ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}") 135 + ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}") 136 + except: 137 + raise KsftSkipEx("Not enough queues for the test") 138 + 139 + ntuple = [] 140 + ctx_id = [] 141 + ports = [] 142 + try: 143 + # Use queues 0 and 1 for normal traffic 144 + ethtool(f"-X {cfg.ifname} equal 2") 145 + 146 + for i in range(ctx_cnt): 147 + want_cfg = f"start {2 + i * 2} equal 2" 148 + create_cfg = want_cfg if create_with_cfg else "" 149 + 150 + try: 151 + ctx_id.append(ethtool_create(cfg, "-X", f"context new {create_cfg}")) 152 + except CmdExitFailure: 153 + # try to carry on and skip at the end 154 + if i == 0: 155 + raise 156 + ksft_pr(f"Failed to create context {i + 1}, trying to test what we got") 157 + ctx_cnt = i 158 + break 159 + 160 + if not create_with_cfg: 161 + ethtool(f"-X {cfg.ifname} context {ctx_id[i]} {want_cfg}") 162 + 163 + # Sanity check the context we just created 164 + data = get_rss(cfg, ctx_id[i]) 165 + ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data)) 166 + ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data)) 167 + 168 + ports.append(rand_port()) 169 + flow = f"flow-type tcp{cfg.addr_ipver} dst-port {ports[i]} context {ctx_id[i]}" 170 + ntuple.append(ethtool_create(cfg, "-N", flow)) 171 + 172 + for i in range(ctx_cnt): 173 + cnts = _get_rx_cnts(cfg) 174 + GenerateTraffic(cfg, port=ports[i]).wait_pkts_and_stop(20000) 175 + cnts = _get_rx_cnts(cfg, prev=cnts) 176 + 177 + ksft_lt(sum(cnts[ :2]), 10000, "traffic on main context:" + str(cnts)) 178 + ksft_ge(sum(cnts[2+i*2:4+i*2]), 20000, f"traffic on context {i}: " + str(cnts)) 179 + ksft_eq(sum(cnts[2:2+i*2] + cnts[4+i*2:]), 0, "traffic on other contexts: " + str(cnts)) 180 + finally: 181 + for nid in ntuple: 182 + ethtool(f"-N {cfg.ifname} delete {nid}") 183 + for cid in ctx_id: 184 + ethtool(f"-X {cfg.ifname} context {cid} delete") 185 + ethtool(f"-X {cfg.ifname} default") 186 + if qcnt: 187 + ethtool(f"-L {cfg.ifname} combined {qcnt}") 188 + 189 + if requested_ctx_cnt != ctx_cnt: 190 + raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}") 191 + 192 + 193 + def test_rss_context4(cfg): 194 + test_rss_context(cfg, 4) 195 + 196 + 197 + def test_rss_context32(cfg): 198 + test_rss_context(cfg, 32) 199 + 200 + 201 + def test_rss_context4_create_with_cfg(cfg): 202 + test_rss_context(cfg, 4, create_with_cfg=True) 203 + 204 + 205 + def test_rss_context_out_of_order(cfg, ctx_cnt=4): 206 + """ 207 + Test separating traffic into RSS contexts. 208 + Contexts are removed in semi-random order, and steering re-tested 209 + to make sure removal doesn't break steering to surviving contexts. 210 + Test requires 3 contexts to work. 211 + """ 212 + 213 + require_ntuple(cfg) 214 + 215 + requested_ctx_cnt = ctx_cnt 216 + 217 + # Try to allocate more queues when necessary 218 + qcnt = len(_get_rx_cnts(cfg)) 219 + if qcnt >= 2 + 2 * ctx_cnt: 220 + qcnt = None 221 + else: 222 + try: 223 + ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}") 224 + ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}") 225 + except: 226 + raise KsftSkipEx("Not enough queues for the test") 227 + 228 + ntuple = [] 229 + ctx_id = [] 230 + ports = [] 231 + 232 + def remove_ctx(idx): 233 + ethtool(f"-N {cfg.ifname} delete {ntuple[idx]}") 234 + ntuple[idx] = None 235 + ethtool(f"-X {cfg.ifname} context {ctx_id[idx]} delete") 236 + ctx_id[idx] = None 237 + 238 + def check_traffic(): 239 + for i in range(ctx_cnt): 240 + cnts = _get_rx_cnts(cfg) 241 + GenerateTraffic(cfg, port=ports[i]).wait_pkts_and_stop(20000) 242 + cnts = _get_rx_cnts(cfg, prev=cnts) 243 + 244 + if ctx_id[i] is None: 245 + ksft_lt(sum(cnts[ :2]), 10000, "traffic on main context:" + str(cnts)) 246 + ksft_ge(sum(cnts[2+i*2:4+i*2]), 20000, f"traffic on context {i}: " + str(cnts)) 247 + ksft_eq(sum(cnts[2:2+i*2] + cnts[4+i*2:]), 0, "traffic on other contexts: " + str(cnts)) 248 + else: 249 + ksft_ge(sum(cnts[ :2]), 20000, "traffic on main context:" + str(cnts)) 250 + ksft_eq(sum(cnts[2: ]), 0, "traffic on other contexts: " + str(cnts)) 251 + 252 + try: 253 + # Use queues 0 and 1 for normal traffic 254 + ethtool(f"-X {cfg.ifname} equal 2") 255 + 256 + for i in range(ctx_cnt): 257 + ctx_id.append(ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2")) 258 + 259 + ports.append(rand_port()) 260 + flow = f"flow-type tcp{cfg.addr_ipver} dst-port {ports[i]} context {ctx_id[i]}" 261 + ntuple.append(ethtool_create(cfg, "-N", flow)) 262 + 263 + check_traffic() 264 + 265 + # Remove middle context 266 + remove_ctx(ctx_cnt // 2) 267 + check_traffic() 268 + 269 + # Remove first context 270 + remove_ctx(0) 271 + check_traffic() 272 + 273 + # Remove last context 274 + remove_ctx(-1) 275 + check_traffic() 276 + 277 + finally: 278 + for nid in ntuple: 279 + if nid is not None: 280 + ethtool(f"-N {cfg.ifname} delete {nid}") 281 + for cid in ctx_id: 282 + if cid is not None: 283 + ethtool(f"-X {cfg.ifname} context {cid} delete") 284 + ethtool(f"-X {cfg.ifname} default") 285 + if qcnt: 286 + ethtool(f"-L {cfg.ifname} combined {qcnt}") 287 + 288 + if requested_ctx_cnt != ctx_cnt: 289 + raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}") 290 + 291 + 292 + def test_rss_context_overlap(cfg, other_ctx=0): 293 + """ 294 + Test contexts overlapping with each other. 295 + Use 4 queues for the main context, but only queues 2 and 3 for context 1. 296 + """ 297 + 298 + require_ntuple(cfg) 299 + 300 + queue_cnt = len(_get_rx_cnts(cfg)) 301 + if queue_cnt >= 4: 302 + queue_cnt = None 303 + else: 304 + try: 305 + ksft_pr(f"Increasing queue count {queue_cnt} -> 4") 306 + ethtool(f"-L {cfg.ifname} combined 4") 307 + except: 308 + raise KsftSkipEx("Not enough queues for the test") 309 + 310 + ctx_id = None 311 + ntuple = None 312 + if other_ctx == 0: 313 + ethtool(f"-X {cfg.ifname} equal 4") 314 + else: 315 + other_ctx = ethtool_create(cfg, "-X", "context new") 316 + ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4") 317 + 318 + try: 319 + ctx_id = ethtool_create(cfg, "-X", "context new") 320 + ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2") 321 + 322 + port = rand_port() 323 + if other_ctx: 324 + flow = f"flow-type tcp{cfg.addr_ipver} dst-port {port} context {other_ctx}" 325 + ntuple = ethtool_create(cfg, "-N", flow) 326 + 327 + # Test the main context 328 + cnts = _get_rx_cnts(cfg) 329 + GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 330 + cnts = _get_rx_cnts(cfg, prev=cnts) 331 + 332 + ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts)) 333 + ksft_ge(sum(cnts[ :2]), 7000, "traffic on main context (1/2): " + str(cnts)) 334 + ksft_ge(sum(cnts[2:4]), 7000, "traffic on main context (2/2): " + str(cnts)) 335 + if other_ctx == 0: 336 + ksft_eq(sum(cnts[4: ]), 0, "traffic on other queues: " + str(cnts)) 337 + 338 + # Now create a rule for context 1 and make sure traffic goes to a subset 339 + if other_ctx: 340 + ethtool(f"-N {cfg.ifname} delete {ntuple}") 341 + ntuple = None 342 + flow = f"flow-type tcp{cfg.addr_ipver} dst-port {port} context {ctx_id}" 343 + ntuple = ethtool_create(cfg, "-N", flow) 344 + 345 + cnts = _get_rx_cnts(cfg) 346 + GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 347 + cnts = _get_rx_cnts(cfg, prev=cnts) 348 + 349 + ksft_lt(sum(cnts[ :2]), 7000, "traffic on main context: " + str(cnts)) 350 + ksft_ge(sum(cnts[2:4]), 20000, "traffic on extra context: " + str(cnts)) 351 + if other_ctx == 0: 352 + ksft_eq(sum(cnts[4: ]), 0, "traffic on other queues: " + str(cnts)) 353 + finally: 354 + if ntuple is not None: 355 + ethtool(f"-N {cfg.ifname} delete {ntuple}") 356 + if ctx_id: 357 + ethtool(f"-X {cfg.ifname} context {ctx_id} delete") 358 + if other_ctx == 0: 359 + ethtool(f"-X {cfg.ifname} default") 360 + else: 361 + ethtool(f"-X {cfg.ifname} context {other_ctx} delete") 362 + if queue_cnt: 363 + ethtool(f"-L {cfg.ifname} combined {queue_cnt}") 364 + 365 + 366 + def test_rss_context_overlap2(cfg): 367 + test_rss_context_overlap(cfg, True) 368 + 369 + 370 + def main() -> None: 371 + with NetDrvEpEnv(__file__, nsim_test=False) as cfg: 372 + cfg.netdevnl = NetdevFamily() 373 + 374 + ksft_run([test_rss_key_indir, 375 + test_rss_context, test_rss_context4, test_rss_context32, 376 + test_rss_context_overlap, test_rss_context_overlap2, 377 + test_rss_context_out_of_order, test_rss_context4_create_with_cfg], 378 + args=(cfg, )) 379 + ksft_exit() 380 + 381 + 382 + if __name__ == "__main__": 383 + main()
+4 -3
tools/testing/selftests/drivers/net/lib/py/load.py
··· 5 5 from lib.py import ksft_pr, cmd, ip, rand_port, wait_port_listen 6 6 7 7 class GenerateTraffic: 8 - def __init__(self, env): 8 + def __init__(self, env, port=None): 9 9 env.require_cmd("iperf3", remote=True) 10 10 11 11 self.env = env 12 12 13 - port = rand_port() 14 - self._iperf_server = cmd(f"iperf3 -s -p {port}", background=True) 13 + if port is None: 14 + port = rand_port() 15 + self._iperf_server = cmd(f"iperf3 -s -1 -p {port}", background=True) 15 16 wait_port_listen(port) 16 17 time.sleep(0.1) 17 18 self._iperf_client = cmd(f"iperf3 -c {env.addr} -P 16 -p {port} -t 86400",
+5
tools/testing/selftests/net/lib/py/ksft.py
··· 57 57 _fail("Check failed", a, "<", b, comment) 58 58 59 59 60 + def ksft_lt(a, b, comment=""): 61 + if a >= b: 62 + _fail("Check failed", a, ">=", b, comment) 63 + 64 + 60 65 class ksft_raises: 61 66 def __init__(self, expected_type): 62 67 self.exception = None
+6 -2
tools/testing/selftests/net/lib/py/utils.py
··· 9 9 import time 10 10 11 11 12 + class CmdExitFailure(Exception): 13 + pass 14 + 15 + 12 16 class cmd: 13 17 def __init__(self, comm, shell=True, fail=True, ns=None, background=False, host=None, timeout=5): 14 18 if ns: ··· 47 43 if self.proc.returncode != 0 and fail: 48 44 if len(stderr) > 0 and stderr[-1] == "\n": 49 45 stderr = stderr[:-1] 50 - raise Exception("Command failed: %s\nSTDOUT: %s\nSTDERR: %s" % 51 - (self.proc.args, stdout, stderr)) 46 + raise CmdExitFailure("Command failed: %s\nSTDOUT: %s\nSTDERR: %s" % 47 + (self.proc.args, stdout, stderr)) 52 48 53 49 54 50 class bkg(cmd):