Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Doc/library/gc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,16 @@ The :mod:`gc` module provides the following functions:

* ``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.
list) inside this generation;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
list) inside this generation;
list) inside this generation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did this for consistency with the above points, haha.


* ``duration`` is the total time in seconds spent in collections for this
generation.

.. versionadded:: 3.4

.. versionchanged:: next
Add ``duration``.


.. function:: set_threshold(threshold0, [threshold1, [threshold2]])

Expand Down Expand Up @@ -313,6 +319,9 @@ values but should not rebind them):
"uncollectable": When *phase* is "stop", the number of objects
that could not be collected and were put in :data:`garbage`.

"duration": When *phase* is "stop", the time in seconds spent in the
collection.

Applications can add their own callbacks to this list. The primary
use cases are:

Expand All @@ -325,6 +334,9 @@ values but should not rebind them):

.. versionadded:: 3.3

.. versionchanged:: next
Add "duration".


The following constants are provided for use with :func:`set_debug`:

Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ struct gc_collection_stats {
Py_ssize_t collected;
/* total number of uncollectable objects (put into gc.garbage) */
Py_ssize_t uncollectable;
// Duration of the collection in seconds:
double duration;
};

/* Running stats per generation */
Expand All @@ -189,6 +191,8 @@ struct gc_generation_stats {
Py_ssize_t collected;
/* total number of uncollectable objects (put into gc.garbage) */
Py_ssize_t uncollectable;
// Duration of the collection in seconds:
double duration;
};

enum _GCPhase {
Expand Down
28 changes: 22 additions & 6 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,10 +847,11 @@ def test_get_stats(self):
for st in stats:
self.assertIsInstance(st, dict)
self.assertEqual(set(st),
{"collected", "collections", "uncollectable"})
{"collected", "collections", "uncollectable", "duration"})
self.assertGreaterEqual(st["collected"], 0)
self.assertGreaterEqual(st["collections"], 0)
self.assertGreaterEqual(st["uncollectable"], 0)
self.assertGreaterEqual(st["duration"], 0)
# Check that collection counts are incremented correctly
if gc.isenabled():
self.addCleanup(gc.enable)
Expand All @@ -861,11 +862,25 @@ def test_get_stats(self):
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"])
self.assertGreater(new[0]["duration"], old[0]["duration"])
self.assertEqual(new[1]["duration"], old[1]["duration"])
self.assertEqual(new[2]["duration"], old[2]["duration"])
for stat in ["collected", "uncollectable"]:
self.assertGreaterEqual(new[0][stat], old[0][stat])
self.assertEqual(new[1][stat], old[1][stat])
self.assertEqual(new[2][stat], old[2][stat])
gc.collect(2)
new = gc.get_stats()
self.assertEqual(new[0]["collections"], old[0]["collections"] + 1)
old, new = new, gc.get_stats()
self.assertEqual(new[0]["collections"], old[0]["collections"])
self.assertEqual(new[1]["collections"], old[1]["collections"])
self.assertEqual(new[2]["collections"], old[2]["collections"] + 1)
self.assertEqual(new[0]["duration"], old[0]["duration"])
self.assertEqual(new[1]["duration"], old[1]["duration"])
self.assertGreater(new[2]["duration"], old[2]["duration"])
for stat in ["collected", "uncollectable"]:
self.assertEqual(new[0][stat], old[0][stat])
self.assertEqual(new[1][stat], old[1][stat])
self.assertGreaterEqual(new[2][stat], old[2][stat])

def test_freeze(self):
gc.freeze()
Expand Down Expand Up @@ -1298,9 +1313,10 @@ def test_collect(self):
# Check that we got the right info dict for all callbacks
for v in self.visit:
info = v[2]
self.assertTrue("generation" in info)
self.assertTrue("collected" in info)
self.assertTrue("uncollectable" in info)
self.assertIn("generation", info)
self.assertIn("collected", info)
self.assertIn("uncollectable", info)
self.assertIn("duration", info)

def test_collect_generation(self):
self.preclean()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Expose a ``"duration"`` stat in :func:`gc.get_stats` and
:data:`gc.callbacks`.
5 changes: 3 additions & 2 deletions Modules/gcmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,11 @@ gc_get_stats_impl(PyObject *module)
for (i = 0; i < NUM_GENERATIONS; i++) {
PyObject *dict;
st = &stats[i];
dict = Py_BuildValue("{snsnsn}",
dict = Py_BuildValue("{snsnsnsd}",
"collections", st->collections,
"collected", st->collected,
"uncollectable", st->uncollectable
"uncollectable", st->uncollectable,
"duration", st->duration
);
if (dict == NULL)
goto error;
Expand Down
21 changes: 10 additions & 11 deletions Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,7 @@ gc_list_set_space(PyGC_Head *list, int space)
static void
add_stats(GCState *gcstate, int gen, struct gc_collection_stats *stats)
{
gcstate->generation_stats[gen].duration += stats->duration;
gcstate->generation_stats[gen].collected += stats->collected;
gcstate->generation_stats[gen].uncollectable += stats->uncollectable;
gcstate->generation_stats[gen].collections += 1;
Expand All @@ -1387,7 +1388,6 @@ gc_collect_young(PyThreadState *tstate,
validate_spaces(gcstate);
gcstate->young.count = 0;
gcstate->old[gcstate->visited_space].count++;
add_stats(gcstate, 0, stats);
validate_spaces(gcstate);
}

Expand Down Expand Up @@ -1701,7 +1701,6 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
assert(gc_list_is_empty(&increment));
gcstate->work_to_do -= increment_size;

add_stats(gcstate, 1, stats);
if (gc_list_is_empty(not_visited)) {
completed_scavenge(gcstate);
}
Expand Down Expand Up @@ -1736,7 +1735,6 @@ gc_collect_full(PyThreadState *tstate,
completed_scavenge(gcstate);
_PyGC_ClearAllFreeLists(tstate->interp);
validate_spaces(gcstate);
add_stats(gcstate, 2, stats);
}

/* This is the main function. Read this to understand how the
Expand Down Expand Up @@ -1846,10 +1844,11 @@ do_gc_callback(GCState *gcstate, const char *phase,
assert(PyList_CheckExact(gcstate->callbacks));
PyObject *info = NULL;
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
info = Py_BuildValue("{sisnsn}",
info = Py_BuildValue("{sisnsnsd}",
"generation", generation,
"collected", stats->collected,
"uncollectable", stats->uncollectable);
"uncollectable", stats->uncollectable,
"duration", stats->duration);
if (info == NULL) {
PyErr_FormatUnraisable("Exception ignored while invoking gc callbacks");
return;
Expand Down Expand Up @@ -2080,15 +2079,15 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
if (reason != _Py_GC_REASON_SHUTDOWN) {
invoke_gc_callback(gcstate, "start", generation, &stats);
}
PyTime_t t1;
if (gcstate->debug & _PyGC_DEBUG_STATS) {
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
(void)PyTime_PerfCounterRaw(&t1);
show_stats_each_generations(gcstate);
}
if (PyDTrace_GC_START_ENABLED()) {
PyDTrace_GC_START(generation);
}
PyTime_t start, stop;
(void)PyTime_PerfCounterRaw(&start);
PyObject *exc = _PyErr_GetRaisedException(tstate);
switch(generation) {
case 0:
Expand All @@ -2103,6 +2102,9 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
default:
Py_UNREACHABLE();
}
(void)PyTime_PerfCounterRaw(&stop);
stats.duration = PyTime_AsSecondsDouble(stop - start);
add_stats(gcstate, generation, &stats);
if (PyDTrace_GC_DONE_ENABLED()) {
PyDTrace_GC_DONE(stats.uncollectable + stats.collected);
}
Expand All @@ -2124,12 +2126,9 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
_Py_atomic_store_int(&gcstate->collecting, 0);

if (gcstate->debug & _PyGC_DEBUG_STATS) {
PyTime_t t2;
(void)PyTime_PerfCounterRaw(&t2);
double d = PyTime_AsSecondsDouble(t2 - t1);
PySys_WriteStderr(
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
stats.collected + stats.uncollectable, stats.uncollectable, d
stats.collected + stats.uncollectable, stats.uncollectable, stats.duration
);
}

Expand Down
25 changes: 13 additions & 12 deletions Python/gc_free_threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -1911,7 +1911,7 @@ handle_resurrected_objects(struct collection_state *state)
static void
invoke_gc_callback(PyThreadState *tstate, const char *phase,
int generation, Py_ssize_t collected,
Py_ssize_t uncollectable)
Py_ssize_t uncollectable, double duration)
{
assert(!_PyErr_Occurred(tstate));

Expand All @@ -1925,10 +1925,11 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase,
assert(PyList_CheckExact(gcstate->callbacks));
PyObject *info = NULL;
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
info = Py_BuildValue("{sisnsn}",
info = Py_BuildValue("{sisnsnsd}",
"generation", generation,
"collected", collected,
"uncollectable", uncollectable);
"uncollectable", uncollectable,
"duration", duration);
if (info == NULL) {
PyErr_FormatUnraisable("Exception ignored while "
"invoking gc callbacks");
Expand Down Expand Up @@ -2340,7 +2341,6 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
{
Py_ssize_t m = 0; /* # objects collected */
Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
PyTime_t t1 = 0; /* initialize to prevent a compiler warning */
GCState *gcstate = &tstate->interp->gc;

// gc_collect_main() must not be called before _PyGC_Init
Expand Down Expand Up @@ -2372,19 +2372,19 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
GC_STAT_ADD(generation, collections, 1);

if (reason != _Py_GC_REASON_SHUTDOWN) {
invoke_gc_callback(tstate, "start", generation, 0, 0);
invoke_gc_callback(tstate, "start", generation, 0, 0, 0);
}

if (gcstate->debug & _PyGC_DEBUG_STATS) {
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
show_stats_each_generations(gcstate);
// ignore error: don't interrupt the GC if reading the clock fails
(void)PyTime_PerfCounterRaw(&t1);
}

if (PyDTrace_GC_START_ENABLED()) {
PyDTrace_GC_START(generation);
}
PyTime_t start, stop;
(void)PyTime_PerfCounterRaw(&start);

PyInterpreterState *interp = tstate->interp;

Expand All @@ -2399,13 +2399,13 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
m = state.collected;
n = state.uncollectable;

(void)PyTime_PerfCounterRaw(&stop);
double duration = PyTime_AsSecondsDouble(stop - start);

if (gcstate->debug & _PyGC_DEBUG_STATS) {
PyTime_t t2;
(void)PyTime_PerfCounterRaw(&t2);
double d = PyTime_AsSecondsDouble(t2 - t1);
PySys_WriteStderr(
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
n+m, n, d);
n+m, n, duration);
}

// Clear the current thread's free-list again.
Expand All @@ -2426,6 +2426,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
stats->collections++;
stats->collected += m;
stats->uncollectable += n;
stats->duration += duration;

GC_STAT_ADD(generation, objects_collected, m);
#ifdef Py_STATS
Expand All @@ -2444,7 +2445,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
}

if (reason != _Py_GC_REASON_SHUTDOWN) {
invoke_gc_callback(tstate, "stop", generation, m, n);
invoke_gc_callback(tstate, "stop", generation, m, n, duration);
}

assert(!_PyErr_Occurred(tstate));
Expand Down
Loading