xref: /linux/tools/testing/selftests/drivers/net/hw/rss_drv.py (revision dfecb0c5af3b07ebfa84be63a7a21bfc9e29a872)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5Driver-related behavior tests for RSS.
6"""
7
8from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
9from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, ksft_raises
10from lib.py import defer, ethtool, CmdExitFailure
11from lib.py import EthtoolFamily, NlError
12from lib.py import NetDrvEnv
13
14
15def _is_power_of_two(n):
16    return n > 0 and (n & (n - 1)) == 0
17
18
19def _get_rss(cfg, context=0):
20    return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
21
22
23def _test_rss_indir_size(cfg, qcnt, context=0):
24    """Test that indirection table size is at least 4x queue count."""
25    ethtool(f"-L {cfg.ifname} combined {qcnt}")
26
27    rss = _get_rss(cfg, context=context)
28    indir = rss['rss-indirection-table']
29    ksft_ge(len(indir), 4 * qcnt, "Table smaller than 4x")
30    return len(indir)
31
32
33def _maybe_create_context(cfg, create_context):
34    """ Either create a context and return its ID or return 0 for main ctx """
35    if not create_context:
36        return 0
37    try:
38        ctx = cfg.ethnl.rss_create_act({'header': {'dev-index': cfg.ifindex}})
39        ctx_id = ctx['context']
40        defer(cfg.ethnl.rss_delete_act,
41              {'header': {'dev-index': cfg.ifindex}, 'context': ctx_id})
42    except NlError:
43        raise KsftSkipEx("Device does not support additional RSS contexts")
44
45    return ctx_id
46
47
48def _require_dynamic_indir_size(cfg, ch_max):
49    """Skip if the device does not dynamically size its indirection table."""
50    ethtool(f"-X {cfg.ifname} default")
51    ethtool(f"-L {cfg.ifname} combined 2")
52    small = len(_get_rss(cfg)['rss-indirection-table'])
53    ethtool(f"-L {cfg.ifname} combined {ch_max}")
54    large = len(_get_rss(cfg)['rss-indirection-table'])
55
56    if small == large:
57        raise KsftSkipEx("Device does not dynamically size indirection table")
58
59
60@ksft_variants([
61    KsftNamedVariant("main", False),
62    KsftNamedVariant("ctx", True),
63])
64def indir_size_4x(cfg, create_context):
65    """
66    Test that the indirection table has at least 4 entries per queue.
67    Empirically network-heavy workloads like memcache suffer with the 33%
68    imbalance of a 2x indirection table size.
69    4x table translates to a 16% imbalance.
70    """
71    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
72    ch_max = channels.get('combined-max', 0)
73    qcnt = channels['combined-count']
74
75    if ch_max < 3:
76        raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
77
78    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
79    ethtool(f"-L {cfg.ifname} combined 3")
80
81    ctx_id = _maybe_create_context(cfg, create_context)
82
83    indir_sz = _test_rss_indir_size(cfg, 3, context=ctx_id)
84
85    # Test with max queue count (max - 1 if max is a power of two)
86    test_max = ch_max - 1 if _is_power_of_two(ch_max) else ch_max
87    if test_max > 3 and indir_sz < test_max * 4:
88        _test_rss_indir_size(cfg, test_max, context=ctx_id)
89
90
91@ksft_variants([
92    KsftNamedVariant("main", False),
93    KsftNamedVariant("ctx", True),
94])
95def resize_periodic(cfg, create_context):
96    """Test that a periodic indirection table survives channel changes.
97
98    Set a non-default periodic table ([3, 2, 1, 0] x N) via netlink,
99    reduce channels to trigger a fold, then increase to trigger an
100    unfold. Using a reversed pattern (instead of [0, 1, 2, 3]) ensures
101    the test can distinguish a correct fold from a driver that silently
102    resets the table to defaults. Verify the exact pattern is preserved
103    and the size tracks the channel count.
104    """
105    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
106    ch_max = channels.get('combined-max', 0)
107    qcnt = channels['combined-count']
108
109    if ch_max < 4:
110        raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
111
112    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
113
114    _require_dynamic_indir_size(cfg, ch_max)
115
116    ctx_id = _maybe_create_context(cfg, create_context)
117
118    # Set a non-default periodic pattern via netlink.
119    # Send only 4 entries (user_size=4) so the kernel replicates it
120    # to fill the device table. This allows folding down to 4 entries.
121    rss = _get_rss(cfg, context=ctx_id)
122    orig_size = len(rss['rss-indirection-table'])
123    pattern = [3, 2, 1, 0]
124    req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
125    if ctx_id:
126        req['context'] = ctx_id
127    else:
128        defer(ethtool, f"-X {cfg.ifname} default")
129    cfg.ethnl.rss_set(req)
130
131    # Shrink — should fold
132    ethtool(f"-L {cfg.ifname} combined 4")
133    rss = _get_rss(cfg, context=ctx_id)
134    indir = rss['rss-indirection-table']
135
136    ksft_ge(orig_size, len(indir), "Table did not shrink")
137    ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
138            "Folded table has wrong pattern")
139
140    # Grow back — should unfold
141    ethtool(f"-L {cfg.ifname} combined {ch_max}")
142    rss = _get_rss(cfg, context=ctx_id)
143    indir = rss['rss-indirection-table']
144
145    ksft_eq(len(indir), orig_size, "Table size not restored")
146    ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
147            "Unfolded table has wrong pattern")
148
149
150@ksft_variants([
151    KsftNamedVariant("main", False),
152    KsftNamedVariant("ctx", True),
153])
154def resize_below_user_size_reject(cfg, create_context):
155    """Test that shrinking below user_size is rejected.
156
157    Send a table via netlink whose size (user_size) sits between
158    the small and large device table sizes. The table is periodic,
159    so folding would normally succeed, but the user_size floor must
160    prevent it.
161    """
162    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
163    ch_max = channels.get('combined-max', 0)
164    qcnt = channels['combined-count']
165
166    if ch_max < 4:
167        raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
168
169    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
170
171    _require_dynamic_indir_size(cfg, ch_max)
172
173    ctx_id = _maybe_create_context(cfg, create_context)
174
175    # Measure the table size at max channels
176    rss = _get_rss(cfg, context=ctx_id)
177    big_size = len(rss['rss-indirection-table'])
178
179    # Measure the table size at reduced channels
180    ethtool(f"-L {cfg.ifname} combined 4")
181    rss = _get_rss(cfg, context=ctx_id)
182    small_size = len(rss['rss-indirection-table'])
183    ethtool(f"-L {cfg.ifname} combined {ch_max}")
184
185    if small_size >= big_size:
186        raise KsftSkipEx("Table did not shrink at reduced channels")
187
188    # Find a user_size
189    user_size = None
190    for div in [2, 4]:
191        candidate = big_size // div
192        if candidate > small_size and big_size % candidate == 0:
193            user_size = candidate
194            break
195    if user_size is None:
196        raise KsftSkipEx("No suitable user_size between small and big table")
197
198    # Send a periodic sub-table of exactly user_size entries.
199    # Pattern safe for 4 channels.
200    pattern = [0, 1, 2, 3] * (user_size // 4)
201    if len(pattern) != user_size:
202        raise KsftSkipEx(f"user_size ({user_size}) not divisible by 4")
203    req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
204    if ctx_id:
205        req['context'] = ctx_id
206    else:
207        defer(ethtool, f"-X {cfg.ifname} default")
208    cfg.ethnl.rss_set(req)
209
210    # Shrink channels — table would go to small_size < user_size.
211    # The table is periodic so folding would work, but user_size
212    # floor must reject it.
213    with ksft_raises(CmdExitFailure):
214        ethtool(f"-L {cfg.ifname} combined 4")
215
216
217@ksft_variants([
218    KsftNamedVariant("main", False),
219    KsftNamedVariant("ctx", True),
220])
221def resize_nonperiodic_reject(cfg, create_context):
222    """Test that a non-periodic table blocks channel reduction.
223
224    Set equal weight across all queues so the table is not periodic
225    at any smaller size, then verify channel reduction is rejected.
226    An additional context with a periodic table is created to verify
227    that validation catches the non-periodic one even when others
228    are fine.
229    """
230    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
231    ch_max = channels.get('combined-max', 0)
232    qcnt = channels['combined-count']
233
234    if ch_max < 4:
235        raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
236
237    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
238
239    _require_dynamic_indir_size(cfg, ch_max)
240
241    ctx_id = _maybe_create_context(cfg, create_context)
242    ctx_ref = f"context {ctx_id}" if ctx_id else ""
243
244    # Create an extra context with a periodic (foldable) table so that
245    # the validation must iterate all contexts to find the bad one.
246    extra_ctx = _maybe_create_context(cfg, True)
247    ethtool(f"-X {cfg.ifname} context {extra_ctx} equal 2")
248
249    ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
250    if not create_context:
251        defer(ethtool, f"-X {cfg.ifname} default")
252
253    with ksft_raises(CmdExitFailure):
254        ethtool(f"-L {cfg.ifname} combined 2")
255
256
257@ksft_variants([
258    KsftNamedVariant("main", False),
259    KsftNamedVariant("ctx", True),
260])
261def resize_nonperiodic_no_corruption(cfg, create_context):
262    """Test that a failed resize does not corrupt table or channel count.
263
264    Set a non-periodic table, attempt a channel reduction (which must
265    fail), then verify both the indirection table contents and the
266    channel count are unchanged.
267    """
268    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
269    ch_max = channels.get('combined-max', 0)
270    qcnt = channels['combined-count']
271
272    if ch_max < 4:
273        raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
274
275    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
276
277    _require_dynamic_indir_size(cfg, ch_max)
278
279    ctx_id = _maybe_create_context(cfg, create_context)
280    ctx_ref = f"context {ctx_id}" if ctx_id else ""
281
282    ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
283    if not create_context:
284        defer(ethtool, f"-X {cfg.ifname} default")
285
286    rss_before = _get_rss(cfg, context=ctx_id)
287
288    with ksft_raises(CmdExitFailure):
289        ethtool(f"-L {cfg.ifname} combined 2")
290
291    rss_after = _get_rss(cfg, context=ctx_id)
292    ksft_eq(rss_after['rss-indirection-table'],
293            rss_before['rss-indirection-table'],
294            "Indirection table corrupted after failed resize")
295
296    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
297    ksft_eq(channels['combined-count'], ch_max,
298            "Channel count changed after failed resize")
299
300
301def main() -> None:
302    """ Ksft boiler plate main """
303    with NetDrvEnv(__file__) as cfg:
304        cfg.ethnl = EthtoolFamily()
305        ksft_run([indir_size_4x, resize_periodic,
306                  resize_below_user_size_reject,
307                  resize_nonperiodic_reject,
308                  resize_nonperiodic_no_corruption], args=(cfg, ))
309    ksft_exit()
310
311
312if __name__ == "__main__":
313    main()
314