Added CBench regression scripts
authorDaniel Farrell <dfarrell@redhat.com>
Thu, 17 Jul 2014 22:46:29 +0000 (18:46 -0400)
committerDaniel Farrell <dfarrell@redhat.com>
Thu, 17 Jul 2014 22:46:29 +0000 (18:46 -0400)
Signed-off-by: Daniel Farrell <dfarrell@redhat.com>
test/tools/cbench_regression/.gitignore [new file with mode: 0644]
test/tools/cbench_regression/cbench.sh [new file with mode: 0755]
test/tools/cbench_regression/loop_cbench.sh [new file with mode: 0755]
test/tools/cbench_regression/stats.py [new file with mode: 0755]

diff --git a/test/tools/cbench_regression/.gitignore b/test/tools/cbench_regression/.gitignore
new file mode 100644 (file)
index 0000000..0121e53
--- /dev/null
@@ -0,0 +1,6 @@
+*.swp
+*.swo
+*.swn
+*.swm
+*.csv
+logs/
diff --git a/test/tools/cbench_regression/cbench.sh b/test/tools/cbench_regression/cbench.sh
new file mode 100755 (executable)
index 0000000..321c488
--- /dev/null
@@ -0,0 +1,541 @@
+#!/usr/bin/env sh
+# Script for running automated CBench regression tests
+
+# For remote runs, you must have .ssh/config setup such that `ssh $SSH_HOSTMANE` works w/o pw
+# Example host setup in .ssh/config:
+# Host cbench
+#     Hostname 209.132.178.170
+#     User fedora
+#     IdentityFile /home/daniel/.ssh/id_rsa_nopass
+#     StrictHostKeyChecking no
+
+# Exit codes
+EX_USAGE=64
+EX_NOT_FOUND=65
+EX_OK=0
+EX_ERR=1
+
+# Params for CBench test and ODL config
+NUM_SWITCHES=256
+NUM_MACS=100000
+TESTS_PER_SWITCH=10  # Comment out to speed up testing of this script
+#TESTS_PER_SWITCH=2  # ^^Then uncomment this one
+MS_PER_TEST=10000
+CBENCH_WARMUP=1
+OSGI_PORT=2400
+ODL_STARTUP_DELAY=90
+ODL_RUNNING_STATUS=0
+ODL_STOPPED_STATUS=255
+ODL_BROKEN_STATUS=1
+CONTROLLER="OpenDaylight"
+CONTROLLER_IP="localhost"
+#CONTROLLER_IP="172.18.14.26"
+CONTROLLER_PORT=6633
+SSH_HOSTNAME="cbenchc"
+
+# Array that stores results in indexes defined by cols array
+declare -a results
+
+# The order of these array values determines column order in RESULTS_FILE
+cols=(run_num cbench_avg start_time end_time controller_ip human_time
+      num_switches num_macs tests_per_switch ms_per_test start_steal_time
+      end_steal_time total_ram used_ram free_ram cpus one_min_load five_min_load
+      fifteen_min_load controller start_iowait end_iowait)
+
+# This two-stat-array system is needed until I find an answer to this question:
+# http://goo.gl/e0M8Tp
+
+# Associative array with stats-collecting commands for local system
+declare -A local_stats_cmds
+local_stats_cmds=([total_ram]="$(free -m | awk '/^Mem:/{print $2}')"
+            [used_ram]="$(free -m | awk '/^Mem:/{print $3}')"
+            [free_ram]="$(free -m | awk '/^Mem:/{print $4}')"
+            [cpus]="`nproc`"
+            [one_min_load]="`uptime | awk -F'[a-z]:' '{print $2}' | awk -F "," '{print $1}' | tr -d " "`"
+            [five_min_load]="`uptime | awk -F'[a-z]:' '{print $2}' | awk -F "," '{print $2}' | tr -d " "`"
+            [fifteen_min_load]="`uptime | awk -F'[a-z]:' '{print $2}' | awk -F "," '{print $3}' | tr -d " "`"
+            [iowait]="`cat /proc/stat | awk 'NR==1 {print $6}'`"
+            [steal_time]="`cat /proc/stat | awk 'NR==1 {print $9}'`")
+
+# Associative array with stats-collecting commands for remote system
+# See this for explanation of horrible-looking quoting: http://goo.gl/PMI5ag
+declare -A remote_stats_cmds
+remote_stats_cmds=([total_ram]='free -m | awk '"'"'/^Mem:/{print $2}'"'"''
+            [used_ram]='free -m | awk '"'"'/^Mem:/{print $3}'"'"''
+            [free_ram]='free -m | awk '"'"'/^Mem:/{print $4}'"'"''
+            [cpus]='nproc'
+            [one_min_load]='uptime | awk -F'"'"'[a-z]:'"'"' '"'"'{print $2}'"'"' | awk -F "," '"'"'{print $1}'"'"' | tr -d " "'
+            [five_min_load]='uptime | awk -F'"'"'[a-z]:'"'"' '"'"'{print $2}'"'"' | awk -F "," '"'"'{print $2}'"'"' | tr -d " "'
+            [fifteen_min_load]='uptime | awk -F'"'"'[a-z]:'"'"' '"'"'{print $2}'"'"' | awk -F "," '"'"'{print $3}'"'"' | tr -d " "'
+            [iowait]='cat /proc/stat | awk '"'"'NR==1 {print $6}'"'"''
+            [steal_time]='cat /proc/stat | awk '"'"'NR==1 {print $9}'"'"'')
+
+# Paths used in this script
+BASE_DIR=$HOME
+OF_DIR=$BASE_DIR/openflow
+OFLOPS_DIR=$BASE_DIR/oflops
+ODL_DIR=$BASE_DIR/opendaylight
+ODL_ZIP="distributions-base-0.2.0-SNAPSHOT-osgipackage.zip"
+ODL_ZIP_PATH=$BASE_DIR/$ODL_ZIP
+PLUGIN_DIR=$ODL_DIR/plugins
+RESULTS_FILE=$BASE_DIR/"results.csv"
+CBENCH_LOG=$BASE_DIR/"cbench.log"
+CBENCH_BIN="/usr/local/bin/cbench"
+
+usage()
+{
+    # Print usage message
+    cat << EOF
+Usage $0 [options]
+
+Setup and/or run CBench and/or OpenDaylight.
+
+OPTIONS:
+    -h Show this message
+    -c Install CBench
+    -t <time> Run CBench for given number of minutes
+    -r Run CBench against OpenDaylight
+    -i Install ODL from last sucessful build
+    -p <processors> Pin ODL to given number of processors
+    -o Run ODL from last sucessful build
+    -k Kill OpenDaylight
+    -d Delete local ODL and CBench code
+EOF
+}
+
+cbench_installed()
+{
+    # Checks if CBench is installed
+    if command -v cbench &>/dev/null; then
+        echo "CBench is installed"
+        return $EX_OK
+    else
+        echo "CBench is not installed"
+        return $EX_NOT_FOUND
+    fi
+}
+
+install_cbench()
+{
+    # Installs CBench, including its dependencies
+    # This function is idempotent
+    # This has been tested on fresh cloud versions of Fedora 20 and CentOS 6.5
+    # Note that I'm not currently building oflops/netfpga-packet-generator-c-library (optional)
+    if cbench_installed; then
+        return $EX_OK
+    fi
+
+    # Install required packages
+    echo "Installing CBench dependencies"
+    sudo yum install -y net-snmp-devel libpcap-devel autoconf make automake libtool libconfig-devel git &> /dev/null
+
+    # Clone repo that contains CBench
+    echo "Cloning CBench repo"
+    git clone https://github.com/andi-bigswitch/oflops.git $OFLOPS_DIR &> /dev/null
+
+    # CBench requires the OpenFlow source code, clone it
+    echo "Cloning openflow source code"
+    git clone git://gitosis.stanford.edu/openflow.git $OF_DIR &> /dev/null
+
+    # Build the oflops/configure file
+    old_cwd=$PWD
+    cd $OFLOPS_DIR
+    echo "Building oflops/configure file"
+    ./boot.sh &> /dev/null
+
+    # Build oflops
+    echo "Building CBench"
+    ./configure --with-openflow-src-dir=$OF_DIR &> /dev/null
+    make &> /dev/null
+    sudo make install &> /dev/null
+    cd $old_cwd
+
+    # Validate that the install worked
+    if ! cbench_installed; then
+        echo "Failed to install CBench" >&2
+        exit $EX_ERR
+    else
+        echo "Successfully installed CBench"
+        return $EX_OK
+    fi
+}
+
+next_run_num()
+{
+    # Get the number of the next run
+    # Assumes that the file hasn't had rows removed by a human
+    # Check if there's actually a results file
+    if [ ! -s $RESULTS_FILE ]; then
+        echo 0
+        return
+    fi
+
+    # There should be one header row, then rows starting with 0, counting up
+    num_lines=`wc -l $RESULTS_FILE | awk '{print $1}'`
+    echo $(expr $num_lines - 1)
+}
+
+name_to_index()
+{
+    # Convert results column name to column index
+    name=$1
+    for (( i = 0; i < ${#cols[@]}; i++ )); do
+        if [ "${cols[$i]}" = $name ]; then
+            echo $i
+            return
+        fi
+    done
+}
+
+write_csv_row()
+{
+    # Accepts an array and writes it in CSV format to the results file
+    declare -a array_to_write=("${!1}")
+    i=0
+    while [ $i -lt $(expr ${#array_to_write[@]} - 1) ]; do
+        # Only use echo with comma and no newline for all but last col
+        echo -n "${array_to_write[$i]}," >> $RESULTS_FILE
+        let i+=1
+    done
+    # Finish CSV row with no comma and a newline
+    echo "${array_to_write[$i]}" >> $RESULTS_FILE
+}
+
+get_pre_test_stats()
+{
+    echo "Collecting pre-test stats"
+    results[$(name_to_index "start_time")]=`date +%s`
+    if [ $CONTROLLER_IP = "localhost" ]; then
+        results[$(name_to_index "start_iowait")]=${local_stats_cmds[iowait]}
+        results[$(name_to_index "start_steal_time")]=${local_stats_cmds[steal_time]}
+    else
+        results[$(name_to_index "start_iowait")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[iowait]}" 2> /dev/null)
+        results[$(name_to_index "start_steal_time")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[steal_time]}" 2> /dev/null)
+    fi
+}
+
+get_post_test_stats()
+{
+    # Collect system stats post CBench test
+    # Pre and post test collection is needed for computing the change in stats
+    # Start by collecting always-local stats that are time-sensitive
+    echo "Collecting post-test stats"
+    results[$(name_to_index "end_time")]=`date +%s`
+    results[$(name_to_index "human_time")]=`date`
+
+    # Now collect local/remote stats that are time-sensative
+    if [ $CONTROLLER_IP = "localhost" ]; then
+        results[$(name_to_index "end_iowait")]=${local_stats_cmds[iowait]}
+        results[$(name_to_index "end_steal_time")]=${local_stats_cmds[steal_time]}
+        results[$(name_to_index "one_min_load")]=${local_stats_cmds[one_min_load]}
+        results[$(name_to_index "five_min_load")]=${local_stats_cmds[five_min_load]}
+        results[$(name_to_index "fifteen_min_load")]=${local_stats_cmds[fifteen_min_load]}
+    else
+        results[$(name_to_index "end_iowait")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[iowait]}" 2> /dev/null)
+        results[$(name_to_index "end_steal_time")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[steal_time]}" 2> /dev/null)
+        results[$(name_to_index "one_min_load")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[one_min_load]}" 2> /dev/null)
+        results[$(name_to_index "five_min_load")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[five_min_load]}" 2> /dev/null)
+        results[$(name_to_index "fifteen_min_load")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[fifteen_min_load]}" 2> /dev/null)
+    fi
+}
+
+get_time_irrelevant_stats()
+{
+    # Collect always-local stats that aren't time-sensitive
+    echo "Collecting time-irrelevant stats"
+    results[$(name_to_index "run_num")]=$(next_run_num)
+    results[$(name_to_index "controller_ip")]=$CONTROLLER_IP
+    results[$(name_to_index "num_switches")]=$NUM_SWITCHES
+    results[$(name_to_index "num_macs")]=$NUM_MACS
+    results[$(name_to_index "tests_per_switch")]=$TESTS_PER_SWITCH
+    results[$(name_to_index "ms_per_test")]=$MS_PER_TEST
+    results[$(name_to_index "controller")]=$CONTROLLER
+
+    # Store local or remote stats that aren't time-sensitive
+    if [ $CONTROLLER_IP = "localhost" ]; then
+        results[$(name_to_index "total_ram")]=${local_stats_cmds[total_ram]}
+        results[$(name_to_index "used_ram")]=${local_stats_cmds[used_ram]}
+        results[$(name_to_index "free_ram")]=${local_stats_cmds[free_ram]}
+        results[$(name_to_index "cpus")]=${local_stats_cmds[cpus]}
+    else
+        results[$(name_to_index "total_ram")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[total_ram]}" 2> /dev/null)
+        results[$(name_to_index "used_ram")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[used_ram]}" 2> /dev/null)
+        results[$(name_to_index "free_ram")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[free_ram]}" 2> /dev/null)
+        results[$(name_to_index "cpus")]=$(ssh $SSH_HOSTNAME "${remote_stats_cmds[cpus]}" 2> /dev/null)
+    fi
+}
+
+write_results()
+{
+    # Write data stored in results array to results file
+    # Write header if this is a fresh results file
+    if [ ! -s $RESULTS_FILE ]; then
+        echo "$RESULTS_FILE not found or empty, building fresh one" >&2
+        write_csv_row cols[@]
+    fi
+    write_csv_row results[@]
+}
+
+run_cbench()
+{
+    # Runs the CBench test against the controller
+    get_pre_test_stats
+    echo "Running CBench against ODL on $CONTROLLER_IP:$CONTROLLER_PORT"
+    cbench_output=`cbench -c $CONTROLLER_IP -p $CONTROLLER_PORT -m $MS_PER_TEST -l $TESTS_PER_SWITCH -s $NUM_SWITCHES -M $NUM_MACS -w $CBENCH_WARMUP 2>&1`
+    get_post_test_stats
+    get_time_irrelevant_stats
+
+    # Parse out average responses/sec, log/handle very rare unexplained errors
+    # This logic can be removed if/when the root cause of this error is discovered and fixed
+    cbench_avg=`echo "$cbench_output" | grep RESULT | awk '{print $8}' | awk -F'/' '{print $3}'`
+    if [ -z "$cbench_avg" ]; then
+        echo "WARNING: Rare error occurred: failed to parse avg. See $CBENCH_LOG." >&2
+        echo "Run $(next_run_num) failed to record a CBench average. CBench details:" >> $CBENCH_LOG
+        echo "$cbench_output" >> $CBENCH_LOG
+        return
+    else
+        echo "Average responses/second: $cbench_avg"
+        results[$(name_to_index "cbench_avg")]=$cbench_avg
+    fi
+
+    # Write results to results file
+    write_results
+
+    # TODO: Integrate with Jenkins Plot Plugin
+}
+
+uninstall_odl()
+{
+    # Uninstall OpenDaylight zipped and unzipped code
+    if [ -d $ODL_DIR ]; then
+        echo "Removing $ODL_DIR"
+        rm -rf $ODL_DIR
+    fi
+    if [ -f $ODL_ZIP_PATH ]; then
+        echo "Removing $ODL_ZIP_PATH"
+        rm -f $ODL_ZIP_PATH
+    fi
+}
+
+install_opendaylight()
+{
+    # Installs latest build of the OpenDaylight controller
+    # Remove old controller code
+    uninstall_odl
+
+    # Install required packages
+    echo "Installing OpenDaylight dependencies"
+    sudo yum install -y java-1.7.0-openjdk unzip wget &> /dev/null
+
+    # Grab last successful build
+    echo "Downloading last successful ODL build"
+    wget -P $BASE_DIR "https://jenkins.opendaylight.org/integration/job/integration-master-project-centralized-integration/lastSuccessfulBuild/artifact/distributions/base/target/$ODL_ZIP" &> /dev/null
+    if [ ! -f $ODL_ZIP_PATH ]; then
+        echo "WARNING: Failed to dl ODL. Version bumped? If so, update \$ODL_ZIP" >&2
+        return $EX_ERR
+    fi
+    echo "Unzipping last successful ODL build"
+    unzip -d $BASE_DIR $ODL_ZIP_PATH &> /dev/null
+
+    # Make some plugin changes that are apparently required for CBench
+    echo "Downloading openflowplugin"
+    wget -P $PLUGIN_DIR 'https://jenkins.opendaylight.org/openflowplugin/job/openflowplugin-merge/lastSuccessfulBuild/org.opendaylight.openflowplugin$drop-test/artifact/org.opendaylight.openflowplugin/drop-test/0.0.3-SNAPSHOT/drop-test-0.0.3-SNAPSHOT.jar' &> /dev/null
+    echo "Removing simpleforwarding plugin"
+    rm $PLUGIN_DIR/org.opendaylight.controller.samples.simpleforwarding-0.4.2-SNAPSHOT.jar
+    echo "Removing arphandler plugin"
+    rm $PLUGIN_DIR/org.opendaylight.controller.arphandler-0.5.2-SNAPSHOT.jar
+
+    # TODO: Change controller log level to ERROR. Confirm this is necessary.
+}
+
+odl_installed()
+{
+    # Checks if OpenDaylight is installed
+    if [ ! -d $ODL_DIR ]; then
+        return $EX_NOT_FOUND
+    fi
+}
+
+odl_started()
+{
+    # Checks if OpenDaylight is running
+    # Assumes you've checked that ODL is installed
+    old_cwd=$PWD
+    cd $ODL_DIR
+    ./run.sh -status &> /dev/null
+    if [ $? = 0 ]; then
+        return $EX_OK
+    else
+        return $EX_NOT_FOUND
+    fi
+    cd $old_cwd
+}
+
+start_opendaylight()
+{
+    # Starts the OpenDaylight controller
+    old_cwd=$PWD
+    cd $ODL_DIR
+    if odl_started; then
+        echo "OpenDaylight is already running"
+        return $EX_OK
+    else
+        echo "Starting OpenDaylight"
+        if [ -z $processors ]; then
+            ./run.sh -start $OSGI_PORT -of13 -Xms1g -Xmx4g &> /dev/null
+        else
+            echo "Pinning ODL to $processors processor(s)"
+            if [ $processors == 1 ]; then
+                echo "Increasing ODL start time, as 1 processor will slow it down"
+                ODL_STARTUP_DELAY=120
+            fi
+            taskset -c 0-$(expr $processors - 1) ./run.sh -start $OSGI_PORT -of13 -Xms1g -Xmx4g &> /dev/null
+        fi
+    fi
+    cd $old_cwd
+    # TODO: Smarter block until ODL is actually up
+    echo "Giving ODL $ODL_STARTUP_DELAY seconds to get up and running"
+    while [ $ODL_STARTUP_DELAY -gt 0 ]; do
+        sleep 10
+        let ODL_STARTUP_DELAY=ODL_STARTUP_DELAY-10
+        echo "$ODL_STARTUP_DELAY seconds remaining"
+    done
+    issue_odl_config
+}
+
+issue_odl_config()
+{
+    # Give dropAllPackets command via telnet to OSGi
+    # This is a bit of a hack, but it's the only method I know of
+    # See: https://ask.opendaylight.org/question/146/issue-non-interactive-gogo-shell-command/
+    # TODO: There seems to be a timing-related bug here. More delay? Smarter check?
+    if ! command -v telnet &> /dev/null; then
+        echo "Installing telnet, as it's required for issuing ODL config."
+        sudo yum install -y telnet &> /dev/null
+    fi
+    echo "Issuing \`dropAllPacketsRpc on\` command via telnet to localhost:$OSGI_PORT"
+    # NB: Not using sleeps results in silent failures (cmd has no effect)
+    (sleep 3; echo dropAllPacketsRpc on; sleep 3) | telnet localhost $OSGI_PORT
+}
+
+stop_opendaylight()
+{
+    # Stops OpenDaylight using run.sh
+    old_cwd=$PWD
+    cd $ODL_DIR
+    if odl_started; then
+        echo "Stopping OpenDaylight"
+        ./run.sh -stop &> /dev/null
+    else
+        echo "OpenDaylight isn't running"
+    fi
+    cd $old_cwd
+}
+
+uninstall_cbench()
+{
+    # Uninstall CBench binary and the code that built it
+    if [ -d $OF_DIR ]; then
+        echo "Removing $OF_DIR"
+        rm -rf $OF_DIR
+    fi
+    if [ -d $OFLOPS_DIR ]; then
+        echo "Removing $OFLOPS_DIR"
+        rm -rf $OFLOPS_DIR
+    fi
+    if [ -f $CBENCH_BIN ]; then
+        echo "Removing $CBENCH_BIN"
+        sudo rm -f $CBENCH_BIN
+    fi
+}
+
+# If executed with no options
+if [ $# -eq 0 ]; then
+    usage
+    exit $EX_USAGE
+fi
+
+while getopts ":hrcip:ot:kd" opt; do
+    case "$opt" in
+        h)
+            # Help message
+            usage
+            exit $EX_OK
+            ;;
+        r)
+            # Run CBench against OpenDaylight
+            if [ $CONTROLLER_IP = "localhost" ]; then
+                if ! odl_installed; then
+                    echo "OpenDaylight isn't installed, can't run test"
+                    exit $EX_ERR
+                fi
+                if ! odl_started; then
+                    echo "OpenDaylight isn't started, can't run test"
+                    exit $EX_ERR
+                fi
+            fi
+            run_cbench
+            ;;
+        c)
+            # Install CBench
+            install_cbench
+            ;;
+        i)
+            # Install OpenDaylight from last successful build
+            install_opendaylight
+            ;;
+        p)
+            # Pin a given number of processors
+            # Note that this option must be given before -o (start ODL)
+            if odl_started; then
+                echo "OpenDaylight is already running, can't adjust processors"
+                exit $EX_ERR
+            fi
+            processors=${OPTARG}
+            if [ $processors -lt 1 ]; then
+                echo "Can't pin ODL to less than one processor"
+                exit $EX_USAGE
+            fi
+            ;;
+        o)
+            # Run OpenDaylight from last successful build
+            if ! odl_installed; then
+                echo "OpenDaylight isn't installed, can't start it"
+                exit $EX_ERR
+            fi
+            start_opendaylight
+            ;;
+        t)
+            # Set CBench run time in minutes
+            if ! odl_installed; then
+                echo "OpenDaylight isn't installed, can't start it"
+                exit $EX_ERR
+            fi
+            # Convert minutes to milliseconds
+            MS_PER_TEST=$((${OPTARG} * 60 * 1000))
+            TESTS_PER_SWITCH=1
+            CBENCH_WARMUP=0
+            echo "Set MS_PER_TEST to $MS_PER_TEST, TESTS_PER_SWITCH to $TESTS_PER_SWITCH, CBENCH_WARMUP to $CBENCH_WARMUP"
+            ;;
+        k)
+            # Kill OpenDaylight
+            if ! odl_installed; then
+                echo "OpenDaylight isn't installed, can't stop it"
+                exit $EX_ERR
+            fi
+            if ! odl_started; then
+                echo "OpenDaylight isn't started, can't stop it"
+                exit $EX_ERR
+            fi
+            stop_opendaylight
+            ;;
+        d)
+            # Delete local ODL and CBench code
+            uninstall_odl
+            uninstall_cbench
+            ;;
+        *)
+            usage
+            exit $EX_USAGE
+    esac
+done
diff --git a/test/tools/cbench_regression/loop_cbench.sh b/test/tools/cbench_regression/loop_cbench.sh
new file mode 100755 (executable)
index 0000000..9b02935
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/env sh
+# Helper script to run CBench tests in a loop, used for testing
+# Script assumes it lives in the same dir as cbench.sh
+
+# Exit codes
+EX_USAGE=64
+EX_OK=0
+
+usage()
+{
+    # Print usage message
+    cat << EOF
+Usage $0 [options]
+
+Run CBench against OpenDaylight in a loop.
+
+OPTIONS:
+    -h Show this help message
+    -l Loop CBench runs without restarting ODL
+    -r Loop CBench runs, restart ODL between runs
+    -t <time> Run CBench for a given number of minutes
+    -p <processors> Pin ODL to given number of processors
+EOF
+}
+
+start_odl()
+{
+    # Starts ODL, optionally pinning it to a given number of processors
+    if [ -z $processors ]; then
+        # Start ODL, don't pass processor info
+        echo "Starting ODL, not passing processor info"
+        ./cbench.sh -o
+    else
+        # Start ODL, pinning it to given number of processors
+        echo "Pinning ODL to $processors processor(s)"
+        ./cbench.sh -p $processors -o
+    fi
+}
+
+run_cbench()
+{
+    # Run CBench against ODL, optionally passing a CBench run time
+    if [ -z $run_time ]; then
+        # Flag means run CBench
+        echo "Running CBench, not passing run time info"
+        ./cbench.sh -r
+    else
+        # Flags mean use $run_time CBench runs, run CBench
+        echo "Running CBench with $run_time minute(s) run time"
+        ./cbench.sh -t $run_time -r
+    fi
+}
+
+loop_no_restart()
+{
+    # Repeatedly run CBench against ODL without restarting ODL
+    echo "Looping CBench against ODL without restarting ODL"
+    while :; do
+        start_odl
+        run_cbench
+    done
+}
+
+loop_with_restart()
+{
+    # Repeatedly run CBench against ODL, restart ODL between runs 
+    echo "Looping CBench against ODL, restarting ODL each run"
+    while :; do
+        start_odl
+        run_cbench
+        # Stop ODL
+        ./cbench.sh -k
+    done
+}
+
+# If executed with no options
+if [ $# -eq 0 ]; then
+    usage
+    exit $EX_USAGE
+fi
+
+while getopts ":hlp:rt:" opt; do
+    case "$opt" in
+        h)
+            # Help message
+            usage
+            exit $EX_OK
+            ;;
+        l)
+            # Loop without restarting ODL between CBench runs
+            loop_no_restart
+            ;;
+        p)
+            # Pin a given number of processors
+            # Note that this option must be given before -o (start ODL)
+            processors=${OPTARG}
+            if [ $processors -lt 1 ]; then
+                echo "Can't pin ODL to less than one processor"
+                exit $EX_USAGE
+            fi
+            ;;
+        r)
+            # Restart ODL between each CBench run
+            loop_with_restart
+            ;;
+        t)
+            # Set length of CBench run in minutes
+            run_time=${OPTARG}
+            ;;
+        *)
+            usage
+            exit $EX_USAGE
+    esac
+done
diff --git a/test/tools/cbench_regression/stats.py b/test/tools/cbench_regression/stats.py
new file mode 100755 (executable)
index 0000000..0d3ca56
--- /dev/null
@@ -0,0 +1,318 @@
+#!/usr/bin/env python
+"""Compute basic stats about CBench data."""
+
+import csv
+import numpy
+import pprint
+import matplotlib.pyplot as pyplot
+import argparse
+import sys
+
+
+class Stats(object):
+
+    """Compute stats and/or graph data.
+
+    I know I could convert these fns that simply punt to a helper
+    to a dict/list data structure, but that would remove some of the
+    flexabilty I get by simply calling a graph/stat fn for each
+    graph/stat arg. All current fns just punt to helpers, but future
+    ones might not.
+
+    """
+
+    results_file = "results.csv"
+    log_file = "cbench.log"
+    precision = 3
+    run_index = 0
+    flow_index = 1
+    start_time_index = 2
+    end_time_index = 3
+    start_steal_time_index = 10
+    end_steal_time_index = 11
+    used_ram_index = 13
+    one_load_index = 16
+    five_load_index = 17
+    fifteen_load_index = 18
+    start_iowait_index = 20
+    end_iowait_index = 21
+
+    def __init__(self):
+        """Setup some flags and data structures, kick off build_cols call."""
+        self.build_cols()
+        self.results = {}
+        self.results["sample_size"] = len(self.run_col)
+
+    def build_cols(self):
+        """Parse results file into lists of values, one per column."""
+        self.run_col= []
+        self.flows_col = []
+        self.runtime_col = []
+        self.used_ram_col = []
+        self.iowait_col = []
+        self.steal_time_col = []
+        self.one_load_col = []
+        self.five_load_col = []
+        self.fifteen_load_col = []
+
+        with open(self.results_file, "rb") as results_fd:
+            results_reader = csv.reader(results_fd)
+            for row in results_reader:
+                try:
+                    self.run_col.append(float(row[self.run_index]))
+                    self.flows_col.append(float(row[self.flow_index]))
+                    self.runtime_col.append(float(row[self.end_time_index]) - \
+                        float(row[self.start_time_index]))
+                    self.used_ram_col.append(float(row[self.used_ram_index]))
+                    self.iowait_col.append(float(row[self.end_iowait_index]) - \
+                        float(row[self.start_iowait_index]))
+                    self.steal_time_col.append(float(row[self.end_steal_time_index]) - \
+                        float(row[self.start_steal_time_index]))
+                    self.one_load_col.append(float(row[self.one_load_index]))
+                    self.five_load_col.append(float(row[self.five_load_index]))
+                    self.fifteen_load_col.append(float(row[self.fifteen_load_index]))
+                except ValueError:
+                    # Skips header
+                    continue
+
+    def compute_flow_stats(self):
+        """Compute CBench flows/second stats."""
+        self.compute_generic_stats("flows", self.flows_col)
+
+    def build_flow_graph(self, total_gcount, graph_num):
+        """Plot flows/sec data.
+
+        :param total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :param graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "Flows per Second", self.flows_col)
+
+    def compute_ram_stats(self):
+        """Compute used RAM stats."""
+        self.compute_generic_stats("used_ram", self.used_ram_col)
+
+    def build_ram_graph(self, total_gcount, graph_num):
+        """Plot used RAM data.
+
+        :param total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :param graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "Used RAM (MB)", self.used_ram_col)
+
+    def compute_runtime_stats(self):
+        """Compute CBench runtime length stats."""
+        self.compute_generic_stats("runtime", self.runtime_col)
+
+    def build_runtime_graph(self, total_gcount, graph_num):
+        """Plot CBench runtime length data.
+
+        :paruntime total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :paruntime graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "CBench Runtime (sec)", self.runtime_col)
+
+    def compute_iowait_stats(self):
+        """Compute iowait stats."""
+        self.compute_generic_stats("iowait", self.iowait_col)
+
+    def build_iowait_graph(self, total_gcount, graph_num):
+        """Plot iowait data.
+
+        :paiowait total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :paiowait graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "IOWait Time (sec)", self.iowait_col)
+
+    def compute_steal_time_stats(self):
+        """Compute steal time stats."""
+        self.compute_generic_stats("steal_time", self.steal_time_col)
+
+    def build_steal_time_graph(self, total_gcount, graph_num):
+        """Plot steal time data.
+
+        :pasteal_time total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :pasteal_time graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "Steal Time (sec)", self.steal_time_col)
+
+    def compute_one_load_stats(self):
+        """Compute one minute load stats."""
+        self.compute_generic_stats("one_load", self.one_load_col)
+
+    def build_one_load_graph(self, total_gcount, graph_num):
+        """Plot one minute load data.
+
+        :paone_load total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :paone_load graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "One Minute Load", self.one_load_col)
+
+    def compute_five_load_stats(self):
+        """Compute five minute load stats."""
+        self.compute_generic_stats("five_load", self.five_load_col)
+
+    def build_five_load_graph(self, total_gcount, graph_num):
+        """Plot five minute load data.
+
+        :pafive_load total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :pafive_load graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "Five Minute Load", self.five_load_col)
+
+    def compute_fifteen_load_stats(self):
+        """Compute fifteen minute load stats."""
+        self.compute_generic_stats("fifteen_load", self.fifteen_load_col)
+
+    def build_fifteen_load_graph(self, total_gcount, graph_num):
+        """Plot fifteen minute load data.
+
+        :pafifteen_load total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :pafifteen_load graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+
+        """
+        self.build_generic_graph(total_gcount, graph_num,
+            "Fifteen Minute Load", self.fifteen_load_col)
+
+    def compute_generic_stats(self, stats_name, stats_col):
+        """Helper for computing generic stats."""
+        generic_stats = {}
+        generic_stats["min"] = int(numpy.amin(stats_col))
+        generic_stats["max"] = int(numpy.amax(stats_col))
+        generic_stats["mean"] = round(numpy.mean(stats_col), self.precision)
+        generic_stats["stddev"] = round(numpy.std(stats_col), self.precision)
+        try:
+            generic_stats["relstddev"] = round(generic_stats["stddev"] / \
+                generic_stats["mean"] * 100, self.precision)
+        except ZeroDivisionError:
+            generic_stats["relstddev"] = 0.
+        self.results[stats_name] = generic_stats
+
+    def build_generic_graph(self, total_gcount, graph_num, y_label, data_col):
+        """Helper for plotting generic data.
+
+        :pageneric total_gcount: Total number of graphs to render.
+        :type total_gcount: int
+        :pageneric graph_num: Number for this graph, <= total_gcount.
+        :type graph_num: int
+        :param y_label: Lable of Y axis.
+        :type y_label: string
+        :param data_col: Data to graph.
+        :type data_col: list
+
+        """
+        # Pagenerics are numrows, numcols, fignum
+        pyplot.subplot(total_gcount, 1, graph_num)
+        # "go" means green O's
+        pyplot.plot(self.run_col, data_col, "go")
+        pyplot.xlabel("Run Number")
+        pyplot.ylabel(y_label)
+
+
+# Build stats object
+stats = Stats()
+
+# Map of graph names to the Stats.fns that build them
+graph_map = {"flows": stats.build_flow_graph,
+             "runtime": stats.build_runtime_graph,
+             "iowait": stats.build_iowait_graph,
+             "steal_time": stats.build_steal_time_graph,
+             "one_load": stats.build_one_load_graph,
+             "five_load": stats.build_five_load_graph,
+             "fifteen_load": stats.build_fifteen_load_graph,
+             "ram": stats.build_ram_graph}
+stats_map = {"flows": stats.compute_flow_stats,
+             "runtime": stats.compute_runtime_stats,
+             "iowait": stats.compute_iowait_stats,
+             "steal_time": stats.compute_steal_time_stats,
+             "one_load": stats.compute_one_load_stats,
+             "five_load": stats.compute_five_load_stats,
+             "fifteen_load": stats.compute_fifteen_load_stats,
+             "ram": stats.compute_ram_stats}
+
+# Build argument parser
+parser = argparse.ArgumentParser(description="Compute stats about CBench data")
+parser.add_argument("-S", "--all-stats", action="store_true",
+    help="compute all stats")
+parser.add_argument("-s", "--stats", choices=stats_map.keys(),
+    help="compute stats on specified data", nargs="+")
+parser.add_argument("-G", "--all-graphs", action="store_true",
+    help="graph all data")
+parser.add_argument("-g", "--graphs", choices=graph_map.keys(),
+    help="graph specified data", nargs="+")
+
+
+# Print help if no arguments are given
+if len(sys.argv) == 1:
+    parser.print_help()
+    sys.exit(1)
+
+# Parse the given args
+args = parser.parse_args()
+
+# Build graphs
+if args.all_graphs:
+    graphs_to_build = graph_map.keys()
+elif args.graphs:
+    graphs_to_build = args.graphs
+else:
+    graphs_to_build = []
+for graph, graph_num in zip(graphs_to_build, range(len(graphs_to_build))):
+    graph_map[graph](len(graphs_to_build), graph_num+1)
+
+# Compute stats
+if args.all_stats:
+    stats_to_compute = stats_map.keys()
+elif args.stats:
+    stats_to_compute = args.stats
+else:
+    stats_to_compute = []
+for stat in stats_to_compute:
+    stats_map[stat]()
+
+# Render graphs
+if args.graphs or args.all_graphs:
+    # Attempt to adjust plot spacing, just a simple heuristic
+    if len(graphs_to_build) <= 3:
+        pyplot.subplots_adjust(hspace=.2)
+    elif len(graphs_to_build) <= 6:
+        pyplot.subplots_adjust(hspace=.4)
+    elif len(graphs_to_build) <= 9:
+        pyplot.subplots_adjust(hspace=.7)
+    else:
+        pyplot.subplots_adjust(hspace=.7)
+        print "WARNING: That's a lot of graphs. Add a second column?"
+    pyplot.show()
+
+# Print stats
+if args.stats or args.all_stats:
+    pprint.pprint(stats.results)