#!/usr/bin/perl -w

use strict;

use POSIX qw(strftime);
use Time::HiRes;
use IO::Handle;


############################################################################
#  UnixBench - Release 5.1.1, based on:
#  The BYTE UNIX Benchmarks - Release 3
#          Module: Run   SID: 3.11 5/15/91 19:30:14
# Original Byte benchmarks written by:
#       Ben Smith,              Tom Yager at BYTE Magazine
#       ben@bytepb.byte.com     tyager@bytepb.byte.com
# BIX:  bensmith                tyager
#
#######################################################################
# General Purpose Benchmark
# based on the work by Ken McDonell, Computer Science, Monash University
#
#  You will need ...
#       perl Time::HiRes IO::Handlecat cc chmod comm cp date dc df echo
#       kill ls make mkdir rm sed test time touch tty umask who
###############################################################################
#  Modification Log:
# $Header: run,v 5.2 88/01/12 06:23:43 kenj Exp $
#     Ken McDonell, Computer Science, Monash University
#     August 1, 1983
# 3/89 - Ben Smith - BYTE: globalized many variables, modernized syntax
# 5/89 - commented and modernized. Removed workload items till they
#        have been modernized. Added database server test.
# 11/14/89 - Made modifications to reflect new version of fstime
#        and elimination of mem tests.
# 10/22/90 - Many tests have been flipped so that they run for
#        a specified length of time and loops are counted.
# 4/3/91 - Cleaned up and debugged several test parameters - Ben
# 4/9/91 - Added structure for creating index and determing flavor of UNIX
# 4/26/91 - Made changes and corrections suggested by Tin Le of Sony
# 5/15/91 - Removed db from distribution
# 4/4/92    Jon Tombs <jon@robots.ox.ac.uk> fixed for GNU time to look like
#               BSD (don't know the format of sysV!)
# 12/95   - Massive changes for portability, speed, and more meaningful index
#               DCN     David C Niemi <niemi@tux.org>
# 1997.06.20    DCN     Fixed overflow condition in fstime.c on fast machines
# 1997.08.24    DCN     Modified "system", replaced double with
#                       whetstone-double in "index"
# 1997.09.10    DCN     Added perlbench as an Exhibition benchmark
# 1997.09.23    DCN     Added rgooch's select as an Exhibition benchmark
# 1999.07.28    DCN     "select" not compiled or run by default, because it
#                       does not compile on many platforms.  PerlBench also
#                       not run by default.
# 2007.09.26    IS      Huge rewrite -- see release notes in README.
# 2007.10.12    IS      Added graphics tests, categories feature.
# 2007.10.14    IS      Set and report LANG.  Added "grep" and "sysexec".
# 2007.12.22    IS      Tiny fixes; see README.


############################################################################
# CONFIGURATION
############################################################################

# Version number of the script.
my $version = "5.1.2";

# The setting of LANG makes a huge difference to some of the scores,
# particularly depending on whether UTF-8 is used.  So we always set
# it to the same value, which is configured here.
#
# If you want your results to be meaningful when compared to other peoples'
# results, you should not change this.  Change it if you want to measure the
# effect of different languages.
my $language = "en_US.utf8";

# The number of iterations per test.
my $longIterCount = 10;
my $shortIterCount = 3;

# C compiler to use in compilation tests.
my $cCompiler = $ENV{CC};

# Establish full paths to directories.  These need to be full pathnames
# (or do they, any more?).  They can be set in env.
# variables whose names are the first parameter to getDir() below.
my $BASEDIR = `pwd`;
chomp($BASEDIR);

# Directory where the test programs live.
my $BINDIR = getDir('UB_BINDIR', $BASEDIR . "/pgms");

# Temp directory, for temp files.
my $TMPDIR = getDir('UB_TMPDIR', $BASEDIR . "/tmp");

# Directory to put results in.
my $RESULTDIR = getDir('UB_RESULTDIR', $BASEDIR . "/results");

# Directory where the tests are executed.
my $TESTDIR = getDir('UB_TESTDIR', $BASEDIR . "/testdir");


############################################################################
# TEST SPECIFICATIONS
############################################################################

# Configure the categories to which tests can belong.
my $testCats = {
    'system'    => { 'name' => "System Benchmarks", 'maxCopies' => 16 },
    '2d'        => { 'name' => "2D Graphics Benchmarks", 'maxCopies' => 1 },
    '3d'        => { 'name' => "3D Graphics Benchmarks", 'maxCopies' => 1 },
    'misc'      => { 'name' => "Non-Index Benchmarks", 'maxCopies' => 16 },
};


my $arithmetic = [
    "arithoh", "short", "int", "long", "float", "double", "whetstone-double"
];

my $fs = [
    "fstime-w", "fstime-r", "fstime",
    "fsbuffer-w", "fsbuffer-r", "fsbuffer",
    "fsdisk-w", "fsdisk-r", "fsdisk"
];

my $oldsystem = [
    "execl", "fstime", "fsbuffer", "fsdisk", "pipe", "context1", "spawn",
    "syscall"
];

my $system = [
    @$oldsystem, "shell1", "shell8" # , "shell16"
];

my $index = [
   "dhry2reg", "whetstone-double", @$oldsystem, "shell1", "shell8"
];

my $graphics = [
    "2d-rects", "2d-ellipse", "2d-aashapes", "2d-text", "2d-blit",
    "2d-window", "ubgears"
];


# List of all supported test names.
my $testList = {
    # Individual tests.
    "dhry2reg"      => undef,
    "whetstone-double"   => undef,
    "syscall"       => undef,
    "pipe"          => undef,
    "context1"      => undef,
    "spawn"         => undef,
    "execl"         => undef,
    "fstime-w"      => undef,
    "fstime-r"      => undef,
    "fstime"        => undef,
    "fsbuffer-w"    => undef,
    "fsbuffer-r"    => undef,
    "fsbuffer"      => undef,
    "fsdisk-w"      => undef,
    "fsdisk-r"      => undef,
    "fsdisk"        => undef,
    "shell1"        => undef,
    "shell8"        => undef,
    "shell16"       => undef,
    "short"         => undef,
    "int"           => undef,
    "long"          => undef,
    "float"         => undef,
    "double"        => undef,
    "arithoh"       => undef,
    "C"             => undef,
    "dc"            => undef,
    "hanoi"         => undef,
    "grep"          => undef,
    "sysexec"       => undef,

    "2d-rects"      => undef,
    "2d-lines"      => undef,
    "2d-circle"     => undef,
    "2d-ellipse"    => undef,
    "2d-shapes"     => undef,
    "2d-aashapes"   => undef,
    "2d-polys"      => undef,
    "2d-text"       => undef,
    "2d-blit"       => undef,
    "2d-window"     => undef,

    "ubgears"       => undef,

    # Named combos and shorthands.
    "arithmetic"    => $arithmetic,
    "dhry"          => [ "dhry2reg" ],
    "dhrystone"     => [ "dhry2reg" ],
    "whets"         => [ "whetstone-double" ],
    "whetstone"     => [ "whetstone-double" ],
    "load"          => [ "shell" ],
    "misc"          => [ "C", "dc", "hanoi" ],
    "speed"         => [ @$arithmetic, @$system ],
    "oldsystem"     => $oldsystem,
    "system"        => $system,
    "fs"            => $fs,
    "shell"         => [ "shell1", "shell8" ],
    "graphics"      => $graphics,

    # The tests which constitute the official index.
    "index"         => $index,

    # The tests which constitute the official index plus the graphics
    # index.
    "gindex"         => [ @$index, @$graphics ],
};


# Default parameters for benchmarks.  Note that if "prog" is used,
# it must contain just the program name, as it will be quoted (this
# is necessary if BINDIR contains spaces).  Put any options in "options".
my $baseParams = {
    "prog" => undef,
    "options" => "",
    "repeat" => 'short',
    "stdout" => 1,                  # Non-0 to keep stdout.
    "stdin" => "",
    "logmsg" => "",
};


# Individual parameters for all benchmarks.
my $testParams = {

    ##########################
    ## System Benchmarks    ##
    ##########################

    "dhry2reg" => {
        "logmsg" => "Dhrystone 2 using register variables",
        "cat"    => 'system',
        "options" => "10",
        "repeat" => 'long',
    },
    "whetstone-double" => {
        "logmsg" => "Double-Precision Whetstone",
        "cat"    => 'system',
        "repeat" => 'long',
    },
    "syscall" => {
        "logmsg" => "System Call Overhead",
        "cat"    => 'system',
        "repeat" => 'long',
        "options" => "10",
    },
    "context1" => {
        "logmsg" => "Pipe-based Context Switching",
        "cat"    => 'system',
        "repeat" => 'long',
        "options" => "10",
    },
    "pipe" => {
        "logmsg" => "Pipe Throughput",
        "cat"    => 'system',
        "repeat" => 'long',
        "options" => "10",
    },
    "spawn" => {
        "logmsg" => "Process Creation",
        "cat"    => 'system',
        "options" => "30",
    },
    "execl" => {
        "logmsg" => "Execl Throughput",
        "cat"    => 'system',
        "options" => "30",
    },
    "fstime-w" => {
        "logmsg" => "File Write 1024 bufsize 2000 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-w -t 30 -d \"${TMPDIR}\" -b 1024 -m 2000",
    },
    "fstime-r" => {
        "logmsg" => "File Read 1024 bufsize 2000 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-r -t 30 -d \"${TMPDIR}\" -b 1024 -m 2000",
    },
   "fstime" => {
        "logmsg" => "File Copy 1024 bufsize 2000 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-c -t 30 -d \"${TMPDIR}\" -b 1024 -m 2000",
    },
    "fsbuffer-w" => {
        "logmsg" => "File Write 256 bufsize 500 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-w -t 30 -d \"${TMPDIR}\" -b 256 -m 500",
    },
    "fsbuffer-r" => {
        "logmsg" => "File Read 256 bufsize 500 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-r -t 30 -d \"${TMPDIR}\" -b 256 -m 500",
    },
    "fsbuffer" => {
        "logmsg" => "File Copy 256 bufsize 500 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-c -t 30 -d \"${TMPDIR}\" -b 256 -m 500",
    },
    "fsdisk-w" => {
        "logmsg" => "File Write 4096 bufsize 8000 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-w -t 30 -d \"${TMPDIR}\" -b 4096 -m 8000",
    },
    "fsdisk-r" => {
        "logmsg" => "File Read 4096 bufsize 8000 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-r -t 30 -d \"${TMPDIR}\" -b 4096 -m 8000",
    },
    "fsdisk" => {
        "logmsg" => "File Copy 4096 bufsize 8000 maxblocks",
        "cat"    => 'system',
        "prog" => "${BINDIR}/fstime",
        "options" => "-c -t 30 -d \"${TMPDIR}\" -b 4096 -m 8000",
    },
    "shell1" => {
        "logmsg" => "Shell Scripts (1 concurrent)",
        "cat"    => 'system',
        "prog" => "${BINDIR}/looper",
        "options" => "60 \"${BINDIR}/multi.sh\" 1",
    },
    "shell8" => {
        "logmsg" => "Shell Scripts (8 concurrent)",
        "cat"    => 'system',
        "prog" => "${BINDIR}/looper",
        "options" => "60 \"${BINDIR}/multi.sh\" 8",
    },
    "shell16" => {
        "logmsg" => "Shell Scripts (16 concurrent)",
        "cat"    => 'system',
        "prog" => "${BINDIR}/looper",
        "options" => "60 \"${BINDIR}/multi.sh\" 16",
    },

    ##########################
    ## Graphics Benchmarks  ##
    ##########################

    "2d-rects" => {
        "logmsg" => "2D graphics: rectangles",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "rects 3 2",
    },

    "2d-lines" => {
        "logmsg" => "2D graphics: lines",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "lines 3 2",
    },

    "2d-circle" => {
        "logmsg" => "2D graphics: circles",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "circle 3 2",
    },

    "2d-ellipse" => {
        "logmsg" => "2D graphics: ellipses",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "ellipse 3 2",
    },

    "2d-shapes" => {
        "logmsg" => "2D graphics: polygons",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "shapes 3 2",
    },

    "2d-aashapes" => {
        "logmsg" => "2D graphics: aa polygons",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "aashapes 3 2",
    },

    "2d-polys" => {
        "logmsg" => "2D graphics: complex polygons",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "polys 3 2",
    },

    "2d-text" => {
        "logmsg" => "2D graphics: text",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "text 3 2",
    },

    "2d-blit" => {
        "logmsg" => "2D graphics: images and blits",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "blit 3 2",
    },

    "2d-window" => {
        "logmsg" => "2D graphics: windows",
        "cat"    => '2d',
        "prog" => "${BINDIR}/gfx-x11",
        "options" => "window 3 2",
    },

    "ubgears" => {
        "logmsg" => "3D graphics: gears",
        "cat"    => '3d',
        "options" => "-time 20 -v",
    },


    ##########################
    ## Non-Index Benchmarks ##
    ##########################

    "C" => {
        "logmsg" => "C Compiler Throughput ($cCompiler)",
        "cat"    => 'misc',
        "prog" => "${BINDIR}/looper",
        "options" => "60 $cCompiler cctest.c",
    },
    "arithoh" => {
        "logmsg" => "Arithoh",
        "cat"    => 'misc',
        "options" => "10",
    },
    "short" => {
        "logmsg" => "Arithmetic Test (short)",
        "cat"    => 'misc',
        "options" => "10",
    },
    "int" => {
        "logmsg" => "Arithmetic Test (int)",
        "cat"    => 'misc',
        "options" => "10",
    },
    "long" => {
        "logmsg" => "Arithmetic Test (long)",
        "cat"    => 'misc',
        "options" => "10",
    },
    "float" => {
        "logmsg" => "Arithmetic Test (float)",
        "cat"    => 'misc',
        "options" => "10",
    },
    "double" => {
        "logmsg" => "Arithmetic Test (double)",
        "cat"    => 'misc',
        "options" => "10",
    },
    "dc" => {
        "logmsg" => "Dc: sqrt(2) to 99 decimal places",
        "cat"    => 'misc',
        "prog" => "${BINDIR}/looper",
        "options" => "30 dc",
        "stdin" => "dc.dat",
    },
    "hanoi" => {
        "logmsg" => "Recursion Test -- Tower of Hanoi",
        "cat"    => 'misc',
        "options" => "20",
    },
    "grep" => {
        "logmsg" => "Grep a large file (system's grep)",
        "cat"    => 'misc',
        "prog" => "${BINDIR}/looper",
        "options" => "30 grep -c gimp large.txt",
    },
    "sysexec" => {
        "logmsg" => "Exec System Call Overhead",
        "cat"    => 'misc',
        "repeat" => 'long',
        "prog" => "${BINDIR}/syscall",
        "options" => "10 exec",
    },
};


# CPU flags of interest.
my $x86CpuFlags = {
    'pae' => "Physical Address Ext",
    'sep' => "SYSENTER/SYSEXIT",
    'syscall' => "SYSCALL/SYSRET",
    'mmx' => "MMX",
    'mmxext' => "AMD MMX",
    'cxmmx' => "Cyrix MMX",
    'xmm' => "Streaming SIMD",
    'xmm2' => "Streaming SIMD-2",
    'xmm3' => "Streaming SIMD-3",
    'ht' => "Hyper-Threading",
    'ia64' => "IA-64 processor",
    'lm' => "x86-64",
    'vmx' => "Intel virtualization",
    'svm' => "AMD virtualization",
};


############################################################################
# UTILITIES
############################################################################

# Exec the given command, and catch its standard output.
# We return an array containing the PID and the filehandle on the
# process' standard output.  It's up to the caller to wait for the command
# to terminate.
sub command {
    my ( $cmd ) = @_;

    my $pid = open(my $childFd, "-|");
    if (!defined($pid)) {
        die("Run: fork() failed (undef)\n");
    } elsif ($pid == 0) {
        exec($cmd);
        die("Run: exec() failed (returned)\n");
    }

    return ( $pid, $childFd );
}


# Get data from running a system command.  Used for things like getting
# the host OS from `uname -o` etc.
#
# Ignores initial blank lines from the command and returns the first
# non-blank line, with white space trimmed off.  Returns a blank string
# if there is no output; undef if the command fails.
sub getCmdOutput {
    my ( $cmd ) = @_;

    my ( $pid, $fd ) = command($cmd . " 2>/dev/null");
    my $result = "";
    while (<$fd>) {
        chomp;
        next if /^[ \t]*$/;

        $result = $_;
        $result =~ s/^[ \t]+//;
        $result =~ s/[ \t]+$//;
        last;
    }

    # Close the command and wait for it to die.
    waitpid($pid, 0);
    my $status = $?;

    return $status == 0 ? $result : undef;
}


# Get a directory pathname from an environment variable, or the given
# default.  Canonicalise and return the value.
sub getDir {
    my ( $var, $def ) = @_;

    my $val = $ENV{$var} || $def;

    # Canonicalise the value.
    my $wd;
    chomp($wd = `pwd`);
    chdir($val);
    chomp($val = `pwd`);
    chdir($wd);
    $ENV{$var} = $val;

    $val;
}


# Get the name of the file we're going to log to.  The name uses the hostname
# and date, plus a sequence number to make it unique.
sub logFile {
    my ( $sysInfo ) = @_;

    my $count = 1;

    # Use the date in the base file name.
    my $ymd = strftime "%Y-%m-%d", localtime;

    while (1) {
        my $log = sprintf "%s/%s-%s-%02d",
                        ${RESULTDIR}, $sysInfo->{'name'}, $ymd, $count;
        return $log if (! -e $log);
        ++$count;
    }
}


# Print a message to the named log file.  We use this method rather than
# keeping the FD open because we use shell redirection to send command
# output to the same file.
sub printLog {
    my ( $logFile, @args ) = @_;

    open(my $fd, ">>", $logFile) || abortRun("can't append to $logFile");
    printf $fd @args;
    close($fd);
}


# Display a number of something, auto-selecting the plural form
# if appropriate.  We are given the number, the singular, and the
# plural; if the plural is omitted, it defaults to singular + "s".
sub number {
    my ( $n, $what, $plural ) = @_;

    $plural = $what . "s" if !defined($plural);

    if (!defined($n)) {
        return sprintf "unknown %s", $plural;
    } else {
        return sprintf "%d %s", $n, $n == 1 ? $what : $plural;
    }
}


# Merge two sets of test parameters -- defaults and actual parameters.
# Return the merged parameter hash.
sub mergeParams {
    my ( $def, $vals ) = @_;

    my $params = { };
    foreach my $k (keys(%$def)) {
        $params->{$k} = $def->{$k};
    }
    foreach my $k (keys(%$vals)) {
        $params->{$k} = $vals->{$k};
    }

    $params;
}


############################################################################
# SYSTEM ANALYSIS
############################################################################

# Extract interesting flags from the given processor flags string and
# convert them to descriptive names.
sub processCpuFlags {
    my ( $flagStr ) = @_;

    my @names;
    foreach my $f (sort split(/\s+/, $flagStr)) {
        my $name = $x86CpuFlags->{$f};
        push(@names, $name) if $name;
    }

    join(", ", @names);
}


# Get information on the CPUs in the system.  Returns a reference to an
# array of N entries, one per CPU, where each entry is a hash containing
# these fields:
# describing the model etc.  Returns undef if the information can't be got.
sub getCpuInfo {
    open(my $fd, "<", "/proc/cpuinfo") || return undef;

    my $cpus = [ ];
    my $cpu = 0;
    while (<$fd>) {
        chomp;
        my ( $field, $val ) = split(/[ \t]*:[ \t]*/);
        next if (!$field || !$val);
        if ($field eq "processor") {
            $cpu = $val;
        } elsif ($field eq "model name") {
            my $model = $val;
            $model =~ s/  +/ /g;
            $cpus->[$cpu]{'model'} = $model;
        } elsif ($field eq "bogomips") {
            $cpus->[$cpu]{'bogo'} = $val;
        } elsif ($field eq "flags") {
            $cpus->[$cpu]{'flags'} = processCpuFlags($val);
        }
    }

    close($fd);

    $cpus;
}


# Get information on the host system.  Returns a reference to a hash
# with the following fields:
#    name           Host name
#    os             Host OS name
#    osRel          Host OS release
#    osVer          Host OS version
#    mach           Host machine name (eg. "SparcStation 20", but on
#                   PC/Linux usually "i686" etc.)
#    platform       Hardware platform; on Linux, the base CPU type?
#    system         System name (eg. hostname and Linux distro, like
#                   "hostname: openSUSE 10.2 (i586)").
#    cpus           Value returned by getCpuInfo(), undef if not avail.
#    numCpus        Number of CPUs if known, else undef.
#    load           System load message as per "uptime".
#    numUsers       Number of users and/or open shell sessions.
sub getSystemInfo {
    my $info = { };

    # Get host system data.
    if ($ENV{MINIX}) {
        $info->{'name'} = getCmdOutput("uname -a");
    } else {
        $info->{'name'} = getCmdOutput("hostname");
    }
    $info->{'os'} = getCmdOutput("uname -o") || getCmdOutput("uname -s");
    $info->{'osRel'} = getCmdOutput("uname -r");
    $info->{'osVer'} = getCmdOutput("uname -v");
    $info->{'mach'} = getCmdOutput("uname -m");
    if (!$ENV{MINIX}) {
        $info->{'platform'} = getCmdOutput("uname -i");
    }

    # Get the system name (SUSE, Red Hat, etc.) if possible.
    $info->{'system'} = $info->{'os'};
    if ( -r "/etc/SuSE-release" ) {
        $info->{'system'} = getCmdOutput("cat /etc/SuSE-release");
    } elsif ( -r "/etc/release" ) {
        $info->{'system'} = getCmdOutput("cat /etc/release");
    }

    # Get the language info.
    if (!$ENV{MINIX}) {
        my $lang = getCmdOutput("printenv LANG");
        my $map = getCmdOutput("locale -k LC_CTYPE | grep charmap");
        $map =~ s/.*=//;
        my $coll = getCmdOutput("locale -k LC_COLLATE | grep collate-codeset");
        $coll =~ s/.*=//;
        $info->{'language'} = sprintf "%s (charmap=%s, collate=%s)",
                                      $lang, $map, $coll;
    }

    # Get details on the CPUs, if possible.
    my $cpus = getCpuInfo();
    if (defined($cpus)) {
        $info->{'cpus'} = $cpus;
        $info->{'numCpus'} = scalar(@$cpus);
    }

    # Get graphics hardware info.
    # if (!$ENV{MINIX}) {
    #     $info->{'graphics'} = getCmdOutput("3dinfo | cut -f1 -d\'(\'");
    # }

    # Get system run state, load and usage info.
    if (!$ENV{MINIX}) {
        $info->{'runlevel'} = getCmdOutput("runlevel | cut -f2 -d\" \"");
    }
    $info->{'load'} = getCmdOutput("uptime");
    $info->{'numUsers'} = getCmdOutput("who | wc -l");

    $info;
}


############################################################################
# ERROR HANDLING
############################################################################

# Abort the benchmarking run with an error message.
sub abortRun {
    my ( $err ) = @_;

    printf STDERR "\n**********************************************\n";
    printf STDERR "Run: %s; aborting\n", $err;
    exit(1);
}


############################################################################
# TEST SETUP
############################################################################

# Do checks that everything's ready for testing.
sub preChecks {
    # Set the language.
    $ENV{'LANG'} = $language;

    # Check that the required files are in the proper places.
    system("make check");
    if ($? != 0) {
        system("make all");
        if ($? != 0) {
            abortRun("\"make all\" failed");
        }
    }

    # Create a script to kill this run.
    system("echo \"kill -9 $$\" > \"${TMPDIR}/kill_run\"");
    chmod(0755, $TMPDIR . "/kill_run");
}


# Parse the command arguments.
sub parseArgs {
    my @words = @_;

    # The accumulator for the bench units to be run.
    my $tests = [ ];
    my $params = { 'tests' => $tests };

    # Generate the requested list of bench programs.
    my $opt;
    my $word;
    while ($word = shift(@words)) {
        if ($word !~ m/^-/) {               # A test name.
            if ($word eq "all") {
                foreach my $t (keys(%$testList)) {
                    push(@$tests, $t) if (!defined($testList->{$t}));
                }
            } elsif (exists($testList->{$word})) {
                my $val = $testList->{$word} || [ $word ];
                push(@$tests, @$val);
            } else {
                die("Run: unknown test \"$word\"\n");
            }
        } elsif ($word eq "-q") {
            $params->{'verbose'} = 0;
        } elsif ($word eq "-v") {
            $params->{'verbose'} = 2;
        } elsif ($word eq "-i") {
            $params->{'iterations'} = shift(@words);
        } elsif ($word eq "-c") {
            if (!defined($params->{'copies'})) {
                $params->{'copies'} = [ ];
            }
            push(@{$params->{'copies'}}, shift(@words));
        } else {
            die("Run: unknown option $word\n");
        }
    }

    $params;
}


############################################################################
# RESULTS INPUT / OUTPUT
############################################################################

# Read a set of benchmarking results from the given file.
# Returns results in the form returned by runTests(), but without the
# individual pass results.
sub readResultsFromFile {
    my ( $file ) = @_;

    # Attempt to get the baseline data file; if we can't, just return undef.
    open(my $fd, "<", $file) || return undef;

    my $results = { };
    while (<$fd>) {
        chomp;

        # Dump comments, ignore blank lines.
        s/#.*//;
        next if /^\s*$/;

        my ( $name, $time, $slab, $sum, $score, $iters ) = split(/\|/);
        my $bresult = { };
        $bresult->{'score'} = $score;
        $bresult->{'scorelabel'} = $slab;
        $bresult->{'time'} = $time;
        $bresult->{'iterations'} = $iters;

        $results->{$name} = $bresult;
    }

    close($fd);

    $results;
}


############################################################################
# RESULTS PROCESSING
############################################################################

# Process a set of results from a single test by averaging the individal
# pass results into a single final value.
# First, though, dump the worst 1/3 of the scores.  The logic is that a
# glitch in the system (background process waking up, for example) may
# make one or two runs go slow, so let's discard those.
#
# $bresult is a hashed array representing the results of a single test;
# $bresult->{'passes'} is an array of the output from the individual
# passes.
sub combinePassResults {
    my ( $bench, $tdata, $bresult, $logFile ) = @_;

    $bresult->{'cat'} = $tdata->{'cat'};

    # Computed results.
    my $iterations = 0;
    my $totalTime = 0;
    my $sum = 0;
    my $product = 0;
    my $label;

    my $pres = $bresult->{'passes'};

    # We're going to throw away the worst 1/3 of the pass results.
    # Figure out how many to keep.
    my $npasses = scalar(@$pres);
    my $ndump = int($npasses / 3);

    foreach my $presult (sort { $a->{'COUNT0'} <=> $b->{'COUNT0'} } @$pres) {
        my $count = $presult->{'COUNT0'};
        my $timebase = $presult->{'COUNT1'};
        $label = $presult->{'COUNT2'};
        my $time = $presult->{'TIME'} || $presult->{'elapsed'};

        # Skip this result if it's one of the worst ones.
        if ($ndump > 0) {
            printLog($logFile, "*Dump score: %12.1f\n", $count);
            --$ndump;
            next;
        }

        # Count this result.
        ++$iterations;
        printLog($logFile, "Count score: %12.1f\n", $count);

        # If $timebase is 0 the figure is a rate; else compute
        # counts per $timebase.  $time is always seconds.
        if ($timebase > 0) {
            $sum += $count / ($time / $timebase);
            $product += log($count) - log($time / $timebase);
        } else {
            $sum += $count;
            $product += log($count);
        }
        $totalTime += $time;
    }

    # Save the results for the benchmark.
    if ($iterations > 0) {
        $bresult->{'score'} = exp($product / $iterations);
        $bresult->{'scorelabel'} = $label;
        $bresult->{'time'} = $totalTime / $iterations;
        $bresult->{'iterations'} = $iterations;
    } else {
        $bresult->{'error'} = "No measured results";
    }
}


# Index the given full benchmark results against the baseline results.
# $results is a hashed array of test names to test results.
#
# Adds the following fields to each benchmark result:
#    iscore         The baseline score for this test
#    index          The index of this test against the baseline
# Adds the following fields to $results:
#    indexed        The number of tests for which index values were
#                   generated
#    fullindex      Non-0 if all the index tests were indexed
#    index          The computed overall index for the run
# Note that the index values are computed as
#    result / baseline * 10
# so an index of 523 indicates that a test ran 52.3 times faster than
# the baseline.
sub indexResults {
    my ( $results ) = @_;

    # Read in the baseline result data.  If we can't get it, just return
    # without making indexed results.
    my $index = readResultsFromFile($BINDIR . "/index.base");
    if (!defined($index)) {
        return;
    }

    # Count the number of results we have (indexed or not) in
    # each category.
    my $numCat = { };
    foreach my $bench (@{$results->{'list'}}) {
        my $bresult = $results->{$bench};
        ++$numCat->{$bresult->{'cat'}};
    }
    $results->{'numCat'} = $numCat;

    my $numIndex = { };
    my $indexed = { };
    my $sum = { };
    foreach my $bench (sort(keys(%$index))) {
        # Get the test data for this benchmark.
        my $tdata = $testParams->{$bench};
        if (!defined($tdata)) {
            abortRun("unknown benchmark \"$bench\" in $BINDIR/index.base");
        }

        # Get the test category.  Count the total tests in this cat.
        my $cat = $tdata->{'cat'};
        ++$numIndex->{$cat};

        # If we don't have a result for this test, skip.
        next if (!defined($results->{$bench}));

        # Get the index and actual results.  Calcluate the score.
        my $iresult = $index->{$bench};
        my $bresult = $results->{$bench};
        my $ratio = $bresult->{'score'} / $iresult->{'score'};

        # Save the indexed score.
        $bresult->{'iscore'} = $iresult->{'score'};
        $bresult->{'index'} = $ratio * 10;

        # Sun the scores, and count this test for this category.
        $sum->{$cat} += log($ratio);
        ++$indexed->{$cat};
    }

    # Calculate the index scores per category.
    $results->{'indexed'} = $indexed;
    $results->{'numIndex'} = $numIndex;
    foreach my $c (keys(%$indexed)) {
        if ($indexed->{$c} > 0) {
            $results->{'index'}{$c} = exp($sum->{$c} / $indexed->{$c}) * 10;
        }
    }
}


############################################################################
# TEST EXECUTION
############################################################################

# Exec the given command in a sub-process.
#
# In the child process, we run the command and store its standard output.
# We also time its execution, and catch its exit status.  We then write
# the command's output, plus lines containing the execution time and status,
# to a pipe.
#
# In the parent process, we immediately return an array containing the
# child PID and the filehandle to the pipe.  This allows the caller to
# kick off multiple commands in parallel, then gather their output.
sub commandBuffered {
    my ( $cmd ) = @_;

    # Create a pipe for parent-child communication.
    my $childReader;
    my $parentWriter;
    pipe($childReader, $parentWriter) || abortRun("pipe() failed");
    $parentWriter->autoflush(1);

    # Fork off the child process.
    my $pid = fork();
    if (!defined($pid)) {
        abortRun("fork() failed (undef)");
    } elsif ($pid == 0) {
        # Close the other end of the pipe.
        close $childReader;

        # Start the clock and spawn the command.
        my $benchStart = Time::HiRes::time();
        my ( $cmdPid, $cmdFd ) = command($cmd);

        # Read and buffer all the command's output.
        my $output = [ ];
        while (<$cmdFd>) {
            push(@$output, $_);
        }

        # Stop the clock and save the time.
        my $elTime = Time::HiRes::time() - $benchStart;
        push(@$output, sprintf "elapsed|%f\n", $elTime);

        # Wait for the child to die so we can get its status.
        # close($cmdFd);  Doesn't work???
        waitpid($cmdPid, 0);
        my $status = $?;
        push(@$output, sprintf "status|%d\n", $status);

        # Now that we've got the time, play back all the output to the pipe.
        # The parent can read this at its leisure.
        foreach my $line (@$output) {
            print $parentWriter $line;
        }

        # Terminate this child.
        close $parentWriter;
        exit(0);
    }

    # Close the other end of the pipe.
    close $parentWriter;

    return ( $pid, $childReader );
}


# Read the results of a benchmark execution from a child process, given
# its process ID and its filehandle.  Create a results hash structure
# containing the fields returned by the child, plus:
#    pid            The child's process ID
#    status         The child's exit status
#    ERROR          Any stderr output from the child that isn't result data
# Note that ay result fields with ultiple values are split; so eg.
#    COUNT|x|y|x
# becomes
#    COUNT0 = x
#    COUNT1 = y
#    COUNT2 = z
sub readResults {
    my ( $pid, $fd ) = @_;

    my $presult = { 'pid' => $pid };

    # Read all the result lines from the child.
    while (<$fd>) {
        chomp;

        my ( $field, @params ) = split(/\|/);
        if (scalar(@params) == 0) {            # Error message.
            $presult->{'ERROR'} .= "\n" if ($presult->{'ERROR'});
            $presult->{'ERROR'} .= $field;
        } elsif (scalar(@params) == 1) {       # Simple data.
            $presult->{$field} = $params[0];
        } else {                               # Compound data.
            # Store the values in separate fields, named "FIELD$i".
            for (my $x = 0; $x < scalar(@params); ++$x) {
                $presult->{$field . $x} = $params[$x];
            }
        }
    }

    # If the command had an error, make an appropriate message if we
    # don't have one.
    if ($presult->{'status'} != 0 && !defined($presult->{'ERROR'})) {
        $presult->{'ERROR'} = "command returned status " . $presult->{'status'};
    }

    # Wait for the child to die.
    close($fd);
    waitpid($pid, 0);

    $presult;
}


# Execute a benchmark command.  We set off a given number of copies in
# parallel to exercise multiple CPUs.
#
# We return an array of results hashes, one per copy; each one is as
# returned by readResults().
sub executeBenchmark {
    my ( $command, $copies ) = @_;

    # Array of contexts for all the copies we're running.
    my $ctxt = [ ];

    # Kick off all the commands at once.
    for (my $i = 0; $i < $copies; ++$i) {
        my ( $cmdPid, $cmdFd ) = commandBuffered($command);
        $ctxt->[$i] = {
            'pid'     => $cmdPid,
            'fd'      => $cmdFd,
        };
    }

    # Now, we can simply read back the command results in order.  Because
    # the child processes read and buffer the results and time the commands,
    # there's no need to use select() to read the results as they appear.
    my $pres = [ ];
    for (my $i = 0; $i < $copies; ++$i) {
        my $presult = readResults($ctxt->[$i]{'pid'}, $ctxt->[$i]{'fd'});
        push(@$pres, $presult);
    }

    $pres;
}


# Run one iteration of a benchmark, as specified by the given
# benchmark parameters.  We run multiple parallel copies as
# specified by $copies.
sub runOnePass {
    my ( $params, $verbose, $logFile, $copies ) = @_;

    # Get the command to run.
    my $command = $params->{'command'};
    if ($verbose > 1) {
        printf "\n";
        printf "COMMAND: \"%s\"\n", $command;
        printf "COPIES: \"%d\"\n", $copies;
    }

    # Remember where we are, and move to the test directory.
    my $pwd = `pwd`;
    chdir($TESTDIR);

    # Execute N copies of the benchmark in parallel.
    my $copyResults = executeBenchmark($command, $copies);
    printLog($logFile, "\n");

    # Move back home.
    chdir($pwd);

    # Sum up the scores of the copies.
    my $count = 0;
    my $time = 0;
    my $elap = 0;
    foreach my $res (@$copyResults) {
        # Log the result data for each copy.
        foreach my $k (sort(keys(%$res))) {
            printLog($logFile, "# %s: %s\n", $k, $res->{$k});
        }
        printLog($logFile, "\n");

        # If it failed, bomb out.
        if (defined($res->{'ERROR'})) {
            my $name = $params->{'logmsg'};
            abortRun("\"$name\": " . $res->{'ERROR'});
        }

        # Count up the score.
        $count += $res->{'COUNT0'};
        $time += $res->{'TIME'} || $res->{'elapsed'};
        $elap += $res->{'elapsed'};
    }

    # Make up a combined result.
    my $passResult = $copyResults->[0];
    $passResult->{'COUNT0'} = $count;
    $passResult->{'TIME'} = $time / $copies;
    $passResult->{'elapsed'} = $elap / $copies;

    $passResult;
}


sub runBenchmark {
    my ( $bench, $tparams, $verbose, $logFile, $copies ) = @_;

    # Make up the actual benchmark parameters.
    my $params = mergeParams($baseParams, $tparams);

    # Make up the command string based on the parameters.
    my $prog = $params->{'prog'} || $BINDIR . "/" . $bench;
    my $command = sprintf "\"%s\" %s", $prog, $params->{'options'};
    $command .= " < \"" . $params->{'stdin'} . "\"" if ($params->{'stdin'});
    $command .= " 2>&1";
    $command .= $params->{'stdout'} ? (" >> \"" . $logFile . "\"") : " > /dev/null";
    $params->{'command'} = $command;

    # Set up the benchmark results structure.
    my $bresult = { 'name' => $bench, 'msg' => $params->{'logmsg'} };

    if ($verbose > 0) {
        printf "\n%d x %s ", $copies, $params->{'logmsg'};
    }

    printLog($logFile,
             "\n########################################################\n");
    printLog($logFile, "%s -- %s\n",
             $params->{'logmsg'}, number($copies, "copy", "copies"));
    printLog($logFile, "==> %s\n\n", $command);

    # Run the test iterations, as given by the "repeat" parameter.
    my $repeats = $shortIterCount;
    $repeats = $longIterCount if $params->{'repeat'} eq 'long';
    $repeats = 1 if $params->{'repeat'} eq 'single';
    my $pres = [ ];
    for (my $i = 1; $i <= $repeats; ++$i) {
        printLog($logFile, "#### Pass %d\n\n", $i);

        # make an attempt to flush buffers
        system("sync; sleep 1; sync; sleep 2");
        # display heartbeat
        if ($verbose > 0) {
            printf " %d", $i;
        }

        # Execute one pass of the benchmark.
        my $presult = runOnePass($params, $verbose, $logFile, $copies);
        push(@$pres, $presult);
    }
    $bresult->{'passes'} = $pres;

    # Calculate the averaged results for this benchmark.
    combinePassResults($bench, $tparams, $bresult, $logFile);

    # Log the results.
    if ($copies == 1) {
        printLog($logFile, "\n>>>> Results of 1 copy\n");
    } else {
        printLog($logFile, "\n>>>> Sum of %d copies\n", $copies);
    }
    foreach my $k ( 'score', 'time', 'iterations' ) {
        printLog($logFile, ">>>> %s: %s\n", $k, $bresult->{$k});
    }
    printLog($logFile, "\n");

    # Some specific cleanup routines.
    if ($bench eq "C") {
        unlink(${TESTDIR} . "/cctest.o");
        unlink(${TESTDIR} . "/a.out");
    }

    if ($verbose > 0) {
        printf "\n";
    }

    $bresult;
}


# Run the named benchmarks.
sub runTests {
    my ( $tests, $verbose, $logFile, $copies ) = @_;

    # Run all the requested tests and gather the results.
    my $results = { 'start' => time(), 'copies' => $copies };
    foreach my $bench (@$tests) {
        # Get the parameters for this benchmark.
        my $params = $testParams->{$bench};
        if (!defined($params)) {
            abortRun("unknown benchmark \"$bench\"");
        }

        # If the benchmark doesn't want to run with this many copies, skip it.
        my $cat = $params->{'cat'};
        my $maxCopies = $testCats->{$cat}{'maxCopies'};
        next if ($copies > $maxCopies);

        # Run the benchmark.
        my $bresult = runBenchmark($bench, $params, $verbose, $logFile, $copies);
        $results->{$bench} = $bresult;
    }
    $results->{'end'} = time();

    # Generate a sorted list of benchmarks for which we have results.
    my @benches = grep {
        ref($results->{$_}) eq "HASH" && defined($results->{$_}{'msg'})
    } keys(%$results);
    @benches = sort {
        $results->{$a}{'msg'} cmp $results->{$b}{'msg'}
    } @benches;
    $results->{'list'} = \@benches;

    # Generate index scores for the results relative to the baseline data.
    indexResults($results);

    $results;
}


############################################################################
# TEXT REPORTS
############################################################################

# Display a banner indicating the configuration of the system under test
# to the given file desc.
sub displaySystem {
    my ( $info, $fd ) = @_;

    # Display basic system info.
    printf $fd "   System: %s: %s\n", $info->{'name'}, $info->{'system'};
    printf $fd "   OS: %s -- %s -- %s\n",
                        $info->{'os'}, $info->{'osRel'}, $info->{'osVer'};
    if (!$ENV{MINIX}) {
        printf $fd "   Machine: %s (%s)\n", $info->{'mach'}, $info->{'platform'};
    }
    printf $fd "   Machine: %s\n", $info->{'mach'};
    if (!$ENV{MINIX}) {
        printf $fd "   Language: %s\n", $info->{'language'};
    }

    # Get and display details on the CPUs, if possible.
    my $cpus = $info->{'cpus'};
    if (!defined($cpus)) {
        printf $fd "   CPU: no details available\n";
    } else {
        for (my $i = 0; $i <= $#$cpus; ++$i) {
            printf $fd "   CPU %d: %s (%.1f bogomips)\n",
                       $i, $cpus->[$i]{'model'}, $cpus->[$i]{'bogo'};
            printf $fd "          %s\n", $cpus->[$i]{'flags'};
        }
    }

    # if (!$ENV{MINIX}) {    
    #     if ($info->{'graphics'}) {
    #         printf $fd "   Graphics: %s\n", $info->{'graphics'};
    #     }
    # }

    # Display system load and usage info.
    if (!$ENV{MINIX}) {
        printf $fd "   %s; runlevel %s\n\n", $info->{'load'}, $info->{'runlevel'};
    }
}


# Display the test scores from the given set of test results.
sub logResults {
    my ( $results, $outFd ) = @_;

    # Display the individual test scores.
    foreach my $bench (@{$results->{'list'}}) {
        my $bresult = $results->{$bench};

        printf $outFd "%-40s %12.1f %-5s (%.1f s, %d samples)\n",
                      $bresult->{'msg'},
                      $bresult->{'score'},
                      $bresult->{'scorelabel'},
                      $bresult->{'time'},
                      $bresult->{'iterations'};
    }

    printf $outFd "\n";
}


# Display index scores, if any, for the given run results.
sub logIndexCat {
    my ( $results, $cat, $outFd ) = @_;

    my $total = $results->{'numIndex'}{$cat};
    my $indexed = $results->{'indexed'}{$cat};
    my $iscore = $results->{'index'}{$cat};
    my $full = $total == $indexed;

    # If there are no indexed scores, just say so.
    if (!defined($indexed) || $indexed == 0) {
        printf $outFd "No index results available for %s\n\n",
                      $testCats->{$cat}{'name'};
        return;
    }

    # Display the header, depending on whether we have a full set of index
    # scores, or a partial set.
    my $head = $testCats->{$cat}{'name'} .
                        ($full ? " Index Values" : " Partial Index");
    printf $outFd "%-40s %12s %12s %8s\n",
                  $head, "BASELINE", "RESULT", "INDEX";

    # Display the individual test scores.
    foreach my $bench (@{$results->{'list'}}) {
        my $bresult = $results->{$bench};
        next if $bresult->{'cat'} ne $cat;

	if (defined($bresult->{'iscore'}) && defined($bresult->{'index'})) {
            printf $outFd "%-40s %12.1f %12.1f %8.1f\n",
                      $bresult->{'msg'}, $bresult->{'iscore'},
                      $bresult->{'score'}, $bresult->{'index'};
	} else {
            printf $outFd "%-40s %12s %12.1f %8s\n",
                      $bresult->{'msg'}, "---",
                      $bresult->{'score'}, "---";
	}
    }

    # Display the overall score.
    my $title = $testCats->{$cat}{'name'} . " Index Score";
    if (!$full) {
        $title .= " (Partial Only)";
    }
    printf $outFd "%-40s %12s %12s %8s\n", "", "", "", "========";
    printf $outFd "%-66s %8.1f\n", $title, $iscore;

    printf $outFd "\n";
}


# Display index scores, if any, for the given run results.
sub logIndex {
    my ( $results, $outFd ) = @_;

    my $count = $results->{'indexed'};
    foreach my $cat (keys(%$count)) {
        logIndexCat($results, $cat, $outFd);
    }
}


# Dump the given run results into the given report file.
sub summarizeRun {
    my ( $systemInfo, $results, $verbose, $reportFd ) = @_;

    # Display information about this test run.
    printf $reportFd "------------------------------------------------------------------------\n";
    printf $reportFd "Benchmark Run: %s %s - %s\n",
           strftime("%a %b %d %Y", localtime($results->{'start'})),
           strftime("%H:%M:%S", localtime($results->{'start'})),
           strftime("%H:%M:%S", localtime($results->{'end'}));
    printf $reportFd "%s in system; running %s of tests\n",
           number($systemInfo->{'numCpus'}, "CPU"),
           number($results->{'copies'}, "parallel copy", "parallel copies");
    printf $reportFd "\n";

    # Display the run scores.
    logResults($results, $reportFd);

    # Display the indexed scores, if any.
    logIndex($results, $reportFd);
}


############################################################################
# HTML REPORTS
############################################################################

# Dump the given run results into the given report file.
sub runHeaderHtml {
    my ( $systemInfo, $reportFd ) = @_;

    # Display information about this test run.
    my $title = sprintf "Benchmark of %s / %s on %s",
                     $systemInfo->{'name'}, $systemInfo->{'system'},
                     strftime("%a %b %d %Y", localtime());

    print $reportFd <<EOF;
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="keywords" content="linux, benchmarks, benchmarking">
  <title>$title</title>
  <style type="text/css">
    table {
      margin: 1em 1em 1em 0;
      background: #f9f9f9;
      border: 1px #aaaaaa solid;
      border-collapse: collapse;
    }

    table th, table td {
      border: 1px #aaaaaa solid;
      padding: 0.2em;
    }

    table th {
      background: #f2f2f2;
      text-align: center;
    }
  </style>
</head>
<body>
EOF

    # Display information about this test run.
    printf $reportFd "<h2>%s</h2>\n", $title;
    printf $reportFd "<p><b>BYTE UNIX Benchmarks (Version %s)</b></p>\n\n",
                     $version;
}


# Display a banner indicating the configuration of the system under test
# to the given file desc.
sub displaySystemHtml {
    my ( $info, $fd ) = @_;

    printf $fd "<h3>Test System Information</h3>\n";
    printf $fd "<p><table>\n";

    # Display basic system info.
    printf $fd "<tr>\n";
    printf $fd "   <td><b>System:</b></td>\n";
    printf $fd "   <td colspan=2>%s: %s</td>\n",
               $info->{'name'}, $info->{'system'};
    printf $fd "</tr><tr>\n";
    printf $fd "   <td><b>OS:</b></td>\n";
    printf $fd "   <td colspan=2>%s -- %s -- %s</td>\n",
               $info->{'os'}, $info->{'osRel'}, $info->{'osVer'};
    printf $fd "</tr><tr>\n";
    printf $fd "   <td><b>Machine:</b></td>\n";
    if (!$ENV{MINIX}) {
        printf $fd "   <td colspan=2>%s: %s</td>\n",
                   $info->{'mach'}, $info->{'platform'};
    }
    printf $fd "   <td colspan=2>%s</td>\n",
                   $info->{'mach'};
    if (!$ENV{MINIX}) {
        printf $fd "</tr><tr>\n";
        printf $fd "   <td><b>Language:</b></td>\n";
        printf $fd "   <td colspan=2>%s</td>\n", $info->{'language'};
    }
    printf $fd "</tr>\n";

    # Get and display details on the CPUs, if possible.
    my $cpus = $info->{'cpus'};
    if (!defined($cpus)) {
        printf $fd "<tr>\n";
        printf $fd "   <td><b>CPUs:</b></td>\n";
        printf $fd "   <td colspan=2>no details available</td>\n";
        printf $fd "</tr>\n";
    } else {
        for (my $i = 0; $i <= $#$cpus; ++$i) {
            printf $fd "<tr>\n";
            if ($i == 0) {
                printf $fd "   <td rowspan=%d><b>CPUs:</b></td>\n", $#$cpus + 1;
            }
            printf $fd "   <td><b>%d:</b></td>\n", $i;
            printf $fd "   <td>%s (%.1f bogomips)<br/>\n",
                        $cpus->[$i]{'model'}, $cpus->[$i]{'bogo'};
            printf $fd "       %s</td>\n", $cpus->[$i]{'flags'};
            printf $fd "</tr>\n";
        }
    }

    # Display graphics hardware info.
    # if (!$ENV{MINIX}) {
    #     if ($info->{'graphics'}) {
    #         printf $fd "<tr>\n";
    #         printf $fd "   <td><b>Graphics:</b></td>\n";
    #         printf $fd "   <td colspan=2>%s</td>\n", $info->{'graphics'};
    #         printf $fd "</tr>\n";
    #     }
    # }

    # Display system runlevel, load and usage info.
    printf $fd "<tr>\n";
    printf $fd "   <td><b>Uptime:</b></td>\n";
    if (!$ENV{MINIX}) {
        printf $fd "   <td colspan=2>%s; runlevel %s</td>\n",
                       $info->{'load'}, $info->{'runlevel'};
    }
    printf $fd "   <td colspan=2>%s\n",
                   $info->{'load'};
    printf $fd "</tr>\n";

    printf $fd "</table></p>\n\n";
}


# Display the test scores from the given set of test results
# for a given category of tests.
sub logCatResultsHtml {
    my ( $results, $cat, $fd ) = @_;

    my $numIndex = $results->{'numIndex'}{$cat};
    my $indexed = $results->{'indexed'}{$cat};
    my $iscore = $results->{'index'}{$cat};
    my $full = defined($indexed) && $indexed == $numIndex;

    # If there are no results in this category, just ignore it.
    if (!defined($results->{'numCat'}{$cat}) ||
                            $results->{'numCat'}{$cat} == 0) {
        return;
    }

    # Say the category.  If there are no indexed scores, just say so.
    my $warn = "";
    if (!defined($indexed) || $indexed == 0) {
        $warn = " — no index results available";
    } elsif (!$full) {
        $warn = " — not all index tests were run;" .
                " only a partial index score is available";
    }
    printf $fd "<h4>%s%s</h4>\n", $testCats->{$cat}{'name'}, $warn;

    printf $fd "<p><table width=\"100%%\">\n";

    printf $fd "<tr>\n";
    printf $fd "   <th align=left>Test</th>\n";
    printf $fd "   <th align=right>Score</th>\n";
    printf $fd "   <th align=left>Unit</th>\n";
    printf $fd "   <th align=right>Time</th>\n";
    printf $fd "   <th align=right>Iters.</th>\n";
    printf $fd "   <th align=right>Baseline</th>\n";
    printf $fd "   <th align=right>Index</th>\n";
    printf $fd "</tr>\n";

    # Display the individual test scores.
    foreach my $bench (@{$results->{'list'}}) {
        my $bresult = $results->{$bench};
        next if $bresult->{'cat'} ne $cat;

        printf $fd "<tr>\n";
        printf $fd "   <td><b>%s</b></td>\n", $bresult->{'msg'};
        printf $fd "   <td align=right><tt>%.1f</tt></td>\n",
                   $bresult->{'score'};
        printf $fd "   <td align=left><tt>%s</tt></td>\n",
                   $bresult->{'scorelabel'};
        printf $fd "   <td align=right><tt>%.1f s</tt></td>\n",
                   $bresult->{'time'};
        printf $fd "   <td align=right><tt>%d</tt></td>\n",
                   $bresult->{'iterations'};

        if (defined($bresult->{'index'})) {
            printf $fd "   <td align=right><tt>%.1f</tt></td>\n",
                       $bresult->{'iscore'};
            printf $fd "   <td align=right><tt>%.1f</tt></td>\n",
                       $bresult->{'index'};
        }
        printf $fd "</tr>\n";
    }

    # Display the overall score.
    if (defined($indexed) && $indexed > 0) {
        my $title = $testCats->{$cat}{'name'} . " Index Score";
        if (!$full) {
            $title .= " (Partial Only)";
        }
        printf $fd "<tr>\n";
        printf $fd "   <td colspan=6><b>%s:</b></td>\n", $title;
        printf $fd "   <td align=right><b><tt>%.1f</tt></b></td>\n", $iscore;
        printf $fd "</tr>\n";
    }

    printf $fd "</table></p>\n\n";
}


# Display index scores, if any, for the given run results.
sub logResultsHtml {
    my ( $results, $fd ) = @_;

    foreach my $cat (keys(%$testCats)) {
        logCatResultsHtml($results, $cat, $fd);
    }
}


# Dump the given run results into the given report file.
sub summarizeRunHtml {
    my ( $systemInfo, $results, $verbose, $reportFd ) = @_;

    # Display information about this test run.
    my $time = $results->{'end'} - $results->{'start'};
    printf $reportFd "<p><hr/></p>\n";
    printf $reportFd "<h3>Benchmark Run: %s; %s</h3>\n",
           number($systemInfo->{'numCpus'}, "CPU"),
           number($results->{'copies'}, "parallel process", "parallel processes");
    printf $reportFd "<p>Time: %s - %s; %dm %02ds</p>\n",
                     strftime("%H:%M:%S", localtime($results->{'start'})),
                     strftime("%H:%M:%S", localtime($results->{'end'})),
                     int($time / 60), $time % 60;
    printf $reportFd "\n";

    # Display the run scores.
    logResultsHtml($results, $reportFd);
}


sub runFooterHtml {
    my ( $reportFd ) = @_;

    print $reportFd <<EOF;
<p><hr/></p>
<div><b>No Warranties:</b> This information is provided free of charge and "as
is" without any warranty, condition, or representation of any kind,
either express or implied, including but not limited to, any warranty
respecting non-infringement, and the implied warranties of conditions
of merchantability and fitness for a particular purpose. All logos or
trademarks on this site are the property of their respective owner. In
no event shall the author be liable for any
direct, indirect, special, incidental, consequential or other damages
howsoever caused whether arising in contract, tort, or otherwise,
arising out of or in connection with the use or performance of the
information contained on this web site.</div>
</body>
</html>
EOF
}


############################################################################
# MAIN
############################################################################

sub main {
    my @args = @_;

    my $params = parseArgs(@args);
    my $verbose = $params->{'verbose'} || 1;
    if ($params->{'iterations'}) {
        $longIterCount = $params->{'iterations'};
        $shortIterCount = int(($params->{'iterations'} + 1) / 3);
        $shortIterCount = 1 if ($shortIterCount < 1);
    }

    # If no benchmark units have be specified, do "index".
    my $tests = $params->{'tests'};
    if ($#$tests < 0) {
        $tests = $index;
    }

    preChecks();
    my $systemInfo = getSystemInfo();

    # If the number of copies to run was not set, set it to 1
    # and the number of CPUs in the system (if > 1).
    my $copies = $params->{'copies'};
    if (!$copies || scalar(@$copies) == 0) {
        push(@$copies, 1);
        if (defined($systemInfo->{'numCpus'}) && $systemInfo->{'numCpus'} > 1) {
            push(@$copies, $systemInfo->{'numCpus'});
        }
    }

    # Display the program banner.
    system("cat \"${BINDIR}/unixbench.logo\"");

    if ($verbose > 1) {
        printf "\n", join(", ", @$tests);
        printf "Tests to run: %s\n", join(", ", @$tests);
    }

    # Generate unique file names for the report and log file.
    my $reportFile = logFile($systemInfo);
    my $reportHtml = $reportFile . ".html";
    my $logFile = $reportFile . ".log";

    # Open the log file for writing.
    open(my $reportFd, ">", $reportFile) ||
                            die("Run: can't write to $reportFile\n");
    open(my $reportFd2, ">", $reportHtml) ||
                            die("Run: can't write to $reportHtml\n");
    printf $reportFd "   BYTE UNIX Benchmarks (Version %s)\n\n", $version;
    runHeaderHtml($systemInfo, $reportFd2);

    # Dump information about the system under test.
    displaySystem($systemInfo, $reportFd);
    displaySystemHtml($systemInfo, $reportFd2);

    # Run the tests!  Do a test run once for each desired number of copies;
    # for example, on a 2-CPU system, we may do a single-processing run
    # followed by a dual-processing run.
    foreach my $c (@$copies) {
        if ($verbose > 1) {
            printf "Run with %s\n", number($c, "copy", "copies");
        }
        my $results = runTests($tests, $verbose, $logFile, $c);

        summarizeRun($systemInfo, $results, $verbose, $reportFd);
        summarizeRunHtml($systemInfo, $results, $verbose, $reportFd2);
    }

    runFooterHtml($reportFd2);

    # Finish the report.
    close($reportFd);
    close($reportFd2);

    # Display the report, if not in quiet mode.
    if ($verbose > 0) {
        printf "\n";
        printf  "========================================================================\n";
        system("cat \"$reportFile\"");
    }

    0;
}


exit(main(@ARGV));

