Skip to content

Commit 2f2b091

Browse files
committed
Move local imports to module level in sampling profiler
The sampling profiler code had numerous imports placed inside functions rather than at module level. While deferred imports can reduce startup time for rarely-used code paths, these imports were in functions called during normal profiler operation, adding repeated import overhead. Moving imports to module level follows Python best practices for code that runs frequently. This makes import dependencies explicit at file scope and eliminates per-call import lookup costs. The test files also had redundant local imports that duplicated module-level imports.
1 parent 3ccc76f commit 2f2b091

File tree

16 files changed

+54
-140
lines changed

16 files changed

+54
-140
lines changed

Lib/profiling/sampling/binary_reader.py

Lines changed: 6 additions & 5 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']

Lib/profiling/sampling/cli.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
SORT_MODE_NSAMPLES_CUMUL,
3636
)
3737

38+
from ._child_monitor import ChildProcessMonitor
39+
3840
try:
3941
from .live_collector import LiveStatsCollector
4042
except ImportError:
@@ -93,8 +95,6 @@ class CustomFormatter(
9395
}
9496

9597
def _setup_child_monitor(args, parent_pid):
96-
from ._child_monitor import ChildProcessMonitor
97-
9898
# Build CLI args for child profilers (excluding --subprocesses to avoid recursion)
9999
child_cli_args = _build_child_profiler_args(args)
100100

@@ -1123,8 +1123,6 @@ def _handle_live_run(args):
11231123

11241124
def _handle_replay(args):
11251125
"""Handle the 'replay' command - convert binary profile to another format."""
1126-
import os
1127-
11281126
if not os.path.exists(args.input_file):
11291127
sys.exit(f"Error: Input file not found: {args.input_file}")
11301128

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

@@ -634,8 +635,6 @@ def _get_bytecode_data_for_line(self, filename, lineno):
634635
Returns:
635636
List of dicts with instruction info, sorted by samples descending
636637
"""
637-
from .opcode_utils import get_opcode_info, format_opcode
638-
639638
key = (filename, lineno)
640639
opcode_data = self.line_opcodes.get(key, {})
641640

@@ -1038,8 +1037,6 @@ def _render_source_with_highlights(self, line_content: str, line_num: int,
10381037
Simple: collect ranges with sample counts, assign each byte position to
10391038
smallest covering range, then emit spans for contiguous runs with sample data.
10401039
"""
1041-
import html as html_module
1042-
10431040
content = line_content.rstrip('\n')
10441041
if not content:
10451042
return ''
@@ -1062,7 +1059,7 @@ def _render_source_with_highlights(self, line_content: str, line_num: int,
10621059
range_data[key]['opcodes'].append(opname)
10631060

10641061
if not range_data:
1065-
return html_module.escape(content)
1062+
return html.escape(content)
10661063

10671064
# For each byte position, find the smallest covering range
10681065
byte_to_range = {}
@@ -1090,7 +1087,7 @@ def _render_source_with_highlights(self, line_content: str, line_num: int,
10901087
def flush_span():
10911088
nonlocal span_chars, current_range
10921089
if span_chars:
1093-
text = html_module.escape(''.join(span_chars))
1090+
text = html.escape(''.join(span_chars))
10941091
if current_range:
10951092
data = range_data.get(current_range, {'samples': 0, 'opcodes': []})
10961093
samples = data['samples']
@@ -1104,7 +1101,7 @@ def flush_span():
11041101
f'data-samples="{samples}" '
11051102
f'data-max-samples="{max_range_samples}" '
11061103
f'data-pct="{pct}" '
1107-
f'data-opcodes="{html_module.escape(opcodes)}">{text}</span>')
1104+
f'data-opcodes="{html.escape(opcodes)}">{text}</span>')
11081105
else:
11091106
result.append(text)
11101107
span_chars = []

Lib/profiling/sampling/live_collector/collector.py

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

933933
def _handle_input(self):
934934
"""Handle keyboard input (non-blocking)."""
935-
from . import constants
936-
937935
self.display.set_nodelay(True)
938936
ch = self.display.get_input()
939937

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):
@@ -1007,8 +1008,6 @@ def render(self, line, width, **kwargs):
10071008
Returns:
10081009
Next available line number
10091010
"""
1010-
from ..opcode_utils import get_opcode_info, format_opcode
1011-
10121011
stats_list = kwargs.get("stats_list", [])
10131012
height = kwargs.get("height", 24)
10141013
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/sample.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _remote_debugging
22
import contextlib
3+
import curses
34
import os
45
import statistics
56
import sys
@@ -459,8 +460,6 @@ def sample_live(
459460
Returns:
460461
The collector with collected samples
461462
"""
462-
import curses
463-
464463
# Check if process is alive before doing any heavy initialization
465464
if not _is_process_running(pid):
466465
print(f"No samples collected - process {pid} exited before profiling could begin.", file=sys.stderr)

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)