1515import numpy as np
1616import pyvista
1717import sphinx .util .logging
18- from refleak .testing import assert_no_instances
18+ from refleak .testing import Snapshot , assert_no_instances , gc_collect_once
1919from sphinx .errors import ExtensionError
2020
2121import mne
22- from mne .utils import Bunch , _get_extra_data_path , sizeof_fmt
22+ from mne .utils import Bunch , _get_extra_data_path , _is_vtk , sizeof_fmt
2323from mne .viz import Brain
2424
2525sphinx_logger = sphinx .util .logging .getLogger ("mne" )
2626_np_print_defaults = np .get_printoptions ()
2727
28+ # Taken at each example's "before" reset and diffed at its "after" reset, so
29+ # any VTK object an example creates but fails to release is flagged -- not
30+ # just the specific classes checked by name below.
31+ _vtk_snapshot = None
32+
2833
2934def reset_warnings (gallery_conf , fname ):
3035 """Ensure we are future compatible and ignore silly warnings."""
@@ -148,10 +153,6 @@ def reset_modules(gallery_conf, fname, when):
148153 from pyvistaqt import BackgroundPlotter # noqa
149154 except ImportError :
150155 BackgroundPlotter = None # noqa
151- try :
152- from vtkmodules .vtkCommonDataModel import vtkPolyData # noqa
153- except ImportError :
154- vtkPolyData = None # noqa
155156 try :
156157 from mne_qt_browser ._pg_figure import MNEQtBrowser
157158 except ImportError :
@@ -187,6 +188,9 @@ def reset_modules(gallery_conf, fname, when):
187188 IPython .core .completer .__main__ = sys .modules ["__main__" ]
188189 except Exception :
189190 pass
191+ # A plain collect (not the deduped one): dead figures must drop out of
192+ # ui_events._event_channels (a WeakKeyDictionary) before the emptiness
193+ # assert below.
190194 gc .collect ()
191195
192196 # Agg does not call close_event so let's clean up on our own :(
@@ -198,25 +202,16 @@ def reset_modules(gallery_conf, fname, when):
198202 orig_when = when
199203 when = f"mne/conf.py:Resetter.__call__:{ when } :{ fname } "
200204 # Support stuff like
201- # MNE_SKIP_INSTANCE_ASSERTIONS="Brain,Plotter,BackgroundPlotter,vtkPolyData ,_Renderer" make html-memory # noqa: E501
205+ # MNE_SKIP_INSTANCE_ASSERTIONS="Brain,Plotter,BackgroundPlotter,vtk ,_Renderer" make html-memory # noqa: E501
202206 # to just test MNEQtBrowser
203207 skips = os .getenv ("MNE_SKIP_INSTANCE_ASSERTIONS" , "" ).lower ()
204208 prefix = ""
205209 request = Bunch () # just give it something to say "we have done GC already"
206210 request .node = Bunch ()
211+ global _vtk_snapshot
207212 if skips not in ("true" , "1" , "all" ):
208213 prefix = "Clean "
209214 skips = skips .split ("," )
210- if "brain" not in skips :
211- assert_no_instances (Brain , when = when , request = request )
212- if Plotter is not None and "plotter" not in skips :
213- assert_no_instances (Plotter , when = when , request = request )
214- if BackgroundPlotter is not None and "backgroundplotter" not in skips :
215- assert_no_instances (BackgroundPlotter , when = when , request = request )
216- if vtkPolyData is not None and "vtkpolydata" not in skips :
217- assert_no_instances (vtkPolyData , when = when , request = request )
218- if "_renderer" not in skips :
219- assert_no_instances (_Renderer , when = when , request = request )
220215 if MNEQtBrowser is not None and "mneqtbrowser" not in skips :
221216 # Ensure any manual fig.close() events get properly handled
222217 from mne_qt_browser ._pg_figure import QApplication
@@ -225,7 +220,36 @@ def reset_modules(gallery_conf, fname, when):
225220 if inst is not None :
226221 for _ in range (2 ):
227222 inst .processEvents ()
228- assert_no_instances (MNEQtBrowser , when = when , request = request )
223+ # This collect must come AFTER _cleanup_agg() above: an example's
224+ # subscribed ui-events callback can be the last anchor of its whole
225+ # object graph (channel dict -> callback -> __globals__ -> figures),
226+ # and only cleanup breaks that loop. It is the once-per-reset collect
227+ # that the checks below dedupe against (see gc_collect_once).
228+ gc_collect_once (request )
229+ objs = gc .get_objects () # scan the heap once, share across all checks
230+ if "brain" not in skips :
231+ assert_no_instances (Brain , when = when , request = request , objs = objs )
232+ if Plotter is not None and "plotter" not in skips :
233+ assert_no_instances (Plotter , when = when , request = request , objs = objs )
234+ if BackgroundPlotter is not None and "backgroundplotter" not in skips :
235+ assert_no_instances (
236+ BackgroundPlotter , when = when , request = request , objs = objs
237+ )
238+ if "_renderer" not in skips :
239+ assert_no_instances (_Renderer , when = when , request = request , objs = objs )
240+ if MNEQtBrowser is not None and "mneqtbrowser" not in skips :
241+ assert_no_instances (MNEQtBrowser , when = when , request = request , objs = objs )
242+ if "vtk" not in skips :
243+ # Diff all VTK objects across each example: catches leaks of any
244+ # VTK class (actors, mappers, arrays, ...), not just the specific
245+ # classes above, while tolerating VTK state that legitimately
246+ # pre-dates the example.
247+ if orig_when == "after" and _vtk_snapshot is not None :
248+ _vtk_snapshot .assert_no_new (when = when , request = request , objs = objs )
249+ _vtk_snapshot = None
250+ if orig_when == "before" :
251+ _vtk_snapshot = Snapshot (_is_vtk , label = "VTK" , objs = objs )
252+ del objs
229253 # This will overwrite some Sphinx printing but it's useful
230254 # for memory timestamps
231255 if os .getenv ("SG_STAMP_STARTS" , "" ).lower () == "true" :
0 commit comments