]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142927: Auto-open HTML output in browser after generation (#143178)
authorivonastojanovic <80911834+ivonastojanovic@users.noreply.github.com>
Thu, 1 Jan 2026 19:05:45 +0000 (20:05 +0100)
committerGitHub <noreply@github.com>
Thu, 1 Jan 2026 19:05:45 +0000 (19:05 +0000)
Doc/library/profiling.sampling.rst
Lib/profiling/sampling/cli.py
Lib/test/test_profiling/test_sampling_profiler/test_children.py

index dae67cca66d9b44b3659e87612e9885cd1ce5f21..9bc58b4d1bc9765dfdd270120cced0c393baa2f1 100644 (file)
@@ -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
 ----------------------
index e43925ea8595f00909c2d8a9c77d91584d2f36b0..ea3926c95658096f82536b04e8a3e4a9e4204e43 100644 (file)
@@ -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")
 
 
index b7dc878a238f8ddb0a8bcb355bc16c1ac28b7d09..84d50cd2088a9eb7ba41741cbe50207494ede3e8 100644 (file)
@@ -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."""