44import time
55import _remote_debugging
66import argparse
7+ import _colorize
8+ from _colorize import ANSIColors
79
810
911class 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
133323def 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