Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9184787

Browse files
authored
Merge pull request #19480 from vallsv/fix-callback-registry-mem-leak
Fix CallbackRegistry memory leak
2 parents 9a242b2 + 3204907 commit 9184787

File tree

2 files changed

+122
-32
lines changed

2 files changed

+122
-32
lines changed

lib/matplotlib/cbook/__init__.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,17 @@ def __setstate__(self, state):
197197
s: {proxy: cid for cid, proxy in d.items()}
198198
for s, d in self.callbacks.items()}
199199

200-
def connect(self, s, func):
201-
"""Register *func* to be called when signal *s* is generated."""
202-
self._func_cid_map.setdefault(s, {})
200+
@_api.rename_parameter("3.4", "s", "signal")
201+
def connect(self, signal, func):
202+
"""Register *func* to be called when signal *signal* is generated."""
203+
self._func_cid_map.setdefault(signal, {})
203204
proxy = _weak_or_strong_ref(func, self._remove_proxy)
204-
if proxy in self._func_cid_map[s]:
205-
return self._func_cid_map[s][proxy]
205+
if proxy in self._func_cid_map[signal]:
206+
return self._func_cid_map[signal][proxy]
206207
cid = next(self._cid_gen)
207-
self._func_cid_map[s][proxy] = cid
208-
self.callbacks.setdefault(s, {})
209-
self.callbacks[s][cid] = proxy
208+
self._func_cid_map[signal][proxy] = cid
209+
self.callbacks.setdefault(signal, {})
210+
self.callbacks[signal][cid] = proxy
210211
return cid
211212

212213
# Keep a reference to sys.is_finalizing, as sys may have been cleared out
@@ -215,32 +216,45 @@ def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing):
215216
if _is_finalizing():
216217
# Weakrefs can't be properly torn down at that point anymore.
217218
return
218-
for signal, proxies in list(self._func_cid_map.items()):
219-
try:
220-
del self.callbacks[signal][proxies[proxy]]
221-
except KeyError:
222-
pass
223-
if len(self.callbacks[signal]) == 0:
224-
del self.callbacks[signal]
225-
del self._func_cid_map[signal]
219+
for signal, proxy_to_cid in list(self._func_cid_map.items()):
220+
cid = proxy_to_cid.pop(proxy, None)
221+
if cid is not None:
222+
del self.callbacks[signal][cid]
223+
self._pickled_cids.discard(cid)
224+
break
225+
else:
226+
# Not found
227+
return
228+
# Clean up empty dicts
229+
if len(self.callbacks[signal]) == 0:
230+
del self.callbacks[signal]
231+
del self._func_cid_map[signal]
226232

227233
def disconnect(self, cid):
228234
"""
229235
Disconnect the callback registered with callback id *cid*.
230236
231237
No error is raised if such a callback does not exist.
232238
"""
233-
for eventname, callbackd in list(self.callbacks.items()):
234-
try:
235-
del callbackd[cid]
236-
except KeyError:
237-
continue
238-
else:
239-
for signal, functions in list(self._func_cid_map.items()):
240-
for function, value in list(functions.items()):
241-
if value == cid:
242-
del functions[function]
243-
return
239+
self._pickled_cids.discard(cid)
240+
# Clean up callbacks
241+
for signal, cid_to_proxy in list(self.callbacks.items()):
242+
proxy = cid_to_proxy.pop(cid, None)
243+
if proxy is not None:
244+
break
245+
else:
246+
# Not found
247+
return
248+
249+
proxy_to_cid = self._func_cid_map[signal]
250+
for current_proxy, current_cid in list(proxy_to_cid.items()):
251+
if current_cid == cid:
252+
assert proxy is current_proxy
253+
del proxy_to_cid[current_proxy]
254+
# Clean up empty dicts
255+
if len(self.callbacks[signal]) == 0:
256+
del self.callbacks[signal]
257+
del self._func_cid_map[signal]
244258

245259
def process(self, s, *args, **kwargs):
246260
"""

lib/matplotlib/tests/test_cbook.py

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,31 +182,45 @@ def setup(self):
182182
self.signal = 'test'
183183
self.callbacks = cbook.CallbackRegistry()
184184

185-
def connect(self, s, func):
186-
return self.callbacks.connect(s, func)
185+
def connect(self, s, func, pickle):
186+
cid = self.callbacks.connect(s, func)
187+
if pickle:
188+
self.callbacks._pickled_cids.add(cid)
189+
return cid
190+
191+
def disconnect(self, cid):
192+
return self.callbacks.disconnect(cid)
193+
194+
def count(self):
195+
count1 = len(self.callbacks._func_cid_map.get(self.signal, []))
196+
count2 = len(self.callbacks.callbacks.get(self.signal))
197+
assert count1 == count2
198+
return count1
187199

188200
def is_empty(self):
189201
assert self.callbacks._func_cid_map == {}
190202
assert self.callbacks.callbacks == {}
203+
assert self.callbacks._pickled_cids == set()
191204

192205
def is_not_empty(self):
193206
assert self.callbacks._func_cid_map != {}
194207
assert self.callbacks.callbacks != {}
195208

196-
def test_callback_complete(self):
209+
@pytest.mark.parametrize('pickle', [True, False])
210+
def test_callback_complete(self, pickle):
197211
# ensure we start with an empty registry
198212
self.is_empty()
199213

200214
# create a class for testing
201215
mini_me = Test_callback_registry()
202216

203217
# test that we can add a callback
204-
cid1 = self.connect(self.signal, mini_me.dummy)
218+
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
205219
assert type(cid1) == int
206220
self.is_not_empty()
207221

208222
# test that we don't add a second callback
209-
cid2 = self.connect(self.signal, mini_me.dummy)
223+
cid2 = self.connect(self.signal, mini_me.dummy, pickle)
210224
assert cid1 == cid2
211225
self.is_not_empty()
212226
assert len(self.callbacks._func_cid_map) == 1
@@ -217,6 +231,68 @@ def test_callback_complete(self):
217231
# check we now have no callbacks registered
218232
self.is_empty()
219233

234+
@pytest.mark.parametrize('pickle', [True, False])
235+
def test_callback_disconnect(self, pickle):
236+
# ensure we start with an empty registry
237+
self.is_empty()
238+
239+
# create a class for testing
240+
mini_me = Test_callback_registry()
241+
242+
# test that we can add a callback
243+
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
244+
assert type(cid1) == int
245+
self.is_not_empty()
246+
247+
self.disconnect(cid1)
248+
249+
# check we now have no callbacks registered
250+
self.is_empty()
251+
252+
@pytest.mark.parametrize('pickle', [True, False])
253+
def test_callback_wrong_disconnect(self, pickle):
254+
# ensure we start with an empty registry
255+
self.is_empty()
256+
257+
# create a class for testing
258+
mini_me = Test_callback_registry()
259+
260+
# test that we can add a callback
261+
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
262+
assert type(cid1) == int
263+
self.is_not_empty()
264+
265+
self.disconnect("foo")
266+
267+
# check we still have callbacks registered
268+
self.is_not_empty()
269+
270+
@pytest.mark.parametrize('pickle', [True, False])
271+
def test_registration_on_non_empty_registry(self, pickle):
272+
# ensure we start with an empty registry
273+
self.is_empty()
274+
275+
# setup the registry with a callback
276+
mini_me = Test_callback_registry()
277+
self.connect(self.signal, mini_me.dummy, pickle)
278+
279+
# Add another callback
280+
mini_me2 = Test_callback_registry()
281+
self.connect(self.signal, mini_me2.dummy, pickle)
282+
283+
# Remove and add the second callback
284+
mini_me2 = Test_callback_registry()
285+
self.connect(self.signal, mini_me2.dummy, pickle)
286+
287+
# We still have 2 references
288+
self.is_not_empty()
289+
assert self.count() == 2
290+
291+
# Removing the last 2 references
292+
mini_me = None
293+
mini_me2 = None
294+
self.is_empty()
295+
220296
def dummy(self):
221297
pass
222298

0 commit comments

Comments
 (0)