cpython icon indicating copy to clipboard operation
cpython copied to clipboard

`_FREE_THREADED_BUILD` mis-detected when Py_GIL_DISABLED is defined but set to 0

Open hyongtao-code opened this issue 2 weeks ago • 1 comments

Bug report

Bug description:

I found an issue related to free-threaded build detection in Lib/profiling/sampling/sample.py.

_FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None

I am running a non-free-threaded Windows build, where Py_GIL_DISABLED = 0.

Minimal repro script

Here is a minimal script that demonstrates the issue:

import sysconfig
import profiling.sampling.sample as sp

print("sp._FREE_THREADED_BUILD =", sp._FREE_THREADED_BUILD)
print("sysconfig Py_GIL_DISABLED =", sysconfig.get_config_var("Py_GIL_DISABLED"))
print("expression (is not None) =", sysconfig.get_config_var("Py_GIL_DISABLED") is not None)
print("expression (bool)        =", bool(sysconfig.get_config_var("Py_GIL_DISABLED")))
print("expression (==1)         =", sysconfig.get_config_var("Py_GIL_DISABLED") == 1)
print("expression (==0)         =", sysconfig.get_config_var("Py_GIL_DISABLED") == 0)
print("sample module file       =", sp.__file__)

Result and discussion

On my system, this produces:

sp._FREE_THREADED_BUILD = True   <------!!! here
sysconfig Py_GIL_DISABLED = 0
expression (is not None) = True
expression (bool)        = False
expression (==1)         = False
expression (==0)         = True
sample module file       = d:\MyCode\cpython\Lib\profiling\sampling\sample.py

This appears to mis-detect the build type when Py_GIL_DISABLED is defined but set to 0. In that case, the variable is not None, so _FREE_THREADED_BUILD becomes True, even though the interpreter is not free-threaded.

Proposed Fix

Taking the following constraints into account:

  • only_active_thread=False is the default behavior.
  • only_active_thread=True is only meaningful when running in GIL profiling mode.
  • only_active_thread and all_threads must not be True at the same time.
  • Profiling mode must not be used together with all_threads=True.
  • the original code took the branch where _FREE_THREADED_BUILD = False, and since the RemoteUnwinder function did not accept an all_threads parameter, all_threads defaulted to False.

I aimed to preserve the original semantics as much as possible while ensuring all test cases pass, so I believe this fix maybe appropriate. However, I find my code changes quite ugly.

diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py
index e73306ebf2..7b43916b0c 100644
--- a/Lib/profiling/sampling/sample.py
+++ b/Lib/profiling/sampling/sample.py
@@ -41,7 +41,7 @@ def _pause_threads(unwinder, blocking):
 except ImportError:
     LiveStatsCollector = None
 
-_FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None
+_FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
 # Minimum number of samples required before showing the TUI
 # If fewer samples are collected, we skip the TUI and just print a message
 MIN_SAMPLES_FOR_TUI = 200
@@ -71,11 +71,18 @@ def _new_unwinder(self, native, gc, opcodes, skip_non_matching_threads):
                 cache_frames=True, stats=self.collect_stats
             )
         else:
-            unwinder = _remote_debugging.RemoteUnwinder(
-                self.pid, only_active_thread=bool(self.all_threads), mode=self.mode, native=native, gc=gc,
-                opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
-                cache_frames=True, stats=self.collect_stats
-            )
+            if self.all_threads:
+                unwinder = _remote_debugging.RemoteUnwinder(
+                    self.pid, all_threads=self.all_threads, mode=self.mode, native=native, gc=gc,
+                    opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
+                    cache_frames=True, stats=self.collect_stats
+                )
+            else:
+                unwinder = _remote_debugging.RemoteUnwinder(
+                    self.pid, only_active_thread=bool(self.all_threads), mode=self.mode, native=native, gc=gc,
+                    opcodes=opcodes, skip_non_matching_threads=skip_non_matching_threads,
+                    cache_frames=True, stats=self.collect_stats
+                )
         return unwinder

     def sample(self, collector, duration_sec=None, *, async_aware=False):

https://github.com/python/cpython/issues/143405#issuecomment-3707774216

CPython versions tested on:

CPython main branch

Operating systems tested on:

Windows

Linked PRs

  • gh-143426

hyongtao-code avatar Jan 05 '26 03:01 hyongtao-code

I would like to work on this , please assign this to me

DCchoudhury15 avatar Jan 05 '26 04:01 DCchoudhury15

Hi, I'd like to work on this issue. Looking at the previous closed PR #143426, I think the fix can be much simpler just changing is not None to bool() on line 41, without modifying the _new_unwinder method. Can I submit a minimal fix?

arunkumargururaj07-star avatar Jan 07 '26 04:01 arunkumargururaj07-star