| 
2 | 2 | import _remote_debugging  | 
3 | 3 | import os  | 
4 | 4 | import pstats  | 
 | 5 | +import subprocess  | 
5 | 6 | import statistics  | 
6 | 7 | import sys  | 
7 | 8 | import sysconfig  | 
@@ -542,46 +543,66 @@ def main():  | 
542 | 543 |     parser = argparse.ArgumentParser(  | 
543 | 544 |         description=(  | 
544 | 545 |             "Sample a process's stack frames and generate profiling data.\n"  | 
545 |  | -            "Supports two output formats:\n"  | 
546 |  | -            "  - pstats: Detailed profiling statistics with sorting options\n"  | 
547 |  | -            "  - collapsed: Stack traces for generating flamegraphs\n"  | 
 | 546 | +            "Supports the following target modes:\n"  | 
 | 547 | +            "  - -p PID: Profile an existing process by PID\n"  | 
 | 548 | +            "  - -m MODULE [ARGS...]: Profile a module as python -m module ... \n"  | 
 | 549 | +            "  - filename [ARGS...]: Profile the specified script by running it in a subprocess\n"  | 
548 | 550 |             "\n"  | 
549 | 551 |             "Examples:\n"  | 
550 | 552 |             "  # Profile process 1234 for 10 seconds with default settings\n"  | 
551 |  | -            "  python -m profile.sample 1234\n"  | 
 | 553 | +            "  python -m profile.sample -p 1234\n"  | 
 | 554 | +            "\n"  | 
 | 555 | +            "  # Profile a script by running it in a subprocess\n"  | 
 | 556 | +            "  python -m profile.sample myscript.py arg1 arg2\n"  | 
 | 557 | +            "\n"  | 
 | 558 | +            "  # Profile a module by running it as python -m module in a subprocess\n"  | 
 | 559 | +            "  python -m profile.sample -m mymodule arg1 arg2\n"  | 
552 | 560 |             "\n"  | 
553 | 561 |             "  # Profile with custom interval and duration, save to file\n"  | 
554 |  | -            "  python -m profile.sample -i 50 -d 30 -o profile.stats 1234\n"  | 
 | 562 | +            "  python -m profile.sample -i 50 -d 30 -o profile.stats -p 1234\n"  | 
555 | 563 |             "\n"  | 
556 | 564 |             "  # Generate collapsed stacks for flamegraph\n"  | 
557 |  | -            "  python -m profile.sample --collapsed 1234\n"  | 
 | 565 | +            "  python -m profile.sample --collapsed -p 1234\n"  | 
558 | 566 |             "\n"  | 
559 | 567 |             "  # Profile all threads, sort by total time\n"  | 
560 |  | -            "  python -m profile.sample -a --sort-tottime 1234\n"  | 
 | 568 | +            "  python -m profile.sample -a --sort-tottime -p 1234\n"  | 
561 | 569 |             "\n"  | 
562 | 570 |             "  # Profile for 1 minute with 1ms sampling interval\n"  | 
563 |  | -            "  python -m profile.sample -i 1000 -d 60 1234\n"  | 
 | 571 | +            "  python -m profile.sample -i 1000 -d 60 -p 1234\n"  | 
564 | 572 |             "\n"  | 
565 | 573 |             "  # Show only top 20 functions sorted by direct samples\n"  | 
566 |  | -            "  python -m profile.sample --sort-nsamples -l 20 1234\n"  | 
 | 574 | +            "  python -m profile.sample --sort-nsamples -l 20 -p 1234\n"  | 
567 | 575 |             "\n"  | 
568 | 576 |             "  # Profile all threads and save collapsed stacks\n"  | 
569 |  | -            "  python -m profile.sample -a --collapsed -o stacks.txt 1234\n"  | 
 | 577 | +            "  python -m profile.sample -a --collapsed -o stacks.txt -p 1234\n"  | 
570 | 578 |             "\n"  | 
571 | 579 |             "  # Profile with real-time sampling statistics\n"  | 
572 |  | -            "  python -m profile.sample --realtime-stats 1234\n"  | 
 | 580 | +            "  python -m profile.sample --realtime-stats -p 1234\n"  | 
573 | 581 |             "\n"  | 
574 | 582 |             "  # Sort by sample percentage to find most sampled functions\n"  | 
575 |  | -            "  python -m profile.sample --sort-sample-pct 1234\n"  | 
 | 583 | +            "  python -m profile.sample --sort-sample-pct -p 1234\n"  | 
576 | 584 |             "\n"  | 
577 | 585 |             "  # Sort by cumulative samples to find functions most on call stack\n"  | 
578 |  | -            "  python -m profile.sample --sort-nsamples-cumul 1234"  | 
 | 586 | +            "  python -m profile.sample --sort-nsamples-cumul -p 1234\n"  | 
579 | 587 |         ),  | 
580 | 588 |         formatter_class=argparse.RawDescriptionHelpFormatter,  | 
581 | 589 |     )  | 
582 | 590 | 
 
  | 
583 |  | -    # Required arguments  | 
584 |  | -    parser.add_argument("pid", type=int, help="Process ID to sample")  | 
 | 591 | +    # Target selection  | 
 | 592 | +    target_group = parser.add_mutually_exclusive_group(required=True)  | 
 | 593 | +    target_group.add_argument(  | 
 | 594 | +        "-p", "--pid", type=int, help="Process ID to sample"  | 
 | 595 | +    )  | 
 | 596 | +    target_group.add_argument(  | 
 | 597 | +        "-m", "--module",  | 
 | 598 | +        nargs=argparse.REMAINDER,  | 
 | 599 | +        help="Run and profile a module as python -m module [ARGS...]"  | 
 | 600 | +    )  | 
 | 601 | +    target_group.add_argument(  | 
 | 602 | +        "script",  | 
 | 603 | +        nargs=argparse.REMAINDER,  | 
 | 604 | +        help="Script to run and profile, with optional arguments"  | 
 | 605 | +    )  | 
585 | 606 | 
 
  | 
586 | 607 |     # Sampling options  | 
587 | 608 |     sampling_group = parser.add_argument_group("Sampling configuration")  | 
@@ -712,19 +733,59 @@ def main():  | 
712 | 733 | 
 
  | 
713 | 734 |     sort_value = args.sort if args.sort is not None else 2  | 
714 | 735 | 
 
  | 
715 |  | -    sample(  | 
716 |  | -        args.pid,  | 
717 |  | -        sample_interval_usec=args.interval,  | 
718 |  | -        duration_sec=args.duration,  | 
719 |  | -        filename=args.outfile,  | 
720 |  | -        all_threads=args.all_threads,  | 
721 |  | -        limit=args.limit,  | 
722 |  | -        sort=sort_value,  | 
723 |  | -        show_summary=not args.no_summary,  | 
724 |  | -        output_format=args.format,  | 
725 |  | -        realtime_stats=args.realtime_stats,  | 
726 |  | -    )  | 
 | 736 | +    if not(args.pid or args.module or args.script):  | 
 | 737 | +        parser.error(  | 
 | 738 | +            "You must specify either a process ID (-p), a module (-m), or a script to run."  | 
 | 739 | +        )  | 
727 | 740 | 
 
  | 
 | 741 | +    if args.pid:  | 
 | 742 | +        sample(  | 
 | 743 | +            args.pid,  | 
 | 744 | +            sample_interval_usec=args.interval,  | 
 | 745 | +            duration_sec=args.duration,  | 
 | 746 | +            filename=args.outfile,  | 
 | 747 | +            all_threads=args.all_threads,  | 
 | 748 | +            limit=args.limit,  | 
 | 749 | +            sort=sort_value,  | 
 | 750 | +            show_summary=not args.no_summary,  | 
 | 751 | +            output_format=args.format,  | 
 | 752 | +            realtime_stats=args.realtime_stats,  | 
 | 753 | +        )  | 
 | 754 | +    elif args.module or args.script:  | 
 | 755 | +        if args.module:  | 
 | 756 | +            cmd = [sys.executable, "-m", *args.module]  | 
 | 757 | +        else:  | 
 | 758 | +            cmd = [sys.executable, *args.script]  | 
 | 759 | + | 
 | 760 | +        process = subprocess.Popen(cmd)  | 
 | 761 | + | 
 | 762 | +        try:  | 
 | 763 | +            exit_code = process.wait(timeout=0.1)  | 
 | 764 | +            sys.exit(exit_code)  | 
 | 765 | +        except subprocess.TimeoutExpired:  | 
 | 766 | +            pass  | 
 | 767 | + | 
 | 768 | +        try:  | 
 | 769 | +            sample(  | 
 | 770 | +                process.pid,  | 
 | 771 | +                sort=sort_value,  | 
 | 772 | +                sample_interval_usec=args.interval,  | 
 | 773 | +                duration_sec=args.duration,  | 
 | 774 | +                filename=args.outfile,  | 
 | 775 | +                all_threads=args.all_threads,  | 
 | 776 | +                limit=args.limit,  | 
 | 777 | +                show_summary=not args.no_summary,  | 
 | 778 | +                output_format=args.format,  | 
 | 779 | +                realtime_stats=args.realtime_stats,  | 
 | 780 | +            )  | 
 | 781 | +        finally:  | 
 | 782 | +            if process.poll() is None:  | 
 | 783 | +                process.terminate()  | 
 | 784 | +                try:  | 
 | 785 | +                    process.wait(timeout=2)  | 
 | 786 | +                except subprocess.TimeoutExpired:  | 
 | 787 | +                    process.kill()  | 
 | 788 | +                    process.wait()  | 
728 | 789 | 
 
  | 
729 | 790 | if __name__ == "__main__":  | 
730 | 791 |     main()  | 
0 commit comments