@@ -166,70 +166,94 @@ def _print_top_functions(stats_list, title, key_func, format_line, n=3):
166166 f"\n { ANSIColors .BOLD_BLUE } Summary of Interesting Functions:{ ANSIColors .RESET } "
167167 )
168168
169+ # Aggregate stats by fully qualified function name (ignoring line numbers)
170+ func_aggregated = {}
171+ for func , prim_calls , total_calls , total_time , cumulative_time , callers in stats_list :
172+ # Use filename:function_name as the key to get fully qualified name
173+ qualified_name = f"{ func [0 ]} :{ func [2 ]} "
174+ if qualified_name not in func_aggregated :
175+ func_aggregated [qualified_name ] = [0 , 0 , 0 , 0 ] # prim_calls, total_calls, total_time, cumulative_time
176+ func_aggregated [qualified_name ][0 ] += prim_calls
177+ func_aggregated [qualified_name ][1 ] += total_calls
178+ func_aggregated [qualified_name ][2 ] += total_time
179+ func_aggregated [qualified_name ][3 ] += cumulative_time
180+
181+ # Convert aggregated data back to list format for processing
182+ aggregated_stats = []
183+ for qualified_name , (prim_calls , total_calls , total_time , cumulative_time ) in func_aggregated .items ():
184+ # Parse the qualified name back to filename and function name
185+ if ":" in qualified_name :
186+ filename , func_name = qualified_name .rsplit (":" , 1 )
187+ else :
188+ filename , func_name = "" , qualified_name
189+ # Create a dummy func tuple with filename and function name for display
190+ dummy_func = (filename , "" , func_name )
191+ aggregated_stats .append ((dummy_func , prim_calls , total_calls , total_time , cumulative_time , {}))
192+
169193 # Most time-consuming functions (by total time)
170194 def format_time_consuming (stat ):
171- func , _ , nc , tt , _ , _ = stat
172- if tt > 0 :
195+ func , _ , total_calls , total_time , _ , _ = stat
196+ if total_time > 0 :
173197 return (
174- f"{ tt * tt_scale :8.3f} { tt_unit } total time, "
175- f"{ (tt / nc ) * tt_scale :8.3f} { tt_unit } per call: { _format_func_name (func )} "
198+ f"{ total_time * tt_scale :8.3f} { tt_unit } total time, "
199+ f"{ (total_time / total_calls ) * tt_scale :8.3f} { tt_unit } per call: { _format_func_name (func )} "
176200 )
177201 return None
178202
179203 _print_top_functions (
180- stats_list ,
204+ aggregated_stats ,
181205 "Most Time-Consuming Functions" ,
182206 key_func = lambda x : x [3 ],
183207 format_line = format_time_consuming ,
184208 )
185209
186210 # Most called functions
187211 def format_most_called (stat ):
188- func , _ , nc , tt , _ , _ = stat
189- if nc > 0 :
212+ func , _ , total_calls , total_time , _ , _ = stat
213+ if total_calls > 0 :
190214 return (
191- f"{ nc :8d} calls, { (tt / nc ) * tt_scale :8.3f} { tt_unit } "
215+ f"{ total_calls :8d} calls, { (total_time / total_calls ) * tt_scale :8.3f} { tt_unit } "
192216 f"per call: { _format_func_name (func )} "
193217 )
194218 return None
195219
196220 _print_top_functions (
197- stats_list ,
221+ aggregated_stats ,
198222 "Most Called Functions" ,
199223 key_func = lambda x : x [2 ],
200224 format_line = format_most_called ,
201225 )
202226
203227 # Functions with highest per-call overhead
204228 def format_overhead (stat ):
205- func , _ , nc , tt , _ , _ = stat
206- if nc > 0 and tt > 0 :
229+ func , _ , total_calls , total_time , _ , _ = stat
230+ if total_calls > 0 and total_time > 0 :
207231 return (
208- f"{ (tt / nc ) * tt_scale :8.3f} { tt_unit } per call, "
209- f"{ nc :8d} calls: { _format_func_name (func )} "
232+ f"{ (total_time / total_calls ) * tt_scale :8.3f} { tt_unit } per call, "
233+ f"{ total_calls :8d} calls: { _format_func_name (func )} "
210234 )
211235 return None
212236
213237 _print_top_functions (
214- stats_list ,
238+ aggregated_stats ,
215239 "Functions with Highest Per-Call Overhead" ,
216240 key_func = lambda x : x [3 ] / x [2 ] if x [2 ] > 0 else 0 ,
217241 format_line = format_overhead ,
218242 )
219243
220244 # Functions with highest cumulative impact
221245 def format_cumulative (stat ):
222- func , _ , nc , _ , ct , _ = stat
223- if ct > 0 :
246+ func , _ , total_calls , _ , cumulative_time , _ = stat
247+ if cumulative_time > 0 :
224248 return (
225- f"{ ct * ct_scale :8.3f} { ct_unit } cumulative time, "
226- f"{ (ct / nc ) * ct_scale :8.3f} { ct_unit } per call: "
249+ f"{ cumulative_time * ct_scale :8.3f} { ct_unit } cumulative time, "
250+ f"{ (cumulative_time / total_calls ) * ct_scale :8.3f} { ct_unit } per call: "
227251 f"{ _format_func_name (func )} "
228252 )
229253 return None
230254
231255 _print_top_functions (
232- stats_list ,
256+ aggregated_stats ,
233257 "Functions with Highest Cumulative Impact" ,
234258 key_func = lambda x : x [4 ],
235259 format_line = format_cumulative ,
@@ -270,125 +294,185 @@ def sample(
270294 else :
271295 collector .export (filename )
272296
297+ def _validate_collapsed_format_args (args , parser ):
298+ # Check for incompatible pstats options
299+ invalid_opts = []
300+
301+ # Get list of pstats-specific options
302+ pstats_options = {
303+ 'sort' : None ,
304+ 'limit' : None ,
305+ 'no_summary' : False
306+ }
307+
308+ # Find the default values from the argument definitions
309+ for action in parser ._actions :
310+ if action .dest in pstats_options and hasattr (action , 'default' ):
311+ pstats_options [action .dest ] = action .default
312+
313+ # Check if any pstats-specific options were provided by comparing with defaults
314+ for opt , default in pstats_options .items ():
315+ if getattr (args , opt ) != default :
316+ invalid_opts .append (opt .replace ('no_' , '' ))
317+
318+ if invalid_opts :
319+ parser .error (f"The following options are only valid with --pstats format: { ', ' .join (invalid_opts )} " )
320+
321+ # Set default output filename for collapsed format
322+ if not args .outfile :
323+ args .outfile = f"collapsed.{ args .pid } .txt"
324+
273325
274326def main ():
327+ # Create the main parser
275328 parser = argparse .ArgumentParser (
276329 description = (
277- "Sample a process's stack frames.\n \n "
278- "Sort options:\n "
279- " --sort-calls Sort by number of calls (most called functions first)\n "
280- " --sort-time Sort by total time (most time-consuming functions first)\n "
281- " --sort-cumulative Sort by cumulative time (functions with highest total impact first)\n "
282- " --sort-percall Sort by time per call (functions with highest per-call overhead first)\n "
283- " --sort-cumpercall Sort by cumulative time per call (functions with highest cumulative overhead per call)\n "
284- " --sort-name Sort by function name (alphabetical order)\n \n "
285- "The default sort is by cumulative time (--sort-cumulative)."
286- "Format descriptions:\n "
287- " pstats Standard Python profiler output format\n "
288- " collapsed Stack traces in collapsed format (file:function:line;file:function:line;... count)\n "
289- " Useful for generating flamegraphs with tools like flamegraph.pl"
330+ "Sample a process's stack frames and generate profiling data.\n "
331+ "Supports two output formats:\n "
332+ " - pstats: Detailed profiling statistics with sorting options\n "
333+ " - collapsed: Stack traces for generating flamegraphs\n "
334+ "\n "
335+ "Examples:\n "
336+ " # Profile process 1234 for 10 seconds with default settings\n "
337+ " python -m profile.sample 1234\n "
338+ "\n "
339+ " # Profile with custom interval and duration, save to file\n "
340+ " python -m profile.sample -i 50 -d 30 -o profile.stats 1234\n "
341+ "\n "
342+ " # Generate collapsed stacks for flamegraph\n "
343+ " python -m profile.sample --collapsed 1234\n "
344+ "\n "
345+ " # Profile all threads, sort by total time\n "
346+ " python -m profile.sample -a --sort-time 1234\n "
347+ "\n "
348+ " # Profile for 1 minute with 1ms sampling interval\n "
349+ " python -m profile.sample -i 1000 -d 60 1234\n "
350+ "\n "
351+ " # Show only top 20 functions sorted by calls\n "
352+ " python -m profile.sample --sort-calls -l 20 1234\n "
353+ "\n "
354+ " # Profile all threads and save collapsed stacks\n "
355+ " python -m profile.sample -a --collapsed -o stacks.txt 1234"
290356 ),
291- formatter_class = argparse .RawDescriptionHelpFormatter ,
292- color = True ,
357+ formatter_class = argparse .RawDescriptionHelpFormatter
293358 )
294- parser .add_argument ("pid" , type = int , help = "Process ID to sample." )
359+
360+ # Required arguments
295361 parser .add_argument (
296- "-i" ,
297- "--interval" ,
362+ "pid" ,
298363 type = int ,
299- default = 10 ,
300- help = "Sampling interval in microseconds (default: 10 usec)" ,
364+ help = "Process ID to sample"
301365 )
302- parser .add_argument (
303- "-d" ,
304- "--duration" ,
366+
367+ # Sampling options
368+ sampling_group = parser .add_argument_group ("Sampling configuration" )
369+ sampling_group .add_argument (
370+ "-i" , "--interval" ,
305371 type = int ,
306- default = 10 ,
307- help = "Sampling duration in seconds (default: 10 seconds)" ,
308- )
309- parser .add_argument (
310- "-a" ,
311- "--all-threads" ,
312- action = "store_true" ,
313- help = "Sample all threads in the process" ,
314- )
315- parser .add_argument ("-o" , "--outfile" , help = "Save stats to <outfile>" )
316- parser .add_argument (
317- "--no-color" ,
318- action = "store_true" ,
319- help = "Disable color output" ,
372+ default = 100 ,
373+ help = "Sampling interval in microseconds (default: 100)"
320374 )
321- parser .add_argument (
322- "-l" ,
323- "--limit" ,
375+ sampling_group .add_argument (
376+ "-d" , "--duration" ,
324377 type = int ,
325- help = "Limit the number of rows in the output" ,
378+ default = 10 ,
379+ help = "Sampling duration in seconds (default: 10)"
326380 )
327- parser .add_argument (
328- "--no-summary " ,
381+ sampling_group .add_argument (
382+ "-a" , "--all-threads " ,
329383 action = "store_true" ,
330- help = "Disable the summary section at the end of the output" ,
384+ help = "Sample all threads in the process instead of just the main thread"
331385 )
332- parser .add_argument (
333- "--format" ,
334- choices = ["pstats" , "collapsed" ],
386+
387+ # Output format selection
388+ output_group = parser .add_argument_group ("Output options" )
389+ output_format = output_group .add_mutually_exclusive_group ()
390+ output_format .add_argument (
391+ "--pstats" ,
392+ action = "store_const" ,
393+ const = "pstats" ,
394+ dest = "format" ,
335395 default = "pstats" ,
336- help = "Output format (default: pstats)" ,
396+ help = "Generate pstats output (default)"
397+ )
398+ output_format .add_argument (
399+ "--collapsed" ,
400+ action = "store_const" ,
401+ const = "collapsed" ,
402+ dest = "format" ,
403+ help = "Generate collapsed stack traces for flamegraphs"
404+ )
405+
406+ output_group .add_argument (
407+ "-o" , "--outfile" ,
408+ help = "Save output to a file (if omitted, prints to stdout for pstats, "
409+ "or saves to collapsed.<pid>.txt for collapsed format)"
337410 )
338411
339- # Add sorting options
340- sort_group = parser .add_mutually_exclusive_group ()
412+ # pstats-specific options
413+ pstats_group = parser .add_argument_group ("pstats format options" )
414+ sort_group = pstats_group .add_mutually_exclusive_group ()
341415 sort_group .add_argument (
342416 "--sort-calls" ,
343417 action = "store_const" ,
344418 const = 0 ,
345419 dest = "sort" ,
346- help = "Sort by number of calls (most called functions first)" ,
420+ help = "Sort by number of calls"
347421 )
348422 sort_group .add_argument (
349423 "--sort-time" ,
350424 action = "store_const" ,
351425 const = 1 ,
352426 dest = "sort" ,
353- help = "Sort by total time (most time-consuming functions first)" ,
427+ help = "Sort by total time"
354428 )
355429 sort_group .add_argument (
356430 "--sort-cumulative" ,
357431 action = "store_const" ,
358432 const = 2 ,
359433 dest = "sort" ,
360- help = "Sort by cumulative time (functions with highest total impact first)" ,
434+ default = 2 ,
435+ help = "Sort by cumulative time (default)"
361436 )
362437 sort_group .add_argument (
363438 "--sort-percall" ,
364439 action = "store_const" ,
365440 const = 3 ,
366441 dest = "sort" ,
367- help = "Sort by time per call (functions with highest per-call overhead first)" ,
442+ help = "Sort by time per call"
368443 )
369444 sort_group .add_argument (
370445 "--sort-cumpercall" ,
371446 action = "store_const" ,
372447 const = 4 ,
373448 dest = "sort" ,
374- help = "Sort by cumulative time per call (functions with highest cumulative overhead per call)" ,
449+ help = "Sort by cumulative time per call"
375450 )
376451 sort_group .add_argument (
377452 "--sort-name" ,
378453 action = "store_const" ,
379454 const = 5 ,
380455 dest = "sort" ,
381- help = "Sort by function name (alphabetical order)" ,
456+ help = "Sort by function name"
382457 )
383458
384- # Set default sort to cumulative time
385- parser .set_defaults (sort = 2 )
459+ pstats_group .add_argument (
460+ "-l" , "--limit" ,
461+ type = int ,
462+ help = "Limit the number of rows in the output" ,
463+ default = 15 ,
464+ )
465+ pstats_group .add_argument (
466+ "--no-summary" ,
467+ action = "store_true" ,
468+ help = "Disable the summary section in the output"
469+ )
386470
387471 args = parser .parse_args ()
388472
389- # Set color theme based on --no-color flag
390- if args .no_color :
391- _colorize . set_theme ( _colorize . theme_no_color )
473+ # Validate format-specific arguments
474+ if args .format == "collapsed" :
475+ _validate_collapsed_format_args ( args , parser )
392476
393477 sample (
394478 args .pid ,
@@ -401,7 +485,5 @@ def main():
401485 show_summary = not args .no_summary ,
402486 output_format = args .format ,
403487 )
404-
405-
406488if __name__ == "__main__" :
407489 main ()
0 commit comments