#!/usr/local/bin/perl
#
# sprofalyze.pl
#
# Analyzes the output files created by the profile command for
# Statistical Profiling.
#
# Changes:
#   14 Aug, 2006  Created (Rogier Meurs)
#

# Configuration options:

# Location and parameters of nm program to extract symbol tables
$nm = "/usr/bin/nm -dn";

# Location of src (including trailing /)
	$src_root = qw(
/usr/src/
	);

# Location of system executables within src. Add new servers/drivers here.
	@exes = qw(
kernel/kernel

servers/ds/ds
servers/vfs/vfs
servers/mfs/mfs
servers/inet/inet
servers/is/is
servers/pm/pm
servers/rs/rs
servers/rs/service

drivers/at_wini/at_wini
drivers/bios_wini/bios_wini
drivers/cmos/cmos
drivers/dp8390/dp8390
drivers/dpeth/dpeth
drivers/floppy/floppy
drivers/fxp/fxp
drivers/lance/lance
drivers/log/log
drivers/memory/memory
drivers/pci/pci
drivers/printer/printer
drivers/random/random
drivers/rtl8139/rtl8139
drivers/sb16/sb16_dsp
drivers/sb16/sb16_mixer
drivers/ti1225/ti1225
drivers/tty/tty
	);

# 8< ----------- no user configurable parameters below this line ----------- >8

$SAMPLE_SIZE = 12;
$MINIMUM_PERC = 1.0;


if ($#ARGV < 0 || process_args(@ARGV)) {
  print "Usage:\n";
  print "  sprofalyze.pl [-p percentage] file ...\n\n";
  print "    percentage  print only processes/functions >= percentage\n";
  exit 1;
}

sub process_args {
  $_ = shift;

  if (/^-p$/) {
	$_ = shift;
	return 1 unless /^(\d{1,2})(.\d+)?$/;
	$MINIMUM_PERC = $1 + $2;
  } else {
	unshift @_, $_;
  }

  @files = @_;

  return 1 unless @files > 0;

  return 0;
}

if (read_symbols()) { exit 1; }

print "Showing processes and functions using at least $MINIMUM_PERC% time.\n";

foreach $file (@files) {
  if (process_datafile($file)) { exit 1; }
}                                                    

exit 0;                                              
                   

sub read_symbols
{
  print "Building indexes from symbol tables:";

  for ($i=0; $i<= $#exes; $i++) {
	my $exe = @exes[$i];
	$shortname = $exe;
	$shortname =~ s/^.*\///;
	print " " if $i <= $#exes;
	print $shortname;

	$fullname = $src_root . $exe;

	if ((! -x $fullname) || (! -r $fullname)) {
		print "\nERROR: $fullname does not exist or not readable.\n";
		print "Did you do a make?\n";
		return 1;
	}

	# Create a hash entry for each symbol table (text) entry.
	foreach $_ (`$nm $fullname`) {
		if (/^0{0,7}(\d{0,8})\s[tT]\s(\w{1,8})\n$/) {
			${$shortname."_hash"}{$1} = $2;
		}
	}

	# Create hash entries for every possible pc value. This will pay off.
	@lines = sort { $a <=> $b } keys %{$shortname."_hash"};

	for ($y = 0; $y <= $#lines; $y++) {
		for ($z = @lines->[$y] + 1; $z < @lines->[$y + 1]; $z++) {
			${$shortname."_hash"}{$z} =
				${$shortname."_hash"}{@lines->[$y]}
		}
	}
  }

  # Clock and system are in kernel image but are seperate processes.
  push(@exes, "clock");
  push(@exes, "system");
  %clock_hash = %kernel_hash;
  %system_hash = %kernel_hash;

  print ".\n\n";
  return 0;
}


sub process_datafile
{
  my %res = ();
  my %merged = ();
  my $file = shift;
  my $buf, $pc, $exe, $total_system_perc;

  unless (open(FILE, $file)) {
	print "\nERROR: Unable to open $file: $!\n";
	return 0;
  }

  # First line: check file type.
  $_ = <FILE>; chomp;
  if (!/^stat$/) {
	if (/^call$/) {
		print "Call Profiling output file: ";
		print "Use cprofalyze.pl instead.\n";
	} else {
		print "Not a profiling output file.\n";
	}
	return 0;
  }      

  $file =~ s/^.*\///;

  printf "\n========================================";
  printf "========================================\n";
  printf("Data file: %s\n", $file);
  printf "========================================";
  printf "========================================\n\n";

  # Read header with total, idle, system and user hits.
  $_ = <FILE>;
  chomp;

  ($total_hits, $idle_hits, $system_hits, $user_hits) = split (/ /);

  my $system_perc	= sprintf("%3.f", $system_hits / $total_hits * 100);
  my $user_perc		= sprintf("%3.f", $user_hits / $total_hits * 100);
  my $idle_perc		= 100 - $system_perc - $user_perc;

  printf("  System process ticks: %10d (%3d%%)\n", $system_hits, $system_perc);
  printf("    User process ticks: %10d (%3d%%)", $user_hits, $user_perc);
  printf("          Details of system process\n");
  printf("       Idle time ticks: %10d (%3d%%)", $idle_hits, $idle_perc);
  printf("          samples, aggregated and\n");
  printf("                        ----------  ----");
  printf("           per process, are below.\n");
  printf("           Total ticks: %10d (100%)\n\n", $total_hits);
 
  # Read sample records from file and increase relevant counters.
  until (eof(FILE)) {
	read(FILE, $buf, $SAMPLE_SIZE) == $SAMPLE_SIZE  or die ("Short read.");
	($exe, $pc) = unpack("Z8i", $buf);

	# p_name "mem" refers to executable "memory".
	$exe =~ s/^mem/memory/;

	# We can access the hash by pc because they are all in there.
       	if (!defined(${$exe."_hash"}{$pc})) {
		print "ERROR: Undefined in symbol table indexes: ";
		print "executable $exe  address $pc\n";
		print "Did you include this executable in the configuration?\n";
		return 1;
	}
	$res{$exe}{${$exe."_hash"}{$pc}} ++;
  }

  # We only need to continue with executables that had any hits.
  my @actives = ();
  foreach my $exe (@exes) {
	$exe =~ s/^.*\///;
	next if (!exists($res{$exe}));
	push(@actives, $exe);
  }

  # Calculate number of samples for each executable and create aggregated hash.
  %exe_hits = ();
  foreach $exe (@actives) {
	foreach my $hits (values %{$res{$exe}}) {
		$exe_hits{$exe} += $hits;
	}

	foreach my $key (keys %{$res{$exe}}) {
		$merged{sprintf("%8s %8s", $exe, $key)} = $res{$exe}{$key};
	}
  }

  $total_system_perc = 0;
  # Print the aggregated results.
  process_hash("", \%merged);

  # Print results for each executable in decreasing order.
  foreach my $exe
  (reverse sort { $exe_hits{$a} <=> $exe_hits{$b} } keys %exe_hits)
  {
	process_hash($exe, \%{$res{$exe}}) if
		$exe_hits{$exe} >= $system_hits / 100 * $MINIMUM_PERC;
  }

  # Print total of processes <threshold.
  printf "----------------------------------------";
  printf "----------------------------------------\n";
  printf("%-47s %5.1f%% of system process samples\n", 
	"processes <$MINIMUM_PERC% (not showing functions)",
	100 - $total_system_perc);
  printf "----------------------------------------";
  printf "----------------------------------------\n";
  printf("%-47s 100.0%%\n\n", "total");

  close(FILE);
  return 0;
}


sub process_hash
{
  my $exe = shift;
  my $ref = shift;
  %hash = %{$ref};
  my $aggr = $exe eq "";

  # Print a header.
  printf "----------------------------------------";
  printf "----------------------------------------\n";
  if ($aggr) {
	$astr_max = 55;
	$perc_hits = $system_hits / 100;
  	printf("Total system process time %46d samples\n", $system_hits);
  } else {
	$astr_max = 64;
	$perc_hits = $exe_hits{$exe} / 100;
	$total_system_perc += $exe_perc =
		sprintf("%5.1f", $exe_hits{$exe} / $system_hits * 100);
	printf("%-47s %5.1f%% of system process samples\n", $exe, $exe_perc);
  }
  printf "----------------------------------------";
  printf "----------------------------------------\n";


  # Delete functions <threshold.  Get percentage for all functions >threshold.
  my $below_thres_hits;
  my $total_exe_perc;
  foreach my $func (keys %hash)
  {
	if ($hash{$func} < $perc_hits * $MINIMUM_PERC) {
		$below_thres_hits += $hash{$func};
		delete($hash{$func});
	} else {
		$total_exe_perc += sprintf("%3.1f", $hash{$func} / $perc_hits);
	}
  }

  # Now print the hash entries in decreasing order.
  @sorted = reverse sort { $hash{$a} <=> $hash{$b} } keys %hash;

  $astr_hits = ($hash{@sorted->[0]} > $below_thres_hits ?
	$hash{@sorted->[0]} : $below_thres_hits) / $astr_max;

  foreach $func (@sorted) {
	process_line($func, $hash{$func});
  }

  # Print <threshold hits.
  my $below_thres;
  if ($aggr) { $below_thres = "         "; }
  $below_thres .= sprintf("%8s", "<" . $MINIMUM_PERC . "%");
  process_line($below_thres, $below_thres_hits, 100 - $total_exe_perc);

  # Print footer.
  printf "----------------------------------------";
  printf "----------------------------------------\n";
  printf("%-73s 100.0%%\n\n", $aggr ? "total" : $exe);
}


sub process_line
{
  my $func = shift;
  my $hits = shift;

  my $perc = $hits / $perc_hits;
  my $astr = $hits / $astr_hits;

  if ($aggr) {
	print "$func ";
  } else {
	printf("%8s ", $func);
  }
  for ($i = 0; $i < $astr_max; $i++) {
	if ($i <= $astr) {
		print "*";
	} else {
		print " ";
	}
  }
  print " ";
  if (my $rest = shift) {
	printf("%5.1f%%\n", $rest);
  } else {
	printf("%5.1f%%\n", $perc);
  }
}

