Revision: 8341 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=8341&view=rev Author: mdboom Date: 2010-05-28 17:42:05 +0000 (Fri, 28 May 2010)
Log Message: ----------- Fix memory leak caused by never disconnecting callbacks. Modified Paths: -------------- trunk/matplotlib/lib/matplotlib/cbook.py Modified: trunk/matplotlib/lib/matplotlib/cbook.py =================================================================== --- trunk/matplotlib/lib/matplotlib/cbook.py 2010-05-28 16:56:45 UTC (rev 8340) +++ trunk/matplotlib/lib/matplotlib/cbook.py 2010-05-28 17:42:05 UTC (rev 8341) @@ -13,6 +13,7 @@ import os.path import random import urllib2 +import new import matplotlib @@ -124,12 +125,87 @@ callbacks.disconnect(ideat) # disconnect oneat callbacks.process('eat', 456) # nothing will be called + In practice, one should always disconnect all callbacks when they + are no longer needed to avoid dangling references (and thus memory + leaks). However, real code in matplotlib rarely does so, and due + to its design, it is rather difficult to place this kind of code. + To get around this, and prevent this class of memory leaks, we + instead store weak references to bound methods only, so when the + destination object needs to die, the CallbackRegistry won't keep + it alive. The Python stdlib weakref module can not create weak + references to bound methods directly, so we need to create a proxy + object to handle weak references to bound methods (or regular free + functions). This technique was shared by Peter Parente on his + `"Mindtrove" blog + <http://mindtrove.info/articles/python-weak-references/>`_. """ + class BoundMethodProxy(object): + ''' + Our own proxy object which enables weak references to bound and unbound + methods and arbitrary callables. Pulls information about the function, + class, and instance out of a bound method. Stores a weak reference to the + instance to support garbage collection. + + @organization: IBM Corporation + @copyright: Copyright (c) 2005, 2006 IBM Corporation + @license: The BSD License + + Minor bugfixes by Michael Droettboom + ''' + def __init__(self, cb): + try: + try: + self.inst = ref(cb.im_self) + except TypeError: + self.inst = None + self.func = cb.im_func + self.klass = cb.im_class + except AttributeError: + self.inst = None + self.func = cb + self.klass = None + + def __call__(self, *args, **kwargs): + ''' + Proxy for a call to the weak referenced object. Take + arbitrary params to pass to the callable. + + Raises `ReferenceError`: When the weak reference refers to + a dead object + ''' + if self.inst is not None and self.inst() is None: + raise ReferenceError + elif self.inst is not None: + # build a new instance method with a strong reference to the instance + mtd = new.instancemethod(self.func, self.inst(), self.klass) + else: + # not a bound method, just return the func + mtd = self.func + # invoke the callable and return the result + return mtd(*args, **kwargs) + + def __eq__(self, other): + ''' + Compare the held function and instance with that held by + another proxy. + ''' + try: + if self.inst is None: + return self.func == other.func and other.inst is None + else: + return self.func == other.func and self.inst() == other.inst() + except Exception: + return False + + def __ne__(self, other): + ''' + Inverse of __eq__. + ''' + return not self.__eq__(other) + def __init__(self, signals): '*signals* is a sequence of valid signals' self.signals = set(signals) - # callbacks is a dict mapping the signal to a dictionary - # mapping callback id to the callback function self.callbacks = dict([(s, dict()) for s in signals]) self._cid = 0 @@ -146,8 +222,15 @@ func will be called """ self._check_signal(s) - self._cid +=1 - self.callbacks[s][self._cid] = func + proxy = self.BoundMethodProxy(func) + for cid, callback in self.callbacks[s].items(): + # Clean out dead references + if callback.inst is not None and callback.inst() is None: + del self.callbacks[s][cid] + elif callback == proxy: + return cid + self._cid += 1 + self.callbacks[s][self._cid] = proxy return self._cid def disconnect(self, cid): @@ -155,9 +238,12 @@ disconnect the callback registered with callback id *cid* """ for eventname, callbackd in self.callbacks.items(): - try: del callbackd[cid] - except KeyError: continue - else: return + try: + del callbackd[cid] + except KeyError: + continue + else: + return def process(self, s, *args, **kwargs): """ @@ -165,8 +251,12 @@ callbacks on *s* will be called with *\*args* and *\*\*kwargs* """ self._check_signal(s) - for func in self.callbacks[s].values(): - func(*args, **kwargs) + for cid, proxy in self.callbacks[s].items(): + # Clean out dead references + if proxy.inst is not None and proxy.inst() is None: + del self.callbacks[s][cid] + else: + proxy(*args, **kwargs) class Scheduler(threading.Thread): This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ _______________________________________________ Matplotlib-checkins mailing list Matplotlib-checkins@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/matplotlib-checkins