3
# Copyright (c) 2006-2008, David Allouche, Jp Calderone, Itamar Shtull-Trauring,
4
# Johan Dahlin, Olivier Grisel <olivier.grisel@ensta.org>
8
# Permission is hereby granted, free of charge, to any person obtaining a copy
9
# of this software and associated documentation files (the "Software"), to deal
10
# in the Software without restriction, including without limitation the rights
11
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
# copies of the Software, and to permit persons to whom the Software is
13
# furnished to do so, subject to the following conditions:
15
# The above copyright notice and this permission notice shall be included in
16
# all copies or substantial portions of the Software.
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
"""pyprof2calltree: profiling output which is readable by kcachegrind
27
This script can either take raw cProfile.Profile.getstats() log entries or
28
take a previously recorded instance of the pstats.Stats class.
38
__all__ = ['convert', 'visualize', 'CalltreeConverter']
46
def pstats2entries(data):
47
"""Helper to convert serialized pstats back to a list of raw entries
49
Converse opperation of cProfile.Profile.snapshot_stats()
54
# first pass over stats to build the list of entry instances
55
for code_info, call_info in data.stats.items():
56
# build a fake code object
58
code.co_filename, code.co_firstlineno, code.co_name = code_info
60
# build a fake entry object
61
cc, nc, tt, ct, callers = call_info
65
entry.reccallcount = nc - cc
69
# to be filled during the second pass over stats
72
# collect the new entry
73
entries[code_info] = entry
74
allcallers[code_info] = callers.items()
76
# second pass of stats to plug callees into callers
77
for entry in entries.itervalues():
78
entry_label = cProfile.label(entry.code)
79
entry_callers = allcallers.get(entry_label, [])
80
for entry_caller, call_info in entry_callers:
81
entries[entry_caller].calls.append((entry, call_info))
83
return entries.values()
85
class CalltreeConverter(object):
86
"""Convert raw cProfile or pstats data to the calltree format"""
88
kcachegrind_command = "kcachegrind %s"
90
def __init__(self, profiling_data):
91
if isinstance(profiling_data, basestring):
92
# treat profiling_data as a filename of pstats serialized data
93
self.entries = pstats2entries(pstats.Stats(profiling_data))
94
elif isinstance(profiling_data, pstats.Stats):
95
# convert pstats data to cProfile list of entries
96
self.entries = pstats2entries(profiling_data)
98
# assume this are direct cProfile entries
99
self.entries = profiling_data
102
def output(self, out_file):
103
"""Write the converted entries to out_file"""
104
self.out_file = out_file
105
print >> out_file, 'events: Ticks'
106
self._print_summary()
107
for entry in self.entries:
111
"""Launch kcachegrind on the converted entries
113
kcachegrind must be present in the system path
116
if self.out_file is None:
117
_, outfile = tempfile.mkstemp(".log", "pyprof2calltree")
118
f = file(outfile, "wb")
122
use_temp_file = False
125
os.system(self.kcachegrind_command % self.out_file.name)
127
# clean the temporary file
133
def _print_summary(self):
135
for entry in self.entries:
136
totaltime = int(entry.totaltime * 1000)
137
max_cost = max(max_cost, totaltime)
138
print >> self.out_file, 'summary: %d' % (max_cost,)
140
def _entry(self, entry):
141
out_file = self.out_file
144
#print >> out_file, 'ob=%s' % (code.co_filename,)
146
co_filename, co_firstlineno, co_name = cProfile.label(code)
147
print >> out_file, 'fi=%s' % (co_filename,)
148
print >> out_file, 'fn=%s %s:%d' % (
149
co_name, co_filename, co_firstlineno)
151
inlinetime = int(entry.inlinetime * 1000)
152
if isinstance(code, str):
153
print >> out_file, '0 ', inlinetime
155
print >> out_file, '%d %d' % (code.co_firstlineno, inlinetime)
157
# recursive calls are counted in entry.calls
163
if isinstance(code, str):
166
lineno = code.co_firstlineno
168
for subentry, call_info in calls:
169
self._subentry(lineno, subentry, call_info)
172
def _subentry(self, lineno, subentry, call_info):
173
out_file = self.out_file
175
#print >> out_file, 'cob=%s' % (code.co_filename,)
176
co_filename, co_firstlineno, co_name = cProfile.label(code)
177
print >> out_file, 'cfn=%s %s:%d' % (
178
co_name, co_filename, co_firstlineno)
179
print >> out_file, 'cfi=%s' % (co_filename,)
180
print >> out_file, 'calls=%d %d' % (call_info[0], co_firstlineno)
182
totaltime = int(call_info[3] * 1000)
183
print >> out_file, '%d %d' % (lineno, totaltime)
186
"""Execute the converter using parameters provided on the command line"""
188
usage = "%s [-k] [-o output_file_path] [-i input_file_path] [-r scriptfile [args]]"
189
parser = optparse.OptionParser(usage=usage % sys.argv[0])
190
parser.allow_interspersed_args = False
191
parser.add_option('-o', '--outfile', dest="outfile",
192
help="Save calltree stats to <outfile>", default=None)
193
parser.add_option('-i', '--infile', dest="infile",
194
help="Read python stats from <infile>", default=None)
195
parser.add_option('-r', '--run-script', dest="script",
196
help="Name of the python script to run to collect"
197
" profiling data", default=None)
198
parser.add_option('-k', '--kcachegrind', dest="kcachegrind",
199
help="Run the kcachegrind tool on the converted data",
201
options, args = parser.parse_args()
204
outfile = options.outfile
206
if options.script is not None:
207
# collect profiling data by running the given script
209
sys.argv[:] = [options.script] + args
210
if not options.outfile:
211
outfile = '%s.log' % os.path.basename(options.script)
213
prof = cProfile.Profile()
216
prof = prof.run('execfile(%r)' % (sys.argv[0],))
220
kg = CalltreeConverter(prof.getstats())
222
elif options.infile is not None:
223
# use the profiling data from some input file
224
if not options.outfile:
225
outfile = '%s.log' % os.path.basename(options.infile)
227
if options.infile == outfile:
228
# prevent name collisions by appending another extension
231
kg = CalltreeConverter(pstats.Stats(options.infile))
234
# at least an input file or a script to run is required
238
if options.outfile is not None or not options.kcachegrind:
239
# user either explicitely required output file or requested by not
240
# explicitely asking to launch kcachegrind
241
print "writing converted data to: " + outfile
242
kg.output(file(outfile, 'wb'))
244
if options.kcachegrind:
245
print "launching kcachegrind"
249
def visualize(profiling_data):
250
"""launch the kcachegrind on `profiling_data`
252
`profiling_data` can either be:
253
- a pstats.Stats instance
254
- the filename of a pstats.Stats dump
255
- the result of a call to cProfile.Profile.getstats()
257
converter = CalltreeConverter(profiling_data)
258
converter.visualize()
260
def convert(profiling_data, outputfile):
261
"""convert `profiling_data` to calltree format and dump it to `outputfile`
263
`profiling_data` can either be:
264
- a pstats.Stats instance
265
- the filename of a pstats.Stats dump
266
- the result of a call to cProfile.Profile.getstats()
268
`outputfile` can either be:
269
- a file() instance open in write mode
272
converter = CalltreeConverter(profiling_data)
273
if isinstance(outputfile, basestring):
274
f = file(outputfile, "wb")
280
converter.output(outputfile)
283
if __name__ == '__main__':