1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3 4""" 5HW GRO tests focusing on device machinery like stats, rather than protocol 6processing. 7""" 8 9import glob 10import re 11 12from lib.py import ksft_run, ksft_exit, ksft_pr 13from lib.py import ksft_eq, ksft_ge, ksft_variants 14from lib.py import NetDrvEpEnv, NetdevFamily 15from lib.py import KsftSkipEx 16from lib.py import bkg, cmd, defer, ethtool, ip 17 18 19# gro.c uses hardcoded DPORT=8000 20GRO_DPORT = 8000 21 22 23def _get_queue_stats(cfg, queue_id): 24 """Get stats for a specific Rx queue.""" 25 cfg.wait_hw_stats_settle() 26 data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, 27 dump=True) 28 for q in data: 29 if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id: 30 return q 31 return {} 32 33 34def _resolve_dmac(cfg, ipver): 35 """Find the destination MAC address for sending packets.""" 36 attr = "dmac" + ipver 37 if hasattr(cfg, attr): 38 return getattr(cfg, attr) 39 40 route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}", 41 json=True, host=cfg.remote)[0] 42 gw = route.get("gateway") 43 if not gw: 44 setattr(cfg, attr, cfg.dev['address']) 45 return getattr(cfg, attr) 46 47 cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote) 48 neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}", 49 json=True, host=cfg.remote)[0] 50 setattr(cfg, attr, neigh['lladdr']) 51 return getattr(cfg, attr) 52 53 54def _setup_isolated_queue(cfg): 55 """Set up an isolated queue for testing using ntuple filter. 56 57 Remove queue 1 from the default RSS context and steer test traffic to it. 58 """ 59 test_queue = 1 60 61 qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*")) 62 if qcnt < 2: 63 raise KsftSkipEx(f"Need at least 2 queues, have {qcnt}") 64 65 # Remove queue 1 from default RSS context by setting its weight to 0 66 weights = ["1"] * qcnt 67 weights[test_queue] = "0" 68 ethtool(f"-X {cfg.ifname} weight " + " ".join(weights)) 69 defer(ethtool, f"-X {cfg.ifname} default") 70 71 # Set up ntuple filter to steer our test traffic to the isolated queue 72 flow = f"flow-type tcp{cfg.addr_ipver} " 73 flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}" 74 output = ethtool(f"-N {cfg.ifname} {flow}").stdout 75 ntuple_id = int(output.split()[-1]) 76 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 77 78 return test_queue 79 80 81def _run_gro_test(cfg, test_name, num_flows=None, ignore_fail=False, 82 order_check=False): 83 """Run gro binary with given test and return output.""" 84 if not hasattr(cfg, "bin_remote"): 85 cfg.bin_local = cfg.net_lib_dir / "gro" 86 cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) 87 88 ipver = cfg.addr_ipver 89 protocol = f"--ipv{ipver}" 90 dmac = _resolve_dmac(cfg, ipver) 91 92 base_args = [ 93 protocol, 94 f"--dmac {dmac}", 95 f"--smac {cfg.remote_dev['address']}", 96 f"--daddr {cfg.addr}", 97 f"--saddr {cfg.remote_addr_v[ipver]}", 98 f"--test {test_name}", 99 ] 100 if num_flows: 101 base_args.append(f"--num-flows {num_flows}") 102 if order_check: 103 base_args.append("--order-check") 104 105 args = " ".join(base_args) 106 107 rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}" 108 tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}" 109 110 with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=False) as rx_proc: 111 cmd(tx_cmd, host=cfg.remote) 112 113 if not ignore_fail: 114 ksft_eq(rx_proc.ret, 0) 115 if rx_proc.ret != 0: 116 ksft_pr(rx_proc) 117 118 return rx_proc.stdout 119 120 121def _require_hw_gro_stats(cfg, queue_id): 122 """Check if device reports HW GRO stats for the queue.""" 123 stats = _get_queue_stats(cfg, queue_id) 124 required = ['rx-packets', 'rx-hw-gro-packets', 'rx-hw-gro-wire-packets'] 125 for stat in required: 126 if stat not in stats: 127 raise KsftSkipEx(f"Driver does not report '{stat}' via qstats") 128 129 130def _set_ethtool_feat(cfg, current, feats): 131 """Set ethtool features with defer to restore original state.""" 132 s2n = {True: "on", False: "off"} 133 134 new = ["-K", cfg.ifname] 135 old = ["-K", cfg.ifname] 136 no_change = True 137 for name, state in feats.items(): 138 new += [name, s2n[state]] 139 old += [name, s2n[current[name]["active"]]] 140 141 if current[name]["active"] != state: 142 no_change = False 143 if current[name]["fixed"]: 144 raise KsftSkipEx(f"Device does not support {name}") 145 if no_change: 146 return 147 148 eth_cmd = ethtool(" ".join(new)) 149 defer(ethtool, " ".join(old)) 150 151 # If ethtool printed something kernel must have modified some features 152 if eth_cmd.stdout: 153 ksft_pr(eth_cmd) 154 155 156def _setup_hw_gro(cfg): 157 """Enable HW GRO on the device, disabling SW GRO.""" 158 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 159 160 # Try to disable SW GRO and enable HW GRO 161 _set_ethtool_feat(cfg, feat, 162 {"generic-receive-offload": False, 163 "rx-gro-hw": True, 164 "large-receive-offload": False}) 165 166 # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO 167 # will also clear HW GRO. Use a hack of installing XDP generic 168 # to skip SW GRO, even when enabled. 169 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 170 if not feat["rx-gro-hw"]["active"]: 171 ksft_pr("Driver clears HW GRO when SW GRO is cleared, using generic XDP workaround") 172 prog = cfg.net_lib_dir / "xdp_dummy.bpf.o" 173 ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp") 174 defer(ip, f"link set dev {cfg.ifname} xdpgeneric off") 175 176 # Attaching XDP may change features, fetch the latest state 177 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 178 179 _set_ethtool_feat(cfg, feat, 180 {"generic-receive-offload": True, 181 "rx-gro-hw": True, 182 "large-receive-offload": False}) 183 184 185def _check_gro_stats(cfg, test_queue, stats_before, 186 expect_rx, expect_gro, expect_wire): 187 """Validate GRO stats against expected values.""" 188 stats_after = _get_queue_stats(cfg, test_queue) 189 190 rx_delta = (stats_after.get('rx-packets', 0) - 191 stats_before.get('rx-packets', 0)) 192 gro_delta = (stats_after.get('rx-hw-gro-packets', 0) - 193 stats_before.get('rx-hw-gro-packets', 0)) 194 wire_delta = (stats_after.get('rx-hw-gro-wire-packets', 0) - 195 stats_before.get('rx-hw-gro-wire-packets', 0)) 196 197 ksft_eq(rx_delta, expect_rx, comment="rx-packets") 198 ksft_eq(gro_delta, expect_gro, comment="rx-hw-gro-packets") 199 ksft_eq(wire_delta, expect_wire, comment="rx-hw-gro-wire-packets") 200 201 202def test_gro_stats_single(cfg): 203 """ 204 Test that a single packet doesn't affect GRO stats. 205 206 Send a single packet that cannot be coalesced (nothing to coalesce with). 207 GRO stats should not increase since no coalescing occurred. 208 rx-packets should increase by 2 (1 data + 1 FIN). 209 """ 210 _setup_hw_gro(cfg) 211 212 test_queue = _setup_isolated_queue(cfg) 213 _require_hw_gro_stats(cfg, test_queue) 214 215 stats_before = _get_queue_stats(cfg, test_queue) 216 217 _run_gro_test(cfg, "single") 218 219 # 1 data + 1 FIN = 2 rx-packets, no coalescing 220 _check_gro_stats(cfg, test_queue, stats_before, 221 expect_rx=2, expect_gro=0, expect_wire=0) 222 223 224def test_gro_stats_full(cfg): 225 """ 226 Test GRO stats when overwhelming HW GRO capacity. 227 228 Send 500 flows to exceed HW GRO flow capacity on a single queue. 229 This should result in some packets not being coalesced. 230 Validate that qstats match what gro.c observed. 231 """ 232 _setup_hw_gro(cfg) 233 234 test_queue = _setup_isolated_queue(cfg) 235 _require_hw_gro_stats(cfg, test_queue) 236 237 num_flows = 500 238 stats_before = _get_queue_stats(cfg, test_queue) 239 240 # Run capacity test - will likely fail because not all packets coalesce 241 output = _run_gro_test(cfg, "capacity", num_flows=num_flows, 242 ignore_fail=True) 243 244 # Parse gro.c output: "STATS: received=X wire=Y coalesced=Z" 245 match = re.search(r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)', 246 output) 247 if not match: 248 raise KsftSkipEx(f"Could not parse gro.c output: {output}") 249 250 rx_frames = int(match.group(2)) 251 gro_coalesced = int(match.group(3)) 252 253 ksft_ge(gro_coalesced, 1, 254 comment="At least some packets should coalesce") 255 256 # received + 1 FIN, coalesced super-packets, coalesced * 2 wire packets 257 _check_gro_stats(cfg, test_queue, stats_before, 258 expect_rx=rx_frames + 1, 259 expect_gro=gro_coalesced, 260 expect_wire=gro_coalesced * 2) 261 262 263@ksft_variants([4, 32, 512]) 264def test_gro_order(cfg, num_flows): 265 """ 266 Test that HW GRO preserves packet ordering between flows. 267 268 Packets may get delayed until the aggregate is released, 269 but reordering between aggregates and packet terminating 270 the aggregate and normal packets should not happen. 271 272 Note that this test is stricter than truly required. 273 Reordering packets between flows should not cause issues. 274 This test will also fail if traffic is run over an ECMP fabric. 275 """ 276 _setup_hw_gro(cfg) 277 _setup_isolated_queue(cfg) 278 279 _run_gro_test(cfg, "capacity", num_flows=num_flows, order_check=True) 280 281 282def main() -> None: 283 """ Ksft boiler plate main """ 284 285 with NetDrvEpEnv(__file__, nsim_test=False) as cfg: 286 cfg.netnl = NetdevFamily() 287 ksft_run([test_gro_stats_single, 288 test_gro_stats_full, 289 test_gro_order], args=(cfg,)) 290 ksft_exit() 291 292 293if __name__ == "__main__": 294 main() 295