summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/net/forwarding/tc_taprio.sh
blob: 8992aeabfe0b439b193e939f06859ca9fc79f847 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0

ALL_TESTS=" \
	test_clock_jump_backward \
	test_taprio_after_ptp \
	test_max_sdu \
	test_clock_jump_backward_forward \
"
NUM_NETIFS=4
source tc_common.sh
source lib.sh
source tsn_lib.sh

require_command python3

# The test assumes the usual topology from the README, where h1 is connected to
# swp1, h2 to swp2, and swp1 and swp2 are together in a bridge.
# Additional assumption: h1 and h2 use the same PHC, and so do swp1 and swp2.
# By synchronizing h1 to swp1 via PTP, h2 is also implicitly synchronized to
# swp1 (and both to CLOCK_REALTIME).
h1=${NETIFS[p1]}
swp1=${NETIFS[p2]}
swp2=${NETIFS[p3]}
h2=${NETIFS[p4]}

UDS_ADDRESS_H1="/var/run/ptp4l_h1"
UDS_ADDRESS_SWP1="/var/run/ptp4l_swp1"

H1_IPV4="192.0.2.1"
H2_IPV4="192.0.2.2"
H1_IPV6="2001:db8:1::1"
H2_IPV6="2001:db8:1::2"

# Tunables
NUM_PKTS=100
STREAM_VID=10
STREAM_PRIO_1=6
STREAM_PRIO_2=5
STREAM_PRIO_3=4
# PTP uses TC 0
ALL_GATES=$((1 << 0 | 1 << STREAM_PRIO_1 | 1 << STREAM_PRIO_2))
# Use a conservative cycle of 10 ms to allow the test to still pass when the
# kernel has some extra overhead like lockdep etc
CYCLE_TIME_NS=10000000
# Create two Gate Control List entries, one OPEN and one CLOSE, of equal
# durations
GATE_DURATION_NS=$((CYCLE_TIME_NS / 2))
# Give 2/3 of the cycle time to user space and 1/3 to the kernel
FUDGE_FACTOR=$((CYCLE_TIME_NS / 3))
# Shift the isochron base time by half the gate time, so that packets are
# always received by swp1 close to the middle of the time slot, to minimize
# inaccuracies due to network sync
SHIFT_TIME_NS=$((GATE_DURATION_NS / 2))

path_delay=

h1_create()
{
	simple_if_init $h1 $H1_IPV4/24 $H1_IPV6/64
}

h1_destroy()
{
	simple_if_fini $h1 $H1_IPV4/24 $H1_IPV6/64
}

h2_create()
{
	simple_if_init $h2 $H2_IPV4/24 $H2_IPV6/64
}

h2_destroy()
{
	simple_if_fini $h2 $H2_IPV4/24 $H2_IPV6/64
}

switch_create()
{
	local h2_mac_addr=$(mac_get $h2)

	ip link set $swp1 up
	ip link set $swp2 up

	ip link add br0 type bridge vlan_filtering 1
	ip link set $swp1 master br0
	ip link set $swp2 master br0
	ip link set br0 up

	bridge vlan add dev $swp2 vid $STREAM_VID
	bridge vlan add dev $swp1 vid $STREAM_VID
	bridge fdb add dev $swp2 \
		$h2_mac_addr vlan $STREAM_VID static master
}

switch_destroy()
{
	ip link del br0
}

ptp_setup()
{
	# Set up swp1 as a master PHC for h1, synchronized to the local
	# CLOCK_REALTIME.
	phc2sys_start $UDS_ADDRESS_SWP1
	ptp4l_start $h1 true $UDS_ADDRESS_H1
	ptp4l_start $swp1 false $UDS_ADDRESS_SWP1
}

ptp_cleanup()
{
	ptp4l_stop $swp1
	ptp4l_stop $h1
	phc2sys_stop
}

txtime_setup()
{
	local if_name=$1

	tc qdisc add dev $if_name clsact
	# Classify PTP on TC 7 and isochron on TC 6
	tc filter add dev $if_name egress protocol 0x88f7 \
		flower action skbedit priority 7
	tc filter add dev $if_name egress protocol 802.1Q \
		flower vlan_ethtype 0xdead action skbedit priority 6
	tc qdisc add dev $if_name handle 100: parent root mqprio num_tc 8 \
		queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 \
		map 0 1 2 3 4 5 6 7 \
		hw 1
	# Set up TC 5, 6, 7 for SO_TXTIME. tc-mqprio queues count from 1.
	tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_1 + 1)) etf \
		clockid CLOCK_TAI offload delta $FUDGE_FACTOR
	tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_2 + 1)) etf \
		clockid CLOCK_TAI offload delta $FUDGE_FACTOR
	tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_3 + 1)) etf \
		clockid CLOCK_TAI offload delta $FUDGE_FACTOR
}

txtime_cleanup()
{
	local if_name=$1

	tc qdisc del dev $if_name clsact
	tc qdisc del dev $if_name root
}

taprio_replace()
{
	local if_name="$1"; shift
	local extra_args="$1"; shift

	# STREAM_PRIO_1 always has an open gate.
	# STREAM_PRIO_2 has a gate open for GATE_DURATION_NS (half the cycle time)
	# STREAM_PRIO_3 always has a closed gate.
	tc qdisc replace dev $if_name root stab overhead 24 taprio num_tc 8 \
		queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 \
		map 0 1 2 3 4 5 6 7 \
		sched-entry S $(printf "%x" $ALL_GATES) $GATE_DURATION_NS \
		sched-entry S $(printf "%x" $((ALL_GATES & ~(1 << STREAM_PRIO_2)))) $GATE_DURATION_NS \
		base-time 0 flags 0x2 $extra_args
	taprio_wait_for_admin $if_name
}

taprio_cleanup()
{
	local if_name=$1

	tc qdisc del dev $if_name root
}

probe_path_delay()
{
	local isochron_dat="$(mktemp)"
	local received

	log_info "Probing path delay"

	isochron_do "$h1" "$h2" "$UDS_ADDRESS_H1" "" 0 \
		"$CYCLE_TIME_NS" "" "" "$NUM_PKTS" \
		"$STREAM_VID" "$STREAM_PRIO_1" "" "$isochron_dat"

	received=$(isochron_report_num_received "$isochron_dat")
	if [ "$received" != "$NUM_PKTS" ]; then
		echo "Cannot establish basic data path between $h1 and $h2"
		exit $ksft_fail
	fi

	printf "pdelay = {}\n" > isochron_data.py
	isochron report --input-file "$isochron_dat" \
		--printf-format "pdelay[%u] = %d - %d\n" \
		--printf-args "qRT" \
		>> isochron_data.py
	cat <<-'EOF' > isochron_postprocess.py
	#!/usr/bin/env python3

	from isochron_data import pdelay
	import numpy as np

	w = np.array(list(pdelay.values()))
	print("{}".format(np.max(w)))
	EOF
	path_delay=$(python3 ./isochron_postprocess.py)

	log_info "Path delay from $h1 to $h2 estimated at $path_delay ns"

	if [ "$path_delay" -gt "$GATE_DURATION_NS" ]; then
		echo "Path delay larger than gate duration, aborting"
		exit $ksft_fail
	fi

	rm -f ./isochron_data.py 2> /dev/null
	rm -f ./isochron_postprocess.py 2> /dev/null
	rm -f "$isochron_dat" 2> /dev/null
}

setup_prepare()
{
	vrf_prepare

	h1_create
	h2_create
	switch_create

	txtime_setup $h1

	# Temporarily set up PTP just to probe the end-to-end path delay.
	ptp_setup
	probe_path_delay
	ptp_cleanup
}

cleanup()
{
	pre_cleanup

	isochron_recv_stop
	txtime_cleanup $h1

	switch_destroy
	h2_destroy
	h1_destroy

	vrf_cleanup
}

run_test()
{
	local base_time=$1; shift
	local stream_prio=$1; shift
	local expected_delay=$1; shift
	local should_fail=$1; shift
	local test_name=$1; shift
	local isochron_dat="$(mktemp)"
	local received
	local median_delay

	RET=0

	# Set the shift time equal to the cycle time, which effectively
	# cancels the default advance time. Packets won't be sent early in
	# software, which ensures that they won't prematurely enter through
	# the open gate in __test_out_of_band(). Also, the gate is open for
	# long enough that this won't cause a problem in __test_in_band().
	isochron_do "$h1" "$h2" "$UDS_ADDRESS_H1" "" "$base_time" \
		"$CYCLE_TIME_NS" "$SHIFT_TIME_NS" "$GATE_DURATION_NS" \
		"$NUM_PKTS" "$STREAM_VID" "$stream_prio" "" "$isochron_dat"

	received=$(isochron_report_num_received "$isochron_dat")
	[ "$received" = "$NUM_PKTS" ]
	check_err_fail $should_fail $? "Reception of $NUM_PKTS packets"

	if [ $should_fail = 0 ] && [ "$received" = "$NUM_PKTS" ]; then
		printf "pdelay = {}\n" > isochron_data.py
		isochron report --input-file "$isochron_dat" \
			--printf-format "pdelay[%u] = %d - %d\n" \
			--printf-args "qRT" \
			>> isochron_data.py
		cat <<-'EOF' > isochron_postprocess.py
		#!/usr/bin/env python3

		from isochron_data import pdelay
		import numpy as np

		w = np.array(list(pdelay.values()))
		print("{}".format(int(np.median(w))))
		EOF
		median_delay=$(python3 ./isochron_postprocess.py)

		# If the condition below is true, packets were delayed by a closed gate
		[ "$median_delay" -gt $((path_delay + expected_delay)) ]
		check_fail $? "Median delay $median_delay is greater than expected delay $expected_delay plus path delay $path_delay"

		# If the condition below is true, packets were sent expecting them to
		# hit a closed gate in the switch, but were not delayed
		[ "$expected_delay" -gt 0 ] && [ "$median_delay" -lt "$expected_delay" ]
		check_fail $? "Median delay $median_delay is less than expected delay $expected_delay"
	fi

	log_test "$test_name"

	rm -f ./isochron_data.py 2> /dev/null
	rm -f ./isochron_postprocess.py 2> /dev/null
	rm -f "$isochron_dat" 2> /dev/null
}

__test_always_open()
{
	run_test 0.000000000 $STREAM_PRIO_1 0 0 "Gate always open"
}

__test_always_closed()
{
	run_test 0.000000000 $STREAM_PRIO_3 0 1 "Gate always closed"
}

__test_in_band()
{
	# Send packets in-band with the OPEN gate entry
	run_test 0.000000000 $STREAM_PRIO_2 0 0 "In band with gate"
}

__test_out_of_band()
{
	# Send packets in-band with the CLOSE gate entry
	run_test 0.005000000 $STREAM_PRIO_2 \
		$((GATE_DURATION_NS - SHIFT_TIME_NS)) 0 \
		"Out of band with gate"
}

run_subtests()
{
	__test_always_open
	__test_always_closed
	__test_in_band
	__test_out_of_band
}

test_taprio_after_ptp()
{
	log_info "Setting up taprio after PTP"
	ptp_setup
	taprio_replace $swp2
	run_subtests
	taprio_cleanup $swp2
	ptp_cleanup
}

__test_under_max_sdu()
{
	# Limit max-sdu for STREAM_PRIO_1
	taprio_replace "$swp2" "max-sdu 0 0 0 0 0 0 100 0"
	run_test 0.000000000 $STREAM_PRIO_1 0 0 "Under maximum SDU"
}

__test_over_max_sdu()
{
	# Limit max-sdu for STREAM_PRIO_1
	taprio_replace "$swp2" "max-sdu 0 0 0 0 0 0 20 0"
	run_test 0.000000000 $STREAM_PRIO_1 0 1 "Over maximum SDU"
}

test_max_sdu()
{
	ptp_setup
	__test_under_max_sdu
	__test_over_max_sdu
	taprio_cleanup $swp2
	ptp_cleanup
}

# Perform a clock jump in the past without synchronization running, so that the
# time base remains where it was set by phc_ctl.
test_clock_jump_backward()
{
	# This is a more complex schedule specifically crafted in a way that
	# has been problematic on NXP LS1028A. Not much to test with it other
	# than the fact that it passes traffic.
	tc qdisc replace dev $swp2 root stab overhead 24 taprio num_tc 8 \
		queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 map 0 1 2 3 4 5 6 7 \
		base-time 0 sched-entry S 20 300000 sched-entry S 10 200000 \
		sched-entry S 20 300000 sched-entry S 48 200000 \
		sched-entry S 20 300000 sched-entry S 83 200000 \
		sched-entry S 40 300000 sched-entry S 00 200000 flags 2

	log_info "Forcing a backward clock jump"
	phc_ctl $swp1 set 0

	ping_test $h1 192.0.2.2
	taprio_cleanup $swp2
}

# Test that taprio tolerates clock jumps.
# Since ptp4l and phc2sys are running, it is expected for the time to
# eventually recover (through yet another clock jump). Isochron waits
# until that is the case.
test_clock_jump_backward_forward()
{
	log_info "Forcing a backward and a forward clock jump"
	taprio_replace $swp2
	phc_ctl $swp1 set 0
	ptp_setup
	ping_test $h1 192.0.2.2
	run_subtests
	ptp_cleanup
	taprio_cleanup $swp2
}

tc_offload_check
if [[ $? -ne 0 ]]; then
	log_test_skip "Could not test offloaded functionality"
	exit $EXIT_STATUS
fi

trap cleanup EXIT

setup_prepare
setup_wait
tests_run

exit $EXIT_STATUS