scripts/iperf_benchmark.sh
#!/bin/bash

# Copyright 2022, The Regents of the University of California.
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#    1. Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
# 
#    2. Redistributions in binary form must reproduce the above copyright notice,
#       this list of conditions and the following disclaimer in the documentation
#       and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE REGENTS OF THE UNIVERSITY OF CALIFORNIA ''AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OF THE UNIVERSITY OF CALIFORNIA OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
# 
# The views and conclusions contained in the software and documentation are those
# of the authors and should not be interpreted as representing official policies,
# either expressed or implied, of The Regents of the University of California.

max_iperf_count=1
repeats=1
iperf_p=4
ip=
netdev=
ifaddr=
netns=
base_port=9000
mtu=
base_logdir=./logs/

while getopts i:n:P:c:p:r:-: option; do
    case "${option}" in
        -)
            case "${OPTARG}" in
                ifaddr)
                    ifaddr="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    ;;
                ifaddr=*)
                    ifaddr=${OPTARG#*=}
                    ;;
                netns)
                    netns="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    ;;
                netns=*)
                    netns=${OPTARG#*=}
                    ;;
                mtu)
                    mtu="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    ;;
                mtu=*)
                    mtu=${OPTARG#*=}
                    ;;
                logdir)
                    base_logdir="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    ;;
                logdir=*)
                    base_logdir=${OPTARG#*=}
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        i) netdev=${OPTARG};;
        n) max_iperf_count=${OPTARG};;
        P) iperf_p=${OPTARG};;
        c) ip=${OPTARG};;
        p) base_port=${OPTARG};;
        r) repeats=${OPTARG};;
    esac
done
shift $((OPTIND -1))

if [ -z "$netdev" ]; then
    netdev=$(ip route get $ip | grep -oP "dev\s+\K\w+")
    echo "Using local device '$netdev'"
fi

numa_cmd=
netns_cmd=

iperf_args=

if [ ! -z "$netdev" ]; then
    if [ ! -z "$ifaddr" ]; then
        if [ -z "$netns" ]; then
            netns=$netdev
        fi
    fi

    if [ ! -x "$(command -v numactl)" ] ; then
        echo "numactl not found; cannot bind iperf to netdev NUMA node" >&2
    else
        numa_cmd="numactl -l -N netdev:$netdev"
    fi
else
    if [ ! -z "$ifaddr" ]; then
        echo "Interface address specified, but interface name not specified" >&2
        exit -1
    fi
fi

if [ ! -z "$netns" ]; then
    netns_cmd="ip netns exec $netns"
    if [ -f "/var/run/netns/$netns" ]; then
        echo "Network namespace '$netns' already exists"
    else
        echo "Creating network namespace '$netns'"
        ip netns add $netns
        echo "Adding interface '$netdev' to network namespace '$netns'"
        ip link set dev $netdev netns $netns
        $netns_cmd ip link set dev $netdev up
    fi
fi

if [ ! -z "$ifaddr" ]; then
    echo "Adding address '$ifaddr' to '$netdev'"
    $netns_cmd ip addr add $ifaddr dev $netdev
fi

if [ ! -z "$mtu" ]; then
    echo "Changing MTU to $mtu on '$netdev'"
    $netns_cmd ip link set mtu $mtu dev $netdev
fi

function cleanup()
{
    echo "Cleaning up..."

    # kill all subprocesses
    trap '' TERM
    pkill -P $$

    # clean up netns
    if [ ! -z "$netns" ]; then
        if [ -f "/var/run/netns/$netns" -a -z "$(ip netns pids $netns)" ]; then
            echo "Deleting network namespace '$netns'"
            ip netns del $netns
        fi
    fi
}

trap "exit" INT TERM
trap cleanup EXIT

# run measurement

function run_meas()
{
    test_type=$1
    iperf_count=$2
    rep=$3

    logdir="$base_logdir/n$iperf_count/$test_type/$rep/"
    mkdir -p $logdir

    # start clients
    case "$test_type" in
        tx)
            for i in $(seq 1 $iperf_count); do
                port=$(($base_port+i))
                logfile="$logdir/iperf-client-tx-$i.log"
                echo Starting iperf3 TX instance $i on port $port
                echo -n > "$logfile"
                $netns_cmd $numa_cmd iperf3 -p $port -P $iperf_p -c $ip -f k -t 12 --logfile "$logfile" $iperf_args &
            done
            ;;
        rx)
            for i in $(seq 1 $iperf_count); do
                port=$(($base_port+i))
                logfile="$logdir/iperf-client-rx-$i.log"
                echo Starting iperf3 RX instance $i on port $port
                echo -n > "$logfile"
                $netns_cmd $numa_cmd iperf3 -p $port -P $iperf_p -c $ip -f k -t 12 --logfile "$logfile" -R $iperf_args &
            done
            ;;
        txrx)
            for i in $(seq 1 $iperf_count); do
                port=$(($base_port+i))
                logfile="$logdir/iperf-client-tx-$i.log"
                echo Starting iperf3 TX instance $i on port $port
                echo -n > "$logfile"
                $netns_cmd $numa_cmd iperf3 -p $port -P $iperf_p -c $ip -f k -t 12 --logfile "$logfile" $iperf_args &
                port=$(($base_port+$iperf_count+i))
                logfile="$logdir/iperf-client-rx-$i.log"
                echo Starting iperf3 RX instance $i on port $port
                echo -n > "$logfile"
                $netns_cmd $numa_cmd iperf3 -p $port -P $iperf_p -c $ip -f k -t 12 --logfile "$logfile" -R $iperf_args &
            done
            ;;
    esac

    sleep 1

    # capture performance counters
    $netns_cmd cat /proc/net/dev > $logdir/proc_net_dev.log
    cat /proc/stat > $logdir/proc_stat.log
    start_time=$(date +%s.%N)
    for i in $(seq 1 10); do
        sleep 1
        $netns_cmd cat /proc/net/dev >> $logdir/proc_net_dev.log
        cat /proc/stat >> $logdir/proc_stat.log
        echo -n .
    done
    end_time=$(date +%s.%N)
    elapsed=$(echo "scale=4; $end_time - $start_time" | bc)

    wait
    echo

    # aggregate
    lcl_txkbps=0
    lcl_txretr=0
    lcl_rxkbps=0
    rmt_txkbps=0
    rmt_txretr=0
    rmt_rxkbps=0
    shopt -s nullglob
    for file in $logdir/iperf-client-tx-*.log; do
        sender=$(cat $file | tr -s ' ' | grep "\[SUM\]" | grep sender)
        receiver=$(cat $file | tr -s ' ' | grep "\[SUM\]" | grep receiver)

        lcl_txkbps=$(($lcl_txkbps + $(echo "$sender" | cut -d ' ' -f 6)))
        lcl_txretr=$(($lcl_txretr + $(echo "$sender" | cut -d ' ' -f 8)))
        rmt_rxkbps=$(($rmt_rxkbps + $(echo "$receiver" | cut -d ' ' -f 6)))
    done
    shopt -s nullglob
    for file in $logdir/iperf-client-rx-*.log; do
        sender=$(cat $file | tr -s ' ' | grep "\[SUM\]" | grep sender)
        receiver=$(cat $file | tr -s ' ' | grep "\[SUM\]" | grep receiver)

        rmt_txkbps=$(($rmt_txkbps + $(echo "$sender" | cut -d ' ' -f 6)))
        rmt_txretr=$(($rmt_txretr + $(echo "$sender" | cut -d ' ' -f 8)))
        lcl_rxkbps=$(($lcl_rxkbps + $(echo "$receiver" | cut -d ' ' -f 6)))
    done

    if_stat=$(grep "$netdev:" "$logdir/proc_net_dev.log" | tr -s ' ' | cut -d ' ' -f 2- | sed -n '1p;$p' | awk 'NR==1{for(i=1;i<=NF;i++){col[i]=$i};next}{for(i=1;i<=NF;i++){printf "%s ",$i-col[i];col[i]=$i};print ""}')
    intr_stat=$(grep "intr" "$logdir/proc_stat.log" | tr -s ' ' | cut -d ' ' -f 2- | sed -n '1p;$p' | awk 'NR==1{for(i=1;i<=NF;i++){col[i]=$i};next}{for(i=1;i<=NF;i++){printf "%s ",$i-col[i];col[i]=$i};print ""}')
    cpu_stat=$(grep "cpu\s" "$logdir/proc_stat.log" | tr -s ' ' | cut -d ' ' -f 2- | sed -n '1p;$p' | awk 'NR==1{for(i=1;i<=NF;i++){col[i]=$i};next}{for(i=1;i<=NF;i++){printf "%s ",$i-col[i];col[i]=$i};print ""}')

    if_rx_b=$(echo $if_stat | cut -d ' ' -f 1)
    if_rx_pkt=$(echo $if_stat | cut -d ' ' -f 2)
    if_rx_err=$(echo $if_stat | cut -d ' ' -f 3)
    if_rx_drop=$(echo $if_stat | cut -d ' ' -f 4)
    if_rx_fifo=$(echo $if_stat | cut -d ' ' -f 5)
    if_rx_frame=$(echo $if_stat | cut -d ' ' -f 6)
    if_tx_b=$(echo $if_stat | cut -d ' ' -f 9)
    if_tx_pkt=$(echo $if_stat | cut -d ' ' -f 10)
    if_tx_err=$(echo $if_stat | cut -d ' ' -f 11)
    if_tx_drop=$(echo $if_stat | cut -d ' ' -f 12)
    if_tx_fifo=$(echo $if_stat | cut -d ' ' -f 13)

    intr=$(echo $intr_stat | cut -d ' ' -f 1)

    cpu_idle=$(echo $cpu_stat | cut -d ' ' -f 4)
    cpu_total=$(echo $cpu_stat | tr " " "\n" | grep . | paste -sd+ - | bc)
    cpu_pct=$(echo "scale=4; ($cpu_total-$cpu_idle) * 100 / $cpu_total" | bc)

    echo $iperf_count, $rep, $elapsed, $if_rx_b, $if_rx_pkt, $if_rx_err, $if_rx_drop, $if_rx_fifo, $if_rx_frame, $if_tx_b, $if_tx_pkt, $if_tx_err, $if_tx_drop, $if_tx_fifo, $lcl_txkbps, $lcl_txretr, $lcl_rxkbps, $rmt_txkbps, $rmt_txretr, $rmt_rxkbps, $intr, $cpu_pct | tee -a "$base_logdir/$test_type.csv"
}

mkdir -p $base_logdir

echo "n, rep, sec, if_rx_b, if_rx_pkt, if_rx_err, if_rx_drop, if_rx_fifo, if_rx_frame, if_tx_b, if_tx_pkt, if_tx_err, if_tx_drop, if_tx_fifo, lcl_txkbps, lcl_txretr, lcl_rxkbps, rmt_txkbps, rmt_txretr, rmt_rxkbps, intr, cpu" > "$base_logdir/tx.csv"
echo "n, rep, sec, if_rx_b, if_rx_pkt, if_rx_err, if_rx_drop, if_rx_fifo, if_rx_frame, if_tx_b, if_tx_pkt, if_tx_err, if_tx_drop, if_tx_fifo, lcl_txkbps, lcl_txretr, lcl_rxkbps, rmt_txkbps, rmt_txretr, rmt_rxkbps, intr, cpu" > "$base_logdir/rx.csv"
echo "n, rep, sec, if_rx_b, if_rx_pkt, if_rx_err, if_rx_drop, if_rx_fifo, if_rx_frame, if_tx_b, if_tx_pkt, if_tx_err, if_tx_drop, if_tx_fifo, lcl_txkbps, lcl_txretr, lcl_rxkbps, rmt_txkbps, rmt_txretr, rmt_rxkbps, intr, cpu" > "$base_logdir/txrx.csv"

for iperf_count in $(seq 1 $max_iperf_count); do
    for rep in $(seq 1 $repeats); do
        echo "Running TX test with $iperf_count processes ($rep/$repeats)"
        run_meas tx $iperf_count $rep

        echo "Running RX test with $iperf_count processes ($rep/$repeats)"
        run_meas rx $iperf_count $rep

        echo "Running TX+RX test with $iperf_count processes ($rep/$repeats)"
        run_meas txrx $iperf_count $rep
    done
done
