New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Plotter and children for proper garbage collection #3037
Conversation
def __del__(self): # pragma: no cover | ||
def __del__(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how this all started. I naively added a __del__
when fixing #2991, only to realise that this is never called. Had to skip this for coverage then, since you can't call __del__
on an object that refuses to die.
Codecov Report
@@ Coverage Diff @@
## main #3037 +/- ##
=======================================
Coverage 94.37% 94.38%
=======================================
Files 76 76
Lines 16541 16552 +11
=======================================
+ Hits 15611 15623 +12
+ Misses 930 929 -1 |
def __del__(self): | ||
"""Delete the plotter.""" | ||
# We have to check here if it has the closed attribute as it | ||
# may not exist should the plotter have failed to initialize. | ||
if hasattr(self, '_closed'): | ||
# We have to check here if the plotter was only partially initialized | ||
if self._initialized: | ||
if not self._closed: | ||
self.close() | ||
self.deep_clean() | ||
if hasattr(self, 'renderers'): | ||
if self._initialized: | ||
del self.renderers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were getting errors inside self.close()
due to missing attributes, turned into warnings because they were raised in __del__()
(I guess more plotters are dying now during the test suite).
Since errors can appear in many places during __init__()
, plotters can have many levels of partial initialization. I've added an _initialized
flag that is only True
when (Base)Plotter.__init__()
is done, and only do self.deep_clean()
unless the plotter was fully initialized. This leaves some potential attributes that aren't cleaned (anything set before the error was raised), but this is probably fairly safe inside __del__
: those resources should be released soon, hopefully.
Again I'm not 100% sure this is the right call, or the right implementation (e.g. set self._initialized
to True
at the end of BasePlotter.__init__()
, but then to False
again at the start of Plotter.__init__()
). Test suite doesn't report VTK leaks at least, and the errors-turned-warnings-during-__del__()
are now gone.
Using delattr(rwr(), '_Renderer__charts') I suspect the issue is that there's a reference import gc
import weakref
class FakeCharts:
def __init__(self):
pass
class FakeRenderer:
def __init__(self):
self._charts = FakeCharts()
class FakePlotter:
def __init__(self):
self.renderer = FakeRenderer()
pl = FakePlotter()
rwr = weakref.ref(pl.renderer)
cwr = weakref.ref(pl.renderer._charts)
del pl
assert cwr() is None Many of your changes here match the work done in #2964, and I bet you've cleaned up a few remaining references as I didn't pay attention to |
tl;dr We were leaking Plotters and some children. I think I've fixed it, even though I don't fully understand some related paranormal activity.
Part 1
Plotters don't get garbage collected. Consider this script:
On main, this prints
This means that even if we
pv.plotting._ALL_PLOTTERS
dict (which should be the only place where plotters are kept track of)the plotter doesn't get garbage collected.
Replacing strong references to the plotter in
Renderers
,Renderer
andCharts
(i.e. the first commit of this PR) changes the output toand an assertion error, because in this case the plotter gets garbage collected once it's also cleared from
_ALL_PLOTTERS
!Part 2
There's a different, lot more subtle issue too, which caused me to find this bug in the first place. The problem is that
Charts
objects don't get garbage collected at all!Consider this script:
The
Plotter
is collected (see Part 1), so is its onlyRenderer
. However, theCharts
object created when we accessedpl.renderer._charts
sticks around!Part 2.5
The reall weird part is how that
Charts
objects sticks around. I have no sane explanation (i.e. I suspect C-land shenanigans via VTK). Consider this script:This prints
with no (assertion) errors.
To recap:
Plotter
-> (Renderers
->)Renderer
->Charts
objectCharts
instance sticks around because it has a remaining reference to itpl.renderer.__dict__
, even though the instance this dict belongs to is dead!Part 2.75 (Appendix)
Normally, the
__dict__
of an instance should die when the instance dies. Consider this example (unfortunately we can't create weakrefs to dicts):This prints
Which makes sense:
__dict__
by 1renderer_dict_ref
.So I'm thinking that we can't end up with the weird situation we have with
charts
vsplotter.renderer.__dict__
unless there's some C-world reference keeping this__dict__
alive, which in turn keeps theCharts
instance alive.Part 3 (Prologue)
Even though I don't understand what's going on, I finally realised how I can prevent this weird situation. If we reassign the
__charts
private attribute referencing theCharts
while theRenderer
is alive, theCharts
instance dies!So if we just make sure to reassign
None
torenderer.__charts
inrenderer.__del__()
->renderer.deep_clean()
, the problem goes away. I'd love to understand what's going on here, but at least this removes the whole issue.Labelling this as [review-critical] because weakrefs might have unintended side-effects, e.g. I originally used a
weakref.proxy
inCharts
to refer to the renderer, but this broke theSetRenderer()
calls. I ended up replacing that with aweakref.ref
, which is called in a property so that we don't have to writeself.renderer()
anywhere. (I think I may have even seen precedent for this in the codebase.) I might even have to replace the otherweakref.proxy
s I've added here to use a similar dereferencing trick, to ensure that the corresponding attributes are bone fide instances of the referenced type.