Skip to content

Commit 12f0da9

Browse files
pablogsallkollar
authored andcommitted
Sampling profiler improvements
Improve the display of sampling profile output, but adding colors and more unit and sorting options. It also provides useful summary of interesting stats, like most called functions, functions with highest impact, etc.
1 parent f4be5a7 commit 12f0da9

File tree

1 file changed

+278
-5
lines changed

1 file changed

+278
-5
lines changed

Lib/profile/sample.py

Lines changed: 278 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import time
55
import _remote_debugging
66
import argparse
7+
import _colorize
8+
from _colorize import ANSIColors
79

810

911
class SampleProfile:
@@ -56,10 +58,196 @@ def sample(self, duration_sec=10):
5658

5759
self.stats = self.convert_to_pstats(result)
5860

59-
def print_stats(self, sort=-1):
61+
def print_stats(self, sort=-1, limit=None, show_summary=True):
6062
if not isinstance(sort, tuple):
6163
sort = (sort,)
62-
pstats.SampledStats(self).strip_dirs().sort_stats(*sort).print_stats()
64+
stats = pstats.SampledStats(self).strip_dirs()
65+
66+
# Get the stats data
67+
stats_list = []
68+
for func, (cc, nc, tt, ct, callers) in stats.stats.items():
69+
stats_list.append((func, cc, nc, tt, ct, callers))
70+
71+
# Sort based on the requested field
72+
sort_field = sort[0]
73+
if sort_field == -1: # stdname
74+
stats_list.sort(key=lambda x: str(x[0]))
75+
elif sort_field == 0: # calls
76+
stats_list.sort(key=lambda x: x[2], reverse=True)
77+
elif sort_field == 1: # time
78+
stats_list.sort(key=lambda x: x[3], reverse=True)
79+
elif sort_field == 2: # cumulative
80+
stats_list.sort(key=lambda x: x[4], reverse=True)
81+
elif sort_field == 3: # percall
82+
stats_list.sort(
83+
key=lambda x: x[3] / x[2] if x[2] > 0 else 0, reverse=True
84+
)
85+
elif sort_field == 4: # cumpercall
86+
stats_list.sort(
87+
key=lambda x: x[4] / x[2] if x[2] > 0 else 0, reverse=True
88+
)
89+
90+
# Apply limit if specified
91+
if limit is not None:
92+
stats_list = stats_list[:limit]
93+
94+
# Find the maximum values for each column to determine units
95+
max_tt = max((tt for _, _, _, tt, _, _ in stats_list), default=0)
96+
max_ct = max((ct for _, _, _, _, ct, _ in stats_list), default=0)
97+
98+
# Determine appropriate units and format strings
99+
if max_tt >= 1.0:
100+
tt_unit = "s"
101+
tt_scale = 1.0
102+
elif max_tt >= 0.001:
103+
tt_unit = "ms"
104+
tt_scale = 1000.0
105+
else:
106+
tt_unit = "μs"
107+
tt_scale = 1000000.0
108+
109+
if max_ct >= 1.0:
110+
ct_unit = "s"
111+
ct_scale = 1.0
112+
elif max_ct >= 0.001:
113+
ct_unit = "ms"
114+
ct_scale = 1000.0
115+
else:
116+
ct_unit = "μs"
117+
ct_scale = 1000000.0
118+
119+
# Print header with colors and units
120+
header = (
121+
f"{ANSIColors.BOLD_BLUE}Profile Stats:{ANSIColors.RESET}\n"
122+
f"{ANSIColors.BOLD_BLUE}ncalls{ANSIColors.RESET} "
123+
f"{ANSIColors.BOLD_BLUE}tottime ({tt_unit}){ANSIColors.RESET} "
124+
f"{ANSIColors.BOLD_BLUE}percall ({tt_unit}){ANSIColors.RESET} "
125+
f"{ANSIColors.BOLD_BLUE}cumtime ({ct_unit}){ANSIColors.RESET} "
126+
f"{ANSIColors.BOLD_BLUE}percall ({ct_unit}){ANSIColors.RESET} "
127+
f"{ANSIColors.BOLD_BLUE}filename:lineno(function){ANSIColors.RESET}"
128+
)
129+
print(header)
130+
131+
# Print each line with colors
132+
for func, cc, nc, tt, ct, callers in stats_list:
133+
if nc != cc:
134+
ncalls = f"{nc}/{cc}"
135+
else:
136+
ncalls = str(nc)
137+
138+
# Format numbers with proper alignment and precision (no colors)
139+
tottime = f"{tt * tt_scale:8.3f}"
140+
percall = f"{(tt / nc) * tt_scale:8.3f}" if nc > 0 else " N/A"
141+
cumtime = f"{ct * ct_scale:8.3f}"
142+
cumpercall = (
143+
f"{(ct / nc) * ct_scale:8.3f}" if nc > 0 else " N/A"
144+
)
145+
146+
# Format the function name with colors
147+
func_name = (
148+
f"{ANSIColors.GREEN}{func[0]}{ANSIColors.RESET}:"
149+
f"{ANSIColors.YELLOW}{func[1]}{ANSIColors.RESET}("
150+
f"{ANSIColors.CYAN}{func[2]}{ANSIColors.RESET})"
151+
)
152+
153+
# Print the formatted line
154+
print(
155+
f"{ncalls:>8} {tottime} {percall} "
156+
f"{cumtime} {cumpercall} {func_name}"
157+
)
158+
159+
def _format_func_name(func):
160+
"""Format function name with colors."""
161+
return (
162+
f"{ANSIColors.GREEN}{func[0]}{ANSIColors.RESET}:"
163+
f"{ANSIColors.YELLOW}{func[1]}{ANSIColors.RESET}("
164+
f"{ANSIColors.CYAN}{func[2]}{ANSIColors.RESET})"
165+
)
166+
167+
def _print_top_functions(
168+
stats_list, title, key_func, format_line, n=3
169+
):
170+
"""Print top N functions sorted by key_func with formatted output."""
171+
print(f"\n{ANSIColors.BOLD_BLUE}{title}:{ANSIColors.RESET}")
172+
sorted_stats = sorted(stats_list, key=key_func, reverse=True)
173+
for stat in sorted_stats[:n]:
174+
if line := format_line(stat):
175+
print(f" {line}")
176+
177+
# Print summary of interesting functions if enabled
178+
if show_summary and stats_list:
179+
print(
180+
f"\n{ANSIColors.BOLD_BLUE}Summary of Interesting Functions:{ANSIColors.RESET}"
181+
)
182+
183+
# Most time-consuming functions (by total time)
184+
def format_time_consuming(stat):
185+
func, _, nc, tt, _, _ = stat
186+
if tt > 0:
187+
return (
188+
f"{tt * tt_scale:8.3f} {tt_unit} total time, "
189+
f"{(tt / nc) * tt_scale:8.3f} {tt_unit} per call: {_format_func_name(func)}"
190+
)
191+
return None
192+
193+
_print_top_functions(
194+
stats_list,
195+
"Most Time-Consuming Functions",
196+
key_func=lambda x: x[3],
197+
format_line=format_time_consuming,
198+
)
199+
200+
# Most called functions
201+
def format_most_called(stat):
202+
func, _, nc, tt, _, _ = stat
203+
if nc > 0:
204+
return (
205+
f"{nc:8d} calls, {(tt / nc) * tt_scale:8.3f} {tt_unit} "
206+
f"per call: {_format_func_name(func)}"
207+
)
208+
return None
209+
210+
_print_top_functions(
211+
stats_list,
212+
"Most Called Functions",
213+
key_func=lambda x: x[2],
214+
format_line=format_most_called,
215+
)
216+
217+
# Functions with highest per-call overhead
218+
def format_overhead(stat):
219+
func, _, nc, tt, _, _ = stat
220+
if nc > 0 and tt > 0:
221+
return (
222+
f"{(tt / nc) * tt_scale:8.3f} {tt_unit} per call, "
223+
f"{nc:8d} calls: {_format_func_name(func)}"
224+
)
225+
return None
226+
227+
_print_top_functions(
228+
stats_list,
229+
"Functions with Highest Per-Call Overhead",
230+
key_func=lambda x: x[3] / x[2] if x[2] > 0 else 0,
231+
format_line=format_overhead,
232+
)
233+
234+
# Functions with highest cumulative impact
235+
def format_cumulative(stat):
236+
func, _, nc, _, ct, _ = stat
237+
if ct > 0:
238+
return (
239+
f"{ct * ct_scale:8.3f} {ct_unit} cumulative time, "
240+
f"{(ct / nc) * ct_scale:8.3f} {ct_unit} per call: "
241+
f"{_format_func_name(func)}"
242+
)
243+
return None
244+
245+
_print_top_functions(
246+
stats_list,
247+
"Functions with Highest Cumulative Impact",
248+
key_func=lambda x: x[4],
249+
format_line=format_cumulative,
250+
)
63251

64252
def dump_stats(self, file):
65253
stats_with_marker = dict(self.stats)
@@ -121,18 +309,31 @@ def sample(
121309
duration_sec=10,
122310
filename=None,
123311
all_threads=False,
312+
limit=None,
313+
show_summary=True,
124314
):
125-
profile = SampleProfile(pid, sample_interval_usec, all_threads=False)
315+
profile = SampleProfile(pid, sample_interval_usec, all_threads=all_threads)
126316
profile.sample(duration_sec)
127317
if filename:
128318
profile.dump_stats(filename)
129319
else:
130-
profile.print_stats(sort)
320+
profile.print_stats(sort, limit, show_summary)
131321

132322

133323
def main():
134324
parser = argparse.ArgumentParser(
135-
description="Sample a process's stack frames.", color=True
325+
description=(
326+
"Sample a process's stack frames.\n\n"
327+
"Sort options:\n"
328+
" --sort-calls Sort by number of calls (most called functions first)\n"
329+
" --sort-time Sort by total time (most time-consuming functions first)\n"
330+
" --sort-cumulative Sort by cumulative time (functions with highest total impact first)\n"
331+
" --sort-percall Sort by time per call (functions with highest per-call overhead first)\n"
332+
" --sort-cumpercall Sort by cumulative time per call (functions with highest cumulative overhead per call)\n"
333+
" --sort-name Sort by function name (alphabetical order)\n\n"
334+
"The default sort is by cumulative time (--sort-cumulative)."
335+
),
336+
formatter_class=argparse.RawDescriptionHelpFormatter,
136337
)
137338
parser.add_argument("pid", type=int, help="Process ID to sample.")
138339
parser.add_argument(
@@ -156,14 +357,86 @@ def main():
156357
help="Sample all threads in the process",
157358
)
158359
parser.add_argument("-o", "--outfile", help="Save stats to <outfile>")
360+
parser.add_argument(
361+
"--no-color",
362+
action="store_true",
363+
help="Disable color output",
364+
)
365+
parser.add_argument(
366+
"-l",
367+
"--limit",
368+
type=int,
369+
help="Limit the number of rows in the output",
370+
)
371+
parser.add_argument(
372+
"--no-summary",
373+
action="store_true",
374+
help="Disable the summary section at the end of the output",
375+
)
376+
377+
# Add sorting options
378+
sort_group = parser.add_mutually_exclusive_group()
379+
sort_group.add_argument(
380+
"--sort-calls",
381+
action="store_const",
382+
const=0,
383+
dest="sort",
384+
help="Sort by number of calls (most called functions first)",
385+
)
386+
sort_group.add_argument(
387+
"--sort-time",
388+
action="store_const",
389+
const=1,
390+
dest="sort",
391+
help="Sort by total time (most time-consuming functions first)",
392+
)
393+
sort_group.add_argument(
394+
"--sort-cumulative",
395+
action="store_const",
396+
const=2,
397+
dest="sort",
398+
help="Sort by cumulative time (functions with highest total impact first)",
399+
)
400+
sort_group.add_argument(
401+
"--sort-percall",
402+
action="store_const",
403+
const=3,
404+
dest="sort",
405+
help="Sort by time per call (functions with highest per-call overhead first)",
406+
)
407+
sort_group.add_argument(
408+
"--sort-cumpercall",
409+
action="store_const",
410+
const=4,
411+
dest="sort",
412+
help="Sort by cumulative time per call (functions with highest cumulative overhead per call)",
413+
)
414+
sort_group.add_argument(
415+
"--sort-name",
416+
action="store_const",
417+
const=5,
418+
dest="sort",
419+
help="Sort by function name (alphabetical order)",
420+
)
421+
422+
# Set default sort to cumulative time
423+
parser.set_defaults(sort=2)
424+
159425
args = parser.parse_args()
160426

427+
# Set color theme based on --no-color flag
428+
if args.no_color:
429+
_colorize.set_theme(_colorize.theme_no_color)
430+
161431
sample(
162432
args.pid,
163433
sample_interval_usec=args.interval,
164434
duration_sec=args.duration,
165435
filename=args.outfile,
166436
all_threads=args.all_threads,
437+
limit=args.limit,
438+
sort=args.sort,
439+
show_summary=not args.no_summary,
167440
)
168441

169442

0 commit comments

Comments
 (0)