From 327bd344bfab87aac14a0a4493c025d69eb91bd0 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 27 May 2026 15:39:23 -0700 Subject: [PATCH 1/5] gh-148932: Fix `profiling.sampling` in Windows venvs --- Lib/profiling/sampling/sample.py | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index 2d379e1e16a35e3..3f672b9adcd6dff 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -50,9 +50,35 @@ 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": + 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): + pass + 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) if os.name == "nt" and sys.prefix != sys.base_prefix else pid self.sample_interval_usec = sample_interval_usec self.all_threads = all_threads self.mode = mode # Store mode for later use @@ -61,9 +87,10 @@ 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"): + if os.name == "nt" and sys.prefix != sys.base_prefix: raise SystemExit( - "Running profiling.sampling from virtualenv on Windows platform is not supported" + f"Failed to initialize profiler from virtualenv: {err}\n" + f"Try running with the base interpreter: {sys._base_executable}" ) from err raise SystemExit(err) from err # Track sample intervals and total sample count From 13e0710f1d3222b492d6721595b9d0e8a0222006 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 27 May 2026 15:48:23 -0700 Subject: [PATCH 2/5] Move logic to _resolve_venv_child_pid --- Lib/profiling/sampling/sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index 3f672b9adcd6dff..b64dd556dcf5fdf 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -61,7 +61,7 @@ def _resolve_venv_child_pid(pid): Returns the original pid if not on Windows, not a venv launcher, or no child process is found. """ - if os.name != "nt": + if os.name != "nt" or sys.prefix == sys.base_prefix: return pid try: children = _remote_debugging.get_child_pids(pid, recursive=False) @@ -78,7 +78,7 @@ def _resolve_venv_child_pid(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 = _resolve_venv_child_pid(pid) if os.name == "nt" and sys.prefix != sys.base_prefix else 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 From fc9464d7947945d91677291342ab1843b1c21c3d Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 27 May 2026 15:57:45 -0700 Subject: [PATCH 3/5] Move error message --- Lib/profiling/sampling/sample.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index b64dd556dcf5fdf..747899e6a5274d0 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -71,8 +71,11 @@ def _resolve_venv_child_pid(pid): ] if len(python_children) == 1: return python_children[0] - except (OSError, RuntimeError): - pass + 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 @@ -87,11 +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.prefix != sys.base_prefix: - raise SystemExit( - f"Failed to initialize profiler from virtualenv: {err}\n" - f"Try running with the base interpreter: {sys._base_executable}" - ) from err raise SystemExit(err) from err # Track sample intervals and total sample count self.sample_intervals = deque(maxlen=100) From 8f123f975d95c228f8d413620036eeb680ed93e8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 27 May 2026 16:19:12 -0700 Subject: [PATCH 4/5] Undo 0f32750: Remove note from Docs --- Doc/library/profiling.sampling.rst | 5 ----- 1 file changed, 5 deletions(-) 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 --------------------- From c91e40efc3cd88876f0874a049759d30145b5dd0 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:47:32 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-05-27-23-47-31.gh-issue-148932.Y1xmvA.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-27-23-47-31.gh-issue-148932.Y1xmvA.rst 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.