Skip to content

Commit 63e8549

Browse files
committed
Default cProfile command line to cumulative time
Display one folder level for pstats.stripdirs() when filename starts with __ (__init__.py, __main__.py primarily)
1 parent 3635388 commit 63e8549

File tree

4 files changed

+128
-6
lines changed

4 files changed

+128
-6
lines changed

Lib/cProfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class Profile(_lsprof.Profiler):
3939

4040
def print_stats(self, sort=-1):
4141
import pstats
42-
pstats.Stats(self).strip_dirs().sort_stats(sort).print_stats()
42+
pstats.Stats(self).strip_non_unique_dirs().sort_stats(sort).print_stats()
4343

4444
def dump_stats(self, file):
4545
import marshal
@@ -140,7 +140,7 @@ def main():
140140
help="Save stats to <outfile>", default=None)
141141
parser.add_option('-s', '--sort', dest="sort",
142142
help="Sort order when printing to stdout, based on pstats.Stats class",
143-
default=-1,
143+
default=2,
144144
choices=sorted(pstats.Stats.sort_arg_dict_default))
145145
parser.add_option('-m', dest="module", action="store_true",
146146
help="Profile a library module", default=False)

Lib/pstats.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
import marshal
2727
import re
2828

29+
from collections import Counter
2930
from enum import Enum
3031
from functools import cmp_to_key
3132
from dataclasses import dataclass
3233
from typing import Dict
34+
from os.path import join
35+
from pathlib import Path
3336

3437
__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
3538

@@ -278,16 +281,19 @@ def reverse_order(self):
278281
return self
279282

280283
def strip_dirs(self):
284+
return self._strip_directory_data(lambda fullpath_linenum_funcname: func_strip_path(fullpath_linenum_funcname))
285+
286+
def _strip_directory_data(self, strip_function):
281287
oldstats = self.stats
282288
self.stats = newstats = {}
283289
max_name_len = 0
284290
for func, (cc, nc, tt, ct, callers) in oldstats.items():
285-
newfunc = func_strip_path(func)
291+
newfunc = strip_function(func)
286292
if len(func_std_string(newfunc)) > max_name_len:
287293
max_name_len = len(func_std_string(newfunc))
288294
newcallers = {}
289295
for func2, caller in callers.items():
290-
newcallers[func_strip_path(func2)] = caller
296+
newcallers[strip_function(func2)] = caller
291297

292298
if newfunc in newstats:
293299
newstats[newfunc] = add_func_stats(
@@ -298,14 +304,30 @@ def strip_dirs(self):
298304
old_top = self.top_level
299305
self.top_level = new_top = set()
300306
for func in old_top:
301-
new_top.add(func_strip_path(func))
307+
new_top.add(strip_function(func))
302308

303309
self.max_name_len = max_name_len
304310

305311
self.fcn_list = None
306312
self.all_callees = None
307313
return self
308314

315+
def strip_non_unique_dirs(self):
316+
full_paths = set()
317+
318+
for (full_path, _, _), (_, _, _, _, callers) in self.stats.items():
319+
full_paths.add(full_path)
320+
for (full_path_caller, _, _), _ in callers.items():
321+
full_paths.add(full_path_caller)
322+
323+
minimal_path_by_full_path = _build_minimal_path_by_full_path(full_paths)
324+
325+
def strip_function(fullpath_linenum_funcname):
326+
fullpath, linenum, funcname = fullpath_linenum_funcname
327+
return minimal_path_by_full_path[fullpath], linenum, funcname
328+
329+
return self._strip_directory_data(strip_function)
330+
309331
def calc_callees(self):
310332
if self.all_callees:
311333
return
@@ -776,4 +798,41 @@ def postcmd(self, stop, line):
776798
except KeyboardInterrupt:
777799
pass
778800

801+
802+
def _build_minimal_path_by_full_path(paths):
803+
if not paths:
804+
return paths
805+
806+
completed = {
807+
full_path: None
808+
for full_path in paths
809+
}
810+
split_path_by_full = {full_path: Path(full_path).parts for full_path in paths}
811+
max_step = max(len(x) for x in split_path_by_full.values())
812+
step = 1
813+
while step <= max_step:
814+
short_path_by_full = {
815+
full_path: split_path_by_full[full_path][-step:]
816+
for full_path, value in completed.items()
817+
if value is None
818+
}
819+
full_path_by_short = {v: k for k, v in short_path_by_full.items()}
820+
821+
for short_path, count in Counter(short_path_by_full.values()).most_common():
822+
if count == 1:
823+
joined_short_path = join(*short_path)
824+
# __init__.py is handled specially because it's a very common
825+
# file name which gives no clue what file is meant
826+
if joined_short_path == '__init__.py':
827+
continue
828+
completed[full_path_by_short[short_path]] = joined_short_path
829+
830+
step += 1
831+
832+
return {
833+
full_path: short_path if short_path is not None else full_path
834+
for full_path, short_path in completed.items()
835+
}
836+
837+
779838
# That's all, folks.

Lib/test/test_pstats.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from test import support
44
from io import StringIO
55
from pstats import SortKey
6-
6+
from pstats import _build_minimal_path_by_full_path
77
import pstats
88
import cProfile
99

@@ -99,5 +99,66 @@ def test_SortKey_enum(self):
9999
self.assertEqual(SortKey.FILENAME, 'filename')
100100
self.assertNotEqual(SortKey.FILENAME, SortKey.CALLS)
101101

102+
class BuildMinimalPathTests(unittest.TestCase):
103+
def test_non_unique(self):
104+
self.assertEqual(
105+
_build_minimal_path_by_full_path([
106+
'foo/bar',
107+
'foo/bar',
108+
]),
109+
{
110+
'foo/bar': 'bar',
111+
},
112+
)
113+
114+
def test_needs_no_minimizing(self):
115+
self.assertEqual(
116+
_build_minimal_path_by_full_path([
117+
'foo',
118+
'bar',
119+
]),
120+
{
121+
'foo': 'foo',
122+
'bar': 'bar',
123+
},
124+
)
125+
126+
def test_normal_case(self):
127+
self.assertEqual(
128+
_build_minimal_path_by_full_path([
129+
'foo/bar',
130+
'baz/bar',
131+
'apple/orange',
132+
]),
133+
{
134+
'foo/bar': 'foo/bar',
135+
'baz/bar': 'baz/bar',
136+
'apple/orange': 'orange',
137+
}
138+
)
139+
140+
def test_intermediate(self):
141+
self.assertEqual(
142+
_build_minimal_path_by_full_path([
143+
'apple/mango/orange/grape/melon',
144+
'apple/mango/lemon/grape/melon',
145+
]),
146+
{
147+
'apple/mango/orange/grape/melon': 'orange/grape/melon',
148+
'apple/mango/lemon/grape/melon': 'lemon/grape/melon',
149+
}
150+
)
151+
152+
def test_dunder_init_special_case(self):
153+
self.assertEqual(
154+
_build_minimal_path_by_full_path([
155+
'apple/mango/orange/grape/__init__.py',
156+
]),
157+
{
158+
'apple/mango/orange/grape/__init__.py': 'grape/__init__.py',
159+
}
160+
)
161+
162+
102163
if __name__ == "__main__":
103164
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cProfile command line output now defaults to cumulative time
2+
cProfile command line gives you unique filenames in the output (plus an extra path level of path if the filename is __init__.py)

0 commit comments

Comments
 (0)