From 7f3285106ed482ecc0af6b39623138e1815738c1 Mon Sep 17 00:00:00 2001 From: Ivona Stojanovic Date: Thu, 25 Dec 2025 18:51:03 +0100 Subject: [PATCH 1/2] Auto-open HTML output in browser after generation Automatically open flamegraph and heatmap HTML output in the default browser after profiling completes. Add --no-browser flag to disable this behavior. --- Lib/profiling/sampling/cli.py | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index 10341c1570ceca..70abad40af8eb8 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -10,6 +10,7 @@ import subprocess import sys import time +import webbrowser from contextlib import nullcontext from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError @@ -492,6 +493,11 @@ def _add_format_options(parser, include_compression=True, include_binary=True): help="Output path (default: stdout for pstats, auto-generated for others). " "For heatmap: directory name (default: heatmap_PID)", ) + output_group.add_argument( + "--no-browser", + action="store_true", + help="Disable automatic browser opening for HTML output (flamegraph, heatmap)", + ) def _add_pstats_options(parser): @@ -591,6 +597,32 @@ def _generate_output_filename(format_type, pid): return f"{format_type}_{pid}.{extension}" +def _open_in_browser(path): + """Open a file or directory in the default web browser. + + Args: + path: File path or directory path to open + + For directories (heatmap), opens the index.html file inside. + """ + abs_path = os.path.abspath(path) + + # For heatmap directories, open the index.html file + if os.path.isdir(abs_path): + index_path = os.path.join(abs_path, 'index.html') + if os.path.exists(index_path): + abs_path = index_path + else: + print(f"Warning: Could not find index.html in {path}") + return + + file_url = f"file://{abs_path}" + try: + webbrowser.open(file_url) + except Exception as e: + print(f"Warning: Could not open browser: {e}") + + def _handle_output(collector, args, pid, mode): """Handle output for the collector based on format and arguments. @@ -630,6 +662,10 @@ def _handle_output(collector, args, pid, mode): filename = args.outfile or _generate_output_filename(args.format, pid) collector.export(filename) + # Auto-open browser for HTML output unless --no-browser flag is set + if args.format in ('flamegraph', 'heatmap') and not getattr(args, 'no_browser', False): + _open_in_browser(filename) + def _validate_args(args, parser): """Validate format-specific options and live mode requirements. @@ -1153,6 +1189,10 @@ def progress_callback(current, total): filename = args.outfile or _generate_output_filename(args.format, os.getpid()) collector.export(filename) + # Auto-open browser for HTML output unless --no-browser flag is set + if args.format in ('flamegraph', 'heatmap') and not getattr(args, 'no_browser', False): + _open_in_browser(filename) + print(f"Replayed {count} samples") From 6c5bc603ff773e54f317d3870f0c03326e2fff99 Mon Sep 17 00:00:00 2001 From: Ivona Stojanovic Date: Thu, 25 Dec 2025 20:25:43 +0100 Subject: [PATCH 2/2] Address comments --- Doc/library/profiling.sampling.rst | 14 ++++++++++++++ Lib/profiling/sampling/cli.py | 10 +++++++--- .../test_sampling_profiler/test_children.py | 6 ++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Doc/library/profiling.sampling.rst b/Doc/library/profiling.sampling.rst index 370bbcd3242526..8d64013bcba859 100644 --- a/Doc/library/profiling.sampling.rst +++ b/Doc/library/profiling.sampling.rst @@ -1491,6 +1491,20 @@ Output options named ``_.`` (for example, ``flamegraph_12345.html``). :option:`--heatmap` creates a directory named ``heatmap_``. + For HTML outputs (:option:`--flamegraph` and :option:`--heatmap`), the + generated file or directory is automatically opened in your default web + browser after profiling completes. Use :option:`--no-browser` to disable + this behavior. When using :option:`--subprocesses`, only the main process + output is opened automatically to avoid opening multiple browser tabs. + +.. option:: --no-browser + + Disable automatic browser opening for HTML output (:option:`--flamegraph` + and :option:`--heatmap`). By default, HTML visualizations are opened in + your default web browser after generation. When profiling with + :option:`--subprocesses`, only the main process opens the browser by + default; subprocess outputs are never auto-opened. + pstats display options ---------------------- diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index 70abad40af8eb8..52dd2d725cddf5 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -148,6 +148,9 @@ def _build_child_profiler_args(args): if args.format != "pstats": child_args.append(f"--{args.format}") + # Always add --no-browser for child profilers to avoid opening multiple browser tabs + child_args.append("--no-browser") + return child_args @@ -496,7 +499,8 @@ def _add_format_options(parser, include_compression=True, include_binary=True): output_group.add_argument( "--no-browser", action="store_true", - help="Disable automatic browser opening for HTML output (flamegraph, heatmap)", + help="Disable automatic browser opening for HTML output (flamegraph, heatmap). " + "When using --subprocesses, only the main process opens the browser by default", ) @@ -613,14 +617,14 @@ def _open_in_browser(path): if os.path.exists(index_path): abs_path = index_path else: - print(f"Warning: Could not find index.html in {path}") + print(f"Warning: Could not find index.html in {path}", file=sys.stderr) return file_url = f"file://{abs_path}" try: webbrowser.open(file_url) except Exception as e: - print(f"Warning: Could not open browser: {e}") + print(f"Warning: Could not open browser: {e}", file=sys.stderr) def _handle_output(collector, args, pid, mode): diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_children.py b/Lib/test/test_profiling/test_sampling_profiler/test_children.py index b7dc878a238f8d..dfeec4f9f6030f 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_children.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_children.py @@ -438,6 +438,11 @@ def assert_flag_value_pair(flag, value): child_args, f"Flag '--flamegraph' not found in args: {child_args}", ) + self.assertIn( + "--no-browser", + child_args, + f"Flag '--no-browser' not found in args: {child_args}", + ) def test_build_child_profiler_args_no_gc(self): """Test building CLI args with --no-gc.""" @@ -992,6 +997,7 @@ def test_subprocesses_flag_with_flamegraph_output(self): "-r", "100", "--flamegraph", + "--no-browser", "-o", output_file, script_file,