From f0f7150feb1aacb063047238bc0e8f113d7c1c37 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 25 Jun 2025 11:50:54 -0400 Subject: [PATCH 1/4] PYTHON-5404 - Add docs for profiling execution --- CONTRIBUTING.md | 14 ++++++++++++++ justfile | 3 +++ tools/generate_flamegraph.sh | 4 ++++ 3 files changed, 21 insertions(+) create mode 100755 tools/generate_flamegraph.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a2bf4d913..6a97f474af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -460,3 +460,17 @@ partially-converted asynchronous version of the same name to the `test/asynchron Use this generated file as a starting point for the completed conversion. The script is used like so: `python tools/convert_test_to_async.py [test_file.py]` + +## Generating a flame graph using py-spy +To profile a test script and generate a flame graph, follow these steps: +1. Install `py-spy` if you haven't already: + ```bash + pip install py-spy + ``` +2. Inside your test script, perform any required setup and then loop over the code you want to profile for improved sampling +3. Run the `flamegraph` justfile target to generate a `.svg` file containing the flame graph: + ```bash + just flamegraph + ``` +4. Profiling should be done on a Linux system, as macOS and Windows do not support the `--native` option of `py-spy`. + Creating an ubuntu Evergreen spawn host and using `scp` to copy the flamegraph `.svg` file back to your local machine is the best way to do this. diff --git a/justfile b/justfile index 74ebb48823..59d1b3810c 100644 --- a/justfile +++ b/justfile @@ -68,6 +68,9 @@ setup-tests *args="": teardown-tests: bash .evergreen/scripts/teardown-tests.sh +flamegraph *args: + bash tools/generate_flamegraph.sh {{args}} + [group('server')] run-server *args="": bash .evergreen/scripts/run-server.sh {{args}} diff --git a/tools/generate_flamegraph.sh b/tools/generate_flamegraph.sh new file mode 100755 index 0000000000..15e5589386 --- /dev/null +++ b/tools/generate_flamegraph.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eu + +sudo py-spy record -o ${1:-profile}.svg -r ${2:-2000} -- python $3 From 8c929bef24b0fdf6065780a69abd3d4c118018fb Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 26 Jun 2025 11:21:32 -0400 Subject: [PATCH 2/4] Address review --- CONTRIBUTING.md | 2 +- tools/generate_flamegraph.py | 58 ++++++++++++++++++++++++++++++++++++ tools/generate_flamegraph.sh | 4 --- 3 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 tools/generate_flamegraph.py delete mode 100755 tools/generate_flamegraph.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a97f474af..9d89c9107f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -472,5 +472,5 @@ To profile a test script and generate a flame graph, follow these steps: ```bash just flamegraph ``` -4. Profiling should be done on a Linux system, as macOS and Windows do not support the `--native` option of `py-spy`. +4. If you need to include native code (for example the C extensions), profiling should be done on a Linux system, as macOS and Windows do not support the `--native` option of `py-spy`. Creating an ubuntu Evergreen spawn host and using `scp` to copy the flamegraph `.svg` file back to your local machine is the best way to do this. diff --git a/tools/generate_flamegraph.py b/tools/generate_flamegraph.py new file mode 100644 index 0000000000..d4f1222b15 --- /dev/null +++ b/tools/generate_flamegraph.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import argparse +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser(description="Generate a flamegraph of a given Python script.") + + parser.add_argument( + "--output", + default="profile", + help="Output filename (default: 'profile')", + ) + parser.add_argument( + "--sampling_rate", + default="2000", + help="Sampling rate in samples/sec (default: 2000)", + ) + parser.add_argument( + "--native", + default=False, + action=argparse.BooleanOptionalAction, + help="Whether to profile native extensions (default: False)", + ) + parser.add_argument( + "--script_path", + required=True, + help="Path to the Python script to be profiled (required)", + ) + + args = parser.parse_args() + + bash_command = [ + "py-spy", + "record", + "-o", + f"{args.output}.svg", + "-r", + f"{args.sampling_rate}", + "--", + "python", + f"{args.script_path}", + ] + + if args.native: + # Insert --native option at the correct position + bash_command.insert(6, "--native") + + try: + subprocess.check_call(bash_command) # noqa: S603 + except Exception: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_flamegraph.sh b/tools/generate_flamegraph.sh deleted file mode 100755 index 15e5589386..0000000000 --- a/tools/generate_flamegraph.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -eu - -sudo py-spy record -o ${1:-profile}.svg -r ${2:-2000} -- python $3 From 57ce526d8078687ac8e4929548a8659891be87a6 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 26 Jun 2025 15:01:47 -0400 Subject: [PATCH 3/4] check_call -> run --- tools/generate_flamegraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/generate_flamegraph.py b/tools/generate_flamegraph.py index d4f1222b15..a04a9bcfd8 100644 --- a/tools/generate_flamegraph.py +++ b/tools/generate_flamegraph.py @@ -49,7 +49,7 @@ def main(): bash_command.insert(6, "--native") try: - subprocess.check_call(bash_command) # noqa: S603 + subprocess.run(bash_command, check=True) # noqa: S603 except Exception: sys.exit(1) From 9587098a9a9c8a38aac22b5f6d72ec3c0666cd52 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 26 Jun 2025 15:54:29 -0400 Subject: [PATCH 4/4] Documentation only, remove script --- CONTRIBUTING.md | 8 ++--- justfile | 3 -- tools/generate_flamegraph.py | 58 ------------------------------------ 3 files changed, 3 insertions(+), 66 deletions(-) delete mode 100644 tools/generate_flamegraph.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d89c9107f..ca98584602 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -467,10 +467,8 @@ To profile a test script and generate a flame graph, follow these steps: ```bash pip install py-spy ``` -2. Inside your test script, perform any required setup and then loop over the code you want to profile for improved sampling -3. Run the `flamegraph` justfile target to generate a `.svg` file containing the flame graph: - ```bash - just flamegraph - ``` +2. Inside your test script, perform any required setup and then loop over the code you want to profile for improved sampling. +3. Run `py-spy record -o -r -- python ` to generate a `.svg` file containing the flame graph. + (Note: on macOS you will need to run this command using `sudo` to allow `py-spy` to attach to the Python process.) 4. If you need to include native code (for example the C extensions), profiling should be done on a Linux system, as macOS and Windows do not support the `--native` option of `py-spy`. Creating an ubuntu Evergreen spawn host and using `scp` to copy the flamegraph `.svg` file back to your local machine is the best way to do this. diff --git a/justfile b/justfile index 59d1b3810c..74ebb48823 100644 --- a/justfile +++ b/justfile @@ -68,9 +68,6 @@ setup-tests *args="": teardown-tests: bash .evergreen/scripts/teardown-tests.sh -flamegraph *args: - bash tools/generate_flamegraph.sh {{args}} - [group('server')] run-server *args="": bash .evergreen/scripts/run-server.sh {{args}} diff --git a/tools/generate_flamegraph.py b/tools/generate_flamegraph.py deleted file mode 100644 index a04a9bcfd8..0000000000 --- a/tools/generate_flamegraph.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import argparse -import subprocess -import sys - - -def main(): - parser = argparse.ArgumentParser(description="Generate a flamegraph of a given Python script.") - - parser.add_argument( - "--output", - default="profile", - help="Output filename (default: 'profile')", - ) - parser.add_argument( - "--sampling_rate", - default="2000", - help="Sampling rate in samples/sec (default: 2000)", - ) - parser.add_argument( - "--native", - default=False, - action=argparse.BooleanOptionalAction, - help="Whether to profile native extensions (default: False)", - ) - parser.add_argument( - "--script_path", - required=True, - help="Path to the Python script to be profiled (required)", - ) - - args = parser.parse_args() - - bash_command = [ - "py-spy", - "record", - "-o", - f"{args.output}.svg", - "-r", - f"{args.sampling_rate}", - "--", - "python", - f"{args.script_path}", - ] - - if args.native: - # Insert --native option at the correct position - bash_command.insert(6, "--native") - - try: - subprocess.run(bash_command, check=True) # noqa: S603 - except Exception: - sys.exit(1) - - -if __name__ == "__main__": - main()