]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-97922: Run the GC only on eval breaker (#97920)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Sat, 8 Oct 2022 14:57:09 +0000 (07:57 -0700)
committerGitHub <noreply@github.com>
Sat, 8 Oct 2022 14:57:09 +0000 (07:57 -0700)
Doc/whatsnew/3.12.rst
Include/internal/pycore_gc.h
Include/internal/pycore_interp.h
Lib/test/test_frame.py
Misc/NEWS.d/next/Core and Builtins/2022-10-05-11-37-15.gh-issue-97922.Zu9Bge.rst [new file with mode: 0644]
Modules/gcmodule.c
Modules/signalmodule.c
Python/ceval_gil.c

index f873974b3e78fed001c5b87aa11cb28d5220a34d..341e85103a3cf7b3b6c995826c0f664da9e80647 100644 (file)
@@ -93,6 +93,13 @@ Other Language Changes
   when parsing source code containing null bytes. (Contributed by Pablo Galindo
   in :gh:`96670`.)
 
+* The Garbage Collector now runs only on the eval breaker mechanism of the
+  Python bytecode evaluation loop instead on object allocations. The GC can
+  also run when :c:func:`PyErr_CheckSignals` is called so C extensions that
+  need to run for a long time without executing any Python code also have a
+  chance to execute the GC periodically. (Contributed by Pablo Galindo in
+  :gh:`97922`.)
+
 New Modules
 ===========
 
index bfab0adfffc9ff60221b0abd6f063145d89eb249..b3abe2030a03dacfc2ea31317ef67c9c26ef64c1 100644 (file)
@@ -202,6 +202,8 @@ extern void _PyList_ClearFreeList(PyInterpreterState *interp);
 extern void _PyDict_ClearFreeList(PyInterpreterState *interp);
 extern void _PyAsyncGen_ClearFreeLists(PyInterpreterState *interp);
 extern void _PyContext_ClearFreeList(PyInterpreterState *interp);
+extern void _Py_ScheduleGC(PyInterpreterState *interp);
+extern void _Py_RunGC(PyThreadState *tstate);
 
 #ifdef __cplusplus
 }
index 8cecd5ab3e541e2c9b668966cac7e9069fb39999..c11e897305d42bb8369f8f52b7b83ee3b33fe7bf 100644 (file)
@@ -49,6 +49,8 @@ struct _ceval_state {
     _Py_atomic_int eval_breaker;
     /* Request for dropping the GIL */
     _Py_atomic_int gil_drop_request;
+    /* The GC is ready to be executed */
+    _Py_atomic_int gc_scheduled;
     struct _pending_calls pending;
 };
 
index 4b86a60d2f4c362b0fcbbf3ee04d97f131baaa61..4b5bb7f94ac4692402e72ac0e9efb64d7407a2a9 100644 (file)
@@ -277,7 +277,7 @@ class TestIncompleteFrameAreInvisible(unittest.TestCase):
             frame!
             """
             nonlocal sneaky_frame_object
-            sneaky_frame_object = sys._getframe().f_back
+            sneaky_frame_object = sys._getframe().f_back.f_back
             # We're done here:
             gc.callbacks.remove(callback)
 
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-10-05-11-37-15.gh-issue-97922.Zu9Bge.rst b/Misc/NEWS.d/next/Core and Builtins/2022-10-05-11-37-15.gh-issue-97922.Zu9Bge.rst
new file mode 100644 (file)
index 0000000..bf78709
--- /dev/null
@@ -0,0 +1,5 @@
+The Garbage Collector now runs only on the eval breaker mechanism of the
+Python bytecode evaluation loop instead on object allocations. The GC can
+also run when :c:func:`PyErr_CheckSignals` is called so C extensions that
+need to run for a long time without executing any Python code also have a
+chance to execute the GC periodically.
index 97cb6e6e1efb1f195d0e513b697a06040d1061c2..75832e9dd3da635b5274af005d8dba7524093c38 100644 (file)
@@ -2252,6 +2252,20 @@ PyObject_IS_GC(PyObject *obj)
     return _PyObject_IS_GC(obj);
 }
 
+void
+_Py_ScheduleGC(PyInterpreterState *interp)
+{
+    GCState *gcstate = &interp->gc;
+    if (gcstate->collecting == 1) {
+        return;
+    }
+    struct _ceval_state *ceval = &interp->ceval;
+    if (!_Py_atomic_load_relaxed(&ceval->gc_scheduled)) {
+        _Py_atomic_store_relaxed(&ceval->gc_scheduled, 1);
+        _Py_atomic_store_relaxed(&ceval->eval_breaker, 1);
+    }
+}
+
 void
 _PyObject_GC_Link(PyObject *op)
 {
@@ -2269,12 +2283,19 @@ _PyObject_GC_Link(PyObject *op)
         !gcstate->collecting &&
         !_PyErr_Occurred(tstate))
     {
-        gcstate->collecting = 1;
-        gc_collect_generations(tstate);
-        gcstate->collecting = 0;
+        _Py_ScheduleGC(tstate->interp);
     }
 }
 
+void
+_Py_RunGC(PyThreadState *tstate)
+{
+    GCState *gcstate = &tstate->interp->gc;
+    gcstate->collecting = 1;
+    gc_collect_generations(tstate);
+    gcstate->collecting = 0;
+}
+
 static PyObject *
 gc_alloc(size_t basicsize, size_t presize)
 {
index 0f30b4da036313e01e9a8488acbdb43ee42a2714..b85d6d19e8cd057c532cacb9ca54a8253b575a1f 100644 (file)
@@ -1798,6 +1798,19 @@ int
 PyErr_CheckSignals(void)
 {
     PyThreadState *tstate = _PyThreadState_GET();
+
+    /* Opportunistically check if the GC is scheduled to run and run it
+       if we have a request. This is done here because native code needs
+       to call this API if is going to run for some time without executing
+       Python code to ensure signals are handled. Checking for the GC here
+       allows long running native code to clean cycles created using the C-API
+       even if it doesn't run the evaluation loop */
+    struct _ceval_state *interp_ceval_state = &tstate->interp->ceval;
+    if (_Py_atomic_load_relaxed(&interp_ceval_state->gc_scheduled)) {
+        _Py_atomic_store_relaxed(&interp_ceval_state->gc_scheduled, 0);
+        _Py_RunGC(tstate);
+    }
+
     if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
         return 0;
     }
index fd737b5738e8896eb5806b5e743dc8ddbeed3d27..9b9d7dc1d1af1e7ce68281446a1442a27cdee569 100644 (file)
@@ -5,6 +5,7 @@
 #include "pycore_pyerrors.h"      // _PyErr_Fetch()
 #include "pycore_pylifecycle.h"   // _PyErr_Print()
 #include "pycore_initconfig.h"    // _PyStatus_OK()
+#include "pycore_interp.h"        // _Py_RunGC()
 #include "pycore_pymem.h"         // _PyMem_IsPtrFreed()
 
 /*
@@ -69,7 +70,8 @@ COMPUTE_EVAL_BREAKER(PyInterpreterState *interp,
            && _Py_ThreadCanHandleSignals(interp))
         | (_Py_atomic_load_relaxed_int32(&ceval2->pending.calls_to_do)
            && _Py_ThreadCanHandlePendingCalls())
-        | ceval2->pending.async_exc);
+        | ceval2->pending.async_exc
+        | _Py_atomic_load_relaxed_int32(&ceval2->gc_scheduled));
 }
 
 
@@ -938,6 +940,7 @@ _Py_HandlePending(PyThreadState *tstate)
 {
     _PyRuntimeState * const runtime = &_PyRuntime;
     struct _ceval_runtime_state *ceval = &runtime->ceval;
+    struct _ceval_state *interp_ceval_state = &tstate->interp->ceval;
 
     /* Pending signals */
     if (_Py_atomic_load_relaxed_int32(&ceval->signals_pending)) {
@@ -947,20 +950,26 @@ _Py_HandlePending(PyThreadState *tstate)
     }
 
     /* Pending calls */
-    struct _ceval_state *ceval2 = &tstate->interp->ceval;
-    if (_Py_atomic_load_relaxed_int32(&ceval2->pending.calls_to_do)) {
+    if (_Py_atomic_load_relaxed_int32(&interp_ceval_state->pending.calls_to_do)) {
         if (make_pending_calls(tstate->interp) != 0) {
             return -1;
         }
     }
 
+    /* GC scheduled to run */
+    if (_Py_atomic_load_relaxed_int32(&interp_ceval_state->gc_scheduled)) {
+        _Py_atomic_store_relaxed(&interp_ceval_state->gc_scheduled, 0);
+        COMPUTE_EVAL_BREAKER(tstate->interp, ceval, interp_ceval_state);
+        _Py_RunGC(tstate);
+    }
+
     /* GIL drop request */
-    if (_Py_atomic_load_relaxed_int32(&ceval2->gil_drop_request)) {
+    if (_Py_atomic_load_relaxed_int32(&interp_ceval_state->gil_drop_request)) {
         /* Give another thread a chance */
         if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
             Py_FatalError("tstate mix-up");
         }
-        drop_gil(ceval, ceval2, tstate);
+        drop_gil(ceval, interp_ceval_state, tstate);
 
         /* Other threads may run now */
 
@@ -981,16 +990,17 @@ _Py_HandlePending(PyThreadState *tstate)
         return -1;
     }
 
-#ifdef MS_WINDOWS
-    // bpo-42296: On Windows, _PyEval_SignalReceived() can be called in a
-    // different thread than the Python thread, in which case
+
+    // It is possible that some of the conditions that trigger the eval breaker
+    // are called in a different thread than the Python thread. An example of
+    // this is bpo-42296: On Windows, _PyEval_SignalReceived() can be called in
+    // a different thread than the Python thread, in which case
     // _Py_ThreadCanHandleSignals() is wrong. Recompute eval_breaker in the
     // current Python thread with the correct _Py_ThreadCanHandleSignals()
     // value. It prevents to interrupt the eval loop at every instruction if
     // the current Python thread cannot handle signals (if
     // _Py_ThreadCanHandleSignals() is false).
-    COMPUTE_EVAL_BREAKER(tstate->interp, ceval, ceval2);
-#endif
+    COMPUTE_EVAL_BREAKER(tstate->interp, ceval, interp_ceval_state);
 
     return 0;
 }