Skip to content

Commit 7c32f7b

Browse files
committed
gh-125885: pstats: Allow sorting by caller/callee
1 parent 843d28f commit 7c32f7b

File tree

2 files changed

+97
-24
lines changed

2 files changed

+97
-24
lines changed

Doc/library/profile.rst

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,13 @@ is still sorted according to the last criteria) do::
196196

197197
p.print_callers(.5, 'init')
198198

199-
and you would get a list of callers for each of the listed functions.
199+
and you would get a list of all the callers for each of the listed functions.
200+
That may be a very long list, so if you want to sort or limit each function's
201+
list of callers, you might put::
202+
203+
p.print_callers(.5, 'init', callers_sort=SortKey.TIME, callers_filter=5)
204+
205+
to get the top 5 callers by time of each ``init`` function in the top 50%.
200206

201207
If you want more functionality, you're going to have to read the manual, or
202208
guess what the following functions do::
@@ -507,7 +513,8 @@ Analysis of the profiler data is done using the :class:`~pstats.Stats` class.
507513
and then proceed to only print the first 10% of them.
508514

509515

510-
.. method:: print_callers(*restrictions)
516+
.. method:: print_callers(*restrictions, callers_sort_key=(),
517+
callers_filter=())
511518

512519
This method for the :class:`Stats` class prints a list of all functions
513520
that called each function in the profiled database. The ordering is
@@ -526,13 +533,23 @@ Analysis of the profiler data is done using the :class:`~pstats.Stats` class.
526533
cumulative times spent in the current function while it was invoked by
527534
this specific caller.
528535

536+
By default, all callers of a function are printed in lexicographic order.
537+
To sort them differently, you can supply sort criteria in the keyword
538+
argument ``callers_sort_key``. These use exactly the same format as in
539+
:meth:`~pstats.Stats.sort_stats`, though you must supply them each time
540+
you call this method. And to limit the number of callers printed per
541+
function, you can supply restrictions in the keyword argument
542+
``callers_filter``, as you would in :meth:`~pstats.Stats.print_stats`.
543+
529544

530-
.. method:: print_callees(*restrictions)
545+
.. method:: print_callees(*restrictions, callees_sort_key=(),
546+
callees_filter=())
531547

532548
This method for the :class:`Stats` class prints a list of all function
533549
that were called by the indicated function. Aside from this reversal of
534-
direction of calls (re: called vs was called by), the arguments and
535-
ordering are identical to the :meth:`~pstats.Stats.print_callers` method.
550+
direction of calls (re: called vs was called by), and names of the
551+
keyword arguments, the arguments and ordering are identical to the
552+
:meth:`~pstats.Stats.print_callers` method.
536553

537554

538555
.. method:: get_stats_profile()

Lib/pstats.py

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,7 @@ def get_sort_arg_defs(self):
232232
del dict[word]
233233
return self.sort_arg_dict
234234

235-
def sort_stats(self, *field):
236-
if not field:
237-
self.fcn_list = 0
238-
return self
235+
def get_sort_tuple_and_type(self, *field):
239236
if len(field) == 1 and isinstance(field[0], int):
240237
# Be compatible with old profiler
241238
field = [ {-1: "stdname",
@@ -249,26 +246,35 @@ def sort_stats(self, *field):
249246

250247
sort_arg_defs = self.get_sort_arg_defs()
251248

252-
sort_tuple = ()
253-
self.sort_type = ""
254-
connector = ""
249+
sort_fields = []
250+
sort_field_types = []
255251
for word in field:
256252
if isinstance(word, SortKey):
257253
word = word.value
258-
sort_tuple = sort_tuple + sort_arg_defs[word][0]
259-
self.sort_type += connector + sort_arg_defs[word][1]
260-
connector = ", "
254+
sort_fields.extend(sort_arg_defs[word][0])
255+
sort_field_types.append(sort_arg_defs[word][1])
256+
257+
return (tuple(sort_fields), ", ".join(sort_field_types))
258+
259+
def sort_stats(self, *field):
260+
if not field:
261+
self.fcn_list = 0
262+
return self
263+
264+
sort_tuple, self.sort_type = self.get_sort_tuple_and_type(*field)
261265

262266
stats_list = []
263267
for func, (cc, nc, tt, ct, callers) in self.stats.items():
268+
# matches sort_arg_defs: cc, nc, tt, ct, module, line, name, stdname
264269
stats_list.append((cc, nc, tt, ct) + func +
265-
(func_std_string(func), func))
270+
(func_std_string(func),))
266271

267272
stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
268273

274+
# store only the order of the funcs
269275
self.fcn_list = fcn_list = []
270276
for tuple in stats_list:
271-
fcn_list.append(tuple[-1])
277+
fcn_list.append(tuple[4:7])
272278
return self
273279

274280
def reverse_order(self):
@@ -432,28 +438,48 @@ def print_stats(self, *amount):
432438
print(file=self.stream)
433439
return self
434440

435-
def print_callees(self, *amount):
441+
def print_callees(self, *amount, callees_sort_key=None, callees_filter=()):
436442
width, list = self.get_print_list(amount)
437443
if list:
438444
self.calc_callees()
439445

446+
sort_tuple = None
447+
sort_type = None
448+
if callees_sort_key:
449+
if isinstance(callees_sort_key, str):
450+
callees_sort_key = (callees_sort_key,)
451+
sort_tuple, sort_type = self.get_sort_tuple_and_type(*callees_sort_key)
452+
print(" Callees ordered by: " + sort_type + "\n")
453+
if not isinstance(callees_filter, tuple):
454+
callees_filter = (callees_filter,)
440455
self.print_call_heading(width, "called...")
441456
for func in list:
442457
if func in self.all_callees:
443-
self.print_call_line(width, func, self.all_callees[func])
458+
self.print_call_line(width, func, self.all_callees[func],
459+
sort_tuple=sort_tuple, sort_type=sort_type, sel_list=callees_filter)
444460
else:
445461
self.print_call_line(width, func, {})
446462
print(file=self.stream)
447463
print(file=self.stream)
448464
return self
449465

450-
def print_callers(self, *amount):
466+
def print_callers(self, *amount, callers_sort_key=None, callers_filter=()):
451467
width, list = self.get_print_list(amount)
452468
if list:
469+
sort_tuple = None
470+
sort_type = None
471+
if callers_sort_key:
472+
if isinstance(callers_sort_key, str):
473+
callers_sort_key = (callers_sort_key,)
474+
sort_tuple, sort_type = self.get_sort_tuple_and_type(*callers_sort_key)
475+
print(" Callers ordered by: " + sort_type + "\n")
476+
if not isinstance(callers_filter, tuple):
477+
callers_filter = (callers_filter,)
453478
self.print_call_heading(width, "was called by...")
454479
for func in list:
455480
cc, nc, tt, ct, callers = self.stats[func]
456-
self.print_call_line(width, func, callers, "<-")
481+
self.print_call_line(width, func, callers, arrow="<-",
482+
sort_tuple=sort_tuple, sort_type=sort_type, sel_list=callers_filter)
457483
print(file=self.stream)
458484
print(file=self.stream)
459485
return self
@@ -470,14 +496,42 @@ def print_call_heading(self, name_size, column_title):
470496
if subheader:
471497
print(" "*name_size + " ncalls tottime cumtime", file=self.stream)
472498

473-
def print_call_line(self, name_size, source, call_dict, arrow="->"):
499+
def print_call_line(self, name_size, source, call_dict, arrow="->", sort_tuple=None, sort_type=None, sel_list=()):
474500
print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)
475501
if not call_dict:
476502
print(file=self.stream)
477503
return
478-
clist = sorted(call_dict.keys())
504+
505+
if sort_tuple:
506+
stats_list = []
507+
calls_only = False
508+
for func, value in call_dict.items():
509+
if isinstance(value, tuple):
510+
nc, cc, tt, ct = value # cProfile orders it this way
511+
# matches sort_arg_defs: cc, nc, tt, ct, module, line, name, stdname
512+
stats_list.append((cc, nc, tt, ct) + func +
513+
(func_std_string(func),))
514+
else:
515+
if not calls_only:
516+
if "time" in sort_type:
517+
raise TypeError("Caller/callee stats for %s do not have time information. "
518+
"Try using cProfile instead of profile if you wish to record time by caller/callee."
519+
% func_std_string(func))
520+
calls_only = True
521+
stats_list.append((None, value, None, None) + func +
522+
(func_std_string(func),))
523+
524+
stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
525+
funclist = [t[4:7] for t in stats_list]
526+
else:
527+
funclist = list(sorted(call_dict.keys()))
528+
529+
msg = ""
530+
for selection in sel_list:
531+
funclist, msg = self.eval_print_amount(selection, funclist, msg)
532+
479533
indent = ""
480-
for func in clist:
534+
for func in funclist:
481535
name = func_std_string(func)
482536
value = call_dict[func]
483537
if isinstance(value, tuple):
@@ -494,6 +548,8 @@ def print_call_line(self, name_size, source, call_dict, arrow="->"):
494548
left_width = name_size + 3
495549
print(indent*left_width + substats, file=self.stream)
496550
indent = " "
551+
if msg:
552+
print(msg, file=self.stream)
497553

498554
def print_title(self):
499555
print(' ncalls tottime percall cumtime percall', end=' ', file=self.stream)

0 commit comments

Comments
 (0)