https://github.com/python/cpython/commit/5d133351c63b20882d85f92c2942c7d99066cebb
commit: 5d133351c63b20882d85f92c2942c7d99066cebb
branch: main
author: ivonastojanovic <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-01-01T19:05:45Z
summary:

gh-142927: Auto-open HTML output in browser after generation (#143178)

files:
M Doc/library/profiling.sampling.rst
M Lib/profiling/sampling/cli.py
M Lib/test/test_profiling/test_sampling_profiler/test_children.py

diff --git a/Doc/library/profiling.sampling.rst 
b/Doc/library/profiling.sampling.rst
index dae67cca66d9b4..9bc58b4d1bc976 100644
--- a/Doc/library/profiling.sampling.rst
+++ b/Doc/library/profiling.sampling.rst
@@ -1490,6 +1490,13 @@ Output options
    named ``<format>_<PID>.<ext>`` (for example, ``flamegraph_12345.html``).
    :option:`--heatmap` creates a directory named ``heatmap_<PID>``.
 
+.. 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 e43925ea8595f0..ea3926c9565809 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
@@ -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 progress_callback(current, total):
             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 b7dc878a238f8d..84d50cd2088a9e 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.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."""

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to