You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

408 lines
12 KiB

  1. #!/bin/sh
  2. # This speed testing script provides a convenient means of on-device network
  3. # performance testing for OpenWrt routers, and subsumes functionality of the
  4. # earlier CeroWrt scripts betterspeedtest.sh and netperfrunner.sh written by
  5. # Rich Brown.
  6. #
  7. # When launched, the script uses netperf to run several upload and download
  8. # streams to an Internet server. This places heavy load on the bottleneck link
  9. # of your network (probably your Internet connection) while measuring the total
  10. # bandwidth of the link during the transfers. Under this network load, the
  11. # script simultaneously measures the latency of pings to see whether the file
  12. # transfers affect the responsiveness of your network. Additionally, the script
  13. # tracks the per-CPU processor usage, as well as the netperf CPU usage used for
  14. # the test. On systems that report CPU frequency scaling, the script can also
  15. # report per-CPU frequencies.
  16. #
  17. # The script operates in two modes of network loading: sequential and
  18. # concurrent. The default sequential mode emulates a web-based speed test by
  19. # first downloading and then uploading network streams, while concurrent mode
  20. # provides a stress test by dowloading and uploading streams simultaneously.
  21. #
  22. # NOTE: The script uses servers and network bandwidth that are provided by
  23. # generous volunteers (not some wealthy "big company"). Feel free to use the
  24. # script to test your SQM configuration or troubleshoot network and latency
  25. # problems. Continuous or high rate use of this script may result in denied
  26. # access. Happy testing!
  27. #
  28. # For more information, consult the online README.md:
  29. # https://github.com/openwrt/packages/blob/master/net/speedtest-netperf/files/README.md
  30. # Usage: speedtest-netperf.sh [-4 | -6] [ -H netperf-server ] [ -t duration ] [ -p host-to-ping ] [ -n simultaneous-streams ] [ -s | -c ]
  31. # Options: If options are present:
  32. #
  33. # -H | --host: netperf server name or IP (default netperf.bufferbloat.net)
  34. # Alternate servers are netperf-east (east coast US),
  35. # netperf-west (California), and netperf-eu (Denmark)
  36. # -4 | -6: Enable ipv4 or ipv6 testing (ipv4 is the default)
  37. # -t | --time: Duration of each direction's test - (default - 60 seconds)
  38. # -p | --ping: Host to ping to measure latency (default - gstatic.com)
  39. # -n | --number: Number of simultaneous sessions (default - 5 sessions)
  40. # based on whether concurrent or sequential upload/downloads)
  41. # -s | -c: Sequential or concurrent download/upload (default - sequential)
  42. # Copyright (c) 2014 - Rich Brown <rich.brown@blueberryhillsoftware.com>
  43. # Copyright (c) 2018 - Tony Ambardar <itugrok@yahoo.com>
  44. # GPLv2
  45. # Summarize contents of the ping's output file as min, avg, median, max, etc.
  46. # input parameter ($1) file contains the output of the ping command
  47. summarize_pings() {
  48. # Process the ping times, and summarize the results
  49. # grep to keep lines with "time=", and sed to isolate time stamps and sort them
  50. # awk builds an array of those values, prints first & last (which are min, max)
  51. # and computes average.
  52. # If the number of samples is >= 10, also computes median, and 10th and 90th
  53. # percentile readings.
  54. sed 's/^.*time=\([^ ]*\) ms/\1 pingtime/' < $1 | grep -v "PING" | sort -n | awk '
  55. BEGIN {numdrops=0; numrows=0;}
  56. {
  57. if ( $2 == "pingtime" ) {
  58. numrows += 1;
  59. arr[numrows]=$1; sum+=$1;
  60. } else {
  61. numdrops += 1;
  62. }
  63. }
  64. END {
  65. pc10="-"; pc90="-"; med="-";
  66. if (numrows>=10) {
  67. ix=int(numrows/10); pc10=arr[ix]; ix=int(numrows*9/10);pc90=arr[ix];
  68. if (numrows%2==1) med=arr[(numrows+1)/2]; else med=(arr[numrows/2]);
  69. }
  70. pktloss = numdrops>0 ? numdrops/(numdrops+numrows) * 100 : 0;
  71. printf(" Latency: [in msec, %d pings, %4.2f%% packet loss]\n",numdrops+numrows,pktloss)
  72. if (numrows>0) {
  73. fmt="%9s: %7.3f\n"
  74. printf(fmt fmt fmt fmt fmt fmt, "Min",arr[1],"10pct",pc10,"Median",med,
  75. "Avg",sum/numrows,"90pct",pc90,"Max",arr[numrows])
  76. }
  77. }'
  78. }
  79. # Summarize the contents of the load file, speedtest process stat file, cpuinfo
  80. # file to show mean/stddev CPU utilization, CPU freq, netperf CPU usage.
  81. # input parameter ($1) file contains CPU load/frequency samples
  82. summarize_load() {
  83. cat $1 /proc/$$/stat | awk -v SCRIPT_PID=$$ '
  84. # track CPU frequencies
  85. $1 == "cpufreq" {
  86. sum_freq[$2]+=$3/1000
  87. n_freq_samp[$2]++
  88. }
  89. # total CPU of speedtest processes
  90. $1 == SCRIPT_PID {
  91. tot=$16+$17
  92. if (init_proc_cpu=="") init_proc_cpu=tot
  93. proc_cpu=tot-init_proc_cpu
  94. }
  95. # track aggregate CPU stats
  96. $1 == "cpu" {
  97. tot=0; for (f=2;f<=8;f++) tot+=$f
  98. if (init_cpu=="") init_cpu=tot
  99. tot_cpu=tot-init_cpu
  100. n_load_samp++
  101. }
  102. # track per-CPU stats
  103. $1 ~ /cpu[0-9]+/ {
  104. tot=0; for (f=2;f<=8;f++) tot+=$f
  105. usg=tot-($5+$6)
  106. if (init_tot[$1]=="") {
  107. init_tot[$1]=tot
  108. init_usg[$1]=usg
  109. cpus[n_cpus++]=$1
  110. }
  111. if (last_tot[$1]>0) {
  112. sum_usg_2[$1] += ((usg-last_usg[$1])/(tot-last_tot[$1]))^2
  113. }
  114. last_tot[$1]=tot
  115. last_usg[$1]=usg
  116. }
  117. END {
  118. printf(" CPU Load: [in %% busy (avg +/- std dev)")
  119. for (i in sum_freq) if (sum_freq[i]>0) {printf(" @ avg frequency"); break}
  120. if (n_load_samp>0) n_load_samp--
  121. printf(", %d samples]\n", n_load_samp)
  122. for (i=0;i<n_cpus;i++) {
  123. c=cpus[i]
  124. if (n_load_samp>0) {
  125. avg_usg=(last_tot[c]-init_tot[c])
  126. avg_usg=avg_usg>0 ? (last_usg[c]-init_usg[c])/avg_usg : 0
  127. std_usg=sum_usg_2[c]/n_load_samp-avg_usg^2
  128. std_usg=std_usg>0 ? sqrt(std_usg) : 0
  129. printf("%9s: %5.1f +/- %4.1f", c, avg_usg*100, std_usg*100)
  130. avg_freq=n_freq_samp[c]>0 ? sum_freq[c]/n_freq_samp[c] : 0
  131. if (avg_freq>0) printf(" @ %4d MHz", avg_freq)
  132. printf("\n")
  133. }
  134. }
  135. printf(" Overhead: [in %% used of total CPU available]\n")
  136. printf("%9s: %5.1f\n", "netperf", tot_cpu>0 ? proc_cpu/tot_cpu*100 : 0)
  137. }'
  138. }
  139. # Summarize the contents of the speed file to show formatted transfer rate.
  140. # input parameter ($1) indicates transfer direction
  141. # input parameter ($2) file contains speed info from netperf
  142. summarize_speed() {
  143. printf "%9s: %6.2f Mbps\n" $1 $(awk '{s+=$1} END {print s}' $2)
  144. }
  145. # Capture process load, then per-CPU load/frequency info at 1-second intervals.
  146. sample_load() {
  147. local cpus="$(find /sys/devices/system/cpu -name 'cpu[0-9]*' 2>/dev/null)"
  148. local f="cpufreq/scaling_cur_freq"
  149. cat /proc/$$/stat
  150. while : ; do
  151. sleep 1s
  152. egrep "^cpu[0-9]*" /proc/stat
  153. for c in $cpus; do
  154. [ -r $c/$f ] && echo "cpufreq $(basename $c) $(cat $c/$f)"
  155. done
  156. done
  157. }
  158. # Print a line of dots as a progress indicator.
  159. print_dots() {
  160. while : ; do
  161. printf "."
  162. sleep 1s
  163. done
  164. }
  165. # Start $MAXSESSIONS datastreams between netperf client and server
  166. # netperf writes the sole output value (in Mbps) to stdout when completed
  167. start_netperf() {
  168. for i in $( seq $MAXSESSIONS ); do
  169. netperf $TESTPROTO -H $TESTHOST -t $1 -l $TESTDUR -v 0 -P 0 >> $2 &
  170. # echo "Starting PID $! params: $TESTPROTO -H $TESTHOST -t $1 -l $TESTDUR -v 0 -P 0 >> $2"
  171. done
  172. }
  173. # Wait until each of the background netperf processes completes
  174. wait_netperf() {
  175. # gets a list of PIDs for child processes named 'netperf'
  176. # echo "Process is $$"
  177. # echo $(pgrep -P $$ netperf)
  178. local err=0
  179. for i in $(pgrep -P $$ netperf); do
  180. # echo "Waiting for $i"
  181. wait $i || err=1
  182. done
  183. return $err
  184. }
  185. # Stop the background netperf processes
  186. kill_netperf() {
  187. # gets a list of PIDs for child processes named 'netperf'
  188. # echo "Process is $$"
  189. # echo $(pgrep -P $$ netperf)
  190. for i in $(pgrep -P $$ netperf); do
  191. # echo "Stopping $i"
  192. kill -9 $i
  193. wait $i 2>/dev/null
  194. done
  195. }
  196. # Stop the current sample_load() process
  197. kill_load() {
  198. # echo "Load: $LOAD_PID"
  199. kill -9 $LOAD_PID
  200. wait $LOAD_PID 2>/dev/null
  201. LOAD_PID=0
  202. }
  203. # Stop the current print_dots() process
  204. kill_dots() {
  205. # echo "Dots: $DOTS_PID"
  206. kill -9 $DOTS_PID
  207. wait $DOTS_PID 2>/dev/null
  208. DOTS_PID=0
  209. }
  210. # Stop the current ping process
  211. kill_pings() {
  212. # echo "Pings: $PING_PID"
  213. kill -9 $PING_PID
  214. wait $PING_PID 2>/dev/null
  215. PING_PID=0
  216. }
  217. # Stop the current load, pings and dots, and exit
  218. # ping command catches and handles first Ctrl-C, so you have to hit it again...
  219. kill_background_and_exit() {
  220. kill_netperf
  221. kill_load
  222. kill_dots
  223. rm -f $DLFILE
  224. rm -f $ULFILE
  225. rm -f $LOADFILE
  226. rm -f $PINGFILE
  227. echo; echo "Stopped"
  228. exit 1
  229. }
  230. # Measure speed, ping latency and cpu usage of netperf data transfers
  231. # Called with direction parameter: "Download", "Upload", or "Bidirectional"
  232. # The function gets other info from globals and command-line arguments.
  233. measure_direction() {
  234. # Create temp files for netperf up/download results
  235. ULFILE=$(mktemp /tmp/netperfUL.XXXXXX) || exit 1
  236. DLFILE=$(mktemp /tmp/netperfDL.XXXXXX) || exit 1
  237. PINGFILE=$(mktemp /tmp/measurepings.XXXXXX) || exit 1
  238. LOADFILE=$(mktemp /tmp/measureload.XXXXXX) || exit 1
  239. # echo $ULFILE $DLFILE $PINGFILE $LOADFILE
  240. local dir=$1
  241. local spd_test
  242. # Start dots
  243. print_dots &
  244. DOTS_PID=$!
  245. # echo "Dots PID: $DOTS_PID"
  246. # Start Ping
  247. if [ $TESTPROTO -eq "-4" ]; then
  248. ping $PINGHOST > $PINGFILE &
  249. else
  250. ping6 $PINGHOST > $PINGFILE &
  251. fi
  252. PING_PID=$!
  253. # echo "Ping PID: $PING_PID"
  254. # Start CPU load sampling
  255. sample_load > $LOADFILE &
  256. LOAD_PID=$!
  257. # echo "Load PID: $LOAD_PID"
  258. # Start netperf datastreams between client and server
  259. if [ $dir = "Bidirectional" ]; then
  260. start_netperf TCP_STREAM $ULFILE
  261. start_netperf TCP_MAERTS $DLFILE
  262. else
  263. # Start unidirectional netperf with the proper direction
  264. case $dir in
  265. Download) spd_test="TCP_MAERTS";;
  266. Upload) spd_test="TCP_STREAM";;
  267. esac
  268. start_netperf $spd_test $DLFILE
  269. fi
  270. # Wait until background netperf processes complete, check errors
  271. if ! wait_netperf; then
  272. echo;echo "WARNING: netperf returned errors. Results may be inaccurate!"
  273. fi
  274. # When netperf completes, stop the CPU monitor, dots and pings
  275. kill_load
  276. kill_pings
  277. kill_dots
  278. echo
  279. # Print TCP Download/Upload speed
  280. if [ $dir = "Bidirectional" ]; then
  281. summarize_speed Download $DLFILE
  282. summarize_speed Upload $ULFILE
  283. else
  284. summarize_speed $dir $DLFILE
  285. fi
  286. # Summarize the ping data
  287. summarize_pings $PINGFILE
  288. # Summarize the load data
  289. summarize_load $LOADFILE
  290. # Clean up
  291. rm -f $DLFILE
  292. rm -f $ULFILE
  293. rm -f $PINGFILE
  294. rm -f $LOADFILE
  295. }
  296. # ------- Start of the main routine --------
  297. # set an initial values for defaults
  298. TESTHOST="netperf.bufferbloat.net"
  299. TESTDUR="60"
  300. PINGHOST="gstatic.com"
  301. MAXSESSIONS=5
  302. TESTPROTO="-4"
  303. TESTSEQ=1
  304. # read the options
  305. # extract options and their arguments into variables.
  306. while [ $# -gt 0 ]
  307. do
  308. case "$1" in
  309. -s|--sequential) TESTSEQ=1 ; shift 1 ;;
  310. -c|--concurrent) TESTSEQ=0 ; shift 1 ;;
  311. -4|-6) TESTPROTO=$1 ; shift 1 ;;
  312. -H|--host)
  313. case "$2" in
  314. "") echo "Missing hostname" ; exit 1 ;;
  315. *) TESTHOST=$2 ; shift 2 ;;
  316. esac ;;
  317. -t|--time)
  318. case "$2" in
  319. "") echo "Missing duration" ; exit 1 ;;
  320. *) TESTDUR=$2 ; shift 2 ;;
  321. esac ;;
  322. -p|--ping)
  323. case "$2" in
  324. "") echo "Missing ping host" ; exit 1 ;;
  325. *) PINGHOST=$2 ; shift 2 ;;
  326. esac ;;
  327. -n|--number)
  328. case "$2" in
  329. "") echo "Missing number of simultaneous streams" ; exit 1 ;;
  330. *) MAXSESSIONS=$2 ; shift 2 ;;
  331. esac ;;
  332. --) shift ; break ;;
  333. *) echo "Usage: speedtest-netperf.sh [ -s | -c ] [-4 | -6] [ -H netperf-server ] [ -t duration ] [ -p host-to-ping ] [ -n simultaneous-sessions ]" ; exit 1 ;;
  334. esac
  335. done
  336. # Check dependencies
  337. if ! netperf -V >/dev/null 2>&1; then
  338. echo "Missing netperf program, please install" ; exit 1
  339. fi
  340. # Start the main test
  341. DATE=$(date "+%Y-%m-%d %H:%M:%S")
  342. echo "$DATE Starting speedtest for $TESTDUR seconds per transfer session."
  343. echo "Measure speed to $TESTHOST (IPv${TESTPROTO#-}) while pinging $PINGHOST."
  344. echo -n "Download and upload sessions are "
  345. [ "$TESTSEQ " -eq "1" ] && echo -n "sequential," || echo -n "concurrent,"
  346. echo " each with $MAXSESSIONS simultaneous streams."
  347. # Catch a Ctl-C and stop background netperf, CPU stats, pinging and print_dots
  348. trap kill_background_and_exit HUP INT TERM
  349. if [ $TESTSEQ -eq "1" ]; then
  350. measure_direction "Download"
  351. measure_direction "Upload"
  352. else
  353. measure_direction "Bidirectional"
  354. fi