]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-116738: make `cProfile` module thread-safe (GH-138229) (#138575)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 7 Oct 2025 18:51:22 +0000 (20:51 +0200)
committerGitHub <noreply@github.com>
Tue, 7 Oct 2025 18:51:22 +0000 (18:51 +0000)
gh-116738: make `cProfile` module thread-safe (GH-138229)
(cherry picked from commit 8554c0917e25a7abe12b3000f1589b6566c91a25)

Co-authored-by: Alper <alperyoney@fb.com>
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Lib/test/test_free_threading/test_cprofile.py [new file with mode: 0644]
Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst [new file with mode: 0644]
Modules/_lsprof.c
Modules/clinic/_lsprof.c.h

diff --git a/Lib/test/test_free_threading/test_cprofile.py b/Lib/test/test_free_threading/test_cprofile.py
new file mode 100644 (file)
index 0000000..361b800
--- /dev/null
@@ -0,0 +1,43 @@
+import unittest
+
+from test.support import threading_helper
+
+import cProfile
+import pstats
+
+
+NTHREADS = 10
+INSERT_PER_THREAD = 1000
+
+
+@threading_helper.requires_working_threading()
+class TestCProfile(unittest.TestCase):
+    def test_cprofile_racing_list_insert(self):
+        def list_insert(lst):
+            for i in range(INSERT_PER_THREAD):
+                lst.insert(0, i)
+
+        lst = []
+
+        with cProfile.Profile() as pr:
+            threading_helper.run_concurrently(
+                worker_func=list_insert, nthreads=NTHREADS, args=(lst,)
+            )
+            pr.create_stats()
+            ps = pstats.Stats(pr)
+            stats_profile = ps.get_stats_profile()
+            list_insert_profile = stats_profile.func_profiles[
+                "<method 'insert' of 'list' objects>"
+            ]
+            # Even though there is no explicit recursive call to insert,
+            # cProfile may record some calls as recursive due to limitations
+            # in its handling of multithreaded programs. This issue is not
+            # directly related to FT Python itself; however, it tends to be
+            # more noticeable when using FT Python. Therefore, consider only
+            # the calls section and disregard the recursive part.
+            list_insert_ncalls = list_insert_profile.ncalls.split("/")[0]
+            self.assertEqual(
+                int(list_insert_ncalls), NTHREADS * INSERT_PER_THREAD
+            )
+
+        self.assertEqual(len(lst), NTHREADS * INSERT_PER_THREAD)
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-28-09-29-46.gh-issue-116738.yLZJpV.rst
new file mode 100644 (file)
index 0000000..2bb6803
--- /dev/null
@@ -0,0 +1,2 @@
+Make :mod:`cProfile` thread-safe on the :term:`free threaded <free
+threading>` build.
index f82b446aff2e8745888d03df1404ab6faa4bc4ea..73602723d19bcf6545a60a419b355b9aaf83d01e 100644 (file)
@@ -534,6 +534,7 @@ static int statsForEntry(rotating_node_t *node, void *arg)
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.getstats
 
     cls: defining_class
@@ -565,7 +566,7 @@ profiler_subentry objects:
 
 static PyObject *
 _lsprof_Profiler_getstats_impl(ProfilerObject *self, PyTypeObject *cls)
-/*[clinic end generated code: output=1806ef720019ee03 input=445e193ef4522902]*/
+/*[clinic end generated code: output=1806ef720019ee03 input=3dc69eb85ed73d91]*/
 {
     statscollector_t collect;
     collect.state = _PyType_GetModuleState(cls);
@@ -613,6 +614,7 @@ setBuiltins(ProfilerObject *pObj, int nvalue)
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._pystart_callback
 
     code: object
@@ -624,7 +626,7 @@ _lsprof.Profiler._pystart_callback
 static PyObject *
 _lsprof_Profiler__pystart_callback_impl(ProfilerObject *self, PyObject *code,
                                         PyObject *instruction_offset)
-/*[clinic end generated code: output=5fec8b7ad5ed25e8 input=b166e6953c579cda]*/
+/*[clinic end generated code: output=5fec8b7ad5ed25e8 input=b61a0e79cf1f8499]*/
 {
     ptrace_enter_call((PyObject*)self, (void *)code, code);
 
@@ -632,6 +634,7 @@ _lsprof_Profiler__pystart_callback_impl(ProfilerObject *self, PyObject *code,
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._pythrow_callback
 
     code: object
@@ -645,7 +648,7 @@ static PyObject *
 _lsprof_Profiler__pythrow_callback_impl(ProfilerObject *self, PyObject *code,
                                         PyObject *instruction_offset,
                                         PyObject *exception)
-/*[clinic end generated code: output=0a32988919dfb94c input=fd728fc2c074f5e6]*/
+/*[clinic end generated code: output=0a32988919dfb94c input=60c7f272206d3758]*/
 {
     ptrace_enter_call((PyObject*)self, (void *)code, code);
 
@@ -653,6 +656,7 @@ _lsprof_Profiler__pythrow_callback_impl(ProfilerObject *self, PyObject *code,
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._pyreturn_callback
 
     code: object
@@ -667,7 +671,7 @@ _lsprof_Profiler__pyreturn_callback_impl(ProfilerObject *self,
                                          PyObject *code,
                                          PyObject *instruction_offset,
                                          PyObject *retval)
-/*[clinic end generated code: output=9e2f6fc1b882c51e input=667ffaeb2fa6fd1f]*/
+/*[clinic end generated code: output=9e2f6fc1b882c51e input=0ddcc1ec53faa928]*/
 {
     ptrace_leave_call((PyObject*)self, (void *)code);
 
@@ -703,6 +707,7 @@ PyObject* get_cfunc_from_callable(PyObject* callable, PyObject* self_arg, PyObje
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._ccall_callback
 
     code: object
@@ -717,7 +722,7 @@ static PyObject *
 _lsprof_Profiler__ccall_callback_impl(ProfilerObject *self, PyObject *code,
                                       PyObject *instruction_offset,
                                       PyObject *callable, PyObject *self_arg)
-/*[clinic end generated code: output=152db83cabd18cad input=0e66687cfb95c001]*/
+/*[clinic end generated code: output=152db83cabd18cad input=2fc1e0630ee5e32b]*/
 {
     if (self->flags & POF_BUILTINS) {
         PyObject* cfunc = get_cfunc_from_callable(callable, self_arg, self->missing);
@@ -733,6 +738,7 @@ _lsprof_Profiler__ccall_callback_impl(ProfilerObject *self, PyObject *code,
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler._creturn_callback
 
     code: object
@@ -748,7 +754,7 @@ _lsprof_Profiler__creturn_callback_impl(ProfilerObject *self, PyObject *code,
                                         PyObject *instruction_offset,
                                         PyObject *callable,
                                         PyObject *self_arg)
-/*[clinic end generated code: output=1e886dde8fed8fb0 input=b18afe023746923a]*/
+/*[clinic end generated code: output=1e886dde8fed8fb0 input=bdc246d6b5b8714a]*/
 {
     if (self->flags & POF_BUILTINS) {
         PyObject* cfunc = get_cfunc_from_callable(callable, self_arg, self->missing);
@@ -780,6 +786,7 @@ static const struct {
 
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.enable
 
     subcalls: bool = True
@@ -796,7 +803,7 @@ Start collecting profiling information.
 static PyObject *
 _lsprof_Profiler_enable_impl(ProfilerObject *self, int subcalls,
                              int builtins)
-/*[clinic end generated code: output=1e747f9dc1edd571 input=9ab81405107ab7f1]*/
+/*[clinic end generated code: output=1e747f9dc1edd571 input=0b88115b1c796173]*/
 {
     int all_events = 0;
     if (setSubcalls(self, subcalls) < 0 || setBuiltins(self, builtins) < 0) {
@@ -869,6 +876,7 @@ flush_unmatched(ProfilerObject *pObj)
 
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.disable
 
 Stop collecting profiling information.
@@ -876,7 +884,7 @@ Stop collecting profiling information.
 
 static PyObject *
 _lsprof_Profiler_disable_impl(ProfilerObject *self)
-/*[clinic end generated code: output=838cffef7f651870 input=05700b3fc68d1f50]*/
+/*[clinic end generated code: output=838cffef7f651870 input=f7e4787cae20f7f6]*/
 {
     if (self->flags & POF_EXT_TIMER) {
         PyErr_SetString(PyExc_RuntimeError,
@@ -928,6 +936,7 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self)
 }
 
 /*[clinic input]
+@critical_section
 _lsprof.Profiler.clear
 
 Clear all profiling information collected so far.
@@ -935,7 +944,7 @@ Clear all profiling information collected so far.
 
 static PyObject *
 _lsprof_Profiler_clear_impl(ProfilerObject *self)
-/*[clinic end generated code: output=dd1c668fb84b1335 input=fbe1f88c28be4f98]*/
+/*[clinic end generated code: output=dd1c668fb84b1335 input=4aab219d5d7a9bec]*/
 {
     if (self->flags & POF_EXT_TIMER) {
         PyErr_SetString(PyExc_RuntimeError,
index c426cd6fe02f398308456f02ba7d3261748d87f6..acb4aaf27e377f2708fbfcd72043770b4f9acae8 100644 (file)
@@ -6,6 +6,7 @@ preserve
 #  include "pycore_gc.h"          // PyGC_Head
 #  include "pycore_runtime.h"     // _Py_ID()
 #endif
+#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION()
 #include "pycore_modsupport.h"    // _PyArg_CheckPositional()
 
 PyDoc_STRVAR(_lsprof_Profiler_getstats__doc__,
@@ -45,11 +46,18 @@ _lsprof_Profiler_getstats_impl(ProfilerObject *self, PyTypeObject *cls);
 static PyObject *
 _lsprof_Profiler_getstats(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
 {
+    PyObject *return_value = NULL;
+
     if (nargs || (kwnames && PyTuple_GET_SIZE(kwnames))) {
         PyErr_SetString(PyExc_TypeError, "getstats() takes no arguments");
-        return NULL;
+        goto exit;
     }
-    return _lsprof_Profiler_getstats_impl((ProfilerObject *)self, cls);
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _lsprof_Profiler_getstats_impl((ProfilerObject *)self, cls);
+    Py_END_CRITICAL_SECTION();
+
+exit:
+    return return_value;
 }
 
 PyDoc_STRVAR(_lsprof_Profiler__pystart_callback__doc__,
@@ -76,7 +84,9 @@ _lsprof_Profiler__pystart_callback(PyObject *self, PyObject *const *args, Py_ssi
     }
     code = args[0];
     instruction_offset = args[1];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__pystart_callback_impl((ProfilerObject *)self, code, instruction_offset);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -109,7 +119,9 @@ _lsprof_Profiler__pythrow_callback(PyObject *self, PyObject *const *args, Py_ssi
     code = args[0];
     instruction_offset = args[1];
     exception = args[2];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__pythrow_callback_impl((ProfilerObject *)self, code, instruction_offset, exception);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -143,7 +155,9 @@ _lsprof_Profiler__pyreturn_callback(PyObject *self, PyObject *const *args, Py_ss
     code = args[0];
     instruction_offset = args[1];
     retval = args[2];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__pyreturn_callback_impl((ProfilerObject *)self, code, instruction_offset, retval);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -178,7 +192,9 @@ _lsprof_Profiler__ccall_callback(PyObject *self, PyObject *const *args, Py_ssize
     instruction_offset = args[1];
     callable = args[2];
     self_arg = args[3];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__ccall_callback_impl((ProfilerObject *)self, code, instruction_offset, callable, self_arg);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -215,7 +231,9 @@ _lsprof_Profiler__creturn_callback(PyObject *self, PyObject *const *args, Py_ssi
     instruction_offset = args[1];
     callable = args[2];
     self_arg = args[3];
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler__creturn_callback_impl((ProfilerObject *)self, code, instruction_offset, callable, self_arg);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -299,7 +317,9 @@ _lsprof_Profiler_enable(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
         goto exit;
     }
 skip_optional_pos:
+    Py_BEGIN_CRITICAL_SECTION(self);
     return_value = _lsprof_Profiler_enable_impl((ProfilerObject *)self, subcalls, builtins);
+    Py_END_CRITICAL_SECTION();
 
 exit:
     return return_value;
@@ -320,7 +340,13 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self);
 static PyObject *
 _lsprof_Profiler_disable(PyObject *self, PyObject *Py_UNUSED(ignored))
 {
-    return _lsprof_Profiler_disable_impl((ProfilerObject *)self);
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _lsprof_Profiler_disable_impl((ProfilerObject *)self);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
 }
 
 PyDoc_STRVAR(_lsprof_Profiler_clear__doc__,
@@ -338,7 +364,13 @@ _lsprof_Profiler_clear_impl(ProfilerObject *self);
 static PyObject *
 _lsprof_Profiler_clear(PyObject *self, PyObject *Py_UNUSED(ignored))
 {
-    return _lsprof_Profiler_clear_impl((ProfilerObject *)self);
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _lsprof_Profiler_clear_impl((ProfilerObject *)self);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
 }
 
 PyDoc_STRVAR(profiler_init__doc__,
@@ -444,4 +476,4 @@ skip_optional_pos:
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=9e46985561166c37 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=af26a0b0ddcc3351 input=a9049054013a1b77]*/