xref: /linux/tools/testing/selftests/drivers/net/hw/gro_hw.py (revision 91a4855d6c03e770e42f17c798a36a3c46e63de2)
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