mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
GH-140638: Add a GC "duration" stat (GH-141720)
This commit is contained in:
parent
b3b63e8d6d
commit
598d4c64de
7 changed files with 67 additions and 32 deletions
|
|
@ -108,10 +108,16 @@ The :mod:`gc` module provides the following functions:
|
||||||
|
|
||||||
* ``uncollectable`` is the total number of objects which were found
|
* ``uncollectable`` is the total number of objects which were found
|
||||||
to be uncollectable (and were therefore moved to the :data:`garbage`
|
to be uncollectable (and were therefore moved to the :data:`garbage`
|
||||||
list) inside this generation.
|
list) inside this generation;
|
||||||
|
|
||||||
|
* ``duration`` is the total time in seconds spent in collections for this
|
||||||
|
generation.
|
||||||
|
|
||||||
.. versionadded:: 3.4
|
.. versionadded:: 3.4
|
||||||
|
|
||||||
|
.. versionchanged:: next
|
||||||
|
Add ``duration``.
|
||||||
|
|
||||||
|
|
||||||
.. function:: set_threshold(threshold0, [threshold1, [threshold2]])
|
.. function:: set_threshold(threshold0, [threshold1, [threshold2]])
|
||||||
|
|
||||||
|
|
@ -313,6 +319,9 @@ values but should not rebind them):
|
||||||
"uncollectable": When *phase* is "stop", the number of objects
|
"uncollectable": When *phase* is "stop", the number of objects
|
||||||
that could not be collected and were put in :data:`garbage`.
|
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
|
Applications can add their own callbacks to this list. The primary
|
||||||
use cases are:
|
use cases are:
|
||||||
|
|
||||||
|
|
@ -325,6 +334,9 @@ values but should not rebind them):
|
||||||
|
|
||||||
.. versionadded:: 3.3
|
.. versionadded:: 3.3
|
||||||
|
|
||||||
|
.. versionchanged:: next
|
||||||
|
Add "duration".
|
||||||
|
|
||||||
|
|
||||||
The following constants are provided for use with :func:`set_debug`:
|
The following constants are provided for use with :func:`set_debug`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,8 @@ struct gc_collection_stats {
|
||||||
Py_ssize_t collected;
|
Py_ssize_t collected;
|
||||||
/* total number of uncollectable objects (put into gc.garbage) */
|
/* total number of uncollectable objects (put into gc.garbage) */
|
||||||
Py_ssize_t uncollectable;
|
Py_ssize_t uncollectable;
|
||||||
|
// Duration of the collection in seconds:
|
||||||
|
double duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Running stats per generation */
|
/* Running stats per generation */
|
||||||
|
|
@ -189,6 +191,8 @@ struct gc_generation_stats {
|
||||||
Py_ssize_t collected;
|
Py_ssize_t collected;
|
||||||
/* total number of uncollectable objects (put into gc.garbage) */
|
/* total number of uncollectable objects (put into gc.garbage) */
|
||||||
Py_ssize_t uncollectable;
|
Py_ssize_t uncollectable;
|
||||||
|
// Duration of the collection in seconds:
|
||||||
|
double duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum _GCPhase {
|
enum _GCPhase {
|
||||||
|
|
|
||||||
|
|
@ -847,10 +847,11 @@ def test_get_stats(self):
|
||||||
for st in stats:
|
for st in stats:
|
||||||
self.assertIsInstance(st, dict)
|
self.assertIsInstance(st, dict)
|
||||||
self.assertEqual(set(st),
|
self.assertEqual(set(st),
|
||||||
{"collected", "collections", "uncollectable"})
|
{"collected", "collections", "uncollectable", "duration"})
|
||||||
self.assertGreaterEqual(st["collected"], 0)
|
self.assertGreaterEqual(st["collected"], 0)
|
||||||
self.assertGreaterEqual(st["collections"], 0)
|
self.assertGreaterEqual(st["collections"], 0)
|
||||||
self.assertGreaterEqual(st["uncollectable"], 0)
|
self.assertGreaterEqual(st["uncollectable"], 0)
|
||||||
|
self.assertGreaterEqual(st["duration"], 0)
|
||||||
# Check that collection counts are incremented correctly
|
# Check that collection counts are incremented correctly
|
||||||
if gc.isenabled():
|
if gc.isenabled():
|
||||||
self.addCleanup(gc.enable)
|
self.addCleanup(gc.enable)
|
||||||
|
|
@ -861,11 +862,25 @@ def test_get_stats(self):
|
||||||
self.assertEqual(new[0]["collections"], old[0]["collections"] + 1)
|
self.assertEqual(new[0]["collections"], old[0]["collections"] + 1)
|
||||||
self.assertEqual(new[1]["collections"], old[1]["collections"])
|
self.assertEqual(new[1]["collections"], old[1]["collections"])
|
||||||
self.assertEqual(new[2]["collections"], old[2]["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)
|
gc.collect(2)
|
||||||
new = gc.get_stats()
|
old, new = new, gc.get_stats()
|
||||||
self.assertEqual(new[0]["collections"], old[0]["collections"] + 1)
|
self.assertEqual(new[0]["collections"], old[0]["collections"])
|
||||||
self.assertEqual(new[1]["collections"], old[1]["collections"])
|
self.assertEqual(new[1]["collections"], old[1]["collections"])
|
||||||
self.assertEqual(new[2]["collections"], old[2]["collections"] + 1)
|
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):
|
def test_freeze(self):
|
||||||
gc.freeze()
|
gc.freeze()
|
||||||
|
|
@ -1298,9 +1313,10 @@ def test_collect(self):
|
||||||
# Check that we got the right info dict for all callbacks
|
# Check that we got the right info dict for all callbacks
|
||||||
for v in self.visit:
|
for v in self.visit:
|
||||||
info = v[2]
|
info = v[2]
|
||||||
self.assertTrue("generation" in info)
|
self.assertIn("generation", info)
|
||||||
self.assertTrue("collected" in info)
|
self.assertIn("collected", info)
|
||||||
self.assertTrue("uncollectable" in info)
|
self.assertIn("uncollectable", info)
|
||||||
|
self.assertIn("duration", info)
|
||||||
|
|
||||||
def test_collect_generation(self):
|
def test_collect_generation(self):
|
||||||
self.preclean()
|
self.preclean()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Expose a ``"duration"`` stat in :func:`gc.get_stats` and
|
||||||
|
:data:`gc.callbacks`.
|
||||||
|
|
@ -358,10 +358,11 @@ gc_get_stats_impl(PyObject *module)
|
||||||
for (i = 0; i < NUM_GENERATIONS; i++) {
|
for (i = 0; i < NUM_GENERATIONS; i++) {
|
||||||
PyObject *dict;
|
PyObject *dict;
|
||||||
st = &stats[i];
|
st = &stats[i];
|
||||||
dict = Py_BuildValue("{snsnsn}",
|
dict = Py_BuildValue("{snsnsnsd}",
|
||||||
"collections", st->collections,
|
"collections", st->collections,
|
||||||
"collected", st->collected,
|
"collected", st->collected,
|
||||||
"uncollectable", st->uncollectable
|
"uncollectable", st->uncollectable,
|
||||||
|
"duration", st->duration
|
||||||
);
|
);
|
||||||
if (dict == NULL)
|
if (dict == NULL)
|
||||||
goto error;
|
goto error;
|
||||||
|
|
|
||||||
21
Python/gc.c
21
Python/gc.c
|
|
@ -1363,6 +1363,7 @@ gc_list_set_space(PyGC_Head *list, int space)
|
||||||
static void
|
static void
|
||||||
add_stats(GCState *gcstate, int gen, struct gc_collection_stats *stats)
|
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].collected += stats->collected;
|
||||||
gcstate->generation_stats[gen].uncollectable += stats->uncollectable;
|
gcstate->generation_stats[gen].uncollectable += stats->uncollectable;
|
||||||
gcstate->generation_stats[gen].collections += 1;
|
gcstate->generation_stats[gen].collections += 1;
|
||||||
|
|
@ -1387,7 +1388,6 @@ gc_collect_young(PyThreadState *tstate,
|
||||||
validate_spaces(gcstate);
|
validate_spaces(gcstate);
|
||||||
gcstate->young.count = 0;
|
gcstate->young.count = 0;
|
||||||
gcstate->old[gcstate->visited_space].count++;
|
gcstate->old[gcstate->visited_space].count++;
|
||||||
add_stats(gcstate, 0, stats);
|
|
||||||
validate_spaces(gcstate);
|
validate_spaces(gcstate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1701,7 +1701,6 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
|
||||||
assert(gc_list_is_empty(&increment));
|
assert(gc_list_is_empty(&increment));
|
||||||
gcstate->work_to_do -= increment_size;
|
gcstate->work_to_do -= increment_size;
|
||||||
|
|
||||||
add_stats(gcstate, 1, stats);
|
|
||||||
if (gc_list_is_empty(not_visited)) {
|
if (gc_list_is_empty(not_visited)) {
|
||||||
completed_scavenge(gcstate);
|
completed_scavenge(gcstate);
|
||||||
}
|
}
|
||||||
|
|
@ -1736,7 +1735,6 @@ gc_collect_full(PyThreadState *tstate,
|
||||||
completed_scavenge(gcstate);
|
completed_scavenge(gcstate);
|
||||||
_PyGC_ClearAllFreeLists(tstate->interp);
|
_PyGC_ClearAllFreeLists(tstate->interp);
|
||||||
validate_spaces(gcstate);
|
validate_spaces(gcstate);
|
||||||
add_stats(gcstate, 2, stats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is the main function. Read this to understand how the
|
/* This is the main function. Read this to understand how the
|
||||||
|
|
@ -1846,10 +1844,11 @@ do_gc_callback(GCState *gcstate, const char *phase,
|
||||||
assert(PyList_CheckExact(gcstate->callbacks));
|
assert(PyList_CheckExact(gcstate->callbacks));
|
||||||
PyObject *info = NULL;
|
PyObject *info = NULL;
|
||||||
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
|
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
|
||||||
info = Py_BuildValue("{sisnsn}",
|
info = Py_BuildValue("{sisnsnsd}",
|
||||||
"generation", generation,
|
"generation", generation,
|
||||||
"collected", stats->collected,
|
"collected", stats->collected,
|
||||||
"uncollectable", stats->uncollectable);
|
"uncollectable", stats->uncollectable,
|
||||||
|
"duration", stats->duration);
|
||||||
if (info == NULL) {
|
if (info == NULL) {
|
||||||
PyErr_FormatUnraisable("Exception ignored while invoking gc callbacks");
|
PyErr_FormatUnraisable("Exception ignored while invoking gc callbacks");
|
||||||
return;
|
return;
|
||||||
|
|
@ -2080,15 +2079,15 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
if (reason != _Py_GC_REASON_SHUTDOWN) {
|
if (reason != _Py_GC_REASON_SHUTDOWN) {
|
||||||
invoke_gc_callback(gcstate, "start", generation, &stats);
|
invoke_gc_callback(gcstate, "start", generation, &stats);
|
||||||
}
|
}
|
||||||
PyTime_t t1;
|
|
||||||
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
||||||
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
|
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
|
||||||
(void)PyTime_PerfCounterRaw(&t1);
|
|
||||||
show_stats_each_generations(gcstate);
|
show_stats_each_generations(gcstate);
|
||||||
}
|
}
|
||||||
if (PyDTrace_GC_START_ENABLED()) {
|
if (PyDTrace_GC_START_ENABLED()) {
|
||||||
PyDTrace_GC_START(generation);
|
PyDTrace_GC_START(generation);
|
||||||
}
|
}
|
||||||
|
PyTime_t start, stop;
|
||||||
|
(void)PyTime_PerfCounterRaw(&start);
|
||||||
PyObject *exc = _PyErr_GetRaisedException(tstate);
|
PyObject *exc = _PyErr_GetRaisedException(tstate);
|
||||||
switch(generation) {
|
switch(generation) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|
@ -2103,6 +2102,9 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
default:
|
default:
|
||||||
Py_UNREACHABLE();
|
Py_UNREACHABLE();
|
||||||
}
|
}
|
||||||
|
(void)PyTime_PerfCounterRaw(&stop);
|
||||||
|
stats.duration = PyTime_AsSecondsDouble(stop - start);
|
||||||
|
add_stats(gcstate, generation, &stats);
|
||||||
if (PyDTrace_GC_DONE_ENABLED()) {
|
if (PyDTrace_GC_DONE_ENABLED()) {
|
||||||
PyDTrace_GC_DONE(stats.uncollectable + stats.collected);
|
PyDTrace_GC_DONE(stats.uncollectable + stats.collected);
|
||||||
}
|
}
|
||||||
|
|
@ -2124,12 +2126,9 @@ _PyGC_Collect(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
_Py_atomic_store_int(&gcstate->collecting, 0);
|
_Py_atomic_store_int(&gcstate->collecting, 0);
|
||||||
|
|
||||||
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
||||||
PyTime_t t2;
|
|
||||||
(void)PyTime_PerfCounterRaw(&t2);
|
|
||||||
double d = PyTime_AsSecondsDouble(t2 - t1);
|
|
||||||
PySys_WriteStderr(
|
PySys_WriteStderr(
|
||||||
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
|
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
|
||||||
stats.collected + stats.uncollectable, stats.uncollectable, d
|
stats.collected + stats.uncollectable, stats.uncollectable, stats.duration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1911,7 +1911,7 @@ handle_resurrected_objects(struct collection_state *state)
|
||||||
static void
|
static void
|
||||||
invoke_gc_callback(PyThreadState *tstate, const char *phase,
|
invoke_gc_callback(PyThreadState *tstate, const char *phase,
|
||||||
int generation, Py_ssize_t collected,
|
int generation, Py_ssize_t collected,
|
||||||
Py_ssize_t uncollectable)
|
Py_ssize_t uncollectable, double duration)
|
||||||
{
|
{
|
||||||
assert(!_PyErr_Occurred(tstate));
|
assert(!_PyErr_Occurred(tstate));
|
||||||
|
|
||||||
|
|
@ -1925,10 +1925,11 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase,
|
||||||
assert(PyList_CheckExact(gcstate->callbacks));
|
assert(PyList_CheckExact(gcstate->callbacks));
|
||||||
PyObject *info = NULL;
|
PyObject *info = NULL;
|
||||||
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
|
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
|
||||||
info = Py_BuildValue("{sisnsn}",
|
info = Py_BuildValue("{sisnsnsd}",
|
||||||
"generation", generation,
|
"generation", generation,
|
||||||
"collected", collected,
|
"collected", collected,
|
||||||
"uncollectable", uncollectable);
|
"uncollectable", uncollectable,
|
||||||
|
"duration", duration);
|
||||||
if (info == NULL) {
|
if (info == NULL) {
|
||||||
PyErr_FormatUnraisable("Exception ignored while "
|
PyErr_FormatUnraisable("Exception ignored while "
|
||||||
"invoking gc callbacks");
|
"invoking gc callbacks");
|
||||||
|
|
@ -2340,7 +2341,6 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
{
|
{
|
||||||
Py_ssize_t m = 0; /* # objects collected */
|
Py_ssize_t m = 0; /* # objects collected */
|
||||||
Py_ssize_t n = 0; /* # unreachable objects that couldn't be 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;
|
GCState *gcstate = &tstate->interp->gc;
|
||||||
|
|
||||||
// gc_collect_main() must not be called before _PyGC_Init
|
// gc_collect_main() must not be called before _PyGC_Init
|
||||||
|
|
@ -2372,19 +2372,19 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
GC_STAT_ADD(generation, collections, 1);
|
GC_STAT_ADD(generation, collections, 1);
|
||||||
|
|
||||||
if (reason != _Py_GC_REASON_SHUTDOWN) {
|
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) {
|
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
||||||
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
|
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
|
||||||
show_stats_each_generations(gcstate);
|
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()) {
|
if (PyDTrace_GC_START_ENABLED()) {
|
||||||
PyDTrace_GC_START(generation);
|
PyDTrace_GC_START(generation);
|
||||||
}
|
}
|
||||||
|
PyTime_t start, stop;
|
||||||
|
(void)PyTime_PerfCounterRaw(&start);
|
||||||
|
|
||||||
PyInterpreterState *interp = tstate->interp;
|
PyInterpreterState *interp = tstate->interp;
|
||||||
|
|
||||||
|
|
@ -2399,13 +2399,13 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
m = state.collected;
|
m = state.collected;
|
||||||
n = state.uncollectable;
|
n = state.uncollectable;
|
||||||
|
|
||||||
|
(void)PyTime_PerfCounterRaw(&stop);
|
||||||
|
double duration = PyTime_AsSecondsDouble(stop - start);
|
||||||
|
|
||||||
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
if (gcstate->debug & _PyGC_DEBUG_STATS) {
|
||||||
PyTime_t t2;
|
|
||||||
(void)PyTime_PerfCounterRaw(&t2);
|
|
||||||
double d = PyTime_AsSecondsDouble(t2 - t1);
|
|
||||||
PySys_WriteStderr(
|
PySys_WriteStderr(
|
||||||
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
|
"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.
|
// Clear the current thread's free-list again.
|
||||||
|
|
@ -2426,6 +2426,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
stats->collections++;
|
stats->collections++;
|
||||||
stats->collected += m;
|
stats->collected += m;
|
||||||
stats->uncollectable += n;
|
stats->uncollectable += n;
|
||||||
|
stats->duration += duration;
|
||||||
|
|
||||||
GC_STAT_ADD(generation, objects_collected, m);
|
GC_STAT_ADD(generation, objects_collected, m);
|
||||||
#ifdef Py_STATS
|
#ifdef Py_STATS
|
||||||
|
|
@ -2444,7 +2445,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reason != _Py_GC_REASON_SHUTDOWN) {
|
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));
|
assert(!_PyErr_Occurred(tstate));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue