Skip to content

Commit a0ade14

Browse files
committed
test-case: latency-baseline: introduce latency tests
Introduce latency tests using `JACK` Audio Connection Kit: run `jackd` audio service, connect ports with loopback and run `jack_iodelay` to collect latency measurements, calculate and save statistics in a json report file. Signed-off-by: Dmitrii Golovanov <dmitrii.golovanov@intel.com>
1 parent fdd7ae4 commit a0ade14

File tree

2 files changed

+235
-0
lines changed

2 files changed

+235
-0
lines changed

env-check.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,18 @@ main "$@"
8181

8282
out_str="" check_res=0
8383
printf "Checking for some OS packages:\t\t"
84+
func_check_pkg gawk
8485
func_check_pkg expect
8586
func_check_pkg aplay
8687
func_check_pkg sox
8788
func_check_pkg tinycap
8889
func_check_pkg tinyplay
90+
# JACK Audio Connection Kit
91+
func_check_pkg jackd
92+
func_check_pkg jack_iodelay
93+
func_check_pkg jack_lsp
94+
func_check_pkg jack_connect
95+
#
8996
func_check_pkg python3
9097
# jq is command-line json parser
9198
func_check_pkg jq

test-case/latency-baseline.sh

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/bin/bash
2+
3+
##
4+
## Case Name: latency baseline statistics collection on a signal path
5+
##
6+
## Preconditions:
7+
## - JACK Audio Connection Kit is installed.
8+
## - loopback connection to measure latency over its signal path.
9+
##
10+
## Description:
11+
## Run `jackd` audio server; execute `jack_iodelay` with its in/out ports
12+
## connected to the loopback-ed ports and give it some time ot collect
13+
## latency measurements (on each 1/2 sec.)
14+
##
15+
## Case step:
16+
## 1. Probe to start `jackd` with parameters given and read configuration.
17+
## 2. Start `jackd` again for latency measurements.
18+
## 3. Start `jack_iodelay` which awaits for its ports connected to loopback.
19+
## 4. Connect `jack_iodelay` ports to signal path ports with the loopback.
20+
## 5. Wait for the period given to collect enough latency measurements.
21+
## 6. Calculate latency statistics and save into a JSON file.
22+
##
23+
## Expect result:
24+
## Latency statistics collected and saved in `results.json`.
25+
## Exit status 0.
26+
##
27+
28+
# shellcheck source=case-lib/lib.sh
29+
source "$(dirname "${BASH_SOURCE[0]}")"/../case-lib/lib.sh
30+
31+
OPT_NAME['R']='run_period' OPT_DESC['R']='Time period (in seconds) to measure latency.'
32+
OPT_HAS_ARG['R']=1 OPT_VAL['R']="30"
33+
34+
OPT_NAME['d']='device' OPT_DESC['d']='ALSA pcm device to use by JACK'
35+
OPT_HAS_ARG['d']=1 OPT_VAL['d']="hw:0"
36+
37+
OPT_NAME['r']='rate' OPT_DESC['r']='Sample rate to try latency with'
38+
OPT_HAS_ARG['r']=1 OPT_VAL['r']=48000
39+
40+
OPT_NAME['S']='shorts' OPT_DESC['S']='Try to use 16-bit samples instead of 32-bit, if possible.'
41+
OPT_HAS_ARG['S']=0 OPT_VAL['S']=0
42+
43+
OPT_NAME['p']='port_p' OPT_DESC['p']='Jack playback port with loopback. Example: system:playback_1'
44+
OPT_HAS_ARG['p']=1 OPT_VAL['p']=''
45+
46+
OPT_NAME['c']='port_c' OPT_DESC['c']='Jack capture port with loopback. Example: system:capture_1'
47+
OPT_HAS_ARG['c']=1 OPT_VAL['c']=''
48+
49+
OPT_NAME['s']='sof-logger' OPT_DESC['s']="Open sof-logger trace the data will store at $LOG_ROOT"
50+
OPT_HAS_ARG['s']=0 OPT_VAL['s']=1
51+
52+
OPT_NAME['v']='verbose' OPT_DESC['v']='Verbose logging.'
53+
OPT_HAS_ARG['v']=0 OPT_VAL['v']=0
54+
55+
func_opt_parse_option "$@"
56+
57+
setup_kernel_check_point
58+
59+
# shellcheck disable=SC2016
60+
AWK_SCRIPT='
61+
function min(in_array)
62+
{
63+
min_value = "N/A"
64+
if (! isarray(in_array) || length(in_array) == 0) return min_value
65+
for(idx in in_array) {
66+
if (min_value == "N/A" || in_array[idx] < min_value) {
67+
min_value = in_array[idx]
68+
}
69+
}
70+
return min_value
71+
}
72+
73+
function max(in_array)
74+
{
75+
max_value = "N/A"
76+
if (! isarray(in_array) || length(in_array) == 0) return max_value
77+
for(idx in in_array) {
78+
if (max_value == "N/A" || in_array[idx] > max_value) {
79+
max_value = in_array[idx]
80+
}
81+
}
82+
return max_value
83+
}
84+
85+
function sum(in_array)
86+
{
87+
if (! isarray(in_array) || length(in_array) == 0) return 0
88+
sum_items=0
89+
for(idx in in_array) {
90+
sum_items += in_array[idx]
91+
}
92+
return sum_items
93+
}
94+
95+
function stddev(in_array)
96+
{
97+
if (! isarray(in_array) || length(in_array) == 0) return -1
98+
sum_items=0
99+
cnt_items=0
100+
for(idx in in_array) {
101+
sum_items += in_array[idx]
102+
cnt_items += 1
103+
}
104+
avg = sum_items / cnt_items
105+
dev = 0
106+
for(idx in in_array) dev += (in_array[idx] - avg)^2
107+
return sqrt(dev/(cnt_items - 1))
108+
}
109+
110+
/^[ ]*[0-9.]+ frames[ ]+[0-9.]+ ms total roundtrip latency/ {
111+
sum_frames+=$1
112+
sum_ms+=$3
113+
latency_frames[NR]=$1
114+
latency_ms[NR]=$3
115+
}
116+
END {
117+
if (length(latency_frames) !=0 && length(latency_ms) != 0) {
118+
printf("\"metrics\":[")
119+
printf("{\"name\":\"roundtrip latency\", ")
120+
printf("\"probes\":%d, ", length(latency_frames))
121+
printf("\"avg_frames\":%0.3f, ", (length(latency_frames) ? sum(latency_frames) / length(latency_frames) : 0))
122+
printf("\"min_frames\":%0.3f, \"max_frames\":%0.3f, ", min(latency_frames), max(latency_frames))
123+
printf("\"avg_ms\":%0.3f, ", (length(latency_ms) ? sum(latency_ms) / length(latency_ms) : 0))
124+
printf("\"min_ms\":%0.3f, \"max_ms\":%0.3f, ", min(latency_ms), max(latency_ms))
125+
printf("\"stdev_frames\":%0.6f, \"stdev_ms\":%0.6f }", stddev(latency_frames), stddev(latency_ms))
126+
printf("]")
127+
fflush()
128+
}
129+
}
130+
'
131+
opts_json()
132+
{
133+
local items_=()
134+
for idx_ in "${!OPT_NAME[@]}" ; do
135+
items_+=("\"${OPT_NAME[$idx_]}\":\"${OPT_VAL[$idx_]}\"")
136+
done
137+
echo "$(IFS=',' ; printf "%s" "${items_[*]}")"
138+
}
139+
140+
alsa_device=${OPT_VAL['d']}
141+
alsa_shorts=$([ "${OPT_VAL['S']}" -eq 1 ] && echo '--shorts' || echo '')
142+
port_playback=${OPT_VAL['p']}
143+
port_capture=${OPT_VAL['c']}
144+
rate=${OPT_VAL['r']}
145+
run_period=${OPT_VAL['R']}
146+
run_verbose=$([ "${OPT_VAL['v']}" -eq 1 ] && echo '--verbose' || echo '')
147+
148+
RUN_PERIOD_MAX="$((run_period + 30))s"
149+
JACKD_TIMEOUT="$((run_period + 15))s"
150+
JACKD_OPTIONS="${run_verbose} --realtime --temporary"
151+
JACKD_BACKEND="alsa"
152+
JACKD_BACKEND_OPTIONS="-d ${alsa_device} -r ${rate} ${alsa_shorts}"
153+
WAIT_JACKD="2s"
154+
IODELAY_TIMEOUT="${run_period}s"
155+
WAIT_IODELAY="2s"
156+
157+
if [ "$port_playback" == "" ] || [ "$port_capture" == "" ];
158+
then
159+
dloge "No playback or capture Jack port is specified. Skip the test."
160+
exit 2
161+
fi
162+
163+
check_jackd_configured()
164+
{
165+
export JACK_NO_AUDIO_RESERVATION=1
166+
dlogc "jackd ${JACKD_OPTIONS} -d ${JACKD_BACKEND} ${JACKD_BACKEND_OPTIONS}"
167+
# shellcheck disable=SC2086
168+
timeout --kill-after ${RUN_PERIOD_MAX} ${run_verbose} ${JACKD_TIMEOUT} \
169+
jackd ${JACKD_OPTIONS} -d ${JACKD_BACKEND} ${JACKD_BACKEND_OPTIONS} & jackdPID=$!
170+
171+
sleep ${WAIT_JACKD}
172+
173+
dlogc "jack_lsp"
174+
jack_lsp -AclLpt
175+
176+
dlogi "Waiting Jackd to stop without a client"
177+
wait ${jackdPID}
178+
}
179+
180+
collect_latency_data()
181+
{
182+
export JACK_NO_AUDIO_RESERVATION=1
183+
dlogc "jackd ${JACKD_OPTIONS} -d ${JACKD_BACKEND} ${JACKD_BACKEND_OPTIONS}"
184+
# shellcheck disable=SC2086
185+
timeout --kill-after ${RUN_PERIOD_MAX} ${run_verbose} ${JACKD_TIMEOUT} \
186+
jackd ${JACKD_OPTIONS} -d ${JACKD_BACKEND} ${JACKD_BACKEND_OPTIONS} & jackdPID=$!
187+
188+
sleep ${WAIT_JACKD}
189+
dlogc "jack_iodelay"
190+
# shellcheck disable=SC2086
191+
timeout --kill-after ${RUN_PERIOD_MAX} ${run_verbose} ${IODELAY_TIMEOUT} \
192+
stdbuf -oL -eL jack_iodelay | tee >(gawk "${AWK_SCRIPT}" > "${LOG_ROOT}/metrics.json") & iodelayPID="$!"
193+
194+
sleep ${WAIT_IODELAY}
195+
dlogi "jack_connect: ${port_capture} ==>[jack_delay]==> ${port_playback}"
196+
jack_connect jack_delay:out "${port_playback}" && jack_connect jack_delay:in "${port_capture}"
197+
198+
dlogi "Latency data collection"
199+
wait ${jackdPID} ${iodelayPID}
200+
dlogi "Latency data collection completed."
201+
}
202+
203+
# TODO: should we set volume to some pre-defined level (parameterized)
204+
# reset_sof_volume
205+
# set_alsa_settings
206+
207+
start_test
208+
209+
logger_disabled || func_lib_start_log_collect
210+
211+
dlogi "Check Jack server can be started"
212+
check_jackd_configured # "$@"
213+
214+
dlogi "Start collecting latency data"
215+
collect_latency_data # "$@"
216+
217+
if [ ! -f "${LOG_ROOT}/metrics.json" ] || [ "$(grep -ce 'metrics' "${LOG_ROOT}/metrics.json")" -lt 1 ];
218+
then
219+
dloge "No metrics collected"
220+
exit 2
221+
fi
222+
223+
dlogi "Compose results.json"
224+
echo -n "{\"options\":{$(opts_json)}," > "${LOG_ROOT}/results.json"
225+
cat "${LOG_ROOT}/metrics.json" >> "${LOG_ROOT}/results.json" && rm "${LOG_ROOT}/metrics.json"
226+
echo "}" >> "${LOG_ROOT}/results.json"
227+
228+
#

0 commit comments

Comments
 (0)