mirror of
https://github.com/python/cpython.git
synced 2025-11-26 21:33:10 +00:00
Issue #10576: Add a progress callback to gcmodule
This commit is contained in:
parent
c014df7edf
commit
69c635266e
4 changed files with 249 additions and 9 deletions
|
|
@ -153,8 +153,8 @@ The :mod:`gc` module provides the following functions:
|
||||||
.. versionadded:: 3.1
|
.. versionadded:: 3.1
|
||||||
|
|
||||||
|
|
||||||
The following variable is provided for read-only access (you can mutate its
|
The following variables are provided for read-only access (you can mutate the
|
||||||
value but should not rebind it):
|
values but should not rebind them):
|
||||||
|
|
||||||
.. data:: garbage
|
.. data:: garbage
|
||||||
|
|
||||||
|
|
@ -183,6 +183,41 @@ value but should not rebind it):
|
||||||
:const:`DEBUG_UNCOLLECTABLE` is set, in addition all uncollectable objects
|
:const:`DEBUG_UNCOLLECTABLE` is set, in addition all uncollectable objects
|
||||||
are printed.
|
are printed.
|
||||||
|
|
||||||
|
.. data:: callbacks
|
||||||
|
|
||||||
|
A list of callbacks that will be invoked by the garbage collector before and
|
||||||
|
after collection. The callbacks will be called with two arguments,
|
||||||
|
:arg:`phase` and :arg:`info`.
|
||||||
|
|
||||||
|
:arg:`phase` can one of two values:
|
||||||
|
|
||||||
|
"start": The garbage collection is about to start.
|
||||||
|
|
||||||
|
"stop": The garbage collection has finished.
|
||||||
|
|
||||||
|
:arg:`info` provides more information for the callback. The following
|
||||||
|
keys are currently defined:
|
||||||
|
|
||||||
|
"generation": The oldest generation being collected.
|
||||||
|
|
||||||
|
"collected": When :arg:`phase` is "stop", the number of objects
|
||||||
|
successfully collected.
|
||||||
|
|
||||||
|
"uncollectable": when :arg:`phase` is "stop", the number of objects
|
||||||
|
that could not be collected and were put in :data:`garbage`.
|
||||||
|
|
||||||
|
Applications can add their own callbacks to this list. The primary
|
||||||
|
use cases are:
|
||||||
|
|
||||||
|
Gathering statistics about garbage collection, such as how often
|
||||||
|
various generations are collected, and how long the collection
|
||||||
|
takes.
|
||||||
|
|
||||||
|
Allowing applications to identify and clear their own uncollectable
|
||||||
|
types when they appear in :data:`garbage`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.3
|
||||||
|
|
||||||
|
|
||||||
The following constants are provided for use with :func:`set_debug`:
|
The following constants are provided for use with :func:`set_debug`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,20 @@ class GC_Detector(object):
|
||||||
# gc collects it.
|
# gc collects it.
|
||||||
self.wr = weakref.ref(C1055820(666), it_happened)
|
self.wr = weakref.ref(C1055820(666), it_happened)
|
||||||
|
|
||||||
|
class Uncollectable(object):
|
||||||
|
"""Create a reference cycle with multiple __del__ methods.
|
||||||
|
|
||||||
|
An object in a reference cycle will never have zero references,
|
||||||
|
and so must be garbage collected. If one or more objects in the
|
||||||
|
cycle have __del__ methods, the gc refuses to guess an order,
|
||||||
|
and leaves the cycle uncollected."""
|
||||||
|
def __init__(self, partner=None):
|
||||||
|
if partner is None:
|
||||||
|
self.partner = Uncollectable(partner=self)
|
||||||
|
else:
|
||||||
|
self.partner = partner
|
||||||
|
def __del__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
@ -528,6 +542,126 @@ class GCTests(unittest.TestCase):
|
||||||
self.assertNotIn(b"uncollectable objects at shutdown", stderr)
|
self.assertNotIn(b"uncollectable objects at shutdown", stderr)
|
||||||
|
|
||||||
|
|
||||||
|
class GCCallbackTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Save gc state and disable it.
|
||||||
|
self.enabled = gc.isenabled()
|
||||||
|
gc.disable()
|
||||||
|
self.debug = gc.get_debug()
|
||||||
|
gc.set_debug(0)
|
||||||
|
gc.callbacks.append(self.cb1)
|
||||||
|
gc.callbacks.append(self.cb2)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# Restore gc state
|
||||||
|
del self.visit
|
||||||
|
gc.callbacks.remove(self.cb1)
|
||||||
|
gc.callbacks.remove(self.cb2)
|
||||||
|
gc.set_debug(self.debug)
|
||||||
|
if self.enabled:
|
||||||
|
gc.enable()
|
||||||
|
# destroy any uncollectables
|
||||||
|
gc.collect()
|
||||||
|
for obj in gc.garbage:
|
||||||
|
if isinstance(obj, Uncollectable):
|
||||||
|
obj.partner = None
|
||||||
|
del gc.garbage[:]
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
othergarbage = []
|
||||||
|
def preclean(self):
|
||||||
|
# Remove all fluff from the system. Invoke this function
|
||||||
|
# manually rather than through self.setUp() for maximum
|
||||||
|
# safety.
|
||||||
|
self.visit = []
|
||||||
|
gc.collect()
|
||||||
|
garbage, gc.garbage[:] = gc.garbage[:], []
|
||||||
|
self.othergarbage.append(garbage)
|
||||||
|
self.visit = []
|
||||||
|
|
||||||
|
def cb1(self, phase, info):
|
||||||
|
self.visit.append((1, phase, dict(info)))
|
||||||
|
|
||||||
|
def cb2(self, phase, info):
|
||||||
|
self.visit.append((2, phase, dict(info)))
|
||||||
|
if phase == "stop" and hasattr(self, "cleanup"):
|
||||||
|
# Clean Uncollectable from garbage
|
||||||
|
uc = [e for e in gc.garbage if isinstance(e, Uncollectable)]
|
||||||
|
gc.garbage[:] = [e for e in gc.garbage
|
||||||
|
if not isinstance(e, Uncollectable)]
|
||||||
|
for e in uc:
|
||||||
|
e.partner = None
|
||||||
|
|
||||||
|
def testCollect(self):
|
||||||
|
self.preclean()
|
||||||
|
gc.collect()
|
||||||
|
# Algorithmically verify the contents of self.visit
|
||||||
|
# because it is long and tortuous.
|
||||||
|
|
||||||
|
# Count the number of visits to each callback
|
||||||
|
n = [v[0] for v in self.visit]
|
||||||
|
n1 = [i for i in n if i == 1]
|
||||||
|
n2 = [i for i in n if i == 2]
|
||||||
|
self.assertEqual(n1, [1]*2)
|
||||||
|
self.assertEqual(n2, [2]*2)
|
||||||
|
|
||||||
|
# Count that we got the right number of start and stop callbacks.
|
||||||
|
n = [v[1] for v in self.visit]
|
||||||
|
n1 = [i for i in n if i == "start"]
|
||||||
|
n2 = [i for i in n if i == "stop"]
|
||||||
|
self.assertEqual(n1, ["start"]*2)
|
||||||
|
self.assertEqual(n2, ["stop"]*2)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def testCollectGen(self):
|
||||||
|
self.preclean()
|
||||||
|
gc.collect(2)
|
||||||
|
for v in self.visit:
|
||||||
|
info = v[2]
|
||||||
|
self.assertEqual(info["generation"], 2)
|
||||||
|
|
||||||
|
def testCollectGarbage(self):
|
||||||
|
self.preclean()
|
||||||
|
# Each of these cause four objects to be garbage: Two
|
||||||
|
# Uncolectables and their instance dicts.
|
||||||
|
Uncollectable()
|
||||||
|
Uncollectable()
|
||||||
|
C1055820(666)
|
||||||
|
gc.collect()
|
||||||
|
for v in self.visit:
|
||||||
|
if v[1] != "stop":
|
||||||
|
continue
|
||||||
|
info = v[2]
|
||||||
|
self.assertEqual(info["collected"], 2)
|
||||||
|
self.assertEqual(info["uncollectable"], 8)
|
||||||
|
|
||||||
|
# We should now have the Uncollectables in gc.garbage
|
||||||
|
self.assertEqual(len(gc.garbage), 4)
|
||||||
|
for e in gc.garbage:
|
||||||
|
self.assertIsInstance(e, Uncollectable)
|
||||||
|
|
||||||
|
# Now, let our callback handle the Uncollectable instances
|
||||||
|
self.cleanup=True
|
||||||
|
self.visit = []
|
||||||
|
gc.garbage[:] = []
|
||||||
|
gc.collect()
|
||||||
|
for v in self.visit:
|
||||||
|
if v[1] != "stop":
|
||||||
|
continue
|
||||||
|
info = v[2]
|
||||||
|
self.assertEqual(info["collected"], 0)
|
||||||
|
self.assertEqual(info["uncollectable"], 4)
|
||||||
|
|
||||||
|
# Uncollectables should be gone
|
||||||
|
self.assertEqual(len(gc.garbage), 0)
|
||||||
|
|
||||||
|
|
||||||
class GCTogglingTests(unittest.TestCase):
|
class GCTogglingTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
gc.enable()
|
gc.enable()
|
||||||
|
|
@ -681,7 +815,7 @@ def test_main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
gc.collect() # Delete 2nd generation garbage
|
gc.collect() # Delete 2nd generation garbage
|
||||||
run_unittest(GCTests, GCTogglingTests)
|
run_unittest(GCTests, GCTogglingTests, GCCallbackTests)
|
||||||
finally:
|
finally:
|
||||||
gc.set_debug(debug)
|
gc.set_debug(debug)
|
||||||
# test gc.enable() even if GC is disabled by default
|
# test gc.enable() even if GC is disabled by default
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,9 @@ Library
|
||||||
- Issue #14310: Sockets can now be with other processes on Windows using
|
- Issue #14310: Sockets can now be with other processes on Windows using
|
||||||
the api socket.socket.share() and socket.fromshare().
|
the api socket.socket.share() and socket.fromshare().
|
||||||
|
|
||||||
|
- Issue #10576: The gc module now has a 'callbacks' member that will get
|
||||||
|
called when garbage collection takes place.
|
||||||
|
|
||||||
Build
|
Build
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,17 @@ static PyObject *garbage = NULL;
|
||||||
/* Python string to use if unhandled exception occurs */
|
/* Python string to use if unhandled exception occurs */
|
||||||
static PyObject *gc_str = NULL;
|
static PyObject *gc_str = NULL;
|
||||||
|
|
||||||
/* This is the number of objects who survived the last full collection. It
|
/* a list of callbacks to be invoked when collection is performed */
|
||||||
|
static PyObject *callbacks = NULL;
|
||||||
|
|
||||||
|
/* This is the number of objects that survived the last full collection. It
|
||||||
approximates the number of long lived objects tracked by the GC.
|
approximates the number of long lived objects tracked by the GC.
|
||||||
|
|
||||||
(by "full collection", we mean a collection of the oldest generation).
|
(by "full collection", we mean a collection of the oldest generation).
|
||||||
*/
|
*/
|
||||||
static Py_ssize_t long_lived_total = 0;
|
static Py_ssize_t long_lived_total = 0;
|
||||||
|
|
||||||
/* This is the number of objects who survived all "non-full" collections,
|
/* This is the number of objects that survived all "non-full" collections,
|
||||||
and are awaiting to undergo a full collection for the first time.
|
and are awaiting to undergo a full collection for the first time.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
@ -787,7 +790,7 @@ get_time(void)
|
||||||
/* This is the main function. Read this to understand how the
|
/* This is the main function. Read this to understand how the
|
||||||
* collection process works. */
|
* collection process works. */
|
||||||
static Py_ssize_t
|
static Py_ssize_t
|
||||||
collect(int generation)
|
collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
Py_ssize_t m = 0; /* # objects collected */
|
Py_ssize_t m = 0; /* # objects collected */
|
||||||
|
|
@ -935,9 +938,64 @@ collect(int generation)
|
||||||
PyErr_WriteUnraisable(gc_str);
|
PyErr_WriteUnraisable(gc_str);
|
||||||
Py_FatalError("unexpected exception during garbage collection");
|
Py_FatalError("unexpected exception during garbage collection");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (n_collected)
|
||||||
|
*n_collected = m;
|
||||||
|
if (n_uncollectable)
|
||||||
|
*n_uncollectable = n;
|
||||||
return n+m;
|
return n+m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Invoke progress callbacks to notify clients that garbage collection
|
||||||
|
* is starting or stopping
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
invoke_gc_callback(const char *phase, int generation,
|
||||||
|
Py_ssize_t collected, Py_ssize_t uncollectable)
|
||||||
|
{
|
||||||
|
Py_ssize_t i;
|
||||||
|
PyObject *info = NULL;
|
||||||
|
|
||||||
|
/* we may get called very early */
|
||||||
|
if (callbacks == NULL)
|
||||||
|
return;
|
||||||
|
/* The local variable cannot be rebound, check it for sanity */
|
||||||
|
assert(callbacks != NULL && PyList_CheckExact(callbacks));
|
||||||
|
if (PyList_GET_SIZE(callbacks) != 0) {
|
||||||
|
info = Py_BuildValue("{sisnsn}",
|
||||||
|
"generation", generation,
|
||||||
|
"collected", collected,
|
||||||
|
"uncollectable", uncollectable);
|
||||||
|
if (info == NULL) {
|
||||||
|
PyErr_WriteUnraisable(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i=0; i<PyList_GET_SIZE(callbacks); i++) {
|
||||||
|
PyObject *r, *cb = PyList_GET_ITEM(callbacks, i);
|
||||||
|
Py_INCREF(cb); /* make sure cb doesn't go away */
|
||||||
|
r = PyObject_CallFunction(cb, "sO", phase, info);
|
||||||
|
Py_XDECREF(r);
|
||||||
|
if (r == NULL)
|
||||||
|
PyErr_WriteUnraisable(cb);
|
||||||
|
Py_DECREF(cb);
|
||||||
|
}
|
||||||
|
Py_XDECREF(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Perform garbage collection of a generation and invoke
|
||||||
|
* progress callbacks.
|
||||||
|
*/
|
||||||
|
static Py_ssize_t
|
||||||
|
collect_with_callback(int generation)
|
||||||
|
{
|
||||||
|
Py_ssize_t result, collected, uncollectable;
|
||||||
|
invoke_gc_callback("start", generation, 0, 0);
|
||||||
|
result = collect(generation, &collected, &uncollectable);
|
||||||
|
invoke_gc_callback("stop", generation, collected, uncollectable);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static Py_ssize_t
|
static Py_ssize_t
|
||||||
collect_generations(void)
|
collect_generations(void)
|
||||||
{
|
{
|
||||||
|
|
@ -956,7 +1014,7 @@ collect_generations(void)
|
||||||
if (i == NUM_GENERATIONS - 1
|
if (i == NUM_GENERATIONS - 1
|
||||||
&& long_lived_pending < long_lived_total / 4)
|
&& long_lived_pending < long_lived_total / 4)
|
||||||
continue;
|
continue;
|
||||||
n = collect(i);
|
n = collect_with_callback(i);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1027,7 +1085,7 @@ gc_collect(PyObject *self, PyObject *args, PyObject *kws)
|
||||||
n = 0; /* already collecting, don't do anything */
|
n = 0; /* already collecting, don't do anything */
|
||||||
else {
|
else {
|
||||||
collecting = 1;
|
collecting = 1;
|
||||||
n = collect(genarg);
|
n = collect_with_callback(genarg);
|
||||||
collecting = 0;
|
collecting = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1320,6 +1378,15 @@ PyInit_gc(void)
|
||||||
if (PyModule_AddObject(m, "garbage", garbage) < 0)
|
if (PyModule_AddObject(m, "garbage", garbage) < 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
if (callbacks == NULL) {
|
||||||
|
callbacks = PyList_New(0);
|
||||||
|
if (callbacks == NULL)
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Py_INCREF(callbacks);
|
||||||
|
if (PyModule_AddObject(m, "callbacks", callbacks) < 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
/* Importing can't be done in collect() because collect()
|
/* Importing can't be done in collect() because collect()
|
||||||
* can be called via PyGC_Collect() in Py_Finalize().
|
* can be called via PyGC_Collect() in Py_Finalize().
|
||||||
* This wouldn't be a problem, except that <initialized> is
|
* This wouldn't be a problem, except that <initialized> is
|
||||||
|
|
@ -1352,7 +1419,7 @@ PyGC_Collect(void)
|
||||||
n = 0; /* already collecting, don't do anything */
|
n = 0; /* already collecting, don't do anything */
|
||||||
else {
|
else {
|
||||||
collecting = 1;
|
collecting = 1;
|
||||||
n = collect(NUM_GENERATIONS - 1);
|
n = collect_with_callback(NUM_GENERATIONS - 1);
|
||||||
collecting = 0;
|
collecting = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1389,6 +1456,7 @@ _PyGC_Fini(void)
|
||||||
Py_XDECREF(bytes);
|
Py_XDECREF(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Py_CLEAR(callbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* for debugging */
|
/* for debugging */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue