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