| #!/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 |