#!/usr/bin/perl -w
# $Id: dmpcntanal 1057 2018-10-19 15:06:42Z mueller $
#
# Copyright 2018- by Walter F.J. Mueller <W.F.J.Mueller@gsi.de>
#
# This program is free software; you may redistribute and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY, without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for complete details.
#
#  Revision History:
# Date         Rev Version  Comment
# 2018-10-19  1057   1.0    Initial version
# 2018-10-06  1053   0.1    First draft
#

use 5.14.0;                                 # require Perl 5.14 or higher
use strict;                                 # require strict checking

use Getopt::Long;
use Scalar::Util qw(looks_like_number);

my %opts = ();

GetOptions(\%opts, "cstate", "cevent", "clk", "cpi", "cpb",
           "cache", "chit", "ibus", "rbus",
           "raw=s@", "dat=s@", "fmt=s@", "sum=s@", "tot",
           "cpu", "all", "hpl=i",
           "help")
  or die "bad options";

my %cnam2ind = (cpu_cpbusy   =>  0,
                cpu_km_prix  =>  1,
                cpu_km_pri0  =>  2,
                cpu_km_wait  =>  3,
                cpu_sm       =>  4,
                cpu_um       =>  5,
                cpu_idec     =>  6,
                cpu_pcload   =>  7,
                cpu_vfetch   =>  8,
                cpu_irupt    =>  9,
                ca_rd        => 10,
                ca_wr        => 11,
                ca_rdhit     => 12,
                ca_wrhit     => 13,
                ca_rdmem     => 14,
                ca_wrmem     => 15,
                ca_rdwait    => 16,
                ca_wrwait    => 17,
                ib_rd        => 18,
                ib_wr        => 19,
                ib_busy      => 20,
                rb_rd        => 21,
                rb_wr        => 22,
                rb_busy      => 23,
                ext_rdrhit   => 24,
                ext_wrrhit   => 25,
                ext_wrflush  => 26,
                ext_rlrxact  => 27,
                ext_rlrxback => 28,
                ext_rltxact  => 29,
                ext_rltxback => 20,
                ext_usec     => 31,
                # calculated values
                cpu_tot      => -1,         # total cpu cyles
                cpu_act      => -2,         # active cpu cycles (no cp + wait)
                ca_totwt     => -3,         # total cache wait cycles
                ib_tot       => -4,         # total ibus requests
                rb_tot       => -5          # total rbus requests
               );

my $sumdt = 0.4;

my @rawnam;
my @datnam;
my @fmtdsc;
my @sumdsc;

my %hraw;
my %hdif;

autoflush STDOUT 1 if (-p STDOUT);          # autoflush if output into pipe

if (exists $opts{help}) {
  print_help();
  exit 0;
}

# process -raw and -dat options
foreach my $opt (@{$opts{raw}}) { push @rawnam, split /,/,$opt; }
foreach my $opt (@{$opts{dat}}) { push @datnam, split /,/,$opt; }

# process -fmt options
foreach my $opt (@{$opts{fmt}}) {
  my @olist = split /,/,$opt;
  if (scalar(@olist) < 4 || scalar(@olist) > 5) {
    print STDERR "dmpcntanal-E: bad -fmt option '$opt'\n";
    exit 1;
  }
  my $head   = $olist[0];
  my $fmt    = $olist[1];
  my $fac    = $olist[2];
  my $namnum = $olist[3];
  my $namden = $olist[4];
  $fac = 1.    unless $fac ne '';
  $fmt = '4.1' unless $fmt ne '';
  my ($fmtlen,$fmtpre) = ($fmt =~ m/^(\d)\.(\d)$/);
  unless (defined $fmtlen && defined $fmtpre) {
    print STDERR "dmpcntanal-E: bad format '$fmt' in -fmt\n";
    exit 1;
  }
  unless (length($head) <= $fmtlen) {
    print STDERR "dmpcntanal-E: header '$head' longer than $fmtlen in -fmt\n";
    exit 1;
  }
  chknum($fac, '-fmt');
  chknam($namnum, '-fmt');
  chknam($namden, '-fmt') if defined $namden;
  push @fmtdsc, {head   => $head,
                 fmt    => '%' . $fmt .'f',
                 fmtlen => $fmtlen,
                 fmtpre => $fmtpre,
                 fac    => $fac,
                 namnum => $namnum,
                 namden => $namden
                };
}

# process -tot option
push @{$opts{sum}}, 'total' if $opts{tot};

# process -sum options
foreach my $opt (@{$opts{sum}}) {
  my @olist = split /,/,$opt;
  if (scalar(@olist) < 1 || scalar(@olist) > 3) {
    print STDERR "dmpcntanal-E: bad -sum option '$opt'\n";
    exit 1;
  } else {
    my $name   = $olist[0];
    my $from   = $olist[1];
    my $to     = $olist[2];
    unless (length($name) <= 6) {
      print STDERR "dmpcntanal-E: name '$name' longer than 6 in -sum\n";
      exit 1;
    }
    $from =   0. unless defined $from && $from ne '';
    $to   = 1.e8 unless defined $to   && $to   ne '';;
    chknum($from, '-sum');
    chknum($to,   '-to');
    if ($to <= $from) {
      print STDERR "dmpcntanal-E: negative time interval [$from,$to] -sum\n";
      exit 1;
    }
    push @sumdsc, {name   => $name,
                   from   => $from,
                   to     => $to,
                   count  => 0,
                   hdif   => {}
                  };
  }
}

foreach my $nam (@rawnam) { chknam($nam, '-raw') }
foreach my $nam (@datnam) { chknam($nam, '-dat') }

# process -cache and -chit options
delete $opts{chit} if $opts{cache};         # hide -chit if -cache given

# default to -cpu if no field specified
unless ($opts{cstate} || $opts{cevent} ||
        $opts{clk} || $opts{cpi} || $opts{cpb} ||
        $opts{cache} || $opts{chit} || $opts{ibus} || $opts{rbus} ||
        $opts{cpu} || $opts{all} ||
        scalar(@rawnam) || scalar(@datnam) || scalar(@fmtdsc)) {
  $opts{cpu} = 1;
}

# process -cpu
if ($opts{cpu}) {
  $opts{cstate} = 1;
  $opts{cevent} = 1;
  $opts{chit} = 1;
}
# process -all
if ($opts{all}) {
  $opts{cstate} = 1;
  $opts{cevent} = 1;
  $opts{cache} = 1;
  $opts{ibus} = 1;
  $opts{rbus} = 1;
}

# and finally process data
foreach my $file (@ARGV) {
  read_file($file);
}

#-------------------------------------------------------------------------------

sub read_file {
  my ($file) = @_;

  # print headers
  prthead();

  # open file
  open IFILE,"<$file" or die "failed to open $file";

  my @vraw  = (0) x 32;
  my @vdif  = (0) x 32;
  my @vlast = (0) x 32;
  my $nline = 0;

  # loop over lines in file
  while (<IFILE>) {
    chomp;
    next if m/^#/;
    if (m/^\s*(\d+\.\d+)\s+((\s*\d+){32})$/x) {
      # prepare data as array
      my $time = $1;
      my @vraw = split /\s+/,$2;
      for (my $i=0; $i<32; $i++) {
        my $dif = $vraw[$i] - $vlast[$i];
        $dif += 4.*1024.*1024.*1024. if ($dif < 0);
        $vdif[$i]  = $dif;
        $vlast[$i] = $vraw[$i];
      }

      # prepare data as hash
      foreach my $nam (keys %cnam2ind) {
        next if $cnam2ind{$nam} < 0;
        $hraw{$nam} = $vraw[$cnam2ind{$nam}];
        $hdif{$nam} = $vdif[$cnam2ind{$nam}];
      }
      $hdif{cpu_tot} = $hdif{cpu_cpbusy} + $hdif{cpu_km_prix} +
                       $hdif{cpu_km_pri0} + $hdif{cpu_km_wait} +
                       $hdif{cpu_sm} + $hdif{cpu_um};
      $hdif{cpu_act} = $hdif{cpu_km_prix} + $hdif{cpu_km_pri0} +
                       $hdif{cpu_sm} + $hdif{cpu_um};
      $hdif{ca_totwt} = $hdif{ca_rdwait} + $hdif{ca_wrwait};
      $hdif{ib_tot} = $hdif{ib_rd} + $hdif{ib_wr};
      $hdif{rb_tot} = $hdif{rb_rd} + $hdif{rb_wr};

      # accululation of (sub-)totals
      foreach my $dsc (@sumdsc) {
        if ($time >= $dsc->{from}-$sumdt && $time <= $dsc->{to}+$sumdt) {
          $dsc->{count} += 1;
          foreach my $k (keys %hdif) { $dsc->{hdif}->{$k} += $hdif{$k} }
        }
      }
      # do printing
      $nline += 1;
      if ($opts{hpl} && $opts{hpl} > 0 && $nline > $opts{hpl}) {
        print "\n";
        prthead();
        $nline = 0;
      }
      prtline(\%hraw, \%hdif, (sprintf "%6.1f", $time));

    } else {
      printf STDERR "bad line: $_\n";
    }
  }

  # print (sub-)totals
  if (scalar @sumdsc) {
    print "\n";
    prthead();
    foreach my $dsc (@sumdsc) {
      if ($dsc->{count} > 0) {
        foreach my $k (keys %hdif) { $dsc->{hdif}->{$k} /= $dsc->{count} }
        prtline(undef, $dsc->{hdif}, $dsc->{name});
      } else {
        printf "%6s  ... empty ... no data in [%4.1f,%4.1f]\n",
          $dsc->{name}, $dsc->{from}-$sumdt, $dsc->{to}+$sumdt;
      }
    }
  }

  close IFILE;

}


#-------------------------------------------------------------------------------

sub prtline {
  my ($rhraw,$rhdif,$tcol) = @_;

  printf "%6s", $tcol;
  if (defined $rhraw) {
    foreach my $nam (@rawnam) { printf " %10.0f", $rhraw->{$nam}; }
  } else {
    foreach my $nam (@rawnam) { printf ' ' x 10 . '-'; }
  }
  foreach my $nam (@datnam) { printf " %9.0f", $rhdif->{$nam}; }

  if ($opts{cstate}) {
    print ' ';
    prtrat($rhdif,'%4.1f',100.,'cpu_cpbusy','cpu_tot');
    prtrat($rhdif,'%4.1f',100.,'cpu_km_prix','cpu_tot');
    prtrat($rhdif,'%4.1f',100.,'cpu_km_pri0','cpu_tot');
    prtrat($rhdif,'%4.1f',100.,'cpu_km_wait','cpu_tot');
    prtrat($rhdif,'%4.1f',100.,'cpu_sm','cpu_tot');
    prtrat($rhdif,'%4.1f',100.,'cpu_um','cpu_tot');
  }
  if ($opts{clk}) {
    prtval($rhdif,'%5.1f',1.e-6,'cpu_tot');
  }
  if ($opts{cevent}) {
    print ' ';
    prtval($rhdif,'%5.2f',1.e-6,'cpu_idec');
    prtval($rhdif,'%5.0f',1.,'cpu_vfetch');
    prtval($rhdif,'%5.0f',1.,'cpu_irupt');
    prtrat($rhdif,'%4.1f',1.,'cpu_idec','cpu_pcload');
  }
  if ($opts{cpi}) {
    prtrat($rhdif,'%4.1f',1.,'cpu_act','cpu_idec');
  }
  if ($opts{cpb}) {
    prtrat($rhdif,'%4.1f',1.,'cpu_act','cpu_pcload');
  }

  if ($opts{cache}) {
    print ' ';
    prtrat($rhdif,'%4.2f',1.,'ca_rd','cpu_idec');
    prtrat($rhdif,'%4.2f',1.,'ca_wr','cpu_idec');
    prtrat($rhdif,'%4.1f',100.,'ca_rdhit','ca_rd');
    prtrat($rhdif,'%4.1f',100.,'ca_wrhit','ca_wr');
    prtrat($rhdif,'%4.1f',1.,'ca_rdwait','ca_rdmem');
    prtrat($rhdif,'%4.1f',1.,'ca_wrwait','ca_wrmem');
    prtrat($rhdif,'%4.1f',1.,'ca_totwt','cpu_idec');
  }
  if ($opts{chit}) {
    print ' ';
    prtrat($rhdif,'%4.1f',100.,'ca_rdhit','ca_rd');
    prtrat($rhdif,'%4.1f',100.,'ca_wrhit','ca_wr');
    prtrat($rhdif,'%4.1f',1.,'ca_totwt','cpu_idec');
  }

  if ($opts{ibus}) {
    print ' ';
    prtval($rhdif,'%4.2f',1.e-3,'ib_rd');
    prtval($rhdif,'%4.2f',1.e-3,'ib_wr');
    prtrat($rhdif,'%4.1f',1.,'ib_busy','ib_tot');
  }
  if ($opts{rbus}) {
    print ' ';
    prtval($rhdif,'%4.2f',1.e-3,'rb_rd');
    prtval($rhdif,'%4.2f',1.e-3,'rb_wr');
    prtrat($rhdif,'%4.1f',1.,'rb_busy','rb_tot');
  }

  foreach my $dsc (@fmtdsc) {
    if (defined $dsc->{namden}) {
      prtrat($rhdif, $dsc->{fmt}, $dsc->{fac}, $dsc->{namnum}, $dsc->{namden});
    } else {
      prtval($rhdif, $dsc->{fmt}, $dsc->{fac}, $dsc->{namnum});
    }
  }
  printf "\n";
}

#-------------------------------------------------------------------------------

sub prthead {
  # print header line 1
  printf '  time';
  foreach my $nam (@rawnam) { printf " %9s_", (split /_/,$nam,2)[0]; }
  foreach my $nam (@datnam) { printf " %8s_",  (split /_/,$nam,2)[0];}
  print '  ------ cpu state in % -------' if $opts{cstate};
  print ' clock' if $opts{clk};
  print '  ----- cpu events -----' if $opts{cevent};
  print ' ----' if $opts{cpi};
  print ' ----' if $opts{cpb};
  print '  -------------- cache -------------' if $opts{cache};
  print '  ---- cache ---' if $opts{chit};
  print '  ---- ibus ----' if $opts{ibus};
  print '  ---- rbus ----' if $opts{rbus};
  my $fmtcol = 0;
  foreach my $dsc (@fmtdsc) { $fmtcol += $dsc->{fmtlen}+1 }
  if ($fmtcol > 0) {
    my $ndash  = $fmtcol - 6;
    my $nleft  = int($ndash/2);
    my $nright = $ndash - $nleft;
    print ' ' . '-'x$nleft . ' fmt ' . '-'x$nright;
  }
  printf "\n";

  # print header line 2
  printf "   sec";
  foreach my $nam (@rawnam) { printf " %10s", (split /_/,$nam,2)[1]; }
  foreach my $nam (@datnam) { printf " %9s", (split /_/,$nam,2)[1]; }
  print '    cp km>0 km=0 wait   sm   um' if $opts{cstate};
  print '   MHz' if $opts{clk};
  print '  Min/s vfetc irupt  i/b' if $opts{cevent};
  print '  c/i' if $opts{cpi};
  print '  c/b' if $opts{cpb};
  print '   r/i  w/i rhit whit  rwt  wwt wt/i' if $opts{cache};
  print '  rhit whit wt/i' if $opts{chit};
  print '  rdkH wrkH wt/r' if $opts{ibus};
  print '  rdkH wrkH wt/r' if $opts{rbus};
  foreach my $dsc (@fmtdsc) { printf " %$dsc->{fmtlen}s", $dsc->{head}}
  printf "\n";
}

#-------------------------------------------------------------------------------

sub prtrat {
  my ($rhdif,$fmt,$mul,$namnum,$namden) = @_;
  if (not exists $rhdif->{$namnum} || not exists $rhdif->{$namden}) {
    prtbad($fmt);
  } else {
    my $rat = 0.;
    $rat = $rhdif->{$namnum} / $rhdif->{$namden} if $rhdif->{$namden} > 0.;
    prtfmt($fmt, $rat*$mul);
  }
}

#-------------------------------------------------------------------------------

sub prtval {
  my ($rhdif,$fmt,$mul,$nam) = @_;
  if (not exists $rhdif->{$nam}) {
    prtbad($fmt);
  } else {
    prtfmt($fmt, $rhdif->{$nam}*$mul);
  }
}

#-------------------------------------------------------------------------------

sub prtfmt {
  my ($fmt,$val) = @_;
  my ($len,$prec) = ($fmt =~ m/(\d)\.(\d)/);
  my $str = sprintf $fmt, $val;
  if (($prec == 0 && length($str) > $len) ||
      ($prec > 0  && length($str) > $len+$prec+1)) {
    print ' ' . '*' x $len;
    return
  }
  $str = substr($str,0,$len) if length($str) > $len;
  print ' ' . $str;
}

#-------------------------------------------------------------------------------

sub prtbad {
  my ($fmt) = @_;
  my ($len,$prec) = ($fmt =~ m/(\d)\.(\d)/);
  print ' ' x $len . '-';
}

#-------------------------------------------------------------------------------

sub chknam {
  my ($nam,$cmd) = @_;
  return if exists $cnam2ind{$nam};
  print STDERR "dmpcntanal-E: unknown counter name '$nam' in $cmd\n";
  exit 1;
}

#-------------------------------------------------------------------------------

sub chknum {
  my ($num,$cmd) = @_;
  return if looks_like_number($num);
  print STDERR "dmpcntanal-E: bad factor '$num' in  $cmd\n";
  exit 1;
}

#-------------------------------------------------------------------------------

sub print_help {
  print "usage: dmpcntanal file\n";
  print "  --cstate          show cpu state colums\n";
  print "  --cevent          show cpu events colums\n";
  print "  --clk             show cpu clock column\n";
  print "  --cpi             show cpu cycle/instruction column\n";
  print "  --cpb             show cpu cycle/branch column\n";
  print "  --cache           show all cache colums\n";
  print "  --chit            show cache hit+wait colums (subset of --cache)\n";
  print "  --ibus            show ibus colums\n";
  print "  --rbus            show rbus colums\n";
  print "  --raw=c[,c,..]    show raw data of counter c\n";
  print "  --dat=c[,c,..]    show rate data of counter c\n";
  print "  --fmt=h,f,m,n[,d] add custom colum with\n";
  print "                      h  header text\n";
  print "                      f  format as len.pre (default 4.1)\n";
  print "                      m  multiplier (default 1.)\n";
  print "                      n  numerator counter name\n";
  print "                      d  denominator counter name (optional)\n";
  print "  --sum=n[,f[,t]]   add sub-total line, with\n";
  print "                      n  name printed in time column\n";
  print "                      f  from, begin of interval (optional)\n";
  print "                      t  to, end of interval (optional)\n";
  print "  --tot             add total line (short for -sum=total)\n";
  print "  --cpu             short for -cstate -cevent -chit (default view)\n";
  print "  --all             short for -cstate -cevent -cache -ibus -rbus\n";
  print "  --hpl=n           print header after n lines\n";
  print "  --help            this message\n";
}
