You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
profiling.sampling run --blocking hangs and writes an empty profile on Windows #152356
On Windows, python -m profiling.sampling run ... --blocking hangs indefinitely and writes a 0-byte profile, even for a trivial script that runs in well under a second natively. The same command without--blocking works and produces a small, loadable binary profile.
python -m profiling.sampling run --binary -o ok.bin --mode=cpu tiny_cpu.py
→ exits 0 in ~1s; ok.bin is ~1.4 KB (~60 samples) and replays fine.
Broken (--blocking):
python -m profiling.sampling run --binary -o blocking.bin --mode=cpu --blocking tiny_cpu.py
→ the target script's own work completes (its print output appears), but the profiler process never exits. After 25s (the script runs in <0.2s natively) it was still running and had to be killed. blocking.bin is 0 bytes, so it cannot be replayed (replay rejects it as "not a binary sampling profile").
Observed vs expected
Observed: on Windows, --blocking hangs (well over 100× the target's native runtime) and leaves an empty, unreplayable artifact.
Expected: --blocking finishes near native runtime and writes a loadable binary profile, like the non-blocking path.
CPython versions tested on
3.15.0b3 (Windows, x86_64). Please verify against current main in case this is already addressed.
I investigated this on the latest upstream/main using a local Windows debug build and was able to reproduce the issue consistently.
The non-blocking path exits normally and produces a valid binary profile, while the --blocking path hangs after the target process has already finished and leaves a 0-byte profile.
After tracing both code paths, the root cause appears to be an exception classification mismatch in the Windows blocking implementation.
The blocking path eventually calls _Py_RemoteDebug_StopAllThreads() via RemoteUnwinder.pause_threads(). Once the target process has already exited, NtSuspendProcess() fails and Windows currently raises a RuntimeError. The sampler treats that exception as a recoverable sampling failure and continues retrying indefinitely, so the profiling loop never terminates and the binary writer is never finalized.
By comparison, the non-blocking path reaches the process-exit condition through ProcessLookupError, which is already treated as a terminal condition by the sampling loop. The macOS and Linux blocking implementations also map terminated processes to ProcessLookupError, so Windows appears to be inconsistent here.
A minimal fix may be to have the Windows remote debugging layer raise ProcessLookupError when NtSuspendProcess() fails because the target process has already terminated, while preserving the existing RuntimeError behavior for genuine suspend failures.
I'm preparing a focused PR with the implementation and regression test unless anyone sees an issue with this approach.
Activity
zainnadeem786 commentedon Jun 28, 2026
Hi @heejaechang @StanFromIreland ,
I investigated this on the latest
upstream/mainusing a local Windows debug build and was able to reproduce the issue consistently.The non-blocking path exits normally and produces a valid binary profile, while the
--blockingpath hangs after the target process has already finished and leaves a 0-byte profile.After tracing both code paths, the root cause appears to be an exception classification mismatch in the Windows blocking implementation.
The blocking path eventually calls
_Py_RemoteDebug_StopAllThreads()viaRemoteUnwinder.pause_threads(). Once the target process has already exited,NtSuspendProcess()fails and Windows currently raises aRuntimeError. The sampler treats that exception as a recoverable sampling failure and continues retrying indefinitely, so the profiling loop never terminates and the binary writer is never finalized.By comparison, the non-blocking path reaches the process-exit condition through
ProcessLookupError, which is already treated as a terminal condition by the sampling loop. The macOS and Linux blocking implementations also map terminated processes toProcessLookupError, so Windows appears to be inconsistent here.A minimal fix may be to have the Windows remote debugging layer raise
ProcessLookupErrorwhenNtSuspendProcess()fails because the target process has already terminated, while preserving the existingRuntimeErrorbehavior for genuine suspend failures.I'm preparing a focused PR with the implementation and regression test unless anyone sees an issue with this approach.
StanFromIreland commentedon Jun 28, 2026
Can you please not ping triagers, we need not be the experts for the module.
zainnadeem786 commentedon Jun 28, 2026
Thanks for the clarification, and apologies about that.
I won't ping triagers on future issues unless specifically requested. Thanks for letting me know.
pythongh-152356: Fix Windows blocking sampling after process exit
gh-152356: Fix Windows blocking sampling after target process exit (#…
[3.15] gh-152356: Fix Windows blocking sampling after target process …