#!/usr/bin/perl
# Analysiert java-hprof-Textausgabe, Datei ist Parameter, ggf. java.hprof.txt. 
# Grund: Diese Ausgabe ist nicht immer einfach zu lesen, weil Datei lang.
# Zudem kann eine Zeile eines Programms/Klasse in mehreren Traces vorkommen.
# Datei besteht aus den wichtigen Teilen 1) TRACE    2) HEAP    3) CPU

# Das Skript bietet für CPU eine TOP-n oder über-Prozent-Abfrage.
# Anhand der Werte aus der CPU-Sicht wird noch der CPU-Verbrauch einzelner Programm/Klassenzeilen
# aus evt. verschiedenen TRACEs summiert. Unterstützt cpu=old nicht !
# Anm.: HEAP-Sites ist kürzer als HEAP-DUmp oder HEAP-All, cpu=times leider oft ohne Zeilennummern

#
# Hier Optionen einstellen: Top-n, Über Prozent oder alle 
#
$modus_topn = 0; # oder 0 fuer nicht; fuer Trace
$modus_prozent = "1.3"; # 2.5 = 2,5%, oder leer fuer nicht; fuer Trace
$modus_prozent2 = "1.0";  # fuer Summe pro Programmzeilen
# use integer; geht nicht, weil unten mit komma gerechnet wird


$datei = "java.hprof.txt";
if ($#ARGV >= 0)
{
  $datei = $ARGV[0]; # falls Datei angegeben
}
open (DAT, "<$datei") || die "$datei nicht gefunden";

# die zeitaufwändigsten Anweisungen holen
$stufe=0; $nr = ""; $DEBUG = 0;
my %karte1;  # Trace-ID -> Anweisung
my %karte2;  # Anweisung -> Summe CPU
my %karte3;  # Anweisung -> Summe Count
my %karte4;  # Anweisung -> Liste von Methoden
# Stufe 0 --> Beginn TRACE-Bereich suchen
# Stufe 1 --> TRACE-Stapel durchsuchen und speichern
# Stufe 2 --> CPU-Bereich durchsuchen und 2 Ausgaben machen (Pro relev. TRACE, pro Programmzeile)
while (<DAT>)
{
  chomp;
  if ($stufe == 0)
  {
    if (index($_, "THREAD END (") >= 0) { $stufe = 1; }
    next; # vor TRACE überspringen
  }
  #print "...$_\n";
  if ($stufe == 1 && $_ =~ /^HEAP DUMP BEGIN/)
  {
    # Interessante Info über Anzahl/Bytes Objekte ausgeben, sonst HEAP DUMP ignorieren
    # SITES BEGIN liefert keine wichtige Überschrift, wohl aber einen Stack der ben. Objekte
    $von = index ($_, "("); $bis = index ($_, ")");
    if ($von < 0 || $bis < 0) { die "HEAP-Format anders\n"; }
    print "Info Heap Dump: " . substr($_, $von + 1, $bis - $von - 1) . "\n";
  }
  if ($stufe == 1 && ($_ =~ /^TRACE / || substr($_, 0,1) ne "\t"))
  {
    # vorhergehende Nummer abschliessen
    if ($nr ne "")
    {
      if ($gef) { $karte1{$nr} = $relev; print "Trace $nr: $relev\n" unless !$DEBUG; }
      else { $karte1{$nr} = $letzter; print "Rest $nr: $letzter\n" unless !$DEBUG; }
    }
    if ($_ =~ /^TRACE /) # neue Nummer übernehmen
    {
      $relev = ""; $gef = 0; $letzter = ""; $von = index($_, " "); $bis = rindex($_, ":");
      if ($von < 0 || $bis < 0) { die "Falsches Format $von $bis\n"; } $nr = substr($_, $von+1, $bis-$von-1);
    }
    elsif ($_ =~ / BEGIN /) { $stufe = 2; }
  }
  if ($stufe == 1 && $nr ne "" && substr($_, 0,1) eq "\t")
  {
    # Trace-Nummer vorhanden, liest den Stapel von oben nach unten, erstes ungleich System ist entscheidend
    $letzter = substr($_,1);
    # bestimmte Pakete/Klassen, die nicht eigen sind, ignorieren
    if ($gef || $_ =~ /^\t(java|sun|com\.sun)/) { next; }
    $relev = $letzter; $gef = 1;
  }
  if ($stufe == 2 && $_ =~ /^CPU .* BEGIN /)  # .*="SAMPLES", "TIME (ms)"
  {
     # Ausgabe der Gesamtzeit in Millisekunden
     $von = index ($_, "(total"); $bis = index ($_, ")", $von);
     if ($von < 0 || $bis < 0 || $von > $bis) { die "CPU-Format anders\n"; }
     print "Info CPU Gesamtzeit Millisekunden: " . substr($_, $von + 1, $bis - $von - 1) . "\n";
     $lauf = 1; $_ = <DAT>; # Überschrift ignorieren
     if ($modus_topn > 0)
     {
       print "\nTop-$modus_topn pro TRACE --> Nummer, Zeit, Anzahl, Methode\n". "-" x 50 . "\n";
     }
     if ($modus_prozent ne "")
     {
       print "\nUeber $modus_prozent% pro TRACE --> Nummer, Zeit, Anzahl, Methode\n". "-" x 50 . "\n";
     }
     while (<DAT>)
     {
       chomp;
       if ($_ =~ /^CPU .* END/) { last; } # fertig
       @feld = split(" ", $_);
       $zeit = substr($feld[1],0,length($feld[1])-1); # Prozent weg
       $count = $feld[3]; $nr = $feld[4]; $methode = $feld[5];
       #print "time=$zeit,count=$count,trace=$nr\n"; 
       if (! exists $karte1{$nr}) { die "Kein TRACE zu $nr gefunden\n"; }
       else { $schl = $karte1{$nr}; } # Schlüssel zur zweiten/dritten karte
       # addieren oder neu einfügen
       if (!exists $karte2{$schl}) { $karte2{$schl} = $zeit; }
       else { $karte2{$schl} += (0.0 + $zeit); }
       if (!exists $karte3{$schl}) { $karte3{$schl} = $count; }
       else { $karte3{$schl} += (0 + $count); }
       if (!exists $karte4{$schl}) { $karte4{$schl} = $methode; }
       elsif (index($karte4{$schl}, $methode) < 0) # keine Duplikate bei Methoden
       {
         $karte4{$schl} = $karte4{$schl}.",".$methode;
       }

       # TOP-n-Abfragen hier ausgeben
       if (($modus_topn > 0 && $lauf <= $modus_topn) ||
           ($modus_prozent ne "" && ($modus_prozent+0.0 <= $zeit+0.0)) )
       {
         print "$schl --> $nr, $zeit%, $count, $methode\n"; 
       }
       ++$lauf;
     }
  }
}
close DAT;

# Ausgabe der Aggregationen - nicht jeder TRACE muss hier auftauchen !
# Annahme: Karte2 und Karte3 haben die gleiche Menge an Schlüsseln
@k1 = keys(%karte2);
print "\nAggr. unsortiert pro Programmanweisung --> Zeit, Anzahl, Liste v. Methoden\n". "-" x 60 . "\n";
for ($i = 0; $i <= $#k1; ++$i)
{
  # nach Prozenten filtern, wenn dieser Modus (modus_prozent2) eingestellt
  $zeit = $karte2{$k1[$i]};
  if ($modus_prozent2 eq "" || $modus_prozent2+0.0 <= $zeit+0.0)
  {
    print "$k1[$i] --> $zeit%, $karte3{$k1[$i]}, $karte4{$k1[$i]}\n";
  }
}



# Kommentare zu Aufrufparametern und Dateistruktur
# java -Xint -Xrunhprof:cpu=samples,depth=5,heap=sites ECM2 113031239864561329679379755323


#Option Name and Value  Description                    Default    Sinnvoll
#---------------------  -----------                    -------    -------
#heap=dump|sites|all    heap profiling                 all        sites
#cpu=samples|times|old  CPU usage                      off        samples|times
#depth=<size>           stack trace depth              4          5
#interval=<ms>          sample interval in ms          10
#lineno=y|n             line number in traces?         y
#thread=y|n             thread in traces?              n
#msa=y|n                Solaris micro state accounting n

#TRACE 300386:
#	java.lang.reflect.AccessibleObject.<init>(AccessibleObject.java:137)
#	java.lang.reflect.Field.<init>(Field.java:104)
#	java.lang.reflect.Field.copy(Field.java:127)
#	java.lang.reflect.ReflectAccess.copyField(ReflectAccess.java:122)
#	sun.reflect.ReflectionFactory.copyField(ReflectionFactory.java:289)
#TRACE 300387:
#	java.lang.Number.<init>(Number.java:32)
#	java.math.BigInteger.<init>(BigInteger.java:269)
#	java.math.BigInteger.<init>(BigInteger.java:451)
#	ECM2.main(ECM2.java:29)
#...
# SITES BEGIN (ordered by live bytes) Thu Sep 06 12:11:43 2012
#          percent          live          alloc'ed  stack class
# rank   self  accum     bytes objs     bytes  objs trace name
#    1  2.73%  2.73%    162432 6495  75530144 3016629 301503 int[]
# ...
# SITES END
# CPU SAMPLES BEGIN (total = 92411) Thu Sep 06 12:11:43 2012
# rank   self  accum   count trace method
#   1  2.43%  2.43%    2241 301503 java.math.MutableBigInteger.leftShift
# ...
# CPU SAMPLES END

