diff --git a/Doc/data/python3.14.abi b/Doc/data/python3.14.abi
index 7dc43dd58ce2d5..78f380ee46d4a5 100644
--- a/Doc/data/python3.14.abi
+++ b/Doc/data/python3.14.abi
@@ -1817,7 +1817,7 @@
-
+
@@ -4202,30 +4202,30 @@
-
+
-
+
-
+
-
+
-
+
-
+
@@ -8404,7 +8404,7 @@
-
+
@@ -21024,7 +21024,7 @@
-
+
@@ -21059,324 +21059,327 @@
-
+
-
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -21388,385 +21391,385 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
-
-
-
+
+
-
+
+
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
+
-
-
+
+
-
+
+
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -22235,7 +22238,7 @@
-
+
@@ -25879,7 +25882,7 @@
-
+
@@ -26900,21 +26903,21 @@
-
+
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
@@ -27832,6 +27835,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -27842,16 +27859,16 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
@@ -27987,7 +28004,7 @@
-
+
@@ -28099,7 +28116,7 @@
-
+
@@ -28124,17 +28141,17 @@
-
-
+
+
-
-
+
+
-
-
-
+
+
+
@@ -28246,15 +28263,6 @@
-
-
-
-
-
-
-
-
-
@@ -28270,11 +28278,6 @@
-
-
-
-
-
@@ -30087,7 +30090,7 @@
-
+
@@ -30636,7 +30639,7 @@
-
+
diff --git a/Include/internal/pycore_gc.h b/Include/internal/pycore_gc.h
index e105677cd2e674..bfe52f42f1141c 100644
--- a/Include/internal/pycore_gc.h
+++ b/Include/internal/pycore_gc.h
@@ -223,12 +223,14 @@ static inline void _PyObject_GC_TRACK(
"object is in generation which is garbage collected",
filename, lineno, __func__);
- PyGC_Head *generation0 = _PyInterpreterState_GET()->gc.generation0;
+ struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc;
+ PyGC_Head *generation0 = gcstate->generation0;
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
_PyGCHead_SET_NEXT(last, gc);
_PyGCHead_SET_PREV(gc, last);
_PyGCHead_SET_NEXT(gc, generation0);
generation0->_gc_prev = (uintptr_t)gc;
+ gcstate->heap_size++;
#endif
}
@@ -263,6 +265,8 @@ static inline void _PyObject_GC_UNTRACK(
_PyGCHead_SET_PREV(next, prev);
gc->_gc_next = 0;
gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED;
+ struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc;
+ gcstate->heap_size--;
#endif
}
diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h
index f186caab42bbf6..972bf7bf6e8054 100644
--- a/Include/internal/pycore_interp_structs.h
+++ b/Include/internal/pycore_interp_structs.h
@@ -227,6 +227,9 @@ struct _gc_runtime_state {
/* a list of callbacks to be invoked when collection is performed */
PyObject *callbacks;
+ /* The number of live objects. */
+ Py_ssize_t heap_size;
+
/* This is the number of objects that survived the last full
collection. It approximates the number of long lived objects
tracked by the GC.
@@ -261,7 +264,8 @@ struct _gc_runtime_state {
{ .threshold = 2000, }, \
{ .threshold = 10, }, \
{ .threshold = 10, }, \
- },
+ }, \
+ .heap_size = 0,
#else
#define GC_GENERATION_INIT \
.young = { .threshold = 2000, }, \
diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py
index cd9b34cb79da51..0fe63332d15c9c 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -1220,6 +1220,15 @@ def test_tuple_untrack_counts(self):
# Use n // 2 just in case some other objects were collected.
self.assertTrue(new_count - count > (n // 2))
+ @requires_gil_enabled('need generational GC')
+ @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
+ def test_heap_size(self):
+ count = _testinternalcapi.get_tracked_heap_size()
+ l = []
+ self.assertEqual(count + 1, _testinternalcapi.get_tracked_heap_size())
+ del l
+ self.assertEqual(count, _testinternalcapi.get_tracked_heap_size())
+
class GCCallbackTests(unittest.TestCase):
def setUp(self):
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index ae4da5fa55e487..158c26d442ed13 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -2353,8 +2353,7 @@ has_deferred_refcount(PyObject *self, PyObject *op)
static PyObject *
get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
{
- // Generational GC doesn't track heap_size, return -1.
- return PyLong_FromInt64(-1);
+ return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size);
}
static PyObject *
diff --git a/Python/gc.c b/Python/gc.c
index 0559dfb11f0e41..469282683604b8 100644
--- a/Python/gc.c
+++ b/Python/gc.c
@@ -1981,6 +1981,8 @@ PyObject_GC_Del(void *op)
PyGC_Head *g = AS_GC(op);
if (_PyObject_GC_IS_TRACKED(op)) {
gc_list_remove(g);
+ GCState *gcstate = get_gc_state();
+ gcstate->heap_size--;
#ifdef Py_DEBUG
PyObject *exc = PyErr_GetRaisedException();
if (PyErr_WarnExplicitFormat(PyExc_ResourceWarning, "gc", 0,