diff --git a/Doc/library/profiling.sampling.rst b/Doc/library/profiling.sampling.rst index 39b6ea4e31cde72..aeb1a429b58515a 100644 --- a/Doc/library/profiling.sampling.rst +++ b/Doc/library/profiling.sampling.rst @@ -387,11 +387,6 @@ This requires one of: On Windows, the profiler requires administrative privileges or the ``SeDebugPrivilege`` privilege to read another process's memory. -*Note*: On Windows, ``python -m profiling.sampling`` fails inside a virtual -environment because the venv's ``python.exe`` is just a launcher shim that -re-executes the base interpreter as a child process. The shim itself isn't -a Python process and has no ``PyRuntime`` section to attach to. Instead, -run it from the global Python installation. Version compatibility --------------------- diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index 2d379e1e16a35e3..747899e6a5274d0 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -50,9 +50,38 @@ def _pause_threads(unwinder, blocking): # Maximum number of consecutive identical samples to keep before flushing. MAX_PENDING_SAMPLES = 8192 + +def _resolve_venv_child_pid(pid): + """On Windows, if pid is a venvlauncher process, return the child Python PID. + + The venvlauncher (used as python.exe in venvs) spawns the real Python + interpreter as a child process via CreateProcessW. The RemoteUnwinder + needs the child's PID, not the launcher's. + + Returns the original pid if not on Windows, not a venv launcher, + or no child process is found. + """ + if os.name != "nt" or sys.prefix == sys.base_prefix: + return pid + try: + children = _remote_debugging.get_child_pids(pid, recursive=False) + python_children = [ + child for child in children + if _remote_debugging.is_python_process(child) + ] + if len(python_children) == 1: + return python_children[0] + except (OSError, RuntimeError) as err: + raise SystemExit( + f"Failed to initialize profiler from virtualenv: {err}\n" + f"Try running with the base interpreter: {sys._base_executable}" + ) from err + return pid + + class SampleProfiler: def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, opcodes=False, skip_non_matching_threads=True, collect_stats=False, blocking=False): - self.pid = pid + self.pid = _resolve_venv_child_pid(pid) self.sample_interval_usec = sample_interval_usec self.all_threads = all_threads self.mode = mode # Store mode for later use @@ -61,10 +90,6 @@ def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MOD try: self.unwinder = self._new_unwinder(native, gc, opcodes, skip_non_matching_threads) except RuntimeError as err: - if os.name == "nt" and sys.executable.endswith("python.exe"): - raise SystemExit( - "Running profiling.sampling from virtualenv on Windows platform is not supported" - ) from err raise SystemExit(err) from err # Track sample intervals and total sample count self.sample_intervals = deque(maxlen=100) diff --git a/Misc/NEWS.d/next/Library/2026-05-27-23-47-31.gh-issue-148932.Y1xmvA.rst b/Misc/NEWS.d/next/Library/2026-05-27-23-47-31.gh-issue-148932.Y1xmvA.rst new file mode 100644 index 000000000000000..a0b7a9740cd518d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-27-23-47-31.gh-issue-148932.Y1xmvA.rst @@ -0,0 +1 @@ +Fix ``profiling.sampling`` on Windows virtual environments to resolve the actual Python PID from a virtual environment shim.