#!/usr/bin/env python
# _*_ coding: latin1 _*_

#
# Copyright (c) 2003 by WEB.DE, Karlsruhe
# Autor: Jörg Beyer <job@webde-ag.de>
#
# hotshot2cachegrind is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# 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 more details.

# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#
#
# This script transforms the pstat output of the hotshot
# python profiler into the input of kcachegrind. 
#
# example usage:
# modify you python script to run this code:
#
# import hotshot
# filename = "pythongrind.prof"
# prof = hotshot.Profile(filename, lineevents=1)
# prof.runcall(run) # assuming that "run" should be called.
# prof.close()
#
# it will run the "run"-method under profiling and write
# the results in a file, called "pythongrind.prof".
#
# then call this script:
# hotshot2cachegrind -o <output> <input>
# or here:
# hotshot2cachegrind cachegrind.out.0 pythongrind.prof
#
# then call kcachegrind:
# kcachegrind cachegrind.out.0
#
# TODO: 
#  * es gibt Probleme mit rekursiven (direkt und indirekt) Aufrufen - dann
#    stimmen die Kosten nicht.
#
#  * einige Funktionen werden mit "?" als Name angezeigt. Evtl sind
#    das nur die C/C++ extensions.
#
#  * es fehlt noch ein Funktionsnamen Mangling, dass die Filenamen berücksichtigt,
#    zZ sind alle __init__'s und alle run's schwer unterscheidbar :-(
#
version = "$Revision: 307068 $"
progname = "hotshot2cachegrind"

import os, sys
from hotshot import stats,log
import os.path 

file_limit=0

what2text = { 
    log.WHAT_ADD_INFO    : "ADD_INFO", 
    log.WHAT_DEFINE_FUNC : "DEFINE_FUNC", 
    log.WHAT_DEFINE_FILE : "DEFINE_FILE", 
    log.WHAT_LINENO      : "LINENO", 
    log.WHAT_EXIT        : "EXIT", 
    log.WHAT_ENTER       : "ENTER"}

# a pseudo caller on the caller stack. This represents
# the Python interpreter that executes the given python 
# code.
root_caller = ("PythonInterpreter",0,"execute")

class CallStack:
    """A tiny Stack implementation, based on python lists"""
    def __init__(self):
       self.stack = []
       self.recursion_counter = {}
    def push(self, elem):
        """put something on the stack"""
        self.stack.append(elem)
        rc = self.recursion_counter.get(elem, 0)
        self.recursion_counter[elem] = rc + 1

    def pop(self):
        """get the head element of the stack and remove it from teh stack"""
        elem = self.stack[-1:][0]
        rc = self.recursion_counter.get(elem) - 1
        if rc>0:
            self.recursion_counter[elem] = rc
        else:
            del self.recursion_counter[elem]
        return self.stack.pop()

    def top(self):
        """get the head element of the stack, stack is unchanged."""
        return self.stack[-1:][0]
    def handleLineCost(self, tdelta):
        p, c = self.stack.pop()
        self.stack.append( (p,c + tdelta) )
    def size(self):
        """ return how many elements the stack has"""
        return len(self.stack)

    def __str__(self):
        return "[stack: %s]" % self.stack

    def recursion(self, pos):
        return self.recursion_counter.get(pos, 0)
        #return self.recursion_dict.has_key((entry[0][0], entry[0][2]))

def return_from_call(caller_stack, call_dict, cost_now):
    """return from a function call
       remove the function from the caller stack,
       add the costs to the calling function.
    """
    called, cost_at_enter = caller_stack.pop()
    caller, caller_cost = caller_stack.top()

    #print "return_from_call: %s ruft %s" % (caller, called,)

    per_file_dict = call_dict.get(called[0], {})
    per_caller_dict = per_file_dict.get(called[2], {})
    cost_so_far, call_counter = per_caller_dict.get(caller, (0, 0))

    if caller_stack.recursion(called):
        per_caller_dict[caller] = (cost_so_far, call_counter + 1)
    else:
        per_caller_dict[caller] = (cost_so_far + cost_now - cost_at_enter, call_counter + 1)

    per_file_dict[called[2]] = per_caller_dict
    call_dict[called[0]] = per_file_dict


def updateStatus(filecount):
    sys.stdout.write("reading File #%d    \r" % filecount)
    sys.stdout.flush()
def convertProfFiles(output, inputfilenames):
    """convert all the given input files into one kcachegrind 
       input file.
    """
    call_dict = {}
    cost_per_pos = {}
    cost_per_function = {}
    caller_stack = CallStack()
    caller_stack.push((root_caller, 0))

    total_cost = 0
    filecount = 1
    number_of_files = len(inputfilenames)
    for inputfilename in inputfilenames:
        updateStatus(filecount)
        cost, filecount = convertHandleFilename(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
        total_cost += cost
        if (file_limit > 0) and (filecount > file_limit):
            break
    
    print
    print "total_cost: % d Ticks",total_cost
    dumpResults(output, call_dict, total_cost, cost_per_pos, cost_per_function)

def convertHandleFilename(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount):
    updateStatus(filecount)
    if not ((file_limit > 0) and (filecount > file_limit)):
        if os.path.isdir(inputfilename):
            cost, filecount = convertProfDir(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
        elif os.path.isfile(inputfilename):
            cost = convertProfFile(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function)
            filecount += 1 
        else:
            sys.stderr.write("warn: ignoring '%s', is no file and no directory\n" % inputfilename)
            cost = 0
    return (cost, filecount)

def convertProfDir(start, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount):
    cost = 0
    filenames = os.listdir(start)
    for f in filenames:
        if (file_limit > 0) and (filecount > file_limit): 
            break
        full = os.path.join(start, f)
        c, filecount = convertHandleFilename(full, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
        cost += c;
    return (cost, filecount)

def handleCostPerPos(cost_per_pos, pos, current_cost):
    """
       the cost per source position are managed in a dict in a dict.

       the cost are handled per file and there per function.
       so, the per-file-dict contains some per-function-dicts
       which sum up the cost per line (in this function and in 
       this file).
    """
    filename  = pos[0]
    lineno    = pos[1]
    funcname  = pos[2]
    file_dict = cost_per_pos.get(filename, {})
    func_dict = file_dict.get(funcname, {})
    func_dict.setdefault(lineno, 0)
    func_dict[lineno] += current_cost
    file_dict[funcname] = func_dict
    cost_per_pos[filename] = file_dict

def convertProfFile(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function):
    """convert a single input file into one kcachegrind
       data.

       this is the most expensive function in this python source :-)
    """

    total_cost = 0
    try:
        logreader = log.LogReader(inputfilename)
        current_cost = 0
        hc = handleCostPerPos # shortcut
        for item in logreader:
            what, pos ,tdelta = item
            (file, lineno, func) = pos
            #line = "%s %s %d %s %d" % (what2text[what], file, lineno, func, tdelta)
            #print line
            # most common cases first
            if what == log.WHAT_LINENO:
                # add the current cost to the current function
                hc(cost_per_pos, pos, tdelta)
                total_cost += tdelta
            elif what == log.WHAT_ENTER:
                caller_stack.push((pos, total_cost))
                hc(cost_per_pos, pos, tdelta)
                total_cost += tdelta
            elif what == log.WHAT_EXIT:
                hc(cost_per_pos, pos, tdelta)
                total_cost += tdelta
                return_from_call(caller_stack, call_dict, total_cost)
            else:
                assert 0, "duh: %d" % what


        # I have no idea, why sometimes the stack is not empty - we
        # have to rewind the stack to get 100% for the root_caller
        while caller_stack.size() > 1:
            return_from_call(caller_stack, call_dict, total_cost)

    except IOError:
        print "could not open inputfile '%s', ignore this." % inputfilename
    except EOFError, m:
        print "EOF: %s" % (m,)
    return total_cost

def pretty_name(file, function):
    #pfile = os.path.splitext(os.path.basename(file)) [0]
    #return "%s_[%s]" % (function, file)
    return "%s" % function
    #return "%s::%s" % (file, function)
    #return "%s_%s" % (pfile, function)

class TagWriter:
    def __init__(self, output):
        self.output = output
        self.last_values = {}

    def clearTag(self, tag):
        if self.last_values.has_key(tag):
            del self.last_values[ tag ]
    def clear(self):
        self.last_values = {}

    def write(self, tag, value):
        self.output.write("%s=%s\n" % (tag, value))
        #if (not self.last_values.has_key(tag)) or self.last_values[tag] != value:
        #    self.last_values[ tag ] = value
        #    self.output.write("%s=%s\n" % (tag, value))

def dumpResults(output, call_dict, total_cost, cost_per_pos, cost_per_function):
    """write the collected results in the format kcachegrind
       could read.
    """
    # the intro
    output.write("events: Tick\n")
    output.write("summary: %d\n" % total_cost)
    output.write("cmd: your python script\n")
    output.write("\n")
    tagwriter = TagWriter(output)

    # now the costs per line
    for file in cost_per_pos.keys():
        func_dict = cost_per_pos[file]
        for func in func_dict.keys():
            line_dict = func_dict[func]
            tagwriter.write("ob", file)
            tagwriter.write("fn", func)# pretty_name(file, func)) ; output.write("# ^--- 2\n")
            tagwriter.write("fl", file)
            for line in line_dict:
                output.write("%d %d\n" %( line, line_dict[line] ))

    output.write("\n\n")
    # now the function calls. For each caller all the called
    # functions and their costs are written.
    for file in call_dict.keys():
        per_file_dict = call_dict[file]
        #print "file %s -> %s" % (file, per_file_dict)
        for called_x in per_file_dict.keys():
            #print "called_x:",called_x
            per_caller_dict = per_file_dict[called_x]
            #print "called_x %s wird gerufen von: %s" % (called_x, per_caller_dict)
            for caller_x in per_caller_dict.keys():
                tagwriter.write("ob", caller_x[0])
                tagwriter.write("fn", caller_x[2])# pretty_name(caller_x[2], caller_x[0])) ; output.write("# ^--- 1\n")
                tagwriter.write("fl", caller_x[0])
                tagwriter.write("cob", file)
                tagwriter.write("cfn", called_x) #pretty_name(file, called_x))
                tagwriter.write("cfl", file)
                cost, count = per_caller_dict[caller_x]
                #print "called_x:",called_x
                output.write("calls=%d\n%d %d\n" % (count, caller_x[1], cost))
                tagwriter.clear()
                #tagwriter.clearTag("cob")
                # is it a bug in kcachegrind, that the "cob=xxx" line has
                # to be rewritten after a calls entry with costline ?
                #assert cost <= total_cost, "caller_x: %s, per_caller_dict: %s " % (caller_x, per_caller_dict, )
                #output.write("calls=%d\n%d %d\n" % (count, caller_x[1], cost))
                output.write("\n")

def run_without_optparse():
    """parse the options without optparse, use sys.argv"""
    if  len(sys.argv) < 4 or sys.argv[1] != "-o" :
        print "usage: hotshot2cachegrind -o outputfile in1 [in2 [in3 [...]]]"
        return
    outputfilename = sys.argv[2]
    try:
        output = file(outputfilename, "w")
        args = sys.argv[3:]
        convertProfFiles(output, args)
        output.close()
    except IOError:
        print "could not open '%s' for writing." % outputfilename

def run_with_optparse():
    """parse the options with optparse"""

    global file_limit

    versiontext = "%s version: %s" % ( progname, version.split()[1], )
    parser = OptionParser(version=versiontext)
    parser.add_option("-o", "--output",
      action="store", type="string", dest="outputfilename",
      help="write output into FILE")
    parser.add_option("--file-limit",
      action="store", dest="file_limit", default=0,
      help="stop after given number of input files")
    output = sys.stdout
    close_output = 0
    (options, args) = parser.parse_args()
    file_limit = int(options.file_limit)
    try:
        if options.outputfilename and options.outputfilename != "-":
            output = file(options.outputfilename, "w")
            close_output = 1
    except IOError:
        print "could not open '%s' for writing." % options.outputfilename
    if output:
        convertProfFiles(output, args)
        if close_output:
            output.close()


def profile_myself():
    import hotshot
    filename = "self.prof"
    if not os.path.exists(filename):
        prof = hotshot.Profile(filename, lineevents=1)
        prof.runcall(run)
        prof.close()
    else:
        print "not profiling myself, since '%s' exists, running normal" % filename
        run()

# check if optparse is available.
try:
    from optparse import OptionParser
    run = run_with_optparse
except ImportError:
    run = run_without_optparse

if __name__ == "__main__":
    try:
        run()
        #profile_myself()
    except KeyboardInterrupt:
        sys.exit(1)
