├── .gitignore
├── .travis.yml
├── CONTRIBUTORS.txt
├── LICENSE
├── MANIFEST.in
├── README.rst
├── pyprof2calltree.py
├── setup.cfg
├── setup.py
├── test
├── __init__.py
├── profile_code.py
└── test_integration.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info/
2 | *.pyc
3 | .tox/
4 | __pycache__/
5 | build/
6 | dist/
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | cache: pip
3 |
4 | matrix:
5 | include:
6 | - env: TOXENV=flake8
7 | - env: TOXENV=isort
8 | - python: "2.7"
9 | env: TOXENV=py27
10 | - python: "3.4"
11 | env: TOXENV=py34
12 | - python: "3.5"
13 | env: TOXENV=py35
14 | - python: "3.6"
15 | env: TOXENV=py36
16 | - python: "3.7"
17 | env: TOXENV=py37
18 | - python: "3.8"
19 | env: TOXENV=py38
20 |
21 | install: pip install tox
22 | script: tox
23 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.txt:
--------------------------------------------------------------------------------
1 | # Contributions to the pyprof2calltree project
2 |
3 | ## Creators
4 |
5 | * David Allouche
6 | * Jp Calderone
7 | * Itamar Shtull-Trauring
8 | * Johan Dahlin
9 |
10 | ## Maintainer
11 |
12 | * Peter Waller
13 |
14 | ## Contributors
15 |
16 | In chronological order:
17 |
18 | * Olivier Grisel
19 | * Repackaging and pstats support
20 |
21 | * David Glick
22 | * Fix in conversion algorithm
23 |
24 | * Peter Waller
25 | * Taking over PyPI maintainance
26 |
27 | * Steven Maude
28 | * Breaking things, documentation
29 |
30 | * Lukas Graf
31 | * Python 3.x compatibility
32 |
33 | * Jamie Wong
34 | * qcachegrind support
35 |
36 | * Yury V. Zaytsev
37 | * Bugfixes
38 |
39 | * Michael Droettboom
40 | * Source code display support
41 |
42 | * Zev Benjamin
43 | * Support for multiple functions with the same name
44 | * Tests
45 |
46 | * Jon Dufresne
47 | * A huge number of small fixes and consistency
48 | improvements across code, docs and setup.py alike.
49 |
50 | * Meesha <44530786+meesha7@users.noreply.github.com>
51 | * Support for multiple time units
52 |
53 | * [Your name or handle] <[email or website]>
54 | * [Brief summary of your changes]
55 |
56 | ## Thanks
57 |
58 | * Jon Dufresne for prompting
59 | a new release and many tidy up fixes!
60 | * Uwe L. Korn for pointing out mismatch in licensing
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2006-2017 David Allouche, Jp Calderone, Itamar Shtull-Trauring, Johan Dahlin, Peter Waller and people listed in CONTRIBUTORS.txt
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include CONTRIBUTORS.txt
3 | recursive-include test *.py
4 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Overview
2 | ========
3 |
4 | Script to help visualize profiling data collected with the cProfile
5 | Python module with the kcachegrind_ (screenshots_) graphical calltree
6 | analyser.
7 |
8 | This is a rebranding of the venerable
9 | http://www.gnome.org/~johan/lsprofcalltree.py script by David Allouche
10 | et Al. It aims at making it easier to distribute (e.g. through PyPI)
11 | and behave more like the scripts of the debian kcachegrind-converters_
12 | package. The final goal is to make it part of the official upstream
13 | kdesdk_ package.
14 |
15 | .. _kcachegrind: http://kcachegrind.sourceforge.net
16 | .. _kcachegrind-converters: https://packages.debian.org/en/stable/kcachegrind-converters
17 | .. _kdesdk: http://websvn.kde.org/trunk/KDE/kdesdk/kcachegrind/converters/
18 | .. _screenshots: http://images.google.fr/images?q=kcachegrind
19 |
20 | Installation
21 | ============
22 |
23 | On Debian ≥ 11, or derivatives such as Ubuntu ≥ 20.04, `sudo apt
24 | install kcachegrind pyprof2calltree`.
25 |
26 | Command line usage
27 | ==================
28 |
29 | Upon installation you should have a `pyprof2calltree` script in your path::
30 |
31 | $ pyprof2calltree --help
32 | usage: pyprof2calltree [-h] [-o output_file_path] [-i input_file_path] [-k]
33 | [-r scriptfile [args ...]]
34 |
35 | optional arguments:
36 | -h, --help show this help message and exit
37 | -o output_file_path, --outfile output_file_path
38 | Save calltree stats to
39 | -i input_file_path, --infile input_file_path
40 | Read Python stats from
41 | -k, --kcachegrind Run the kcachegrind tool on the converted data
42 | -r scriptfile [args ...], --run-script scriptfile [args ...]
43 | Name of the Python script to run to collect profiling
44 | data
45 | -s {s,ms,us,ns}, --scale {s,ms,us,ns}
46 | Time scale
47 |
48 |
49 | Python shell usage
50 | ==================
51 |
52 | `pyprof2calltree` is also best used from an interactive Python shell such as
53 | the default shell. For instance let us profile XML parsing::
54 |
55 | >>> from xml.etree import ElementTree
56 | >>> from cProfile import Profile
57 | >>> xml_content = '\n' + '\ttext\n' * 100 + ''
58 | >>> profiler = Profile()
59 | >>> profiler.runctx(
60 | ... "ElementTree.fromstring(xml_content)",
61 | ... locals(), globals())
62 |
63 | >>> from pyprof2calltree import convert, visualize
64 | >>> visualize(profiler.getstats()) # run kcachegrind
65 | >>> convert(profiler.getstats(), 'profiling_results.kgrind') # save for later
66 |
67 | or with the ipython_::
68 |
69 | In [1]: %doctest_mode
70 | Exception reporting mode: Plain
71 | Doctest mode is: ON
72 |
73 | >>> from xml.etree import ElementTree
74 | >>> xml_content = '\n' + '\ttext\n' * 100 + ''
75 | >>> %prun -D out.stats ElementTree.fromstring(xml_content)
76 |
77 | *** Profile stats marshalled to file 'out.stats'
78 |
79 | >>> from pyprof2calltree import convert, visualize
80 | >>> visualize('out.stats')
81 | >>> convert('out.stats', 'out.kgrind')
82 |
83 | >>> results = %prun -r ElementTree.fromstring(xml_content)
84 | >>> visualize(results)
85 |
86 | .. _ipython: https://ipython.org/
87 |
88 |
89 | Change log
90 | ==========
91 |
92 | - 1.4.4 - 2018-10-19: Numerous small improvements, drop support for EOL python versions
93 | - 1.4.3 - 2017-07-28: Windows support (fixed is_installed check - #21)
94 | - 1.4.2 - 2017-07-19: No feature or bug fixes, just license clarification (#20)
95 | - 1.4.1 - 2017-05-20: No feature or bug fixes, just test distribution (#17)
96 | - 1.4.0 - 2016-09-03: Support multiple functions with the same name, tick unit from millis to nanos, tests added (#15)
97 | - 1.3.2 - 2014-07-05: Bugfix: correct source file paths (#12)
98 | - 1.3.1 - 2013-11-27: Bugfix for broken output writing on Python 3 (#8)
99 | - 1.3.0 - 2013-11-19: qcachegrind support
100 | - 1.2.0 - 2013-11-09: Python 3 support
101 | - 1.1.1 - 2013-09-25: Miscellaneous bugfixes
102 | - 1.1.0 - 2008-12-21: integrate fix in conversion by David Glick
103 | - 1.0.3 - 2008-10-16: fix typos in 1.0 release
104 | - 1.0 - 2008-10-16: initial release under the pyprof2calltree name
105 |
--------------------------------------------------------------------------------
/pyprof2calltree.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (c) 2006-2008, David Allouche, Jp Calderone, Itamar Shtull-Trauring,
3 | # Johan Dahlin, Olivier Grisel
4 | #
5 | # Send maintenance requests needing new PyPI packages to:
6 | # Peter Waller
7 | # https://github.com/pwaller/pyprof2calltree
8 | #
9 | # See CONTRIBUTORS.txt.
10 | #
11 | # All rights reserved.
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | """pyprof2calltree: profiling output which is readable by kcachegrind
31 |
32 | This script can either take raw cProfile.Profile.getstats() log entries or
33 | take a previously recorded instance of the pstats.Stats class.
34 | """
35 |
36 | from __future__ import unicode_literals
37 |
38 | import argparse
39 | import cProfile
40 | import errno
41 | import io
42 | import os
43 | import pstats
44 | import subprocess
45 | import sys
46 | import tempfile
47 | from collections import defaultdict
48 |
49 | __all__ = ['convert', 'visualize', 'CalltreeConverter']
50 |
51 |
52 | class Scale(object):
53 | def __init__(self, unit):
54 | SCALES = {
55 | 's': (1, 's', 'Seconds'),
56 | 'ms': (1e3, 'ms', 'Milliseconds'),
57 | 'us': (1e6, 'us', 'Microseconds'),
58 | 'ns': (1e9, 'ns', 'Nanoseconds')
59 | }
60 |
61 | self.scale, self.unit, self.name = SCALES[unit]
62 |
63 |
64 | class Code(object):
65 | def __init__(self, filename, firstlineno, name):
66 | self.co_filename = filename
67 | self.co_firstlineno = firstlineno
68 | self.co_name = name
69 |
70 | def __repr__(self):
71 | return '' % (self.co_filename, self.co_firstlineno,
72 | self.co_name)
73 |
74 |
75 | class Entry(object):
76 | def __init__(self, code, callcount, reccallcount, inlinetime, totaltime, calls):
77 | self.code = code
78 | self.callcount = callcount
79 | self.reccallcount = reccallcount
80 | self.inlinetime = inlinetime
81 | self.totaltime = totaltime
82 | self.calls = calls
83 |
84 | def __repr__(self):
85 | return '' % (
86 | self.code, self.callcount, self.reccallcount, self.inlinetime,
87 | self.totaltime, self.calls
88 | )
89 |
90 |
91 | class Subentry(object):
92 | def __init__(self, code, callcount, reccallcount, inlinetime, totaltime):
93 | self.code = code
94 | self.callcount = callcount
95 | self.reccallcount = reccallcount
96 | self.inlinetime = inlinetime
97 | self.totaltime = totaltime
98 |
99 | def __repr__(self):
100 | return '' % (
101 | self.code, self.callcount, self.reccallcount, self.inlinetime,
102 | self.totaltime
103 | )
104 |
105 |
106 | def is_basestring(s):
107 | try:
108 | unicode
109 | # Python 2.x
110 | return isinstance(s, basestring)
111 | except NameError:
112 | # Python 3.x
113 | return isinstance(s, (str, bytes))
114 |
115 |
116 | def pstats2entries(data):
117 | """Helper to convert serialized pstats back to a list of raw entries.
118 |
119 | Converse operation of cProfile.Profile.snapshot_stats()
120 | """
121 | # Each entry's key is a tuple of (filename, line number, function name)
122 | entries = {}
123 | allcallers = {}
124 |
125 | # first pass over stats to build the list of entry instances
126 | for code_info, call_info in data.stats.items():
127 | # build a fake code object
128 | code = Code(*code_info)
129 |
130 | # build a fake entry object. entry.calls will be filled during the
131 | # second pass over stats
132 | cc, nc, tt, ct, callers = call_info
133 | entry = Entry(code, callcount=cc, reccallcount=nc - cc, inlinetime=tt,
134 | totaltime=ct, calls=[])
135 |
136 | # collect the new entry
137 | entries[code_info] = entry
138 | allcallers[code_info] = list(callers.items())
139 |
140 | # second pass of stats to plug callees into callers
141 | for entry in entries.values():
142 | entry_label = cProfile.label(entry.code)
143 | entry_callers = allcallers.get(entry_label, [])
144 | for entry_caller, call_info in entry_callers:
145 | cc, nc, tt, ct = call_info
146 | subentry = Subentry(entry.code, callcount=cc, reccallcount=nc - cc,
147 | inlinetime=tt, totaltime=ct)
148 | # entry_caller has the same form as code_info
149 | entries[entry_caller].calls.append(subentry)
150 |
151 | return list(entries.values())
152 |
153 |
154 | def is_installed(prog):
155 | """Return whether or not a given executable is installed on the machine."""
156 | with open(os.devnull, 'w') as devnull:
157 | try:
158 | if os.name == 'nt':
159 | retcode = subprocess.call(['where', prog], stdout=devnull)
160 | else:
161 | retcode = subprocess.call(['which', prog], stdout=devnull)
162 | except OSError as e:
163 | # If where or which doesn't exist, a "ENOENT" error will occur (The
164 | # FileNotFoundError subclass on Python 3).
165 | if e.errno != errno.ENOENT:
166 | raise
167 | retcode = 1
168 |
169 | return retcode == 0
170 |
171 |
172 | def _entry_sort_key(entry):
173 | return cProfile.label(entry.code)
174 |
175 |
176 | KCACHEGRIND_EXECUTABLES = ["kcachegrind", "qcachegrind"]
177 |
178 |
179 | class CalltreeConverter(object):
180 | """Convert raw cProfile or pstats data to the calltree format"""
181 |
182 | def __init__(self, profiling_data, scale=None):
183 | if is_basestring(profiling_data):
184 | # treat profiling_data as a filename of pstats serialized data
185 | self.entries = pstats2entries(pstats.Stats(profiling_data))
186 | elif isinstance(profiling_data, pstats.Stats):
187 | # convert pstats data to cProfile list of entries
188 | self.entries = pstats2entries(profiling_data)
189 | else:
190 | # assume this are direct cProfile entries
191 | self.entries = profiling_data
192 | self.out_file = None
193 | self.scale = scale
194 |
195 | if not scale:
196 | self.scale = Scale('ns')
197 |
198 | self._code_by_position = defaultdict(set)
199 | self._populate_code_by_position()
200 |
201 | def _populate_code_by_position(self):
202 | for entry in self.entries:
203 | self._add_code_by_position(entry.code)
204 | if not entry.calls:
205 | continue
206 | for subentry in entry.calls:
207 | self._add_code_by_position(subentry.code)
208 |
209 | def _add_code_by_position(self, code):
210 | co_filename, _, co_name = cProfile.label(code)
211 | self._code_by_position[(co_filename, co_name)].add(code)
212 |
213 | def munged_function_name(self, code):
214 | co_filename, co_firstlineno, co_name = cProfile.label(code)
215 | if len(self._code_by_position[(co_filename, co_name)]) == 1:
216 | return co_name
217 | return "%s:%d" % (co_name, co_firstlineno)
218 |
219 | def output(self, out_file):
220 | """Write the converted entries to out_file"""
221 | self.out_file = out_file
222 | out_file.write('event: {} : {}\n'.format(self.scale.unit, self.scale.name))
223 | out_file.write('events: {}\n'.format(self.scale.unit))
224 | self._output_summary()
225 | for entry in sorted(self.entries, key=_entry_sort_key):
226 | self._output_entry(entry)
227 |
228 | def visualize(self):
229 | """Launch kcachegrind on the converted entries.
230 |
231 | One of the executables listed in KCACHEGRIND_EXECUTABLES
232 | must be present in the system path.
233 | """
234 |
235 | available_cmd = None
236 | for cmd in KCACHEGRIND_EXECUTABLES:
237 | if is_installed(cmd):
238 | available_cmd = cmd
239 | break
240 |
241 | if available_cmd is None:
242 | sys.stderr.write("Could not find kcachegrind. Tried: %s\n" %
243 | ", ".join(KCACHEGRIND_EXECUTABLES))
244 | return
245 |
246 | if self.out_file is None:
247 | fd, outfile = tempfile.mkstemp(".log", "pyprof2calltree")
248 | use_temp_file = True
249 | else:
250 | outfile = self.out_file.name
251 | use_temp_file = False
252 |
253 | try:
254 | if use_temp_file:
255 | with io.open(fd, "w") as f:
256 | self.output(f)
257 | subprocess.call([available_cmd, outfile])
258 | finally:
259 | # clean the temporary file
260 | if use_temp_file:
261 | os.remove(outfile)
262 | self.out_file = None
263 |
264 | def _output_summary(self):
265 | max_cost = 0
266 | for entry in self.entries:
267 | totaltime = int(entry.totaltime * self.scale.scale)
268 | max_cost = max(max_cost, totaltime)
269 | # Version 0.7.4 of kcachegrind appears to ignore the summary line and
270 | # calculate the total cost by summing the exclusive cost of all
271 | # functions, but it doesn't hurt to output it anyway.
272 | self.out_file.write('summary: %d\n' % (max_cost,))
273 |
274 | def _output_entry(self, entry):
275 | out_file = self.out_file
276 |
277 | code = entry.code
278 |
279 | co_filename, co_firstlineno, co_name = cProfile.label(code)
280 | munged_name = self.munged_function_name(code)
281 | out_file.write('fl=%s\nfn=%s\n' % (co_filename, munged_name))
282 |
283 | inlinetime = int(entry.inlinetime * self.scale.scale)
284 | out_file.write('%d %d\n' % (co_firstlineno, inlinetime))
285 |
286 | # recursive calls are counted in entry.calls
287 | if entry.calls:
288 | for subentry in sorted(entry.calls, key=_entry_sort_key):
289 | self._output_subentry(co_firstlineno, subentry.code,
290 | subentry.callcount,
291 | int(subentry.totaltime * self.scale.scale))
292 |
293 | out_file.write('\n')
294 |
295 | def _output_subentry(self, lineno, code, callcount, totaltime):
296 | out_file = self.out_file
297 | co_filename, co_firstlineno, co_name = cProfile.label(code)
298 | munged_name = self.munged_function_name(code)
299 | out_file.write('cfl=%s\ncfn=%s\n' % (co_filename, munged_name))
300 | out_file.write('calls=%d %d\n' % (callcount, co_firstlineno))
301 | out_file.write('%d %d\n' % (lineno, totaltime))
302 |
303 |
304 | def main():
305 | """Execute the converter using parameters provided on the command line"""
306 |
307 | parser = argparse.ArgumentParser()
308 | parser.add_argument('-o', '--outfile', metavar='output_file_path',
309 | help="Save calltree stats to ")
310 | parser.add_argument('-i', '--infile', metavar='input_file_path',
311 | help="Read Python stats from ")
312 | parser.add_argument('-k', '--kcachegrind',
313 | help="Run the kcachegrind tool on the converted data",
314 | action="store_true")
315 | parser.add_argument('-r', '--run-script',
316 | nargs=argparse.REMAINDER,
317 | metavar=('scriptfile', 'args'),
318 | dest='script',
319 | help="Name of the Python script to run to collect"
320 | " profiling data")
321 | parser.add_argument('-s', '--scale', choices=['s', 'ms', 'us', 'ns'],
322 | default='ns',
323 | help='Time scale')
324 | args = parser.parse_args()
325 |
326 | outfile = args.outfile
327 | scale = Scale(args.scale)
328 |
329 | if args.script is not None:
330 | # collect profiling data by running the given script
331 | if not args.outfile:
332 | outfile = '%s.log' % os.path.basename(args.script[0])
333 |
334 | fd, tmp_path = tempfile.mkstemp(suffix='.prof', prefix='pyprof2calltree')
335 | os.close(fd)
336 | try:
337 | cmd = [
338 | sys.executable,
339 | '-m', 'cProfile',
340 | '-o', tmp_path,
341 | ]
342 | cmd.extend(args.script)
343 | subprocess.check_call(cmd)
344 |
345 | kg = CalltreeConverter(tmp_path, scale)
346 | finally:
347 | os.remove(tmp_path)
348 |
349 | elif args.infile is not None:
350 | # use the profiling data from some input file
351 | if not args.outfile:
352 | outfile = '%s.log' % os.path.basename(args.infile)
353 |
354 | if args.infile == outfile:
355 | # prevent name collisions by appending another extension
356 | outfile += ".log"
357 |
358 | kg = CalltreeConverter(pstats.Stats(args.infile), scale)
359 |
360 | else:
361 | # at least an input file or a script to run is required
362 | parser.print_usage()
363 | sys.exit(2)
364 |
365 | if args.outfile is not None or not args.kcachegrind:
366 | # user either explicitly required output file or requested by not
367 | # explicitly asking to launch kcachegrind
368 | sys.stderr.write("writing converted data to: %s\n" % outfile)
369 | with open(outfile, 'w') as f:
370 | kg.output(f)
371 |
372 | if args.kcachegrind:
373 | sys.stderr.write("launching kcachegrind\n")
374 | kg.visualize()
375 |
376 |
377 | def visualize(profiling_data):
378 | """launch the kcachegrind on `profiling_data`
379 |
380 | `profiling_data` can either be:
381 | - a pstats.Stats instance
382 | - the filename of a pstats.Stats dump
383 | - the result of a call to cProfile.Profile.getstats()
384 | """
385 | converter = CalltreeConverter(profiling_data)
386 | converter.visualize()
387 |
388 |
389 | def convert(profiling_data, outputfile):
390 | """convert `profiling_data` to calltree format and dump it to `outputfile`
391 |
392 | `profiling_data` can either be:
393 | - a pstats.Stats instance
394 | - the filename of a pstats.Stats dump
395 | - the result of a call to cProfile.Profile.getstats()
396 |
397 | `outputfile` can either be:
398 | - a file() instance open in write mode
399 | - a filename
400 | """
401 | converter = CalltreeConverter(profiling_data)
402 | if is_basestring(outputfile):
403 | with open(outputfile, "w") as f:
404 | converter.output(f)
405 | else:
406 | converter.output(outputfile)
407 |
408 |
409 | if __name__ == '__main__':
410 | sys.exit(main())
411 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = pyprof2calltree
3 | version = 1.4.4
4 | description = Help visualize profiling data from cProfile with kcachegrind and qcachegrind
5 | long_description = file: README.rst
6 | url = https://github.com/pwaller/pyprof2calltree/
7 | author = Olivier Grisel
8 | author_email = olivier.grisel@ensta.org
9 | maintainer = Peter Waller
10 | maintainer_email = p@pwaller.net
11 | license = MIT
12 | keywords =
13 | kcachegrind
14 | kde
15 | profiler
16 | programming
17 | qcachegrind
18 | tool
19 | visualization
20 | classifiers =
21 | Development Status :: 5 - Production/Stable
22 | Environment :: Console
23 | Environment :: X11 Applications :: KDE
24 | License :: OSI Approved :: MIT License
25 | Operating System :: POSIX
26 | Operating System :: Unix
27 | Programming Language :: Python
28 | Programming Language :: Python :: 2
29 | Programming Language :: Python :: 2.7
30 | Programming Language :: Python :: 3
31 | Programming Language :: Python :: 3.4
32 | Programming Language :: Python :: 3.5
33 | Programming Language :: Python :: 3.6
34 | Programming Language :: Python :: 3.7
35 | Programming Language :: Python :: 3.8
36 | Topic :: Desktop Environment :: K Desktop Environment (KDE)
37 | Topic :: Software Development
38 | Topic :: Software Development :: Quality Assurance
39 | Topic :: System :: System Shells
40 | Topic :: Utilities
41 |
42 | [options]
43 | python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
44 | py_modules =
45 | pyprof2calltree
46 | zip_safe = true
47 |
48 | [options.entry_points]
49 | console_scripts =
50 | pyprof2calltree = pyprof2calltree:main
51 |
52 | [bdist_wheel]
53 | universal = 1
54 |
55 | [flake8]
56 | max-line-length = 88
57 |
58 | [isort]
59 | combine_as_imports = True
60 | force_grid_wrap = 0
61 | include_trailing_comma = True
62 | line_length = 88
63 | multi_line_output = 3
64 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup()
4 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwaller/pyprof2calltree/366af4cac28b5316326ba7f512c04e154cf942a7/test/__init__.py
--------------------------------------------------------------------------------
/test/profile_code.py:
--------------------------------------------------------------------------------
1 | # We're going to use a custom timer, so we don't actually have to do anything
2 | # in these functions.
3 |
4 |
5 | def top():
6 | mid1()
7 | mid2()
8 | mid3(5)
9 | C1.samename()
10 | C2.samename()
11 |
12 |
13 | def mid1():
14 | bot()
15 | for i in range(5):
16 | mid2()
17 | bot()
18 |
19 |
20 | def mid2():
21 | bot()
22 |
23 |
24 | def bot():
25 | pass
26 |
27 |
28 | def mid3(x):
29 | if x > 0:
30 | mid4(x)
31 |
32 |
33 | def mid4(x):
34 | mid3(x - 1)
35 |
36 |
37 | class C1(object):
38 | @staticmethod
39 | def samename():
40 | pass
41 |
42 |
43 | class C2(object):
44 | @staticmethod
45 | def samename():
46 | pass
47 |
48 |
49 | expected_output_py2 = """event: ns : Nanoseconds
50 | events: ns
51 | summary: 59000
52 | fl=
53 | fn=top
54 | 5 6000
55 | cfl=
56 | cfn=mid1
57 | calls=1 13
58 | 5 27000
59 | cfl=
60 | cfn=mid2
61 | calls=1 20
62 | 5 3000
63 | cfl=
64 | cfn=mid3
65 | calls=1 28
66 | 5 21000
67 | cfl=
68 | cfn=samename:38
69 | calls=1 38
70 | 5 1000
71 | cfl=
72 | cfn=samename:44
73 | calls=1 44
74 | 5 1000
75 |
76 | fl=
77 | fn=mid1
78 | 13 9000
79 | cfl=
80 | cfn=mid2
81 | calls=5 20
82 | 13 15000
83 | cfl=
84 | cfn=bot
85 | calls=2 24
86 | 13 2000
87 | cfl=~
88 | cfn=
89 | calls=1 0
90 | 13 1000
91 |
92 | fl=
93 | fn=mid2
94 | 20 12000
95 | cfl=
96 | cfn=bot
97 | calls=6 24
98 | 20 6000
99 |
100 | fl=
101 | fn=bot
102 | 24 8000
103 |
104 | fl=
105 | fn=mid3
106 | 28 11000
107 | cfl=
108 | cfn=mid4
109 | calls=5 33
110 | 28 19000
111 |
112 | fl=
113 | fn=mid4
114 | 33 10000
115 | cfl=
116 | cfn=mid3
117 | calls=5 28
118 | 33 17000
119 |
120 | fl=
121 | fn=samename:38
122 | 38 1000
123 |
124 | fl=
125 | fn=samename:44
126 | 44 1000
127 |
128 | fl=~
129 | fn=
130 | 0 1000
131 |
132 | fl=~
133 | fn=
134 | 0 1000
135 |
136 | """.replace('', top.__code__.co_filename)
137 |
138 | expected_output_py3 = """event: ns : Nanoseconds
139 | events: ns
140 | summary: 57000
141 | fl=
142 | fn=top
143 | 5 6000
144 | cfl=
145 | cfn=mid1
146 | calls=1 13
147 | 5 25000
148 | cfl=
149 | cfn=mid2
150 | calls=1 20
151 | 5 3000
152 | cfl=
153 | cfn=mid3
154 | calls=1 28
155 | 5 21000
156 | cfl=
157 | cfn=samename:38
158 | calls=1 38
159 | 5 1000
160 | cfl=
161 | cfn=samename:44
162 | calls=1 44
163 | 5 1000
164 |
165 | fl=
166 | fn=mid1
167 | 13 8000
168 | cfl=
169 | cfn=mid2
170 | calls=5 20
171 | 13 15000
172 | cfl=
173 | cfn=bot
174 | calls=2 24
175 | 13 2000
176 |
177 | fl=
178 | fn=mid2
179 | 20 12000
180 | cfl=
181 | cfn=bot
182 | calls=6 24
183 | 20 6000
184 |
185 | fl=
186 | fn=bot
187 | 24 8000
188 |
189 | fl=
190 | fn=mid3
191 | 28 11000
192 | cfl=
193 | cfn=mid4
194 | calls=5 33
195 | 28 19000
196 |
197 | fl=
198 | fn=mid4
199 | 33 10000
200 | cfl=
201 | cfn=mid3
202 | calls=5 28
203 | 33 17000
204 |
205 | fl=
206 | fn=samename:38
207 | 38 1000
208 |
209 | fl=
210 | fn=samename:44
211 | 44 1000
212 |
213 | fl=~
214 | fn=
215 | 0 1000
216 |
217 | """.replace('', __file__)
218 |
--------------------------------------------------------------------------------
/test/test_integration.py:
--------------------------------------------------------------------------------
1 | import cProfile
2 | import pstats
3 | import sys
4 | import unittest
5 |
6 | from pyprof2calltree import CalltreeConverter
7 |
8 | from .profile_code import expected_output_py2, expected_output_py3, top
9 |
10 | try:
11 | from cStringIO import StringIO
12 | except ImportError:
13 | from io import StringIO
14 |
15 | if sys.version_info < (3, 0):
16 | expected_output = expected_output_py2
17 | else:
18 | expected_output = expected_output_py3
19 |
20 |
21 | class MockTimeProfile(cProfile.Profile):
22 | def __init__(self):
23 | self._mock_time = 0
24 | super(MockTimeProfile, self).__init__(self._timer, 1e-9)
25 |
26 | def _timer(self):
27 | now = self._mock_time
28 | self._mock_time += 1000
29 | return now
30 |
31 |
32 | class TestIntegration(unittest.TestCase):
33 | def setUp(self):
34 | self.profile = MockTimeProfile()
35 | self.profile.enable()
36 | top()
37 | self.profile.disable()
38 |
39 | def test_direct_entries(self):
40 | entries = self.profile.getstats()
41 | converter = CalltreeConverter(entries)
42 | out_file = StringIO()
43 |
44 | converter.output(out_file)
45 | self.assertEqual(out_file.getvalue(), expected_output)
46 |
47 | def test_pstats_data(self):
48 | stats = pstats.Stats(self.profile)
49 | converter = CalltreeConverter(stats)
50 | out_file = StringIO()
51 |
52 | converter.output(out_file)
53 | self.assertEqual(out_file.getvalue(), expected_output)
54 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | flake8
4 | isort
5 | py{27,34,35,36,37,38}
6 |
7 | [testenv]
8 | commands = python -m unittest discover
9 |
10 | [testenv:flake8]
11 | deps = flake8
12 | commands = flake8
13 | skip_install = true
14 |
15 | [testenv:isort]
16 | deps = isort >= 5.0.1
17 | commands = isort --check-only --diff .
18 | skip_install = true
19 |
--------------------------------------------------------------------------------