diff --git a/Doc/library/gc.rst b/Doc/library/gc.rst --- a/Doc/library/gc.rst +++ b/Doc/library/gc.rst @@ -67,6 +67,24 @@ The :mod:`gc` module provides the follow returned. +.. function:: get_stats() + + Return a list of 3 per-generation dictionaries containing collection + statistics since interpreter start. At this moment, each dictionary will + contain the following items: + + * ``collections`` is the number of times this generation was collected; + + * ``collected`` is the total number of objects collected inside this + generation; + + * ``uncollectable`` is the total number of objects which were found + to be uncollectable (and were therefore moved to the :data:`garbage` + list) inside this generation. + + .. versionadded:: 3.4 + + .. function:: set_threshold(threshold0[, threshold1[, threshold2]]) Set the garbage collection thresholds (the collection frequency). Setting diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -610,6 +610,32 @@ class GCTests(unittest.TestCase): stderr = run_command(code % "gc.DEBUG_SAVEALL") self.assertNotIn(b"uncollectable objects at shutdown", stderr) + def test_get_stats(self): + stats = gc.get_stats() + self.assertEqual(len(stats), 3) + for st in stats: + self.assertIsInstance(st, dict) + self.assertEqual(set(st), + {"collected", "collections", "uncollectable"}) + self.assertGreaterEqual(st["collected"], 0) + self.assertGreaterEqual(st["collections"], 0) + self.assertGreaterEqual(st["uncollectable"], 0) + # Check that collection counts are incremented correctly + old = gc.get_stats() + if gc.isenabled(): + self.addCleanup(gc.enable) + gc.disable() + gc.collect(0) + new = gc.get_stats() + self.assertEqual(new[0]["collections"], old[0]["collections"] + 1) + self.assertEqual(new[1]["collections"], old[1]["collections"]) + self.assertEqual(new[2]["collections"], old[2]["collections"]) + gc.collect(2) + new = gc.get_stats() + self.assertEqual(new[0]["collections"], old[0]["collections"] + 1) + self.assertEqual(new[1]["collections"], old[1]["collections"]) + self.assertEqual(new[2]["collections"], old[2]["collections"] + 1) + class GCCallbackTests(unittest.TestCase): def setUp(self): diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -168,6 +168,18 @@ static Py_ssize_t long_lived_pending = 0 static int debug; static PyObject *tmod = NULL; +/* Running stats per generation */ +struct gc_generation_stats { + /* total number of collections */ + Py_ssize_t collections; + /* total number of collected objects */ + Py_ssize_t collected; + /* total number of uncollectable objects (put into gc.garbage) */ + Py_ssize_t uncollectable; +}; + +static struct gc_generation_stats generation_stats[NUM_GENERATIONS]; + /*-------------------------------------------------------------------------- gc_refs values. @@ -852,6 +864,7 @@ collect(int generation, Py_ssize_t *n_co PyGC_Head finalizers; /* objects with, & reachable from, __del__ */ PyGC_Head *gc; double t1 = 0.0; + struct gc_generation_stats *stats = &generation_stats[generation]; if (debug & DEBUG_STATS) { PySys_WriteStderr("gc: collecting generation %d...\n", @@ -993,10 +1006,14 @@ collect(int generation, Py_ssize_t *n_co Py_FatalError("unexpected exception during garbage collection"); } + /* Update stats */ if (n_collected) *n_collected = m; if (n_uncollectable) *n_uncollectable = n; + stats->collections++; + stats->collected += m; + stats->uncollectable += n; return n+m; } @@ -1343,6 +1360,52 @@ gc_get_objects(PyObject *self, PyObject return result; } +PyDoc_STRVAR(gc_get_stats__doc__, +"get_stats() -> [...]\n" +"\n" +"Return a list of dictionaries containing per-generation statistics.\n"); + +static PyObject * +gc_get_stats(PyObject *self, PyObject *noargs) +{ + int i; + PyObject *result; + struct gc_generation_stats stats[NUM_GENERATIONS], *st; + + /* To get consistent values despite allocations while constructing + the result list, we use a snapshot of the running stats. */ + for (i = 0; i < NUM_GENERATIONS; i++) { + stats[i] = generation_stats[i]; + } + + result = PyList_New(0); + if (result == NULL) + return NULL; + + for (i = 0; i < NUM_GENERATIONS; i++) { + PyObject *dict; + st = &stats[i]; + dict = Py_BuildValue("{snsnsn}", + "collections", st->collections, + "collected", st->collected, + "uncollectable", st->uncollectable + ); + if (dict == NULL) + goto error; + if (PyList_Append(result, dict)) { + Py_DECREF(dict); + return NULL; + } + Py_DECREF(dict); + } + return result; + +error: + Py_XDECREF(result); + return NULL; +} + + PyDoc_STRVAR(gc_is_tracked__doc__, "is_tracked(obj) -> bool\n" "\n" @@ -1393,6 +1456,7 @@ static PyMethodDef GcMethods[] = { {"collect", (PyCFunction)gc_collect, METH_VARARGS | METH_KEYWORDS, gc_collect__doc__}, {"get_objects", gc_get_objects,METH_NOARGS, gc_get_objects__doc__}, + {"get_stats", gc_get_stats, METH_NOARGS, gc_get_stats__doc__}, {"is_tracked", gc_is_tracked, METH_O, gc_is_tracked__doc__}, {"get_referrers", gc_get_referrers, METH_VARARGS, gc_get_referrers__doc__},