From: ivonastojanovic <80911834+ivonastojanovic@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:05:45 +0000 (+0100) Subject: gh-142927: Auto-open HTML output in browser after generation (#143178) X-Git-Tag: v3.15.0a5~11^2~159 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5d133351c63b20882d85f92c2942c7d99066cebb;p=thirdparty%2FPython%2Fcpython.git gh-142927: Auto-open HTML output in browser after generation (#143178) --- diff --git a/Doc/library/profiling.sampling.rst b/Doc/library/profiling.sampling.rst index dae67cca66d9..9bc58b4d1bc9 100644 --- a/Doc/library/profiling.sampling.rst +++ b/Doc/library/profiling.sampling.rst @@ -1490,6 +1490,13 @@ Output options named ``_.`` (for example, ``flamegraph_12345.html``). :option:`--heatmap` creates a directory named ``heatmap_``. +.. option:: --browser + + Automatically open HTML output (:option:`--flamegraph` and + :option:`--heatmap`) in your default web browser after generation. + When profiling with :option:`--subprocesses`, only the main process + opens the browser; subprocess outputs are never auto-opened. + pstats display options ---------------------- diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index e43925ea8595..ea3926c95658 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -10,6 +10,7 @@ import socket import subprocess import sys import time +import webbrowser from contextlib import nullcontext from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError @@ -487,6 +488,12 @@ 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( + "--browser", + action="store_true", + help="Automatically open HTML output (flamegraph, heatmap) in browser. " + "When using --subprocesses, only the main process opens the browser", + ) def _add_pstats_options(parser): @@ -586,6 +593,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}", 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}", file=sys.stderr) + + def _handle_output(collector, args, pid, mode): """Handle output for the collector based on format and arguments. @@ -625,6 +658,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 if --browser flag is set + if args.format in ('flamegraph', 'heatmap') and getattr(args, 'browser', False): + _open_in_browser(filename) + def _validate_args(args, parser): """Validate format-specific options and live mode requirements. @@ -1161,6 +1198,10 @@ def _handle_replay(args): filename = args.outfile or _generate_output_filename(args.format, os.getpid()) collector.export(filename) + # Auto-open browser for HTML output if --browser flag is set + if args.format in ('flamegraph', 'heatmap') and getattr(args, 'browser', False): + _open_in_browser(filename) + print(f"Replayed {count} samples") 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 b7dc878a238f..84d50cd2088a 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 @@ class TestCLIChildrenFlag(unittest.TestCase): child_args, f"Flag '--flamegraph' not found in args: {child_args}", ) + self.assertNotIn( + "--browser", + child_args, + f"Flag '--browser' should not be in child args: {child_args}", + ) def test_build_child_profiler_args_no_gc(self): """Test building CLI args with --no-gc."""