Skip to content

Commit e89568f

Browse files
GH-148726: Add heap_size to generational GC (#149195)
1 parent de66149 commit e89568f

9 files changed

Lines changed: 36 additions & 10 deletions

File tree

Include/internal/pycore_gc.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,14 @@ static inline void _PyObject_GC_TRACK(
223223
"object is in generation which is garbage collected",
224224
filename, lineno, __func__);
225225

226-
PyGC_Head *generation0 = _PyInterpreterState_GET()->gc.generation0;
226+
struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc;
227+
PyGC_Head *generation0 = gcstate->generation0;
227228
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
228229
_PyGCHead_SET_NEXT(last, gc);
229230
_PyGCHead_SET_PREV(gc, last);
230231
_PyGCHead_SET_NEXT(gc, generation0);
231232
generation0->_gc_prev = (uintptr_t)gc;
233+
gcstate->heap_size++;
232234
#endif
233235
}
234236

@@ -263,6 +265,8 @@ static inline void _PyObject_GC_UNTRACK(
263265
_PyGCHead_SET_PREV(next, prev);
264266
gc->_gc_next = 0;
265267
gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED;
268+
struct _gc_runtime_state *gcstate = &_PyInterpreterState_GET()->gc;
269+
gcstate->heap_size--;
266270
#endif
267271
}
268272

Include/internal/pycore_interp_structs.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ struct gc_generation_stats {
191191
Py_ssize_t candidates;
192192
// Total duration of the collection in seconds:
193193
double duration;
194+
/* heap_size on the start of the collection */
195+
Py_ssize_t heap_size;
194196
};
195197

196198
#ifdef Py_GIL_DISABLED
@@ -226,7 +228,6 @@ struct _gc_runtime_state {
226228
/* linked lists of container objects */
227229
#ifndef Py_GIL_DISABLED
228230
struct gc_generation generations[NUM_GENERATIONS];
229-
PyGC_Head *generation0;
230231
#else
231232
struct gc_generation young;
232233
struct gc_generation old[2];
@@ -244,6 +245,9 @@ struct _gc_runtime_state {
244245
/* a list of callbacks to be invoked when collection is performed */
245246
PyObject *callbacks;
246247

248+
/* The number of live objects. */
249+
Py_ssize_t heap_size;
250+
247251
/* This is the number of objects that survived the last full
248252
collection. It approximates the number of long lived objects
249253
tracked by the GC.
@@ -269,6 +273,8 @@ struct _gc_runtime_state {
269273

270274
/* Mutex held for gc_should_collect_mem_usage(). */
271275
PyMutex mutex;
276+
#else
277+
PyGC_Head *generation0;
272278
#endif
273279
};
274280

@@ -278,7 +284,8 @@ struct _gc_runtime_state {
278284
{ .threshold = 2000, }, \
279285
{ .threshold = 10, }, \
280286
{ .threshold = 10, }, \
281-
},
287+
}, \
288+
.heap_size = 0,
282289
#else
283290
#define GC_GENERATION_INIT \
284291
.young = { .threshold = 2000, }, \

Lib/test/test_gc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,15 @@ def test_tuple_untrack_counts(self):
12881288
# Use n // 2 just in case some other objects were collected.
12891289
self.assertTrue(new_count - count > (n // 2))
12901290

1291+
@requires_gil_enabled('need generational GC')
1292+
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
1293+
def test_heap_size(self):
1294+
count = _testinternalcapi.get_tracked_heap_size()
1295+
l = []
1296+
self.assertEqual(count + 1, _testinternalcapi.get_tracked_heap_size())
1297+
del l
1298+
self.assertEqual(count, _testinternalcapi.get_tracked_heap_size())
1299+
12911300

12921301
class GCCallbackTests(unittest.TestCase):
12931302
def setUp(self):

Lib/test/test_gc_stats.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
GC_STATS_FIELDS = (
2424
"gen", "iid", "ts_start", "ts_stop", "collections", "collected",
25-
"uncollectable", "candidates", "duration")
25+
"uncollectable", "candidates", "heap_size", "duration")
2626

2727

2828
def get_interpreter_identifiers(gc_stats) -> tuple[int,...]:

Modules/_remote_debugging/clinic/module.c.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_remote_debugging/gc_stats.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ read_gc_stats(struct gc_stats *stats, int64_t iid, PyObject *result,
5353
SET_FIELD(PyLong_FromSsize_t, items->collected);
5454
SET_FIELD(PyLong_FromSsize_t, items->uncollectable);
5555
SET_FIELD(PyLong_FromSsize_t, items->candidates);
56+
SET_FIELD(PyLong_FromSsize_t, items->heap_size);
5657

5758
SET_FIELD(PyFloat_FromDouble, items->duration);
5859

Modules/_remote_debugging/module.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ static PyStructSequence_Field GCStatsInfo_fields[] = {
143143
{"collected", "Total number of collected objects"},
144144
{"uncollectable", "Total number of uncollectable objects"},
145145
{"candidates", "Total objects considered and traversed"},
146+
{"heap_size", "Number of live objects"},
146147
{"duration", "Total collection time, in seconds"},
147148
{NULL}
148149
};
@@ -151,7 +152,7 @@ PyStructSequence_Desc GCStatsInfo_desc = {
151152
"_remote_debugging.GCStatsInfo",
152153
"Information about a garbage collector stats sample",
153154
GCStatsInfo_fields,
154-
9
155+
10
155156
};
156157

157158
/* ============================================================================
@@ -1225,6 +1226,7 @@ Returns a list of GCStatsInfo objects with GC statistics data.
12251226
- collected: Total number of collected objects.
12261227
- uncollectable: Total number of uncollectable objects.
12271228
- candidates: Total objects considered and traversed.
1229+
- heap_size: number of live objects.
12281230
- duration: Total collection time, in seconds.
12291231
12301232
Raises:
@@ -1235,7 +1237,7 @@ Returns a list of GCStatsInfo objects with GC statistics data.
12351237
static PyObject *
12361238
_remote_debugging_GCMonitor_get_gc_stats_impl(GCMonitorObject *self,
12371239
int all_interpreters)
1238-
/*[clinic end generated code: output=f73f365725224f7a input=09e647719c65f9e4]*/
1240+
/*[clinic end generated code: output=f73f365725224f7a input=12f7c1a288cf2741]*/
12391241
{
12401242
RemoteDebuggingState *st = RemoteDebugging_GetStateFromType(Py_TYPE(self));
12411243
return get_gc_stats(&self->offsets, all_interpreters, st->GCStatsInfo_Type);

Modules/_testinternalcapi.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2731,8 +2731,7 @@ has_deferred_refcount(PyObject *self, PyObject *op)
27312731
static PyObject *
27322732
get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored))
27332733
{
2734-
// Generational GC doesn't track heap_size, return -1.
2735-
return PyLong_FromInt64(-1);
2734+
return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size);
27362735
}
27372736

27382737
static PyObject *

Python/gc.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1405,13 +1405,13 @@ add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats)
14051405
memcpy(cur_stats, prev_stats, sizeof(struct gc_generation_stats));
14061406

14071407
cur_stats->ts_start = stats->ts_start;
1408-
14091408
cur_stats->collections += 1;
14101409
cur_stats->collected += stats->collected;
14111410
cur_stats->uncollectable += stats->uncollectable;
14121411
cur_stats->candidates += stats->candidates;
14131412

14141413
cur_stats->duration += stats->duration;
1414+
cur_stats->heap_size = stats->heap_size;
14151415
/* Publish ts_stop last so remote readers do not select a partially
14161416
updated stats record as the latest collection. */
14171417
cur_stats->ts_stop = stats->ts_stop;
@@ -1471,6 +1471,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
14711471
invoke_gc_callback(tstate, "start", generation, &stats);
14721472
}
14731473

1474+
stats.heap_size = gcstate->heap_size;
14741475
// ignore error: don't interrupt the GC if reading the clock fails
14751476
(void)PyTime_PerfCounterRaw(&stats.ts_start);
14761477
if (gcstate->debug & _PyGC_DEBUG_STATS) {
@@ -2097,6 +2098,8 @@ PyObject_GC_Del(void *op)
20972098
PyGC_Head *g = AS_GC(op);
20982099
if (_PyObject_GC_IS_TRACKED(op)) {
20992100
gc_list_remove(g);
2101+
GCState *gcstate = get_gc_state();
2102+
gcstate->heap_size--;
21002103
#ifdef Py_DEBUG
21012104
PyObject *exc = PyErr_GetRaisedException();
21022105
if (PyErr_WarnExplicitFormat(PyExc_ResourceWarning, "gc", 0,

0 commit comments

Comments
 (0)