Warning, /sdk/kcachegrind/converters/hotshot2calltree.in is written in an unsupported language. File is not indexed.

0001 #!/usr/bin/env python
0002 # _*_ coding: latin1 _*_
0003 
0004 #
0005 # SPDX-FileCopyrightText: 2003 WEB.DE, Karlsruhe
0006 # SPDX-FileContributor: Jörg Beyer <job@webde-ag.de>
0007 #
0008 # SPDX-License-Identifier: GPL-2.0-only
0009 #
0010 #
0011 # This script transforms the pstat output of the hotshot
0012 # python profiler into the input of kcachegrind. 
0013 #
0014 # example usage:
0015 # modify you python script to run this code:
0016 #
0017 # import hotshot
0018 # filename = "pythongrind.prof"
0019 # prof = hotshot.Profile(filename, lineevents=1)
0020 # prof.runcall(run) # assuming that "run" should be called.
0021 # prof.close()
0022 #
0023 # it will run the "run"-method under profiling and write
0024 # the results in a file, called "pythongrind.prof".
0025 #
0026 # then call this script:
0027 # hotshot2cachegrind -o <output> <input>
0028 # or here:
0029 # hotshot2cachegrind cachegrind.out.0 pythongrind.prof
0030 #
0031 # then call kcachegrind:
0032 # kcachegrind cachegrind.out.0
0033 #
0034 # TODO: 
0035 #  * es gibt Probleme mit rekursiven (direkt und indirekt) Aufrufen - dann
0036 #    stimmen die Kosten nicht.
0037 #
0038 #  * einige Funktionen werden mit "?" als Name angezeigt. Evtl sind
0039 #    das nur die C/C++ extensions.
0040 #
0041 #  * es fehlt noch ein Funktionsnamen Mangling, dass die Filenamen berücksichtigt,
0042 #    zZ sind alle __init__'s und alle run's schwer unterscheidbar :-(
0043 #
0044 version = "Version ${KCACHEGRIND_VERSION}"
0045 progname = "hotshot2cachegrind"
0046 
0047 import os, sys
0048 from hotshot import stats,log
0049 import os.path 
0050 
0051 file_limit=0
0052 
0053 what2text = { 
0054     log.WHAT_ADD_INFO    : "ADD_INFO", 
0055     log.WHAT_DEFINE_FUNC : "DEFINE_FUNC", 
0056     log.WHAT_DEFINE_FILE : "DEFINE_FILE", 
0057     log.WHAT_LINENO      : "LINENO", 
0058     log.WHAT_EXIT        : "EXIT", 
0059     log.WHAT_ENTER       : "ENTER"}
0060 
0061 # a pseudo caller on the caller stack. This represents
0062 # the Python interpreter that executes the given python 
0063 # code.
0064 root_caller = ("PythonInterpreter",0,"execute")
0065 
0066 class CallStack:
0067     """A tiny Stack implementation, based on python lists"""
0068     def __init__(self):
0069        self.stack = []
0070        self.recursion_counter = {}
0071     def push(self, elem):
0072         """put something on the stack"""
0073         self.stack.append(elem)
0074         rc = self.recursion_counter.get(elem, 0)
0075         self.recursion_counter[elem] = rc + 1
0076 
0077     def pop(self):
0078         """get the head element of the stack and remove it from the stack"""
0079         elem = self.stack[-1:][0]
0080         rc = self.recursion_counter.get(elem) - 1
0081         if rc>0:
0082             self.recursion_counter[elem] = rc
0083         else:
0084             del self.recursion_counter[elem]
0085         return self.stack.pop()
0086 
0087     def top(self):
0088         """get the head element of the stack, stack is unchanged."""
0089         return self.stack[-1:][0]
0090     def handleLineCost(self, tdelta):
0091         p, c = self.stack.pop()
0092         self.stack.append( (p,c + tdelta) )
0093     def size(self):
0094         """ return how many elements the stack has"""
0095         return len(self.stack)
0096 
0097     def __str__(self):
0098         return "[stack: %s]" % self.stack
0099 
0100     def recursion(self, pos):
0101         return self.recursion_counter.get(pos, 0)
0102         #return self.recursion_dict.has_key((entry[0][0], entry[0][2]))
0103 
0104 def return_from_call(caller_stack, call_dict, cost_now):
0105     """return from a function call
0106        remove the function from the caller stack,
0107        add the costs to the calling function.
0108     """
0109     called, cost_at_enter = caller_stack.pop()
0110     caller, caller_cost = caller_stack.top()
0111 
0112     #print "return_from_call: %s ruft %s" % (caller, called,)
0113 
0114     per_file_dict = call_dict.get(called[0], {})
0115     per_caller_dict = per_file_dict.get(called[2], {})
0116     cost_so_far, call_counter = per_caller_dict.get(caller, (0, 0))
0117 
0118     if caller_stack.recursion(called):
0119         per_caller_dict[caller] = (cost_so_far, call_counter + 1)
0120     else:
0121         per_caller_dict[caller] = (cost_so_far + cost_now - cost_at_enter, call_counter + 1)
0122 
0123     per_file_dict[called[2]] = per_caller_dict
0124     call_dict[called[0]] = per_file_dict
0125 
0126 
0127 def updateStatus(filecount):
0128     sys.stdout.write("reading File #%d    \r" % filecount)
0129     sys.stdout.flush()
0130 def convertProfFiles(output, inputfilenames):
0131     """convert all the given input files into one kcachegrind 
0132        input file.
0133     """
0134     call_dict = {}
0135     cost_per_pos = {}
0136     cost_per_function = {}
0137     caller_stack = CallStack()
0138     caller_stack.push((root_caller, 0))
0139 
0140     total_cost = 0
0141     filecount = 1
0142     number_of_files = len(inputfilenames)
0143     for inputfilename in inputfilenames:
0144         updateStatus(filecount)
0145         cost, filecount = convertHandleFilename(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
0146         total_cost += cost
0147         if (file_limit > 0) and (filecount > file_limit):
0148             break
0149     
0150     print
0151     print "total_cost: % d Ticks",total_cost
0152     dumpResults(output, call_dict, total_cost, cost_per_pos, cost_per_function)
0153 
0154 def convertHandleFilename(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount):
0155     updateStatus(filecount)
0156     if not ((file_limit > 0) and (filecount > file_limit)):
0157         if os.path.isdir(inputfilename):
0158             cost, filecount = convertProfDir(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
0159         elif os.path.isfile(inputfilename):
0160             cost = convertProfFile(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function)
0161             filecount += 1 
0162         else:
0163             sys.stderr.write("warn: ignoring '%s', is no file and no directory\n" % inputfilename)
0164             cost = 0
0165     return (cost, filecount)
0166 
0167 def convertProfDir(start, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount):
0168     cost = 0
0169     filenames = os.listdir(start)
0170     for f in filenames:
0171         if (file_limit > 0) and (filecount > file_limit): 
0172             break
0173         full = os.path.join(start, f)
0174         c, filecount = convertHandleFilename(full, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
0175         cost += c;
0176     return (cost, filecount)
0177 
0178 def handleCostPerPos(cost_per_pos, pos, current_cost):
0179     """
0180        the cost per source position are managed in a dict in a dict.
0181 
0182        the cost are handled per file and there per function.
0183        so, the per-file-dict contains some per-function-dicts
0184        which sum up the cost per line (in this function and in 
0185        this file).
0186     """
0187     filename  = pos[0]
0188     lineno    = pos[1]
0189     funcname  = pos[2]
0190     file_dict = cost_per_pos.get(filename, {})
0191     func_dict = file_dict.get(funcname, {})
0192     func_dict.setdefault(lineno, 0)
0193     func_dict[lineno] += current_cost
0194     file_dict[funcname] = func_dict
0195     cost_per_pos[filename] = file_dict
0196 
0197 def convertProfFile(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function):
0198     """convert a single input file into one kcachegrind
0199        data.
0200 
0201        this is the most expensive function in this python source :-)
0202     """
0203 
0204     total_cost = 0
0205     try:
0206         logreader = log.LogReader(inputfilename)
0207         current_cost = 0
0208         hc = handleCostPerPos # shortcut
0209         for item in logreader:
0210             what, pos ,tdelta = item
0211             (file, lineno, func) = pos
0212             #line = "%s %s %d %s %d" % (what2text[what], file, lineno, func, tdelta)
0213             #print line
0214             # most common cases first
0215             if what == log.WHAT_LINENO:
0216                 # add the current cost to the current function
0217                 hc(cost_per_pos, pos, tdelta)
0218                 total_cost += tdelta
0219             elif what == log.WHAT_ENTER:
0220                 caller_stack.push((pos, total_cost))
0221                 hc(cost_per_pos, pos, tdelta)
0222                 total_cost += tdelta
0223             elif what == log.WHAT_EXIT:
0224                 hc(cost_per_pos, pos, tdelta)
0225                 total_cost += tdelta
0226                 return_from_call(caller_stack, call_dict, total_cost)
0227             else:
0228                 assert 0, "duh: %d" % what
0229 
0230 
0231         # I have no idea, why sometimes the stack is not empty - we
0232         # have to rewind the stack to get 100% for the root_caller
0233         while caller_stack.size() > 1:
0234             return_from_call(caller_stack, call_dict, total_cost)
0235 
0236     except IOError:
0237         print "could not open inputfile '%s', ignore this." % inputfilename
0238     except EOFError, m:
0239         print "EOF: %s" % (m,)
0240     return total_cost
0241 
0242 def pretty_name(file, function):
0243     #pfile = os.path.splitext(os.path.basename(file)) [0]
0244     #return "%s_[%s]" % (function, file)
0245     return "%s" % function
0246     #return "%s::%s" % (file, function)
0247     #return "%s_%s" % (pfile, function)
0248 
0249 class TagWriter:
0250     def __init__(self, output):
0251         self.output = output
0252         self.last_values = {}
0253 
0254     def clearTag(self, tag):
0255         if self.last_values.has_key(tag):
0256             del self.last_values[ tag ]
0257     def clear(self):
0258         self.last_values = {}
0259 
0260     def write(self, tag, value):
0261         self.output.write("%s=%s\n" % (tag, value))
0262         #if (not self.last_values.has_key(tag)) or self.last_values[tag] != value:
0263         #    self.last_values[ tag ] = value
0264         #    self.output.write("%s=%s\n" % (tag, value))
0265 
0266 def dumpResults(output, call_dict, total_cost, cost_per_pos, cost_per_function):
0267     """write the collected results in the format kcachegrind
0268        could read.
0269     """
0270     # the intro
0271     output.write("events: Tick\n")
0272     output.write("summary: %d\n" % total_cost)
0273     output.write("cmd: your python script\n")
0274     output.write("\n")
0275     tagwriter = TagWriter(output)
0276 
0277     # now the costs per line
0278     for file in cost_per_pos.keys():
0279         func_dict = cost_per_pos[file]
0280         for func in func_dict.keys():
0281             line_dict = func_dict[func]
0282             tagwriter.write("ob", file)
0283             tagwriter.write("fn", func)# pretty_name(file, func)) ; output.write("# ^--- 2\n")
0284             tagwriter.write("fl", file)
0285             for line in line_dict:
0286                 output.write("%d %d\n" %( line, line_dict[line] ))
0287 
0288     output.write("\n\n")
0289     # now the function calls. For each caller all the called
0290     # functions and their costs are written.
0291     for file in call_dict.keys():
0292         per_file_dict = call_dict[file]
0293         #print "file %s -> %s" % (file, per_file_dict)
0294         for called_x in per_file_dict.keys():
0295             #print "called_x:",called_x
0296             per_caller_dict = per_file_dict[called_x]
0297             #print "called_x %s wird gerufen von: %s" % (called_x, per_caller_dict)
0298             for caller_x in per_caller_dict.keys():
0299                 tagwriter.write("ob", caller_x[0])
0300                 tagwriter.write("fn", caller_x[2])# pretty_name(caller_x[2], caller_x[0])) ; output.write("# ^--- 1\n")
0301                 tagwriter.write("fl", caller_x[0])
0302                 tagwriter.write("cob", file)
0303                 tagwriter.write("cfn", called_x) #pretty_name(file, called_x))
0304                 tagwriter.write("cfl", file)
0305                 cost, count = per_caller_dict[caller_x]
0306                 #print "called_x:",called_x
0307                 output.write("calls=%d\n%d %d\n" % (count, caller_x[1], cost))
0308                 tagwriter.clear()
0309                 #tagwriter.clearTag("cob")
0310                 # is it a bug in kcachegrind, that the "cob=xxx" line has
0311                 # to be rewritten after a calls entry with costline ?
0312                 #assert cost <= total_cost, "caller_x: %s, per_caller_dict: %s " % (caller_x, per_caller_dict, )
0313                 #output.write("calls=%d\n%d %d\n" % (count, caller_x[1], cost))
0314                 output.write("\n")
0315 
0316 def run_without_optparse():
0317     """parse the options without optparse, use sys.argv"""
0318     if  len(sys.argv) < 4 or sys.argv[1] != "-o" :
0319         print "usage: hotshot2cachegrind -o outputfile in1 [in2 [in3 [...]]]"
0320         return
0321     outputfilename = sys.argv[2]
0322     try:
0323         output = file(outputfilename, "w")
0324         args = sys.argv[3:]
0325         convertProfFiles(output, args)
0326         output.close()
0327     except IOError:
0328         print "could not open '%s' for writing." % outputfilename
0329 
0330 def run_with_optparse():
0331     """parse the options with optparse"""
0332 
0333     global file_limit
0334 
0335     versiontext = "%s version: %s" % ( progname, version.split()[1], )
0336     parser = OptionParser(version=versiontext)
0337     parser.add_option("-o", "--output",
0338       action="store", type="string", dest="outputfilename",
0339       help="write output into FILE")
0340     parser.add_option("--file-limit",
0341       action="store", dest="file_limit", default=0,
0342       help="stop after given number of input files")
0343     output = sys.stdout
0344     close_output = 0
0345     (options, args) = parser.parse_args()
0346     file_limit = int(options.file_limit)
0347     try:
0348         if options.outputfilename and options.outputfilename != "-":
0349             output = file(options.outputfilename, "w")
0350             close_output = 1
0351     except IOError:
0352         print "could not open '%s' for writing." % options.outputfilename
0353     if output:
0354         convertProfFiles(output, args)
0355         if close_output:
0356             output.close()
0357 
0358 
0359 def profile_myself():
0360     import hotshot
0361     filename = "self.prof"
0362     if not os.path.exists(filename):
0363         prof = hotshot.Profile(filename, lineevents=1)
0364         prof.runcall(run)
0365         prof.close()
0366     else:
0367         print "not profiling myself, since '%s' exists, running normal" % filename
0368         run()
0369 
0370 # check if optparse is available.
0371 try:
0372     from optparse import OptionParser
0373     run = run_with_optparse
0374 except ImportError:
0375     run = run_without_optparse
0376 
0377 if __name__ == "__main__":
0378     try:
0379         run()
0380         #profile_myself()
0381     except KeyboardInterrupt:
0382         sys.exit(1)