# HG changeset patch # User krisvale # Date 1332273789 0 # Node ID 8e4298625f03b0e235985723fedaf217527f0945 # Parent 0554183066b50f3c137f7e11812dba3d67a6179c [mq]: 2012-03-20_20-03-04_r75779+.diff diff --git a/Doc/library/gc.rst b/Doc/library/gc.rst --- a/Doc/library/gc.rst +++ b/Doc/library/gc.rst @@ -153,7 +153,7 @@ .. 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 its value but should not rebind it): .. data:: garbage @@ -183,6 +183,37 @@ :const:`DEBUG_UNCOLLECTABLE` is set, in addition all uncollectable objects are printed. +.. data:: callbacks + + A list of callbacks that will be invoked by the garbage collector during + 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 starting + + "stop": The garbage collection has finishted. + + :arg:`info` provides more information for the callback. It currently has + the follwoing keys: + + "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 couldn't be collected and were put in :data:`garbage`. + + Applications can add their own callbacks to this list. This can be to gather + statistics about garbage collection, such as how often they are performed and + how much time they compute. + + Another important application is for programs to identify and manually clear + their own uncollectable types from :data:`garbage`. + + .. versionadded:: 3.3 + The following constants are provided for use with :func:`set_debug`: 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 @@ -32,6 +32,15 @@ # gc collects it. self.wr = weakref.ref(C1055820(666), it_happened) +class Uncollectable(object): + """ + An object with a reference cycle, which also is uncollecable due + to the __del__ method + """ + def __init__(self): + self.loop = self + def __del__(self): + pass ### Tests ############################################################################### @@ -528,6 +537,98 @@ 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) + self.visit = [] + + 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() + + 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 type(e) is Uncollectable] + gc.garbage[:] = [e for e in gc.garbage if type(e) is not Uncollectable] + for e in uc: + e.loop = None + + def testCollect(self): + gc.collect() + 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) + + 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) + + 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): + gc.collect(2) + for v in self.visit: + info = v[2] + self.assertEqual(info["generation"], 2) + + def testCollectGarbage(self): + #each of these cause two objects to be garbage, the object + #itself, and its dictionary. + 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"], 4) + + #we should now have the Uncollecables in gc.garbage + self.assertEqual(len(gc.garbage), 2) + del gc.garbage[:] + + #now, let our callback handle the Uncollectable instances + self.cleanup=True + self.visit = [] + 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): def setUp(self): gc.enable() @@ -681,7 +782,7 @@ try: gc.collect() # Delete 2nd generation garbage - run_unittest(GCTests, GCTogglingTests) + run_unittest(GCTests, GCTogglingTests, GCCallbackTests) finally: gc.set_debug(debug) # test gc.enable() even if GC is disabled by default diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -68,6 +68,9 @@ /* Python string used to look for __del__ attribute. */ static PyObject *delstr = NULL; +/* a list of callbacks to be invoked when collection is performed */ +static PyObject *callbacks = NULL; + /* This is the number of objects who survived the last full collection. It approximates the number of long lived objects tracked by the GC. @@ -790,7 +793,7 @@ /* This is the main function. Read this to understand how the * collection process works. */ static Py_ssize_t -collect(int generation) +collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable) { int i; Py_ssize_t m = 0; /* # objects collected */ @@ -944,9 +947,62 @@ PyErr_WriteUnraisable(gc_str); Py_FatalError("unexpected exception during garbage collection"); } + + if (n_collected) + *n_collected = m; + if (n_uncollectable) + *n_uncollectable = n; 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) +{ + int i; + PyObject *info = NULL; + + /* make sure we have a list, otherwise do nothing */ + if (callbacks == NULL || !PyList_Check(callbacks)) + return; + 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 is @@ -1361,7 +1426,7 @@ n = 0; /* already collecting, don't do anything */ else { collecting = 1; - n = collect(NUM_GENERATIONS - 1); + n = collect_with_callback(NUM_GENERATIONS - 1); collecting = 0; } @@ -1398,6 +1463,7 @@ Py_XDECREF(bytes); } } + Py_CLEAR(callbacks); } /* for debugging */