xref: /linux/tools/testing/selftests/drivers/net/hw/rss_ctx.py (revision ff015706fc7385c51e8418ced2484b98b239a3a8)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4import datetime
5import random
6from lib.py import ksft_run, ksft_pr, ksft_exit, ksft_eq, ksft_ge, ksft_lt
7from lib.py import NetDrvEpEnv
8from lib.py import NetdevFamily
9from lib.py import KsftSkipEx
10from lib.py import rand_port
11from lib.py import ethtool, ip, defer, GenerateTraffic, CmdExitFailure
12
13
14def _rss_key_str(key):
15    return ":".join(["{:02x}".format(x) for x in key])
16
17
18def _rss_key_rand(length):
19    return [random.randint(0, 255) for _ in range(length)]
20
21
22def get_rss(cfg, context=0):
23    return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
24
25
26def 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
36def 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
43def 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
53def _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
66def 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
116def 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        try:
132            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
133            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
134            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
135        except:
136            raise KsftSkipEx("Not enough queues for the test")
137
138    ports = []
139
140    # Use queues 0 and 1 for normal traffic
141    ethtool(f"-X {cfg.ifname} equal 2")
142    defer(ethtool, f"-X {cfg.ifname} default")
143
144    for i in range(ctx_cnt):
145        want_cfg = f"start {2 + i * 2} equal 2"
146        create_cfg = want_cfg if create_with_cfg else ""
147
148        try:
149            ctx_id = ethtool_create(cfg, "-X", f"context new {create_cfg}")
150            defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
151        except CmdExitFailure:
152            # try to carry on and skip at the end
153            if i == 0:
154                raise
155            ksft_pr(f"Failed to create context {i + 1}, trying to test what we got")
156            ctx_cnt = i
157            break
158
159        if not create_with_cfg:
160            ethtool(f"-X {cfg.ifname} context {ctx_id} {want_cfg}")
161
162        # Sanity check the context we just created
163        data = get_rss(cfg, ctx_id)
164        ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data))
165        ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data))
166
167        ports.append(rand_port())
168        flow = f"flow-type tcp{cfg.addr_ipver} dst-port {ports[i]} context {ctx_id}"
169        ntuple = ethtool_create(cfg, "-N", flow)
170        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
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        directed = sum(cnts[2+i*2:4+i*2])
178
179        ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context:" + str(cnts))
180        ksft_ge(directed, 20000, f"traffic on context {i}: " + str(cnts))
181        ksft_eq(sum(cnts[2:2+i*2] + cnts[4+i*2:]), 0, "traffic on other contexts: " + str(cnts))
182
183    if requested_ctx_cnt != ctx_cnt:
184        raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}")
185
186
187def test_rss_context4(cfg):
188    test_rss_context(cfg, 4)
189
190
191def test_rss_context32(cfg):
192    test_rss_context(cfg, 32)
193
194
195def test_rss_context4_create_with_cfg(cfg):
196    test_rss_context(cfg, 4, create_with_cfg=True)
197
198
199def test_rss_context_out_of_order(cfg, ctx_cnt=4):
200    """
201    Test separating traffic into RSS contexts.
202    Contexts are removed in semi-random order, and steering re-tested
203    to make sure removal doesn't break steering to surviving contexts.
204    Test requires 3 contexts to work.
205    """
206
207    require_ntuple(cfg)
208
209    requested_ctx_cnt = ctx_cnt
210
211    # Try to allocate more queues when necessary
212    qcnt = len(_get_rx_cnts(cfg))
213    if qcnt < 2 + 2 * ctx_cnt:
214        try:
215            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
216            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
217            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
218        except:
219            raise KsftSkipEx("Not enough queues for the test")
220
221    ntuple = []
222    ctx = []
223    ports = []
224
225    def remove_ctx(idx):
226        ntuple[idx].exec()
227        ntuple[idx] = None
228        ctx[idx].exec()
229        ctx[idx] = None
230
231    def check_traffic():
232        for i in range(ctx_cnt):
233            cnts = _get_rx_cnts(cfg)
234            GenerateTraffic(cfg, port=ports[i]).wait_pkts_and_stop(20000)
235            cnts = _get_rx_cnts(cfg, prev=cnts)
236
237            if ctx[i]:
238                directed = sum(cnts[2+i*2:4+i*2])
239                ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context:" + str(cnts))
240                ksft_ge(directed, 20000, f"traffic on context {i}: " + str(cnts))
241                ksft_eq(sum(cnts[2:2+i*2] + cnts[4+i*2:]), 0, "traffic on other contexts: " + str(cnts))
242            else:
243                ksft_ge(sum(cnts[ :2]), 20000, "traffic on main context:" + str(cnts))
244                ksft_eq(sum(cnts[2: ]),     0, "traffic on other contexts: " + str(cnts))
245
246    # Use queues 0 and 1 for normal traffic
247    ethtool(f"-X {cfg.ifname} equal 2")
248    defer(ethtool, f"-X {cfg.ifname} default")
249
250    for i in range(ctx_cnt):
251        ctx_id = ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2")
252        ctx.append(defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete"))
253
254        ports.append(rand_port())
255        flow = f"flow-type tcp{cfg.addr_ipver} dst-port {ports[i]} context {ctx_id}"
256        ntuple_id = ethtool_create(cfg, "-N", flow)
257        ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}"))
258
259    check_traffic()
260
261    # Remove middle context
262    remove_ctx(ctx_cnt // 2)
263    check_traffic()
264
265    # Remove first context
266    remove_ctx(0)
267    check_traffic()
268
269    # Remove last context
270    remove_ctx(-1)
271    check_traffic()
272
273    if requested_ctx_cnt != ctx_cnt:
274        raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}")
275
276
277def test_rss_context_overlap(cfg, other_ctx=0):
278    """
279    Test contexts overlapping with each other.
280    Use 4 queues for the main context, but only queues 2 and 3 for context 1.
281    """
282
283    require_ntuple(cfg)
284
285    queue_cnt = len(_get_rx_cnts(cfg))
286    if queue_cnt < 4:
287        try:
288            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
289            ethtool(f"-L {cfg.ifname} combined 4")
290            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
291        except:
292            raise KsftSkipEx("Not enough queues for the test")
293
294    if other_ctx == 0:
295        ethtool(f"-X {cfg.ifname} equal 4")
296        defer(ethtool, f"-X {cfg.ifname} default")
297    else:
298        other_ctx = ethtool_create(cfg, "-X", "context new")
299        ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4")
300        defer(ethtool, f"-X {cfg.ifname} context {other_ctx} delete")
301
302    ctx_id = ethtool_create(cfg, "-X", "context new")
303    ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2")
304    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
305
306    port = rand_port()
307    if other_ctx:
308        flow = f"flow-type tcp{cfg.addr_ipver} dst-port {port} context {other_ctx}"
309        ntuple_id = ethtool_create(cfg, "-N", flow)
310        ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
311
312    # Test the main context
313    cnts = _get_rx_cnts(cfg)
314    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
315    cnts = _get_rx_cnts(cfg, prev=cnts)
316
317    ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts))
318    ksft_ge(sum(cnts[ :2]),  7000, "traffic on main context (1/2): " + str(cnts))
319    ksft_ge(sum(cnts[2:4]),  7000, "traffic on main context (2/2): " + str(cnts))
320    if other_ctx == 0:
321        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
322
323    # Now create a rule for context 1 and make sure traffic goes to a subset
324    if other_ctx:
325        ntuple.exec()
326    flow = f"flow-type tcp{cfg.addr_ipver} dst-port {port} context {ctx_id}"
327    ntuple_id = ethtool_create(cfg, "-N", flow)
328    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
329
330    cnts = _get_rx_cnts(cfg)
331    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
332    cnts = _get_rx_cnts(cfg, prev=cnts)
333
334    directed = sum(cnts[2:4])
335    ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts))
336    ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts))
337    if other_ctx == 0:
338        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
339
340
341def test_rss_context_overlap2(cfg):
342    test_rss_context_overlap(cfg, True)
343
344
345def main() -> None:
346    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
347        cfg.netdevnl = NetdevFamily()
348
349        ksft_run([test_rss_key_indir,
350                  test_rss_context, test_rss_context4, test_rss_context32,
351                  test_rss_context_overlap, test_rss_context_overlap2,
352                  test_rss_context_out_of_order, test_rss_context4_create_with_cfg],
353                 args=(cfg, ))
354    ksft_exit()
355
356
357if __name__ == "__main__":
358    main()
359