#!/usr/bin/perl

use strict;
use warnings;

use lib $ENV{'pbench_lib_dir'};
use lib $ENV{'pbench_bspp_dir'};
no lib ".";
use GenData qw(gen_data);
use BenchPostprocess qw(
	calc_aggregate_metrics calc_efficiency_metrics create_graph_hash
	convert_samples_hash_to_array create_uid get_cpubusy_series get_json
	get_label get_mean_hash);
use File::Basename;
use Data::Dumper;
use List::Util('sum');
use JSON;

sub mean {
	if ( scalar @_ > 0 ) {
		return sum(@_)/@_;
	} else {
		return 0;
	}
}

my $script = basename($0);
my $dir = $ARGV[0];
my $tool_label_pattern = $ARGV[1];
my $tool_group = $ARGV[2];
my $benchmark_version = $ARGV[6];  # NOTE:  Not used (unlike in uperf).
my $benchmark_name = 'fio';
my $line;
my $primary_metric="iops_sec";


my %workload;  # root hash for all data, contains hash refs to
		# %throughput, %latency, %resource, %efficiency
my %parameters;	# a hash of parameter-type:array-of-parameter-values

my @benchmark;	# each array element contains a hash with paramters
		# for this benchmark:
		# block-size: 256
		# test-type: randr

my %resource;	# a hash of resource-type:array-of-resource-values,

my @cpu_busy;	# each array element contains a hash with
		# hostname: hostname or IP
		# role: client, server, host, kvm-host, container-host, etc.
		# timeseries: a hash of timestamp:value key-pairs
# my %efficiency;
my %latency;	# a hash of latency-type:array-of-latency-values,
		# for example $latency{usec[0..1]
my @clat;
my @slat;
my @lat;	# each array element contains a hash with:
		# hostname: <hostname or IP>
		# role: <client or server>
		# timeseries: a hash of timstamp:value elements

my %throughput; # a hash of throughput-type:array-of-throughput-values,
		# for example $throughput{MB_sec[0..1]
# my @MB_sec;	# each array element contains a hash with:
		# hostname: <hostname or IP>
		# role: <client or server>
		# timeseries: a hash of timstamp:value key-pairs
my @iops_sec;	# each array element contains a hash with:
		# hostname: <hostname or IP>
		# role: <client or server>
		# timeseries: a hash of timstamp:value key-pair


# Get the tool data; define a set of tool directories which we want to use to
# report CPU and efficiency metrics; search for tool directories which match
# the $tool_label_pattern.
my $this_tool_dir;
my %tool_ids;
my $tool_group_dir = "$dir/tools-$tool_group";
my $dh;
if (opendir($dh, $tool_group_dir)) {
	foreach $this_tool_dir (sort readdir($dh)) {
		if ($this_tool_dir =~ /^$tool_label_pattern/) {
			my $tool_dir_id = $this_tool_dir;
			$tool_dir_id =~ s/^$tool_label_pattern//;
			$this_tool_dir = $tool_group_dir . "/" . $this_tool_dir;
			$tool_ids{$this_tool_dir} = $tool_dir_id;
		}
	}
	closedir  $dh;
} else {
	print STDERR "$script: could not find any directories in $tool_group_dir which matched $tool_label_pattern\n";
}

foreach $this_tool_dir (sort keys %tool_ids) {
	my $this_tool_id = $tool_ids{$this_tool_dir};
	my $role = "host";
	my $hostname;
	($role, $hostname) = split(/:/,$this_tool_id);
	my %cpubusy_samples;
	my $res = get_cpubusy_series($this_tool_dir, \%cpubusy_samples );
	if ( $res == 0 ) {
		my %this_cpubusy_dataset;
		my $description = "Total busy CPU usage, averaged over N seconds, where 1.0 = 1 logical CPU";
		my $mean = get_mean_hash(\%cpubusy_samples);
		push(@cpu_busy, {
			get_label('uid_label')         => create_uid('hostname_label'),
			get_label('description_label') => $description,
			get_label('role_label')        => $role,
			get_label('hostname_label')    => $hostname,
			get_label('value_label')       => $mean,
			get_label('timeseries_label')  => \%cpubusy_samples
		});
	}
}
if ( @cpu_busy ) {
	$resource{'cpu_busy'} = \@cpu_busy;
}

# get the fio benchmark parameters from the fio-result.txt (which is mostly json data)
my %benchmark_params;
my $fio_results_path = $dir . "/fio-result.txt";
die "$script: $fio_results_path does not exist!\n" if ! -f $fio_results_path;
die "$script: $fio_results_path is empty!\n" if -z $fio_results_path;
my $fio_results_scalar = get_json($fio_results_path);
if ($fio_results_scalar == 0) {
	die "$script: Failed to read valid JSON from $fio_results_path!\n";
}
my %fio_results = %$fio_results_scalar;

# There are two areas to get benchmark parameters for fio:
#  - global options:  these are the options usually populated in the top of a
#    fio-job file, or given on command line;
#  - per-job options:  there are the options usually populated in the per-job
#    section of a fio-job file.

for my $param (keys (%{ $fio_results{"global options"} })) {
	if (not $param =~/^write_\w+_log$/) {
		$benchmark_params{$param} = $fio_results{'global options'}{$param};
	}
}

# Because we have to deal with two different modes in which fio may be run,
# (with or without "--clients"), the job-specific options might be in two
# different locations within the fio-provided json.  Once we find the right
# location, we save this reference for processing.  Although there are two
# possible locations for the job options, the format is the same, so we can
# process them with the same code.
my $jobs_stats;
if ($fio_results{"client_stats"}) { # when using --clients
	$jobs_stats = \$fio_results{"client_stats"};
}
if ($fio_results{"jobs"}) {
	$jobs_stats = \$fio_results{"jobs"}; # when not using --clients
}

# The remaining benchmark parameters, if any, come from the fio-job-specific
# options.  The values for these parameters are concatenated with commas into
# a single value and assigned to a single benchmark parameter.  For example,
# "rw=read,read" for 2 jobs.
if (defined $jobs_stats) {
	# First, we build a set of all per-job options we can find across all jobs
	my %all_job_params;
	for my $job (@{ $$jobs_stats }) {
		if (defined $$job{"job options"}) {
			for my $job_param (keys (%{ $$job{"job options"} }) ) {
				$all_job_params{$job_param} = "";
			}
		}
	}
	# Now we can search all jobs for all params, and if some are not present,
	# insert ",," in our ordered list of per-job parameters.
	for my $job (@{ $$jobs_stats }) {
		if (defined $$job{"job options"}) {
			for my $job_param (keys %all_job_params) {
				my $value;
				if (not defined $$job{"job options"}{$job_param}) {
					$value = "";
				} else {
					$value = $$job{"job options"}{$job_param};
				}
				if (not defined $benchmark_params{$job_param}) {
					$benchmark_params{$job_param} = $value;
				} else  {
					$benchmark_params{$job_param} = $benchmark_params{$job_param} . "," . $value;
				}
			}
		}
	}
}

$benchmark_params{get_label('benchmark_name_label')} = $benchmark_name;
$benchmark_params{get_label('primary_metric_label')} = $primary_metric;
$benchmark_params{get_label('uid_label')} = create_uid('benchmark_name_label', 'controller_host_label');
push(@benchmark, \%benchmark_params);

# Load the data from fio output and create throughput & latency metrics.
# There can be several result files, for each client involved in this test
# and for each metric type.

my $nr_log_files=0;
my $clients_dh;
opendir($clients_dh, "$dir/clients") || die "$script: could not open directory $dir/clients: $!\n";
my $client_hostname_dir;
my @client_hostname_dirs = grep(!/^\./, (sort readdir($clients_dh)));
closedir $clients_dh;
foreach $client_hostname_dir (@client_hostname_dirs) {
	# Process the log files (bw, lat, clat, etc)
	my $log_file;
	my $client_dh;
	opendir($client_dh, "$dir/clients/$client_hostname_dir") || die "$script: could not open directory $dir/clients/$client_hostname_dir $!\n";
	my @client_log_files = grep(/^.+\.log$/, (sort readdir($client_dh)));
	closedir $client_dh;
	foreach $log_file (@client_log_files) {
		# for each result file, build this:
		#   {
		#     "hostname": "perf104",  <-where this workload was running
		#     "role": "client",  <-where this information came from: workload client or server
		#     "samples" : {1407890808: 7.75, 1407890809: 7.77}  <-time series data from workload output
		#     "value" : "7.76"  <-the average of the time series data
		#   },
		if ( $log_file =~ /^fio_(iops|lat|slat|clat)\.(\d+)\.log$/) {
			my $log_type = $1;
			my $job_id = $2;
			my $role = "client";
			my %samples;
			my $description_label;
			my $this_metric;
			if ($log_type eq "iops") {
				$description_label = "Number of I/O operations sent by client for a period of 1 second";
				$this_metric = \@iops_sec;
			} elsif ($log_type eq "lat") {
				$description_label = "Average total latency per I/O operation";
				$this_metric = \@lat;
			} elsif ($log_type eq "clat") {
				$description_label = "Average completion latency per I/O operation";
				$this_metric = \@clat;
			} elsif ($log_type eq "slat") {
				$description_label = "Average submission latency per I/O operation";
				$this_metric = \@slat;
			}
			my $date_label = get_label('date_label');
			open(IN_FILE, "$dir/clients/$client_hostname_dir/$log_file") || die "$script: could not open file $dir/clients/$client_hostname_dir/$log_file $!\n";
			while (<IN_FILE>) {
				$line = "$_";
				chomp($line);
				if ( $line =~  /(\d+),\s+(\d+),\s+(\d+),\s+(\d+)/ ) {
					my $timestamp_ms = $1;
					my $value = $2;
					my $rwtype = $3;
					if (exists($samples{$timestamp_ms})) {
						# Behold, autovivification!
						push(@{$samples{$timestamp_ms}{'data'}{$rwtype}{'values'}}, int $value);
					} else {
						$samples{$timestamp_ms} = {
							$date_label => int $timestamp_ms,
							'data' => { $rwtype => { 'values' => [ int $value ] } },
							'type' => $log_type
						};
					}
				}
			}
			close(IN_FILE);
			my $value_label = get_label('value_label');
			foreach my $i (keys %samples) {
				my $sample = $samples{$i};
				$sample->{$value_label} = 0;

				foreach my $key (keys %{$sample->{'data'}}) {
					my $sample_data = $sample->{'data'}{$key};
					if (scalar @{$sample_data->{'values'}} > 1) {
						printf STDERR "[WARNING] %s/%s: timestamp %s for rwtype %d found multiple times, values [%s] will be averaged\n",
							$client_hostname_dir, $log_file, $sample->{$date_label}, $key,
							join(', ', @{$sample_data->{'values'}});
					}
					$sample->{$value_label} += mean(@{$sample_data->{'values'}});
				}

				if ($sample->{'type'} =~ /lat$/) {
					$sample->{$value_label} /= scalar keys %{$sample->{'data'}};
				}

				delete $sample->{'type'};
				delete $sample->{'data'};
			}
			$nr_log_files++;
			if ( scalar %samples ) {
				push(@$this_metric, {
					get_label('role_label')            => $role,
					get_label('client_hostname_label') => $client_hostname_dir . "-" . $job_id,
					get_label('value_label')           => get_mean_hash(\%samples),
					get_label('timeseries_label')      => \%samples,
					get_label('uid_label')             => create_uid('client_hostname_label'),
					get_label('description_label')     => $description_label
				});
			}
		}
	}
	if ($nr_log_files == 0) {
		print STDERR "$script: could not find any result files to process, exiting\n";
		exit;
	}
}
# construct what we have so far in a master workload hash
if ( @iops_sec ) {
	$throughput{'iops_sec'} = \@iops_sec;
}
if ( @slat ) {
	$latency{'slat'} = \@slat;
}
if ( @clat ) {
	$latency{'clat'} = \@clat;
}
if ( @lat ) {
	$latency{'lat'} = \@lat;
}
if ( @benchmark ) {
	$parameters{'benchmark'} = \@benchmark;
}
if ( %throughput ) {
	$workload{'throughput'} = \%throughput;
}
if ( %latency ) {
	$workload{'latency'} = \%latency;
}
if ( %resource ) {
	$workload{'resource'} = \%resource;
}
if ( %parameters ) {
	$workload{'parameters'} = \%parameters;
}

calc_aggregate_metrics(\%workload);
calc_efficiency_metrics(\%workload);

# Pbench graphing uses a specific hash hierarchy, where the dimensions of the hash are:
#
# {html-file}{html-graph}{graph-series}{timeseries-data}
#
# For example, an html page which has all of the iops_sec metrics graphed may look like:
#
# %throughput{'uperf-throughput'}{'iops-per-second'}{client:host1-to-server-host2}{$timestamp_ms}
#                                                   {client:host1-to-server-host3}{$timestamp_ms}
#
# If we wanted another graph for transactions per second graphed in a different
# graph, but on the same html page, we would add:
#
# %throughput{'uperf-throughput'}{'transper-second'}{client:host1-to-server-host2}{$timestamp_ms}
#                                                   {client:host1-to-server-host3}{$timestamp_ms}
#
# So, for fio, we need to take parts of the data in %workload and transform it
# into a new hash.  The %workload hash has much more information about the
# workload than needed for graphing, and the timestamp data is stored in a
# different way.  So, we just need to do a minor conversion, provided by
# BenchPostprocess.pm

my %graph;
create_graph_hash(\%graph, \%workload);
my %graph_threshold;
my %graph_type;
gen_data(\%graph, \%graph_type, \%graph_threshold, $dir, 1);

convert_samples_hash_to_array(\%workload);

# write to JSON format file
my $json_file = $dir . "/result.json";
my $json_text = to_json( \%workload, { ascii => 1, pretty => 1, canonical => 1 } );
open(JSON, ">$json_file") || die "$script: could not open file $json_file: $!\n";
print JSON $json_text;
close(JSON);
