#!/usr/bin/perl
#########################################################
#  multitest, by Marcus Sorensen, BetterServers Inc     #
#  modified by David Collins and Robert LeBlanc, EIG    #
#  Licensed under the Open Software License version 3.0 #
#  http://opensource.org/licenses/OSL-3.0               #
#########################################################
use strict;
use Data::Dumper;

$| = 1;
my $colors = { red => "\e[1;31m", def => "\e[0m", green => "\e[1;32m", cyan => "\e[1;36m" };
my $restbetweentests = 15;
my $testtime = 300;   #seconds
my $testsize = "12500MB";
my $testjobs = 8;
my $testiodepth = 8;
my $testname = "multiiotester";
my %final_out;

unless ( `which fio 2>/dev/null`) {
  print "No executable 'fio' found in path, exiting\n";
  exit;
}

print <<EOF;
$colors->{red}
Multiple IO Tester$colors->{def}

  This application emulates a busy server in several states by launching multiple
threads that do various types of IO. This allows us to see what the consequences
are of running in a multitasking environment. This test uses direct IO and 
invalidates caches between tests, testing the disk, not the memory.

$colors->{red}NOTE:$colors->{def} You need at least 100GB of free space in your current working directory.

The following tests currently consist of:

  8 sequential readers
  8 sequential writers
  8 mixed seqential readers/writers (random choice per IO)
  8 random readers
  8 random writers
  8 mixed random readers/writers (random choice per IO)
  A real work simulation of varied read/write requests of various sizes weighted to smaller I/O and 65% read 35% write.

Feel free to modify the script to meet your needs. Enjoy!

The test should take less than 3 hours. Press <ENTER> to begin...
EOF
<STDIN>;

my $tests = { 'read-1024k'      => { 'order' => 1, 
                               'block' => '1024k', 
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'sequential read' }, 
              'write-1024k'     => { 'order' => 2, 
                               'block' => '1024k', 
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'sequential write' }, 
              'rw-1024k'        => { 'order' => 3, 
                               'block' => '1024k', 
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'seq read/seq write' },

              'read-256k'      => { 'order' => 1,
                               'block' => '256k',
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'sequential read' },
              'write-256k'     => { 'order' => 2,
                               'block' => '256k',
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'sequential write' },
              'rw-256k'        => { 'order' => 3,
                               'block' => '256k',
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'seq read/seq write' },


              'read-64k'      => { 'order' => 1,
                               'block' => '64k',
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'sequential read' },
              'write-64k'     => { 'order' => 2,
                               'block' => '64k',
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'sequential write' },
              'rw-64k'        => { 'order' => 3,
                               'block' => '64k',
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'seq read/seq write' },



              'read-16k'      => { 'order' => 1,
                               'block' => '16k',
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'sequential read' },
              'write-16k'     => { 'order' => 2,
                               'block' => '16k',
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'sequential write' },
              'rw-16k'        => { 'order' => 3,
                               'block' => '16k',
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'seq read/seq write' },



              'read-4k'      => { 'order' => 1,
                               'block' => '4k',
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'sequential read' },
              'write-4k'     => { 'order' => 2,
                               'block' => '4k',
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'sequential write' },
              'rw-4k'        => { 'order' => 3,
                               'block' => '4k',
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'seq read/seq write' },




              'randread-4k'  => { 'order' => 4, 
                               'block' => '4k', 
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'random read' }, 
              'randwrite-4k' => { 'order' => 5, 
                               'block' => '4k', 
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'random write' } , 
              'randrw-4k'    => { 'order' => 6, 
                               'block' => '4k', 
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'rand read/rand write' },


              'randread-16k'  => { 'order' => 4, 
                               'block' => '16k', 
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'random read' }, 
              'randwrite-16k' => { 'order' => 5, 
                               'block' => '16k', 
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'random write' } , 
              'randrw-16k'    => { 'order' => 6, 
                               'block' => '16k', 
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'rand read/rand write' },


              'randread-64k'  => { 'order' => 4, 
                               'block' => '64k', 
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'random read' }, 
              'randwrite-64k' => { 'order' => 5, 
                               'block' => '64k', 
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'random write' } , 
              'randrw-64k'    => { 'order' => 6, 
                               'block' => '64k', 
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'rand read/rand write' },



              'randread-256k'  => { 'order' => 4,
                               'block' => '256k',
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'random read' },
              'randwrite-256k' => { 'order' => 5,
                               'block' => '256k',
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'random write' } ,
              'randrw-256k'    => { 'order' => 6,
                               'block' => '256k',
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'rand read/rand write' },



              'randread-1024k'  => { 'order' => 4,
                               'block' => '1024k',
                               'output' => { 'multiiotester'=>'4', '2'=>'5', '3'=>'6' },
                               'name' => 'random read' },
              'randwrite-1024k' => { 'order' => 5,
                               'block' => '1024k',
                               'output' => { 'multiiotester'=>'20', '2'=>'25', '3'=>'47' },
                               'name' => 'random write' } ,
              'randrw-1024k'    => { 'order' => 6,
                               'block' => '1024k',
                               'output' => { 'multiiotester'=>'4,20', '2'=>'5,25', '3'=>'6,47' },
                               'name' => 'rand read/rand write' },



            };

mkdir('./multiiotester') if ! -d './multiiotester';
chdir('./multiiotester') or die "unable to chdir to test directory: $^E";


foreach my $t ( sort{$tests->{$a}->{order} cmp $tests->{$b}->{order}} keys %{$tests} ) {
  print "$colors->{cyan} running IO \"$tests->{$t}->{name} ($t)\" test... $colors->{def}\n";


	# Enable 'next' for testing
	if ( $t !~ /^read\-\d{2}k/ ) {
		#next;
	}

	my $testtype = $t;
	$testtype =~ s/\-.+//;

  my $cmd = "fio --direct=1 --invalidate=1 --ioengine=libaio --iodepth=$testiodepth --thread --time_based --runtime=$testtime --rw=$testtype --bs=$tests->{$t}->{block} --size=$testsize --numjobs=$testjobs --name=$testname --minimal | grep ';'";
  my @output = `$cmd`;
  $output[0] =~ /^(.*?);/;
  my $version = $1;
  my $data;
  my $iop_data;
  
  foreach my $d (@output){
    next unless $d =~ /;/;
    my $field = $tests->{$t}->{output}->{$version};
    my @items = split(";",$d);
    if ($field =~ /(\d+),(\d+)/) {
      $data .= "$items[$1];$items[$2]\n";
			$iop_data .= "$items[$1+1];$items[$2+1]\n";
    } else {
      $data .= "$items[$field]\n";
      $iop_data .= "$items[$field+1]\n";
    }
  }

  my @results = split(/;/,combinejobs($data));
	my @iops = split(/;/,combinejobs($iop_data));

	print "\tresult is $colors->{green}" . join("$colors->{def}/$colors->{green}", map { convert($_) } @results) . "$colors->{def} per second\n";
  print "\tequals $colors->{green}" . join("$colors->{def}/$colors->{green}", @iops) . "$colors->{def} IOs per second\n\n";
  $final_out{$t}{'iops'} = \@iops;
  $final_out{$t}{'rate'} = \@results;


  sleep $restbetweentests;
}

print "$colors->{cyan} running IO \"Real World Test (real)\" test... $colors->{def}\n";

my $cmd = "fio --name $testname --rw randrw --bssplit 4k/85:32k/11:512/3:1m/1,4k/89:32k/10:512k/1 --ioengine libaio --iodepth $testiodepth --numjobs $testjobs --direct 1 --rwmixread 72 --norandommap --minimal --size=$testsize --runtime=$testtime --time_based --thread | grep ';'";
my @output = `$cmd`;
$output[0] =~ /^(.*?);/;
my $version = $1;
my $data;
my $iop_data;

foreach my $d (@output){
  next unless $d =~ /;/;
  my $field = '6,47';
  my $iop_field = '7,48';
  my @items = split(";",$d);
  if ($field =~ /(\d+),(\d+)/) {
    $data .= "$items[$1];$items[$2]\n";
  } else {
    $data .= "$items[$field]\n";
  }
  if ($iop_field =~ /(\d+),(\d+)/) {
    $iop_data .= "$items[$1];$items[$2]\n";
  } else {
    $iop_data .= "$items[$field]\n";
  }
}

my @results = split(/;/,combinejobs($data));
my @iops = split(/;/,combinejobs($iop_data));

print "\tresult is $colors->{green}" . join("$colors->{def}/$colors->{green}", map { convert($_) } @results) . "$colors->{def} per second\n";
print "\tequals $colors->{green}" . join("$colors->{def}/$colors->{green}", @iops) . "$colors->{def} IOs per second\n\n";
$final_out{'real'}{'iops'} = \@iops;
$final_out{'real'}{'rate'} = \@results;


#print "cleaning up files..\n";

#unlink glob "multiiotester*";
#chdir("..");
#rmdir("multiiotester") or print "unable to delete directory 'multiiotester'\n";

###########################
####### subroutines #######
###########################

sub convert {
  my $val = shift;
  my @units = ('KB','MB','GB');
  my $i = 0;

  $val =~ /^\d+/;
  while (length($&) > 3 ) {
    $val = sprintf("%.2f",$val / 1024);
    $i++;
    $val =~ /^\d+/;
  }
  return $val . $units[$i];
}

#sub toiops {
#   my $val = shift;
#   my $blocksize = shift;
# 
#   $blocksize =~ s/k//;
#   my $io = sprintf("%.1f",$val/$blocksize);
#  
#   return $io;
# }

sub combinejobs {
  my $input = shift;
  
  my @lines = split(/\n/,$input);
  my @output = ();

  foreach my $l (0..$#lines) {
    my @temp = split(/;/,$lines[$l]);
    foreach my $t (0..$#temp){
      $output[$t] += $temp[$t];
    }
  }

  return join(";",@output);
}


print "\n\n\n#########################################\n\n";
my $header;
my $csv;
for my $test ( sort keys %final_out ) {
	if ( scalar @{$final_out{$test}{'rate'}} == 1 ) {
		$header .= "$test rate,$test IOPs,";
		$csv .= "@{$final_out{$test}{'rate'}}[0],@{$final_out{$test}{'iops'}}[0],"
	} else {
		$header .= "$test read rate,$test read IOPs,$test write rate,$test write IOPs,";
		$csv .= "@{$final_out{$test}{'rate'}}[0],@{$final_out{$test}{'iops'}}[0],@{$final_out{$test}{'rate'}}[1],@{$final_out{$test}{'iops'}}[1],";
	}
}
chop($header);
chop($csv);
print "$header\n";
print "$csv\n";

print "\n\n#########################################\n\n";
