aboutsummaryrefslogtreecommitdiff
path: root/net/speedtest-netperf/files/speedtest-netperf.sh
blob: f303f63129136b8447b453286885974b00efd80c (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
#!/bin/sh

# This speed testing script provides a convenient means of on-device network
# performance testing for OpenWrt routers, and subsumes functionality of the
# earlier CeroWrt scripts betterspeedtest.sh and netperfrunner.sh written by
# Rich Brown.
#
# When launched, the script uses netperf to run several upload and download
# streams to an Internet server. This places heavy load on the bottleneck link
# of your network (probably your Internet connection) while measuring the total
# bandwidth of the link during the transfers. Under this network load, the
# script simultaneously measures the latency of pings to see whether the file
# transfers affect the responsiveness of your network. Additionally, the script
# tracks the per-CPU processor usage, as well as the netperf CPU usage used for
# the test. On systems that report CPU frequency scaling, the script can also
# report per-CPU frequencies.
#
# The script operates in two modes of network loading: sequential and
# concurrent. The default sequential mode emulates a web-based speed test by
# first downloading and then uploading network streams, while concurrent mode
# provides a stress test by dowloading and uploading streams simultaneously.
#
# NOTE: The script uses servers and network bandwidth that are provided by
# generous volunteers (not some wealthy "big company"). Feel free to use the
# script to test your SQM configuration or troubleshoot network and latency
# problems.  Continuous or high rate use of this script may result in denied
# access. Happy testing!
#
# For more information, consult the online README.md:
# https://github.com/openwrt/packages/blob/master/net/speedtest-netperf/files/README.md

# Usage: speedtest-netperf.sh [-4 | -6] [ -H netperf-server ] [ -t duration ] [ -p host-to-ping ] [ -n simultaneous-streams ] [ -s | -c ]

# Options: If options are present:
#
# -H | --host:   netperf server name or IP (default netperf.bufferbloat.net)
#                Alternate servers are netperf-east (east coast US),
#                netperf-west (California), and netperf-eu (Denmark)
# -4 | -6:       Enable ipv4 or ipv6 testing (ipv4 is the default)
# -t | --time:   Duration of each direction's test - (default - 60 seconds)
# -p | --ping:   Host to ping to measure latency (default - gstatic.com)
# -n | --number: Number of simultaneous sessions (default - 5 sessions)
#                based on whether concurrent or sequential upload/downloads)
# -s | -c:       Sequential or concurrent download/upload (default - sequential)

# Copyright (c) 2014 - Rich Brown <rich.brown@blueberryhillsoftware.com>
# Copyright (c) 2018 - Tony Ambardar <itugrok@yahoo.com>
# GPLv2


# Summarize contents of the ping's output file as min, avg, median, max, etc.
#   input parameter ($1) file contains the output of the ping command

summarize_pings() {

# Process the ping times, and summarize the results
# grep to keep lines with "time=", and sed to isolate time stamps and sort them
# awk builds an array of those values, prints first & last (which are min, max)
# and computes average.
# If the number of samples is >= 10, also computes median, and 10th and 90th
# percentile readings.
	sed 's/^.*time=\([^ ]*\) ms/\1 pingtime/' < $1 | grep -v "PING" | sort -n | awk '
BEGIN {numdrops=0; numrows=0;}
{
	if ( $2 == "pingtime" ) {
		numrows += 1;
		arr[numrows]=$1; sum+=$1;
	} else {
		numdrops += 1;
	}
}
END {
	pc10="-"; pc90="-"; med="-";
	if (numrows>=10) {
		ix=int(numrows/10); pc10=arr[ix]; ix=int(numrows*9/10);pc90=arr[ix];
		if (numrows%2==1) med=arr[(numrows+1)/2]; else med=(arr[numrows/2]);
	}
	pktloss = numdrops>0 ? numdrops/(numdrops+numrows) * 100 : 0;
	printf("  Latency: [in msec, %d pings, %4.2f%% packet loss]\n",numdrops+numrows,pktloss)
	if (numrows>0) {
		fmt="%9s: %7.3f\n"
		printf(fmt fmt fmt fmt fmt fmt, "Min",arr[1],"10pct",pc10,"Median",med,
			"Avg",sum/numrows,"90pct",pc90,"Max",arr[numrows])
	}
}'
}

# Summarize the contents of the load file, speedtest process stat file, cpuinfo
# file to show mean/stddev CPU utilization, CPU freq, netperf CPU usage.
#   input parameter ($1) file contains CPU load/frequency samples

summarize_load() {
	cat $1 /proc/$$/stat | awk -v SCRIPT_PID=$$ '
# track CPU frequencies
$1 == "cpufreq" {
	sum_freq[$2]+=$3/1000
	n_freq_samp[$2]++
}
# total CPU of speedtest processes
$1 == SCRIPT_PID {
	tot=$16+$17
	if (init_proc_cpu=="") init_proc_cpu=tot
	proc_cpu=tot-init_proc_cpu
}
# track aggregate CPU stats
$1 == "cpu" {
	tot=0; for (f=2;f<=8;f++) tot+=$f
	if (init_cpu=="") init_cpu=tot
	tot_cpu=tot-init_cpu
	n_load_samp++
}
# track per-CPU stats
$1 ~ /cpu[0-9]+/ {
	tot=0; for (f=2;f<=8;f++) tot+=$f
	usg=tot-($5+$6)
	if (init_tot[$1]=="") {
		init_tot[$1]=tot
		init_usg[$1]=usg
		cpus[n_cpus++]=$1
	}
	if (last_tot[$1]>0) {
		sum_usg_2[$1] += ((usg-last_usg[$1])/(tot-last_tot[$1]))^2
	}
	last_tot[$1]=tot
	last_usg[$1]=usg
}
END {
	printf(" CPU Load: [in %% busy (avg +/- std dev)")
	for (i in sum_freq) if (sum_freq[i]>0) {printf(" @ avg frequency"); break}
	if (n_load_samp>0) n_load_samp--
	printf(", %d samples]\n", n_load_samp)
	for (i=0;i<n_cpus;i++) {
		c=cpus[i]
		if (n_load_samp>0) {
			avg_usg=(last_tot[c]-init_tot[c])
			avg_usg=avg_usg>0 ? (last_usg[c]-init_usg[c])/avg_usg : 0
			std_usg=sum_usg_2[c]/n_load_samp-avg_usg^2
			std_usg=std_usg>0 ? sqrt(std_usg) : 0
			printf("%9s: %5.1f +/- %4.1f", c, avg_usg*100, std_usg*100)
			avg_freq=n_freq_samp[c]>0 ? sum_freq[c]/n_freq_samp[c] : 0
			if (avg_freq>0) printf("  @ %4d MHz", avg_freq)
			printf("\n")
		}
	}
	printf(" Overhead: [in %% used of total CPU available]\n")
	printf("%9s: %5.1f\n", "netperf", tot_cpu>0 ? proc_cpu/tot_cpu*100 : 0)
}'
}

# Summarize the contents of the speed file to show formatted transfer rate.
#   input parameter ($1) indicates transfer direction
#   input parameter ($2) file contains speed info from netperf

summarize_speed() {
	printf "%9s: %6.2f Mbps\n" $1 $(awk '{s+=$1} END {print s}' $2)
}

# Capture process load, then per-CPU load/frequency info at 1-second intervals.

sample_load() {
	local cpus="$(find /sys/devices/system/cpu -name 'cpu[0-9]*' 2>/dev/null)"
	local f="cpufreq/scaling_cur_freq"
	cat /proc/$$/stat
	while : ; do
		sleep 1s
		egrep "^cpu[0-9]*" /proc/stat
		for c in $cpus; do
			[ -r $c/$f ] && echo "cpufreq $(basename $c) $(cat $c/$f)"
		done
	done
}

# Print a line of dots as a progress indicator.

print_dots() {
	while : ; do
		printf "."
		sleep 1s
	done
}

# Start $MAXSESSIONS datastreams between netperf client and server
# netperf writes the sole output value (in Mbps) to stdout when completed

start_netperf() {
	for i in $( seq $MAXSESSIONS ); do
		netperf $TESTPROTO -H $TESTHOST -t $1 -l $TESTDUR -v 0 -P 0 >> $2 &
#		echo "Starting PID $! params: $TESTPROTO -H $TESTHOST -t $1 -l $TESTDUR -v 0 -P 0 >> $2"
	done
}

# Wait until each of the background netperf processes completes

wait_netperf() {
	# gets a list of PIDs for child processes named 'netperf'
#	echo "Process is $$"
#	echo $(pgrep -P $$ netperf)
	local err=0
	for i in $(pgrep -P $$ netperf); do
#	echo "Waiting for $i"
		wait $i || err=1
	done
	return $err
}

# Stop the background netperf processes

kill_netperf() {
	# gets a list of PIDs for child processes named 'netperf'
#	echo "Process is $$"
#	echo $(pgrep -P $$ netperf)
	for i in $(pgrep -P $$ netperf); do
#	echo "Stopping $i"
		kill -9 $i
		wait $i 2>/dev/null
	done
}

# Stop the current sample_load() process

kill_load() {
#	echo "Load: $LOAD_PID"
	kill -9 $LOAD_PID
	wait $LOAD_PID 2>/dev/null
	LOAD_PID=0
}

# Stop the current print_dots() process

kill_dots() {
#	echo "Dots: $DOTS_PID"
	kill -9 $DOTS_PID
	wait $DOTS_PID 2>/dev/null
	DOTS_PID=0
}

# Stop the current ping process

kill_pings() {
#	echo "Pings: $PING_PID"
	kill -9 $PING_PID
	wait $PING_PID 2>/dev/null
	PING_PID=0
}

# Stop the current load, pings and dots, and exit
# ping command catches and handles first Ctrl-C, so you have to hit it again...

kill_background_and_exit() {
	kill_netperf
	kill_load
	kill_dots
	rm -f $DLFILE
	rm -f $ULFILE
	rm -f $LOADFILE
	rm -f $PINGFILE
	echo; echo "Stopped"
	exit 1
}

# Measure speed, ping latency and cpu usage of netperf data transfers
# Called with direction parameter: "Download", "Upload", or "Bidirectional"
# The function gets other info from globals and command-line arguments.

measure_direction() {

	# Create temp files for netperf up/download results
	ULFILE=$(mktemp /tmp/netperfUL.XXXXXX) || exit 1
	DLFILE=$(mktemp /tmp/netperfDL.XXXXXX) || exit 1
	PINGFILE=$(mktemp /tmp/measurepings.XXXXXX) || exit 1
	LOADFILE=$(mktemp /tmp/measureload.XXXXXX) || exit 1
#	echo $ULFILE $DLFILE $PINGFILE $LOADFILE

	local dir=$1
	local spd_test

	# Start dots
	print_dots &
	DOTS_PID=$!
#	echo "Dots PID: $DOTS_PID"

	# Start Ping
	if [ $TESTPROTO -eq "-4" ]; then
		ping  $PINGHOST > $PINGFILE &
	else
		ping6 $PINGHOST > $PINGFILE &
	fi
	PING_PID=$!
#	echo "Ping PID: $PING_PID"

	# Start CPU load sampling
	sample_load > $LOADFILE &
	LOAD_PID=$!
#	echo "Load PID: $LOAD_PID"

	# Start netperf datastreams between client and server
	if [ $dir = "Bidirectional" ]; then
		start_netperf TCP_STREAM $ULFILE
		start_netperf TCP_MAERTS $DLFILE
	else
		# Start unidirectional netperf with the proper direction
		case $dir in
			Download) spd_test="TCP_MAERTS";;
			Upload) spd_test="TCP_STREAM";;
		esac
		start_netperf $spd_test $DLFILE
	fi

	# Wait until background netperf processes complete, check errors
	if ! wait_netperf; then
		echo;echo "WARNING: netperf returned errors. Results may be inaccurate!"
	fi

	# When netperf completes, stop the CPU monitor, dots and pings
	kill_load
	kill_pings
	kill_dots
	echo

	# Print TCP Download/Upload speed
	if [ $dir = "Bidirectional" ]; then
		summarize_speed Download $DLFILE
		summarize_speed Upload $ULFILE
	else
		summarize_speed $dir $DLFILE
	fi

	# Summarize the ping data
	summarize_pings $PINGFILE

	# Summarize the load data
	summarize_load $LOADFILE

	# Clean up
	rm -f $DLFILE
	rm -f $ULFILE
	rm -f $PINGFILE
	rm -f $LOADFILE
}

# ------- Start of the main routine --------

# set an initial values for defaults
TESTHOST="netperf.bufferbloat.net"
TESTDUR="60"
PINGHOST="gstatic.com"
MAXSESSIONS=5
TESTPROTO="-4"
TESTSEQ=1

# read the options

# extract options and their arguments into variables.
while [ $# -gt 0 ]
do
	case "$1" in
		-s|--sequential) TESTSEQ=1 ; shift 1 ;;
		-c|--concurrent) TESTSEQ=0 ; shift 1 ;;
		-4|-6) TESTPROTO=$1 ; shift 1 ;;
		-H|--host)
			case "$2" in
				"") echo "Missing hostname" ; exit 1 ;;
				*) TESTHOST=$2 ; shift 2 ;;
			esac ;;
		-t|--time)
			case "$2" in
				"") echo "Missing duration" ; exit 1 ;;
				*) TESTDUR=$2 ; shift 2 ;;
			esac ;;
		-p|--ping)
			case "$2" in
				"") echo "Missing ping host" ; exit 1 ;;
				*) PINGHOST=$2 ; shift 2 ;;
			esac ;;
		-n|--number)
			case "$2" in
				"") echo "Missing number of simultaneous streams" ; exit 1 ;;
				*) MAXSESSIONS=$2 ; shift 2 ;;
			esac ;;
		--) shift ; break ;;
		*) echo "Usage: speedtest-netperf.sh [ -s | -c ] [-4 | -6] [ -H netperf-server ] [ -t duration ] [ -p host-to-ping ] [ -n simultaneous-sessions ]" ; exit 1 ;;
	esac
done

# Check dependencies

if ! netperf -V >/dev/null 2>&1; then
	echo "Missing netperf program, please install" ; exit 1
fi

# Start the main test

DATE=$(date "+%Y-%m-%d %H:%M:%S")
echo "$DATE Starting speedtest for $TESTDUR seconds per transfer session."
echo "Measure speed to $TESTHOST (IPv${TESTPROTO#-}) while pinging $PINGHOST."
echo -n "Download and upload sessions are "
[ "$TESTSEQ " -eq "1" ] && echo -n "sequential," || echo -n "concurrent,"
echo " each with $MAXSESSIONS simultaneous streams."

# Catch a Ctl-C and stop background netperf, CPU stats, pinging and print_dots
trap kill_background_and_exit HUP INT TERM

if [ $TESTSEQ -eq "1" ]; then
	measure_direction "Download"
	measure_direction "Upload"
else
	measure_direction "Bidirectional"
fi