Skip to content

Commit 6b9a6c6

Browse files
authored
gh-138122: Move local imports to module level in sampling profiler (#143257)
1 parent e5ad7b7 commit 6b9a6c6

File tree

15 files changed

+82
-139
lines changed

15 files changed

+82
-139
lines changed

Lib/profiling/sampling/binary_reader.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
"""Thin Python wrapper around C binary reader for profiling data."""
22

3+
import _remote_debugging
4+
5+
from .gecko_collector import GeckoCollector
6+
from .stack_collector import FlamegraphCollector, CollapsedStackCollector
7+
from .pstats_collector import PstatsCollector
8+
39

410
class BinaryReader:
511
"""High-performance binary reader using C implementation.
@@ -23,7 +29,6 @@ def __init__(self, filename):
2329
self._reader = None
2430

2531
def __enter__(self):
26-
import _remote_debugging
2732
self._reader = _remote_debugging.BinaryReader(self.filename)
2833
return self
2934

@@ -99,10 +104,6 @@ def convert_binary_to_format(input_file, output_file, output_format,
99104
Returns:
100105
int: Number of samples converted
101106
"""
102-
from .gecko_collector import GeckoCollector
103-
from .stack_collector import FlamegraphCollector, CollapsedStackCollector
104-
from .pstats_collector import PStatsCollector
105-
106107
with BinaryReader(input_file) as reader:
107108
info = reader.get_info()
108109
interval = sample_interval_usec or info['sample_interval_us']
@@ -113,7 +114,7 @@ def convert_binary_to_format(input_file, output_file, output_format,
113114
elif output_format == 'collapsed':
114115
collector = CollapsedStackCollector(interval)
115116
elif output_format == 'pstats':
116-
collector = PStatsCollector(interval)
117+
collector = PstatsCollector(interval)
117118
elif output_format == 'gecko':
118119
collector = GeckoCollector(interval)
119120
else:

Lib/profiling/sampling/cli.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
SORT_MODE_NSAMPLES_CUMUL,
3737
)
3838

39+
try:
40+
from ._child_monitor import ChildProcessMonitor
41+
except ImportError:
42+
# _remote_debugging module not available on this platform (e.g., WASI)
43+
ChildProcessMonitor = None
44+
3945
try:
4046
from .live_collector import LiveStatsCollector
4147
except ImportError:
@@ -94,8 +100,6 @@ class CustomFormatter(
94100
}
95101

96102
def _setup_child_monitor(args, parent_pid):
97-
from ._child_monitor import ChildProcessMonitor
98-
99103
# Build CLI args for child profilers (excluding --subprocesses to avoid recursion)
100104
child_cli_args = _build_child_profiler_args(args)
101105

@@ -691,6 +695,11 @@ def _validate_args(args, parser):
691695

692696
# --subprocesses is incompatible with --live
693697
if hasattr(args, 'subprocesses') and args.subprocesses:
698+
if ChildProcessMonitor is None:
699+
parser.error(
700+
"--subprocesses is not available on this platform "
701+
"(requires _remote_debugging module)."
702+
)
694703
if hasattr(args, 'live') and args.live:
695704
parser.error("--subprocesses is incompatible with --live mode.")
696705

@@ -1160,8 +1169,6 @@ def _handle_live_run(args):
11601169

11611170
def _handle_replay(args):
11621171
"""Handle the 'replay' command - convert binary profile to another format."""
1163-
import os
1164-
11651172
if not os.path.exists(args.input_file):
11661173
sys.exit(f"Error: Input file not found: {args.input_file}")
11671174

Lib/profiling/sampling/heatmap_collector.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ._css_utils import get_combined_css
1919
from ._format_utils import fmt
2020
from .collector import normalize_location, extract_lineno
21+
from .opcode_utils import get_opcode_info, format_opcode
2122
from .stack_collector import StackTraceCollector
2223

2324

@@ -642,8 +643,6 @@ def _get_bytecode_data_for_line(self, filename, lineno):
642643
Returns:
643644
List of dicts with instruction info, sorted by samples descending
644645
"""
645-
from .opcode_utils import get_opcode_info, format_opcode
646-
647646
key = (filename, lineno)
648647
opcode_data = self.line_opcodes.get(key, {})
649648

@@ -1046,8 +1045,6 @@ def _render_source_with_highlights(self, line_content: str, line_num: int,
10461045
Simple: collect ranges with sample counts, assign each byte position to
10471046
smallest covering range, then emit spans for contiguous runs with sample data.
10481047
"""
1049-
import html as html_module
1050-
10511048
content = line_content.rstrip('\n')
10521049
if not content:
10531050
return ''
@@ -1070,7 +1067,7 @@ def _render_source_with_highlights(self, line_content: str, line_num: int,
10701067
range_data[key]['opcodes'].append(opname)
10711068

10721069
if not range_data:
1073-
return html_module.escape(content)
1070+
return html.escape(content)
10741071

10751072
# For each byte position, find the smallest covering range
10761073
byte_to_range = {}
@@ -1098,7 +1095,7 @@ def _render_source_with_highlights(self, line_content: str, line_num: int,
10981095
def flush_span():
10991096
nonlocal span_chars, current_range
11001097
if span_chars:
1101-
text = html_module.escape(''.join(span_chars))
1098+
text = html.escape(''.join(span_chars))
11021099
if current_range:
11031100
data = range_data.get(current_range, {'samples': 0, 'opcodes': []})
11041101
samples = data['samples']
@@ -1112,7 +1109,7 @@ def flush_span():
11121109
f'data-samples="{samples}" '
11131110
f'data-max-samples="{max_range_samples}" '
11141111
f'data-pct="{pct}" '
1115-
f'data-opcodes="{html_module.escape(opcodes)}">{text}</span>')
1112+
f'data-opcodes="{html.escape(opcodes)}">{text}</span>')
11161113
else:
11171114
result.append(text)
11181115
span_chars = []

Lib/profiling/sampling/live_collector/collector.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -916,8 +916,6 @@ def _show_terminal_size_warning_and_wait(self, height, width):
916916

917917
def _handle_input(self):
918918
"""Handle keyboard input (non-blocking)."""
919-
from . import constants
920-
921919
self.display.set_nodelay(True)
922920
ch = self.display.get_input()
923921

Lib/profiling/sampling/live_collector/widgets.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
PROFILING_MODE_GIL,
3232
PROFILING_MODE_WALL,
3333
)
34+
from ..opcode_utils import get_opcode_info, format_opcode
3435

3536

3637
class Widget(ABC):
@@ -1013,8 +1014,6 @@ def render(self, line, width, **kwargs):
10131014
Returns:
10141015
Next available line number
10151016
"""
1016-
from ..opcode_utils import get_opcode_info, format_opcode
1017-
10181017
stats_list = kwargs.get("stats_list", [])
10191018
height = kwargs.get("height", 24)
10201019
selected_row = self.collector.selected_row

Lib/profiling/sampling/pstats_collector.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import collections
22
import marshal
3+
import pstats
34

45
from _colorize import ANSIColors
56
from .collector import Collector, extract_lineno
6-
from .constants import MICROSECONDS_PER_SECOND
7+
from .constants import MICROSECONDS_PER_SECOND, PROFILING_MODE_CPU
78

89

910
class PstatsCollector(Collector):
@@ -86,9 +87,6 @@ def create_stats(self):
8687

8788
def print_stats(self, sort=-1, limit=None, show_summary=True, mode=None):
8889
"""Print formatted statistics to stdout."""
89-
import pstats
90-
from .constants import PROFILING_MODE_CPU
91-
9290
# Create stats object
9391
stats = pstats.SampledStats(self).strip_dirs()
9492
if not stats.stats:

Lib/profiling/sampling/stack_collector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import linecache
77
import os
88
import sys
9+
import sysconfig
910

1011
from ._css_utils import get_combined_css
1112
from .collector import Collector, extract_lineno
@@ -244,7 +245,6 @@ def convert_children(children, min_samples):
244245
}
245246

246247
# Calculate thread status percentages for display
247-
import sysconfig
248248
is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
249249
total_threads = max(1, self.thread_status_counts["total"])
250250
thread_stats = {

Lib/test/test_profiling/test_sampling_profiler/test_advanced.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import _remote_debugging # noqa: F401
1212
import profiling.sampling
1313
import profiling.sampling.sample
14+
from profiling.sampling.pstats_collector import PstatsCollector
15+
from profiling.sampling.stack_collector import CollapsedStackCollector
1416
except ImportError:
1517
raise unittest.SkipTest(
1618
"Test only runs when _remote_debugging is available"
@@ -61,7 +63,6 @@ def test_gc_frames_enabled(self):
6163
io.StringIO() as captured_output,
6264
mock.patch("sys.stdout", captured_output),
6365
):
64-
from profiling.sampling.pstats_collector import PstatsCollector
6566
collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False)
6667
profiling.sampling.sample.sample(
6768
subproc.process.pid,
@@ -88,7 +89,6 @@ def test_gc_frames_disabled(self):
8889
io.StringIO() as captured_output,
8990
mock.patch("sys.stdout", captured_output),
9091
):
91-
from profiling.sampling.pstats_collector import PstatsCollector
9292
collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False)
9393
profiling.sampling.sample.sample(
9494
subproc.process.pid,
@@ -140,7 +140,6 @@ def test_native_frames_enabled(self):
140140
io.StringIO() as captured_output,
141141
mock.patch("sys.stdout", captured_output),
142142
):
143-
from profiling.sampling.stack_collector import CollapsedStackCollector
144143
collector = CollapsedStackCollector(1000, skip_idle=False)
145144
profiling.sampling.sample.sample(
146145
subproc.process.pid,
@@ -176,7 +175,6 @@ def test_native_frames_disabled(self):
176175
io.StringIO() as captured_output,
177176
mock.patch("sys.stdout", captured_output),
178177
):
179-
from profiling.sampling.pstats_collector import PstatsCollector
180178
collector = PstatsCollector(sample_interval_usec=5000, skip_idle=False)
181179
profiling.sampling.sample.sample(
182180
subproc.process.pid,

Lib/test/test_profiling/test_sampling_profiler/test_async.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
3. Stack traversal: _build_linear_stacks() with BFS
77
"""
88

9+
import inspect
910
import unittest
1011

1112
try:
1213
import _remote_debugging # noqa: F401
1314
from profiling.sampling.pstats_collector import PstatsCollector
15+
from profiling.sampling.stack_collector import FlamegraphCollector
16+
from profiling.sampling.sample import sample, sample_live, SampleProfiler
1417
except ImportError:
1518
raise unittest.SkipTest(
1619
"Test only runs when _remote_debugging is available"
@@ -561,8 +564,6 @@ class TestFlamegraphCollectorAsync(unittest.TestCase):
561564

562565
def test_flamegraph_with_async_frames(self):
563566
"""Test FlamegraphCollector correctly processes async task frames."""
564-
from profiling.sampling.stack_collector import FlamegraphCollector
565-
566567
collector = FlamegraphCollector(sample_interval_usec=1000)
567568

568569
# Build async task tree: Root -> Child
@@ -607,8 +608,6 @@ def test_flamegraph_with_async_frames(self):
607608

608609
def test_flamegraph_with_task_markers(self):
609610
"""Test FlamegraphCollector includes <task> boundary markers."""
610-
from profiling.sampling.stack_collector import FlamegraphCollector
611-
612611
collector = FlamegraphCollector(sample_interval_usec=1000)
613612

614613
task = MockTaskInfo(
@@ -643,8 +642,6 @@ def find_task_marker(node, depth=0):
643642

644643
def test_flamegraph_multiple_async_samples(self):
645644
"""Test FlamegraphCollector aggregates multiple async samples correctly."""
646-
from profiling.sampling.stack_collector import FlamegraphCollector
647-
648645
collector = FlamegraphCollector(sample_interval_usec=1000)
649646

650647
task = MockTaskInfo(
@@ -675,25 +672,16 @@ class TestAsyncAwareParameterFlow(unittest.TestCase):
675672

676673
def test_sample_function_accepts_async_aware(self):
677674
"""Test that sample() function accepts async_aware parameter."""
678-
from profiling.sampling.sample import sample
679-
import inspect
680-
681675
sig = inspect.signature(sample)
682676
self.assertIn("async_aware", sig.parameters)
683677

684678
def test_sample_live_function_accepts_async_aware(self):
685679
"""Test that sample_live() function accepts async_aware parameter."""
686-
from profiling.sampling.sample import sample_live
687-
import inspect
688-
689680
sig = inspect.signature(sample_live)
690681
self.assertIn("async_aware", sig.parameters)
691682

692683
def test_sample_profiler_sample_accepts_async_aware(self):
693684
"""Test that SampleProfiler.sample() accepts async_aware parameter."""
694-
from profiling.sampling.sample import SampleProfiler
695-
import inspect
696-
697685
sig = inspect.signature(SampleProfiler.sample)
698686
self.assertIn("async_aware", sig.parameters)
699687

0 commit comments

Comments
 (0)