2
# _*_ coding: latin1 _*_
5
# Copyright (c) 2003 by WEB.DE, Karlsruhe
6
# Autor: Jörg Beyer <job@webde-ag.de>
8
# hotshot2cachegrind is free software; you can redistribute it and/or
9
# modify it under the terms of the GNU General Public
10
# License as published by the Free Software Foundation, version 2.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program; see the file COPYING. If not, write to
19
# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20
# Boston, MA 02110-1301, USA.
23
# This script transforms the pstat output of the hotshot
24
# python profiler into the input of kcachegrind.
27
# modify you python script to run this code:
30
# filename = "pythongrind.prof"
31
# prof = hotshot.Profile(filename, lineevents=1)
32
# prof.runcall(run) # assuming that "run" should be called.
35
# it will run the "run"-method under profiling and write
36
# the results in a file, called "pythongrind.prof".
38
# then call this script:
39
# hotshot2cachegrind -o <output> <input>
41
# hotshot2cachegrind cachegrind.out.0 pythongrind.prof
43
# then call kcachegrind:
44
# kcachegrind cachegrind.out.0
47
# * es gibt Probleme mit rekursiven (direkt und indirekt) Aufrufen - dann
48
# stimmen die Kosten nicht.
50
# * einige Funktionen werden mit "?" als Name angezeigt. Evtl sind
51
# das nur die C/C++ extensions.
53
# * es fehlt noch ein Funktionsnamen Mangling, dass die Filenamen berücksichtigt,
54
# zZ sind alle __init__'s und alle run's schwer unterscheidbar :-(
56
version = "Version ${KCACHEGRIND_VERSION}"
57
progname = "hotshot2cachegrind"
60
from hotshot import stats,log
66
log.WHAT_ADD_INFO : "ADD_INFO",
67
log.WHAT_DEFINE_FUNC : "DEFINE_FUNC",
68
log.WHAT_DEFINE_FILE : "DEFINE_FILE",
69
log.WHAT_LINENO : "LINENO",
70
log.WHAT_EXIT : "EXIT",
71
log.WHAT_ENTER : "ENTER"}
73
# a pseudo caller on the caller stack. This represents
74
# the Python interpreter that executes the given python
76
root_caller = ("PythonInterpreter",0,"execute")
79
"""A tiny Stack implementation, based on python lists"""
82
self.recursion_counter = {}
84
"""put something on the stack"""
85
self.stack.append(elem)
86
rc = self.recursion_counter.get(elem, 0)
87
self.recursion_counter[elem] = rc + 1
90
"""get the head element of the stack and remove it from the stack"""
91
elem = self.stack[-1:][0]
92
rc = self.recursion_counter.get(elem) - 1
94
self.recursion_counter[elem] = rc
96
del self.recursion_counter[elem]
97
return self.stack.pop()
100
"""get the head element of the stack, stack is unchanged."""
101
return self.stack[-1:][0]
102
def handleLineCost(self, tdelta):
103
p, c = self.stack.pop()
104
self.stack.append( (p,c + tdelta) )
106
""" return how many elements the stack has"""
107
return len(self.stack)
110
return "[stack: %s]" % self.stack
112
def recursion(self, pos):
113
return self.recursion_counter.get(pos, 0)
114
#return self.recursion_dict.has_key((entry[0][0], entry[0][2]))
116
def return_from_call(caller_stack, call_dict, cost_now):
117
"""return from a function call
118
remove the function from the caller stack,
119
add the costs to the calling function.
121
called, cost_at_enter = caller_stack.pop()
122
caller, caller_cost = caller_stack.top()
124
#print "return_from_call: %s ruft %s" % (caller, called,)
126
per_file_dict = call_dict.get(called[0], {})
127
per_caller_dict = per_file_dict.get(called[2], {})
128
cost_so_far, call_counter = per_caller_dict.get(caller, (0, 0))
130
if caller_stack.recursion(called):
131
per_caller_dict[caller] = (cost_so_far, call_counter + 1)
133
per_caller_dict[caller] = (cost_so_far + cost_now - cost_at_enter, call_counter + 1)
135
per_file_dict[called[2]] = per_caller_dict
136
call_dict[called[0]] = per_file_dict
139
def updateStatus(filecount):
140
sys.stdout.write("reading File #%d \r" % filecount)
142
def convertProfFiles(output, inputfilenames):
143
"""convert all the given input files into one kcachegrind
148
cost_per_function = {}
149
caller_stack = CallStack()
150
caller_stack.push((root_caller, 0))
154
number_of_files = len(inputfilenames)
155
for inputfilename in inputfilenames:
156
updateStatus(filecount)
157
cost, filecount = convertHandleFilename(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
159
if (file_limit > 0) and (filecount > file_limit):
163
print "total_cost: % d Ticks",total_cost
164
dumpResults(output, call_dict, total_cost, cost_per_pos, cost_per_function)
166
def convertHandleFilename(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount):
167
updateStatus(filecount)
168
if not ((file_limit > 0) and (filecount > file_limit)):
169
if os.path.isdir(inputfilename):
170
cost, filecount = convertProfDir(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
171
elif os.path.isfile(inputfilename):
172
cost = convertProfFile(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function)
175
sys.stderr.write("warn: ignoring '%s', is no file and no directory\n" % inputfilename)
177
return (cost, filecount)
179
def convertProfDir(start, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount):
181
filenames = os.listdir(start)
183
if (file_limit > 0) and (filecount > file_limit):
185
full = os.path.join(start, f)
186
c, filecount = convertHandleFilename(full, caller_stack, call_dict, cost_per_pos, cost_per_function, filecount)
188
return (cost, filecount)
190
def handleCostPerPos(cost_per_pos, pos, current_cost):
192
the cost per source position are managed in a dict in a dict.
194
the cost are handled per file and there per function.
195
so, the per-file-dict contains some per-function-dicts
196
which sum up the cost per line (in this function and in
202
file_dict = cost_per_pos.get(filename, {})
203
func_dict = file_dict.get(funcname, {})
204
func_dict.setdefault(lineno, 0)
205
func_dict[lineno] += current_cost
206
file_dict[funcname] = func_dict
207
cost_per_pos[filename] = file_dict
209
def convertProfFile(inputfilename, caller_stack, call_dict, cost_per_pos, cost_per_function):
210
"""convert a single input file into one kcachegrind
213
this is the most expensive function in this python source :-)
218
logreader = log.LogReader(inputfilename)
220
hc = handleCostPerPos # shortcut
221
for item in logreader:
222
what, pos ,tdelta = item
223
(file, lineno, func) = pos
224
#line = "%s %s %d %s %d" % (what2text[what], file, lineno, func, tdelta)
226
# most common cases first
227
if what == log.WHAT_LINENO:
228
# add the current cost to the current function
229
hc(cost_per_pos, pos, tdelta)
231
elif what == log.WHAT_ENTER:
232
caller_stack.push((pos, total_cost))
233
hc(cost_per_pos, pos, tdelta)
235
elif what == log.WHAT_EXIT:
236
hc(cost_per_pos, pos, tdelta)
238
return_from_call(caller_stack, call_dict, total_cost)
240
assert 0, "duh: %d" % what
243
# I have no idea, why sometimes the stack is not empty - we
244
# have to rewind the stack to get 100% for the root_caller
245
while caller_stack.size() > 1:
246
return_from_call(caller_stack, call_dict, total_cost)
249
print "could not open inputfile '%s', ignore this." % inputfilename
251
print "EOF: %s" % (m,)
254
def pretty_name(file, function):
255
#pfile = os.path.splitext(os.path.basename(file)) [0]
256
#return "%s_[%s]" % (function, file)
257
return "%s" % function
258
#return "%s::%s" % (file, function)
259
#return "%s_%s" % (pfile, function)
262
def __init__(self, output):
264
self.last_values = {}
266
def clearTag(self, tag):
267
if self.last_values.has_key(tag):
268
del self.last_values[ tag ]
270
self.last_values = {}
272
def write(self, tag, value):
273
self.output.write("%s=%s\n" % (tag, value))
274
#if (not self.last_values.has_key(tag)) or self.last_values[tag] != value:
275
# self.last_values[ tag ] = value
276
# self.output.write("%s=%s\n" % (tag, value))
278
def dumpResults(output, call_dict, total_cost, cost_per_pos, cost_per_function):
279
"""write the collected results in the format kcachegrind
283
output.write("events: Tick\n")
284
output.write("summary: %d\n" % total_cost)
285
output.write("cmd: your python script\n")
287
tagwriter = TagWriter(output)
289
# now the costs per line
290
for file in cost_per_pos.keys():
291
func_dict = cost_per_pos[file]
292
for func in func_dict.keys():
293
line_dict = func_dict[func]
294
tagwriter.write("ob", file)
295
tagwriter.write("fn", func)# pretty_name(file, func)) ; output.write("# ^--- 2\n")
296
tagwriter.write("fl", file)
297
for line in line_dict:
298
output.write("%d %d\n" %( line, line_dict[line] ))
301
# now the function calls. For each caller all the called
302
# functions and their costs are written.
303
for file in call_dict.keys():
304
per_file_dict = call_dict[file]
305
#print "file %s -> %s" % (file, per_file_dict)
306
for called_x in per_file_dict.keys():
307
#print "called_x:",called_x
308
per_caller_dict = per_file_dict[called_x]
309
#print "called_x %s wird gerufen von: %s" % (called_x, per_caller_dict)
310
for caller_x in per_caller_dict.keys():
311
tagwriter.write("ob", caller_x[0])
312
tagwriter.write("fn", caller_x[2])# pretty_name(caller_x[2], caller_x[0])) ; output.write("# ^--- 1\n")
313
tagwriter.write("fl", caller_x[0])
314
tagwriter.write("cob", file)
315
tagwriter.write("cfn", called_x) #pretty_name(file, called_x))
316
tagwriter.write("cfl", file)
317
cost, count = per_caller_dict[caller_x]
318
#print "called_x:",called_x
319
output.write("calls=%d\n%d %d\n" % (count, caller_x[1], cost))
321
#tagwriter.clearTag("cob")
322
# is it a bug in kcachegrind, that the "cob=xxx" line has
323
# to be rewritten after a calls entry with costline ?
324
#assert cost <= total_cost, "caller_x: %s, per_caller_dict: %s " % (caller_x, per_caller_dict, )
325
#output.write("calls=%d\n%d %d\n" % (count, caller_x[1], cost))
328
def run_without_optparse():
329
"""parse the options without optparse, use sys.argv"""
330
if len(sys.argv) < 4 or sys.argv[1] != "-o" :
331
print "usage: hotshot2cachegrind -o outputfile in1 [in2 [in3 [...]]]"
333
outputfilename = sys.argv[2]
335
output = file(outputfilename, "w")
337
convertProfFiles(output, args)
340
print "could not open '%s' for writing." % outputfilename
342
def run_with_optparse():
343
"""parse the options with optparse"""
347
versiontext = "%s version: %s" % ( progname, version.split()[1], )
348
parser = OptionParser(version=versiontext)
349
parser.add_option("-o", "--output",
350
action="store", type="string", dest="outputfilename",
351
help="write output into FILE")
352
parser.add_option("--file-limit",
353
action="store", dest="file_limit", default=0,
354
help="stop after given number of input files")
357
(options, args) = parser.parse_args()
358
file_limit = int(options.file_limit)
360
if options.outputfilename and options.outputfilename != "-":
361
output = file(options.outputfilename, "w")
364
print "could not open '%s' for writing." % options.outputfilename
366
convertProfFiles(output, args)
371
def profile_myself():
373
filename = "self.prof"
374
if not os.path.exists(filename):
375
prof = hotshot.Profile(filename, lineevents=1)
379
print "not profiling myself, since '%s' exists, running normal" % filename
382
# check if optparse is available.
384
from optparse import OptionParser
385
run = run_with_optparse
387
run = run_without_optparse
389
if __name__ == "__main__":
393
except KeyboardInterrupt: