#! /usr/bin/env python
# @ORIGINAL_AUTHOR: Robert Muth
#
# python.org has useful info about the Python programming language
#
# The Python library is described here: http://docs.python.org/lib/lib.html
# An the index for the library here: http://docs.python.org/lib/genindex.html

import sys
import os
import getopt
import re
import string
import copy
import bisect

#######################################################################
# Version
#######################################################################

def Version():
    (l,v,x) = string.split('$Revision: 1.6 $')
    return v

#######################################################################
# Usage
#######################################################################

def Usage():
    print "Usage: callgraph.py [OPTION]* symbol-file edge-profile"
    print
    print "OPTIONS:"
    print "\t-m mode       choose output format:  vcg or text"
    print "\t-u            if one routine invokes another from multiple callsites, merge them into one edge"
    print "\t-c cutoff     set cutoff, edges with smaller counts will not be shown "
    print "\t-t threshold  set thresholds, routines with a call count above the threshold will be colored red"   
    print 
    print "the symbol file is compatible to the readelf symbol dump, it should be generated as follows:"
    print "\t readelf -s img | grep FUNC >sym.txt"
    print 
    print "the edge-profile must be generated using the edgcnt Pin tool"
    return -1

#######################################################################
# Messages 
#######################################################################

def Info(str):
    print >> sys.stderr,  str
    return


def Warning(str):
    print >> sys.stderr,  str
    return

def Error(str):
    print >> sys.stderr,  str
    sys.exit(-1)


#######################################################################
ALL_RTN = {}
        
class RTN:
    def __init__(self,start,name,external=0):
        self._start = start
        self._name = name
        self._in = []
        self._out = []
        ALL_RTN[start] = self
        return
    

    def add_out_edg(self, edg ):
        self._out.append(edg)
        return
                             
    def add_in_edg(self, edg ):
        self._in.append(edg)
        return

    def count_in(self):
        count = 0
        for e in  self._in: count += e.count()
        return count

    def count_out(self):
        count = 0
        for e in  self._out: count += e.count()
        return count


    def start(self):
        return self._start

    def is_external(self):
        return  self._external

    def name(self):
        return  self._name

    def find_target(self,dst):
        for e in self._out:
            if e._dst == dst: return e
        return None

    def vcg_name(self):
        return hex(self.start())

    def String(self,cutoff):
        s = "RTN  %s [%x]  icnt %d  ocnt: %d)\n" % (self._name, self._start,  self.count_in(), self.count_out())

        s += "callers:\n"
        for edg in self._in:
            if edg.count() < cutoff: continue
            r = edg._src
            s += "\t%s %s [%x] %d\n" % (edg._type,r.name(),r.start(),edg._count)

        s += "callees:\n"
        for edg in self._out:
            if edg.count() < cutoff: continue
            r = edg._dst
            s += "\t%s %s [%x] %d\n" % (edg._type,r.name(),r.start(),edg._count)
        return s


    def StringVCG(self,threshold):
        s = "\t" + "node:\n"
        s += "\t" + "{\n"
        if self.count_in() > threshold or self.count_out() > threshold:
            s += "\t\t" + "color: red\n"            
        s += "\t\t" + "title: \"" + self.vcg_name() + "\"\n"
        s += "\t\t" + "label: \"" + self.name() + ": " + hex(self._start) + "\\n"
        s += "icnt: %d  ocnt: %d\"" % (self.count_in(),self.count_out())
        s += "\t" + "}\n"
        return s
    
#######################################################################
# 
#######################################################################
ALL_EDG = []

class EDG:
    def __init__(self,src,dst,type,count):
        global ALL_EDG
        self._src = src
        self._dst = dst
        self._type = type
        self._count = count
        src.add_out_edg(self )
        dst.add_in_edg(self )
        ALL_EDG.append(self)
        return

    def count(self):
        return self._count
    
    def add_count(self,count):
        self._count += count
        return
    
    def StringVCG(self, threshold = 100000000000L):
        s = ""
        if self._count > threshold:
            s += "\t" + "nearedge:\n"
        else:
            s += "\t" + "edge:\n"

        s += "\t{\n"    
        s += "\t\t" + "sourcename: \"" + self._src.vcg_name() + "\"\n"
        s += "\t\t" + "targetname: \"" + self._dst.vcg_name() + "\"\n"
        s += "\t\t" + "thickness: 2\n"
        s += "\t\t" + "label: \"%s(%d)\"\n" % (self._type,self._count)

#        s += "\t\t" + "priority: %d\n" % self._count                        
        s += "\t}\n"    
        return s
        
#######################################################################
# 
#######################################################################


def DumpVCG(cutoff, threshold):
    print  "// ###################################################################################"
    print  "// VCG Callgraph: cutoff %d,  threshold %d" %(cutoff, threshold)
    print  "// ###################################################################################"

    print  "graph:"
    print  "{";

    print "title: \"Call Graph\""
    print "label: \"Call Graph\""
    print "display_edge_labels: yes"
    print "layout_downfactor: 100"
    print "layout_nearfactor: 10"
    print "layout_upfactor: 1"
#    print "dirty_edge_labels: yes"
    print "layout_algorithm: mindepth"
    print "manhatten_edges: yes"
    print "edge.arrowsize: 15"
    print "late_edge_labels: yes"    

    for e in ALL_EDG:
        if e._count < cutoff: continue 
        print e.StringVCG()

    list = ALL_RTN.items()
    list.sort()
    for (x,b) in list:
        if b.count_out() < cutoff and b.count_in() < cutoff: continue 
        print b.StringVCG(threshold)

    print "}";
    print "// eof"
    return

#######################################################################
# 
#######################################################################

def DumpText(cutoff):

    print  "# ###################################################################################"
    print  "# Callgraph: cutoff %d" % cutoff
    print  "# ###################################################################################"


    list = ALL_RTN.items()
    list.sort()
    for (x,b) in list:
        if b.count_out() < cutoff and b.count_in() < cutoff: continue 
        print b.String(cutoff)

    print "# eof"
    return

#######################################################################
# SYMBOL PROCESSING
#######################################################################
#   Num:    Value          Size Type    Bind   Vis      Ndx Name
# 24: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c


def ProcessSymbols(lines):

    for l in lines:
        ll = string.split(l)
        (num,value,size,type,bind,vis,ndx,name) = ll[:8]
        start = long(value,16)
        if ALL_RTN.has_key(start):
            name2 = ALL_RTN[start].name()
            if string.find(name2,name) == -1 and   string.find(name,name2) == -1:
                Warning("two routines with same address %x: %s vs %s" %(start,name2,name))
        else:        
            RTN(start,name)
    return    

RTN_MAP = []


MAX_ADDR = 0

PatternSyscall = re.compile(r'^[fF]+$')

def FindRtnByAddress(addr, s):
    global RTN_MAP
    global ALL_RTN
    index = bisect.bisect_left(RTN_MAP,addr)

    if 0 < index and  index < len(RTN_MAP):
        a = RTN_MAP[index]
        if a == addr:
            r = ALL_RTN[a]
#            print "found match rtn", r._name," ", hex(a)," for address ",hex(addr)            
            return r
        else:
            assert( a > addr)
            a = RTN_MAP[index-1]
            assert( a < addr)
            r =  ALL_RTN[a]
            return r
    else:
        if addr == MAX_ADDR:
            Warning("found syscall")
            return RTN(addr,"syscall",1 )

        if not ALL_RTN.has_key(addr):
            RTN(addr,"unknown_%s_0x%x" % (s,addr),1 )
        return ALL_RTN[addr]



#######################################################################
#  0x0000000000400366 0x0000000000402300         2182

PatternEdge = re.compile(r'^\s*0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)\s+([a-zA-Z])\s+([0-9]+)\s+.*$')

def ProcessEdgProfile(lines, unique_edgs_only):
    global MAX_ADDR
    
    version = string.split(lines[0])
    if version[0] != "EDGCOUNT":
        Error("input file  is not an edgcnt profile")

    if version[1] != "4.0":
        Error("unsupported  profile version %s" % version[1])
        
    for l in lines[1:]:
        match = PatternEdge.match(l)
        if not match:
            continue

        src = long(match.group(1),16)
        dst = long(match.group(2),16)
        type = match.group(3)
        count = long(match.group(4))

        if dst > MAX_ADDR: MAX_ADDR = dst
        if src > MAX_ADDR: MAX_ADDR = src

        rtn_src = FindRtnByAddress(src,"call")
        rtn_dst = FindRtnByAddress(dst,"rtn")

        if type =='r': continue
        
        if type != 'c' and type != 'C' and rtn_src == rtn_dst: continue

        if unique_edgs_only:
            e = rtn_src.find_target(rtn_dst)
            if e:
                e.add_count(count)
                continue
                
#        print "found edg ",rtn_src._name," -> ", rtn_dst._name
        e = EDG(rtn_src,rtn_dst,type,count)

    return

#######################################################################
# Main
#######################################################################

def Main(argv):
    global RTN_MAP
    global ALL_RTN
    global ALL_EDG


    try:
        opts, args = getopt.getopt(argv, "m:c:t:u")
    except getopt.error:
        Usage()
        return -1
    
    mode = "vcg"
    cutoff = 1
    unique_edgs_only = 0
    threshold = 1000
    
    for o, a in opts:
        if o == "-m":
            mode = a
        elif o == "-c":
            cutoff = long(a)
        elif o == "-t":
            threshold = long(a)
        elif o == "-u":
            unique_edgs_only = 1
        else:
            Usage()
            return -1

    if len(args) != 2:
        Usage()
        return
    
    Info( "Reading Symbols")


    filename = args[0]
    try:
        input = open(filename, "r")
        lines = input.readlines()
        input.close()
    except:
        raise Error("cannot read data " + filename)

    ProcessSymbols(lines)


    RTN_MAP = map( lambda x: x._start, ALL_RTN.values()  )
    RTN_MAP.sort()
    Info("found %d symbols" % len(RTN_MAP))

    Info( "Reading edges")
    
    filename = args[1]
    try:
        input = open(filename, "r")
        lines = input.readlines()
        input.close()
    except:
        raise Error("cannot read data " + filename)

    ProcessEdgProfile(lines,unique_edgs_only)

    Info("Read %d edges" % len(ALL_EDG))

    if mode == "vcg":
        DumpVCG(cutoff, threshold)
    elif mode == "text":
        DumpText(cutoff)
    else:
        Usage()
        return -1
        
    
    return 0

#######################################################################
# 
#######################################################################

if __name__ == "__main__":
    sys.exit( Main( sys.argv[1:]) )

#######################################################################
# eof
#######################################################################
