Skip to content

Commit 5d13335

Browse files
gh-142927: Auto-open HTML output in browser after generation (#143178)
1 parent faa2604 commit 5d13335

File tree

3 files changed

+53
-0
lines changed

3 files changed

+53
-0
lines changed

Doc/library/profiling.sampling.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,13 @@ Output options
14901490
named ``<format>_<PID>.<ext>`` (for example, ``flamegraph_12345.html``).
14911491
:option:`--heatmap` creates a directory named ``heatmap_<PID>``.
14921492

1493+
.. option:: --browser
1494+
1495+
Automatically open HTML output (:option:`--flamegraph` and
1496+
:option:`--heatmap`) in your default web browser after generation.
1497+
When profiling with :option:`--subprocesses`, only the main process
1498+
opens the browser; subprocess outputs are never auto-opened.
1499+
14931500

14941501
pstats display options
14951502
----------------------

Lib/profiling/sampling/cli.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import subprocess
1111
import sys
1212
import time
13+
import webbrowser
1314
from contextlib import nullcontext
1415

1516
from .errors import SamplingUnknownProcessError, SamplingModuleNotFoundError, SamplingScriptNotFoundError
@@ -487,6 +488,12 @@ def _add_format_options(parser, include_compression=True, include_binary=True):
487488
help="Output path (default: stdout for pstats, auto-generated for others). "
488489
"For heatmap: directory name (default: heatmap_PID)",
489490
)
491+
output_group.add_argument(
492+
"--browser",
493+
action="store_true",
494+
help="Automatically open HTML output (flamegraph, heatmap) in browser. "
495+
"When using --subprocesses, only the main process opens the browser",
496+
)
490497

491498

492499
def _add_pstats_options(parser):
@@ -586,6 +593,32 @@ def _generate_output_filename(format_type, pid):
586593
return f"{format_type}_{pid}.{extension}"
587594

588595

596+
def _open_in_browser(path):
597+
"""Open a file or directory in the default web browser.
598+
599+
Args:
600+
path: File path or directory path to open
601+
602+
For directories (heatmap), opens the index.html file inside.
603+
"""
604+
abs_path = os.path.abspath(path)
605+
606+
# For heatmap directories, open the index.html file
607+
if os.path.isdir(abs_path):
608+
index_path = os.path.join(abs_path, 'index.html')
609+
if os.path.exists(index_path):
610+
abs_path = index_path
611+
else:
612+
print(f"Warning: Could not find index.html in {path}", file=sys.stderr)
613+
return
614+
615+
file_url = f"file://{abs_path}"
616+
try:
617+
webbrowser.open(file_url)
618+
except Exception as e:
619+
print(f"Warning: Could not open browser: {e}", file=sys.stderr)
620+
621+
589622
def _handle_output(collector, args, pid, mode):
590623
"""Handle output for the collector based on format and arguments.
591624
@@ -625,6 +658,10 @@ def _handle_output(collector, args, pid, mode):
625658
filename = args.outfile or _generate_output_filename(args.format, pid)
626659
collector.export(filename)
627660

661+
# Auto-open browser for HTML output if --browser flag is set
662+
if args.format in ('flamegraph', 'heatmap') and getattr(args, 'browser', False):
663+
_open_in_browser(filename)
664+
628665

629666
def _validate_args(args, parser):
630667
"""Validate format-specific options and live mode requirements.
@@ -1161,6 +1198,10 @@ def progress_callback(current, total):
11611198
filename = args.outfile or _generate_output_filename(args.format, os.getpid())
11621199
collector.export(filename)
11631200

1201+
# Auto-open browser for HTML output if --browser flag is set
1202+
if args.format in ('flamegraph', 'heatmap') and getattr(args, 'browser', False):
1203+
_open_in_browser(filename)
1204+
11641205
print(f"Replayed {count} samples")
11651206

11661207

Lib/test/test_profiling/test_sampling_profiler/test_children.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ def assert_flag_value_pair(flag, value):
438438
child_args,
439439
f"Flag '--flamegraph' not found in args: {child_args}",
440440
)
441+
self.assertNotIn(
442+
"--browser",
443+
child_args,
444+
f"Flag '--browser' should not be in child args: {child_args}",
445+
)
441446

442447
def test_build_child_profiler_args_no_gc(self):
443448
"""Test building CLI args with --no-gc."""

0 commit comments

Comments
 (0)