Skip to content

Commit 66603f7

Browse files
committed
Random improvements
1 parent 8544581 commit 66603f7

1 file changed

Lines changed: 229 additions & 4 deletions

File tree

Lib/profile/sample.py

Lines changed: 229 additions & 4 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,148 @@ 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 get_color_for_value(self, value, max_value, theme):
62+
"""Return a color code based on the value's proportion of the maximum.
63+
For profiling, higher values are more significant."""
64+
if max_value == 0:
65+
return ANSIColors.RESET
66+
67+
ratio = value / max_value
68+
if ratio > 0.8:
69+
return ANSIColors.BOLD_RED # Bright red for most significant values
70+
elif ratio > 0.6:
71+
return ANSIColors.RED # Red for very significant values
72+
elif ratio > 0.4:
73+
return ANSIColors.YELLOW # Yellow for significant values
74+
elif ratio > 0.2:
75+
return ANSIColors.GREEN # Green for somewhat significant values
76+
else:
77+
return ANSIColors.RESET # Default for less significant values
78+
79+
def print_stats(self, sort=-1, limit=None, show_summary=True):
6080
if not isinstance(sort, tuple):
6181
sort = (sort,)
62-
pstats.SampledStats(self).strip_dirs().sort_stats(*sort).print_stats()
82+
theme = _colorize.get_theme()
83+
stats = pstats.SampledStats(self).strip_dirs()
84+
85+
# Get the stats data
86+
stats_list = []
87+
for func, (cc, nc, tt, ct, callers) in stats.stats.items():
88+
stats_list.append((func, cc, nc, tt, ct, callers))
89+
90+
# Sort based on the requested field
91+
sort_field = sort[0]
92+
if sort_field == -1: # stdname
93+
stats_list.sort(key=lambda x: str(x[0]))
94+
elif sort_field == 0: # calls
95+
stats_list.sort(key=lambda x: x[2], reverse=True)
96+
elif sort_field == 1: # time
97+
stats_list.sort(key=lambda x: x[3], reverse=True)
98+
elif sort_field == 2: # cumulative
99+
stats_list.sort(key=lambda x: x[4], reverse=True)
100+
elif sort_field == 3: # percall
101+
stats_list.sort(key=lambda x: x[3]/x[2] if x[2] > 0 else 0, reverse=True)
102+
elif sort_field == 4: # cumpercall
103+
stats_list.sort(key=lambda x: x[4]/x[2] if x[2] > 0 else 0, reverse=True)
104+
105+
# Apply limit if specified
106+
if limit is not None:
107+
stats_list = stats_list[:limit]
108+
109+
# Find the maximum values for each column to determine units
110+
max_tt = max((tt for _, _, _, tt, _, _ in stats_list), default=0)
111+
max_ct = max((ct for _, _, _, _, ct, _ in stats_list), default=0)
112+
113+
# Determine appropriate units and format strings
114+
if max_tt >= 1.0:
115+
tt_unit = "s"
116+
tt_scale = 1.0
117+
elif max_tt >= 0.001:
118+
tt_unit = "ms"
119+
tt_scale = 1000.0
120+
else:
121+
tt_unit = "μs"
122+
tt_scale = 1000000.0
123+
124+
if max_ct >= 1.0:
125+
ct_unit = "s"
126+
ct_scale = 1.0
127+
elif max_ct >= 0.001:
128+
ct_unit = "ms"
129+
ct_scale = 1000.0
130+
else:
131+
ct_unit = "μs"
132+
ct_scale = 1000000.0
133+
134+
# Print header with colors and units
135+
header = (
136+
f"{ANSIColors.BOLD_BLUE}Profile Stats:{ANSIColors.RESET}\n"
137+
f"{ANSIColors.BOLD_BLUE}ncalls{ANSIColors.RESET} "
138+
f"{ANSIColors.BOLD_BLUE}tottime ({tt_unit}){ANSIColors.RESET} "
139+
f"{ANSIColors.BOLD_BLUE}percall ({tt_unit}){ANSIColors.RESET} "
140+
f"{ANSIColors.BOLD_BLUE}cumtime ({ct_unit}){ANSIColors.RESET} "
141+
f"{ANSIColors.BOLD_BLUE}percall ({ct_unit}){ANSIColors.RESET} "
142+
f"{ANSIColors.BOLD_BLUE}filename:lineno(function){ANSIColors.RESET}"
143+
)
144+
print(header)
145+
146+
# Print each line with colors
147+
for func, cc, nc, tt, ct, callers in stats_list:
148+
if nc != cc:
149+
ncalls = f"{nc}/{cc}"
150+
else:
151+
ncalls = str(nc)
152+
153+
# Format numbers with proper alignment and precision (no colors)
154+
tottime = f"{tt * tt_scale:8.3f}"
155+
percall = f"{(tt/nc) * tt_scale:8.3f}" if nc > 0 else " N/A"
156+
cumtime = f"{ct * ct_scale:8.3f}"
157+
cumpercall = f"{(ct/nc) * ct_scale:8.3f}" if nc > 0 else " N/A"
158+
159+
# Format the function name with colors
160+
func_name = (
161+
f"{ANSIColors.GREEN}{func[0]}{ANSIColors.RESET}:"
162+
f"{ANSIColors.YELLOW}{func[1]}{ANSIColors.RESET}("
163+
f"{ANSIColors.CYAN}{func[2]}{ANSIColors.RESET})"
164+
)
165+
166+
# Print the formatted line
167+
print(
168+
f"{ncalls:>8} {tottime} {percall} "
169+
f"{cumtime} {cumpercall} {func_name}"
170+
)
171+
172+
# Print summary of interesting functions if enabled
173+
if show_summary and stats_list:
174+
print(f"\n{ANSIColors.BOLD_BLUE}Summary of Interesting Functions:{ANSIColors.RESET}")
175+
176+
# Most time-consuming functions (by total time)
177+
print(f"\n{ANSIColors.BOLD_BLUE}Most Time-Consuming Functions:{ANSIColors.RESET}")
178+
for func, _, nc, tt, ct, _ in sorted(stats_list, key=lambda x: x[3], reverse=True)[:3]:
179+
if tt > 0:
180+
func_name = f"{func[0]}:{func[1]}({func[2]})"
181+
print(f" {tt * tt_scale:8.3f}{tt_unit} total time, {(tt/nc) * tt_scale:8.3f}{tt_unit} per call: {func_name}")
182+
183+
# Most called functions
184+
print(f"\n{ANSIColors.BOLD_BLUE}Most Called Functions:{ANSIColors.RESET}")
185+
for func, _, nc, tt, ct, _ in sorted(stats_list, key=lambda x: x[2], reverse=True)[:3]:
186+
if nc > 0:
187+
func_name = f"{func[0]}:{func[1]}({func[2]})"
188+
print(f" {nc:8d} calls, {(tt/nc) * tt_scale:8.3f}{tt_unit} per call: {func_name}")
189+
190+
# Functions with highest per-call overhead
191+
print(f"\n{ANSIColors.BOLD_BLUE}Functions with Highest Per-Call Overhead:{ANSIColors.RESET}")
192+
for func, _, nc, tt, ct, _ in sorted(stats_list, key=lambda x: x[3]/x[2] if x[2] > 0 else 0, reverse=True)[:3]:
193+
if nc > 0 and tt > 0:
194+
func_name = f"{func[0]}:{func[1]}({func[2]})"
195+
print(f" {(tt/nc) * tt_scale:8.3f}{tt_unit} per call, {nc:8d} calls: {func_name}")
196+
197+
# Functions with highest cumulative impact
198+
print(f"\n{ANSIColors.BOLD_BLUE}Functions with Highest Cumulative Impact:{ANSIColors.RESET}")
199+
for func, _, nc, tt, ct, _ in sorted(stats_list, key=lambda x: x[4], reverse=True)[:3]:
200+
if ct > 0:
201+
func_name = f"{func[0]}:{func[1]}({func[2]})"
202+
print(f" {ct * ct_scale:8.3f}{ct_unit} cumulative time, {(ct/nc) * ct_scale:8.3f}{ct_unit} per call: {func_name}")
63203

64204
def dump_stats(self, file):
65205
stats_with_marker = dict(self.stats)
@@ -121,18 +261,31 @@ def sample(
121261
duration_sec=10,
122262
filename=None,
123263
all_threads=False,
264+
limit=None,
265+
show_summary=True,
124266
):
125267
profile = SampleProfile(pid, sample_interval_usec, all_threads=False)
126268
profile.sample(duration_sec)
127269
if filename:
128270
profile.dump_stats(filename)
129271
else:
130-
profile.print_stats(sort)
272+
profile.print_stats(sort, limit, show_summary)
131273

132274

133275
def main():
134276
parser = argparse.ArgumentParser(
135-
description="Sample a process's stack frames.", color=True
277+
description=(
278+
"Sample a process's stack frames.\n\n"
279+
"Sort options:\n"
280+
" --sort-calls Sort by number of calls (most called functions first)\n"
281+
" --sort-time Sort by total time (most time-consuming functions first)\n"
282+
" --sort-cumulative Sort by cumulative time (functions with highest total impact first)\n"
283+
" --sort-percall Sort by time per call (functions with highest per-call overhead first)\n"
284+
" --sort-cumpercall Sort by cumulative time per call (functions with highest cumulative overhead per call)\n"
285+
" --sort-name Sort by function name (alphabetical order)\n\n"
286+
"The default sort is by cumulative time (--sort-cumulative)."
287+
),
288+
formatter_class=argparse.RawDescriptionHelpFormatter,
136289
)
137290
parser.add_argument("pid", type=int, help="Process ID to sample.")
138291
parser.add_argument(
@@ -156,14 +309,86 @@ def main():
156309
help="Sample all threads in the process",
157310
)
158311
parser.add_argument("-o", "--outfile", help="Save stats to <outfile>")
312+
parser.add_argument(
313+
"--no-color",
314+
action="store_true",
315+
help="Disable color output",
316+
)
317+
parser.add_argument(
318+
"-l",
319+
"--limit",
320+
type=int,
321+
help="Limit the number of rows in the output",
322+
)
323+
parser.add_argument(
324+
"--no-summary",
325+
action="store_true",
326+
help="Disable the summary section at the end of the output",
327+
)
328+
329+
# Add sorting options
330+
sort_group = parser.add_mutually_exclusive_group()
331+
sort_group.add_argument(
332+
"--sort-calls",
333+
action="store_const",
334+
const=0,
335+
dest="sort",
336+
help="Sort by number of calls (most called functions first)",
337+
)
338+
sort_group.add_argument(
339+
"--sort-time",
340+
action="store_const",
341+
const=1,
342+
dest="sort",
343+
help="Sort by total time (most time-consuming functions first)",
344+
)
345+
sort_group.add_argument(
346+
"--sort-cumulative",
347+
action="store_const",
348+
const=2,
349+
dest="sort",
350+
help="Sort by cumulative time (functions with highest total impact first)",
351+
)
352+
sort_group.add_argument(
353+
"--sort-percall",
354+
action="store_const",
355+
const=3,
356+
dest="sort",
357+
help="Sort by time per call (functions with highest per-call overhead first)",
358+
)
359+
sort_group.add_argument(
360+
"--sort-cumpercall",
361+
action="store_const",
362+
const=4,
363+
dest="sort",
364+
help="Sort by cumulative time per call (functions with highest cumulative overhead per call)",
365+
)
366+
sort_group.add_argument(
367+
"--sort-name",
368+
action="store_const",
369+
const=5,
370+
dest="sort",
371+
help="Sort by function name (alphabetical order)",
372+
)
373+
374+
# Set default sort to cumulative time
375+
parser.set_defaults(sort=2)
376+
159377
args = parser.parse_args()
160378

379+
# Set color theme based on --no-color flag
380+
if args.no_color:
381+
_colorize.set_theme(_colorize.theme_no_color)
382+
161383
sample(
162384
args.pid,
163385
sample_interval_usec=args.interval,
164386
duration_sec=args.duration,
165387
filename=args.outfile,
166388
all_threads=args.all_threads,
389+
limit=args.limit,
390+
sort=args.sort,
391+
show_summary=not args.no_summary,
167392
)
168393

169394

0 commit comments

Comments
 (0)