Skip to content

Commit f6da3b6

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 fda056e commit f6da3b6

File tree

4 files changed

+128
-4
lines changed

4 files changed

+128
-4
lines changed

Lib/cProfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def print_stats(self, sort=-1):
4141
import pstats
4242
if not isinstance(sort, tuple):
4343
sort = (sort,)
44-
pstats.Stats(self).strip_dirs().sort_stats(*sort).print_stats()
44+
pstats.Stats(self).strip_non_unique_dirs().sort_stats(*sort).print_stats()
4545

4646
def dump_stats(self, file):
4747
import marshal

Lib/pstats.py

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

29+
from collections import Counter
2930
from enum import StrEnum, _simple_enum
3031
from functools import cmp_to_key
3132
from dataclasses import dataclass
33+
from os.path import join
34+
from pathlib import Path
3235

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

@@ -276,16 +279,19 @@ def reverse_order(self):
276279
return self
277280

278281
def strip_dirs(self):
282+
return self._strip_directory_data(lambda fullpath_linenum_funcname: func_strip_path(fullpath_linenum_funcname))
283+
284+
def _strip_directory_data(self, strip_function):
279285
oldstats = self.stats
280286
self.stats = newstats = {}
281287
max_name_len = 0
282288
for func, (cc, nc, tt, ct, callers) in oldstats.items():
283-
newfunc = func_strip_path(func)
289+
newfunc = strip_function(func)
284290
if len(func_std_string(newfunc)) > max_name_len:
285291
max_name_len = len(func_std_string(newfunc))
286292
newcallers = {}
287293
for func2, caller in callers.items():
288-
newcallers[func_strip_path(func2)] = caller
294+
newcallers[strip_function(func2)] = caller
289295

290296
if newfunc in newstats:
291297
newstats[newfunc] = add_func_stats(
@@ -296,14 +302,30 @@ def strip_dirs(self):
296302
old_top = self.top_level
297303
self.top_level = new_top = set()
298304
for func in old_top:
299-
new_top.add(func_strip_path(func))
305+
new_top.add(strip_function(func))
300306

301307
self.max_name_len = max_name_len
302308

303309
self.fcn_list = None
304310
self.all_callees = None
305311
return self
306312

313+
def strip_non_unique_dirs(self):
314+
full_paths = set()
315+
316+
for (full_path, _, _), (_, _, _, _, callers) in self.stats.items():
317+
full_paths.add(full_path)
318+
for (full_path_caller, _, _), _ in callers.items():
319+
full_paths.add(full_path_caller)
320+
321+
minimal_path_by_full_path = _build_minimal_path_by_full_path(full_paths)
322+
323+
def strip_function(fullpath_linenum_funcname):
324+
fullpath, linenum, funcname = fullpath_linenum_funcname
325+
return minimal_path_by_full_path[fullpath], linenum, funcname
326+
327+
return self._strip_directory_data(strip_function)
328+
307329
def calc_callees(self):
308330
if self.all_callees:
309331
return
@@ -774,4 +796,41 @@ def postcmd(self, stop, line):
774796
except KeyboardInterrupt:
775797
pass
776798

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

Lib/test/test_pstats.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import unittest
2+
from os.path import join
23

34
from test import support
45
from io import StringIO
56
from pstats import SortKey
7+
from pstats import _build_minimal_path_by_full_path
68
from enum import StrEnum, _test_simple_enum
79

810
import os
@@ -148,5 +150,66 @@ def test_SortKey_enum(self):
148150
self.assertEqual(SortKey.FILENAME, 'filename')
149151
self.assertNotEqual(SortKey.FILENAME, SortKey.CALLS)
150152

153+
class BuildMinimalPathTests(unittest.TestCase):
154+
def test_non_unique(self):
155+
self.assertEqual(
156+
_build_minimal_path_by_full_path([
157+
'foo/bar',
158+
'foo/bar',
159+
]),
160+
{
161+
'foo/bar': 'bar',
162+
},
163+
)
164+
165+
def test_needs_no_minimizing(self):
166+
self.assertEqual(
167+
_build_minimal_path_by_full_path([
168+
'foo',
169+
'bar',
170+
]),
171+
{
172+
'foo': 'foo',
173+
'bar': 'bar',
174+
},
175+
)
176+
177+
def test_normal_case(self):
178+
self.assertEqual(
179+
_build_minimal_path_by_full_path([
180+
join('foo', 'bar'),
181+
join('baz', 'bar'),
182+
join('apple', 'orange'),
183+
]),
184+
{
185+
join('foo', 'bar'): join('foo', 'bar'),
186+
join('baz', 'bar'): join('baz', 'bar'),
187+
join('apple', 'orange'): 'orange',
188+
}
189+
)
190+
191+
def test_intermediate(self):
192+
self.assertEqual(
193+
_build_minimal_path_by_full_path([
194+
join('apple', 'mango', 'orange', 'grape', 'melon'),
195+
join('apple', 'mango', 'lemon', 'grape', 'melon'),
196+
]),
197+
{
198+
join('apple', 'mango', 'orange', 'grape', 'melon'): join('orange', 'grape', 'melon'),
199+
join('apple', 'mango', 'lemon', 'grape', 'melon'): join('lemon', 'grape', 'melon'),
200+
}
201+
)
202+
203+
def test_dunder_init_special_case(self):
204+
self.assertEqual(
205+
_build_minimal_path_by_full_path([
206+
join('apple', 'mango', 'orange', 'grape', '__init__.py'),
207+
]),
208+
{
209+
join('apple', 'mango', 'orange', 'grape', '__init__.py'): join('grape', '__init__.py'),
210+
}
211+
)
212+
213+
151214
if __name__ == "__main__":
152215
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)