500 lines
14 KiB
Bash
500 lines
14 KiB
Bash
#!/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.
|
|
#
|
|
# The script also supports measuring latency when idle as a baseline prior to
|
|
# testing under network load.
|
|
#
|
|
# NOTE: The script uses servers and network bandwidth that are provided by
|
|
# generous volunteers. Feel free to use the script to test your SQM
|
|
# configuration or troubleshoot network and latency problems, but be warned
|
|
# that continuous or high-rate use of the servers 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 [duration] ] [ -i [duration] ]
|
|
|
|
# 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 - 30 seconds)
|
|
# -p | --ping: Host to ping to measure latency (default - one.one.one.one)
|
|
# -n | --number: Number of simultaneous streams (default - 5 streams)
|
|
# based on whether concurrent or sequential upload/downloads)
|
|
# -s | -c: Sequential or concurrent download/upload (default - disabled)
|
|
# -i | --idle: Measure idle latency before speed test (default - disabled)
|
|
|
|
# Copyright (c) 2014 - Rich Brown <rich.brown@blueberryhillsoftware.com>
|
|
# Copyright (c) 2018-2024 - 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
|
|
grep "^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 $MAXSTREAMS datastreams between netperf client and server
|
|
# netperf writes the sole output value (in Mbps) to stdout when completed
|
|
|
|
start_netperf() {
|
|
for i in $( seq $MAXSTREAMS ); do
|
|
netperf $TESTPROTO -H $TESTHOST -t $1 -l $SPEEDDUR -v 0 -P 0 >> $2 &
|
|
# echo "Starting PID $! params: $TESTPROTO -H $TESTHOST -t $1 -l $SPEEDDUR -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 ping latency at idle as a baseline for comparison.
|
|
|
|
measure_idle() {
|
|
|
|
# Create temp files for netperf up/download results
|
|
PINGFILE=$(mktemp /tmp/measurepings.XXXXXX) || exit 1
|
|
LOADFILE=$(mktemp /tmp/measureload.XXXXXX) || exit 1
|
|
# echo $PINGFILE $LOADFILE
|
|
|
|
# Start dots
|
|
print_dots &
|
|
DOTS_PID=$!
|
|
# echo "Dots PID: $DOTS_PID"
|
|
|
|
# Start Ping
|
|
ping $TESTPROTO $PINGHOST > $PINGFILE &
|
|
PING_PID=$!
|
|
# echo "Ping PID: $PING_PID"
|
|
|
|
# Start CPU load sampling
|
|
sample_load > $LOADFILE &
|
|
LOAD_PID=$!
|
|
# echo "Load PID: $LOAD_PID"
|
|
|
|
# Wait for idle test period
|
|
sleep $IDLEDUR
|
|
|
|
# When idle time elapses, stop the CPU monitor, dots and pings
|
|
kill_load
|
|
kill_pings
|
|
kill_dots
|
|
echo
|
|
|
|
# Summarize the ping data
|
|
summarize_pings $PINGFILE
|
|
|
|
# Summarize the load data
|
|
summarize_load $LOADFILE
|
|
|
|
# Clean up
|
|
rm -f $PINGFILE
|
|
rm -f $LOADFILE
|
|
}
|
|
|
|
# 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
|
|
ping $TESTPROTO $PINGHOST > $PINGFILE &
|
|
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: Results may be inaccurate since 'netperf' returned errors."
|
|
echo " Run directly for more details:"
|
|
echo " netperf $TESTPROTO -H $TESTHOST"
|
|
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
|
|
}
|
|
|
|
print_usage() {
|
|
echo \
|
|
"Usage: speedtest-netperf.sh [ -H netperf-server ] [ -p host-to-ping ] [-4 | -6]
|
|
[ -i [duration] ] [ -s | -c [duration] ]
|
|
[ -t duration ] [ -n simultaneous-streams ]"
|
|
}
|
|
|
|
is_number() {
|
|
case "$1" in
|
|
""|*[![:digit:]]*) return 1;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
|
|
# ------- Start of the main routine --------
|
|
|
|
# Set initial values for defaults
|
|
TESTHOST="netperf.bufferbloat.net"
|
|
PINGHOST="one.one.one.one"
|
|
MAXSTREAMS=5
|
|
TESTPROTO="-4"
|
|
TESTSPEED=0
|
|
SPEEDDUR="30"
|
|
TESTIDLE=0
|
|
IDLEDUR="30"
|
|
|
|
# Clear temp files
|
|
DLFILE=
|
|
ULFILE=
|
|
PINGFILE=
|
|
LOADFILE=
|
|
|
|
# Parse options and their parameters into variables. Options for --idle,
|
|
# --sequential and --concurrent have optional parameters.
|
|
while [ $# -gt 0 ]
|
|
do
|
|
case "$1" in
|
|
-i|--idle)
|
|
TESTIDLE=1 ; shift 1
|
|
is_number "$1" && { IDLEDUR=$1 ; shift 1 ; } ;;
|
|
-s|--sequential)
|
|
TESTSPEED=1 ; shift 1
|
|
is_number "$1" && { SPEEDDUR=$1 ; shift 1 ; } ;;
|
|
-c|--concurrent)
|
|
TESTSPEED=2 ; shift 1
|
|
is_number "$1" && { SPEEDDUR=$1 ; 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)
|
|
is_number "$2" || { echo "Missing duration" ; exit 1 ; }
|
|
IDLEDUR=$2 ; SPEEDDUR=$2 ; shift 2 ;;
|
|
-p|--ping)
|
|
case "$2" in
|
|
"") echo "Missing ping host" ; exit 1 ;;
|
|
*) PINGHOST=$2 ; shift 2 ;;
|
|
esac ;;
|
|
-n|--number)
|
|
is_number $2 || { echo "Missing number of streams" ; exit 1 ; }
|
|
MAXSTREAMS=$2 ; shift 2 ;;
|
|
--) shift ; break ;;
|
|
*) print_usage ; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# Extra argument validations
|
|
|
|
if [ $TESTIDLE -eq "0" ] && [ $TESTSPEED -eq "0" ]; then
|
|
echo "Please select an idle latency test and/or speed test:"
|
|
print_usage ; exit 1
|
|
fi
|
|
|
|
# Check dependencies
|
|
|
|
if ! netperf -V >/dev/null 2>&1; then
|
|
echo "Missing netperf program, please install" ; exit 1
|
|
fi
|
|
|
|
# Catch a Ctl-C and stop background netperf, CPU stats, pinging and print_dots
|
|
trap kill_background_and_exit HUP INT TERM
|
|
|
|
# Start the main tests
|
|
|
|
DATE=$(date "+%Y-%m-%d %H:%M:%S")
|
|
echo -n "$DATE Begin test with "
|
|
[ $TESTIDLE -eq "1" ] && echo -n "$IDLEDUR-second ping"
|
|
[ $(($TESTIDLE * $TESTSPEED)) -ne "0" ] && echo -n ", "
|
|
[ $TESTSPEED -ne "0" ] && echo -n "$SPEEDDUR-second transfer"
|
|
echo " sessions."
|
|
|
|
if [ $TESTIDLE -eq "1" ]; then
|
|
echo "Measure idle latency by pinging $PINGHOST (IPv${TESTPROTO#-})."
|
|
measure_idle
|
|
echo
|
|
fi
|
|
|
|
if [ $TESTSPEED -ne "0" ]; then
|
|
echo "Measure speed to $TESTHOST (IPv${TESTPROTO#-}) while pinging $PINGHOST."
|
|
echo -n "Download and upload sessions are "
|
|
[ "$TESTSPEED" -eq "1" ] && echo -n "sequential," || echo -n "concurrent,"
|
|
echo " each with $MAXSTREAMS simultaneous streams."
|
|
|
|
if [ $TESTSPEED -eq "1" ]; then
|
|
measure_direction "Download"
|
|
measure_direction "Upload"
|
|
else
|
|
measure_direction "Bidirectional"
|
|
fi
|
|
fi
|