1+ import ctypes
12import psutil
23import param
34import time
45import gc
6+ from copy import deepcopy
57
68import panel as pn
79
5961 zone8 = ["correlogram" , "metrics" , "mainsettings" ],
6062)
6163
64+ def _malloc_trim ():
65+ """Force glibc to return freed memory to the OS (Linux only)."""
66+ try :
67+ ctypes .CDLL ("libc.so.6" ).malloc_trim (0 )
68+ except Exception :
69+ pass
70+
6271
6372class EphysGuiView (param .Parameterized ):
6473
@@ -73,6 +82,7 @@ def __init__(self, analyzer_path, recording_path, fast_mode=False, **params):
7382 self .fast_mode = fast_mode
7483 self .analyzer = None
7584 self ._cleanup_registered = False
85+ self ._init_cb = None
7686
7787 self .spinner = pn .indicators .LoadingSpinner (value = True , sizing_mode = "stretch_width" )
7888 self .log_output = pn .widgets .TextAreaInput (value = "" , sizing_mode = "stretch_both" )
@@ -102,9 +112,15 @@ def _register_cleanup(self):
102112 return
103113 doc = pn .state .curdoc
104114 if doc is not None and hasattr (doc , "on_session_destroyed" ):
115+ # Use a weak-like reference pattern: store the id and look up via the doc
116+ # to avoid the closure preventing GC of self
117+ view_ref = [self ] # mutable container we can clear
105118
106- def _on_destroy (session_context , view = self ):
107- view .cleanup ()
119+ def _on_destroy (session_context ):
120+ view = view_ref [0 ]
121+ if view is not None :
122+ view .cleanup ()
123+ view_ref [0 ] = None # break the reference
108124
109125 doc .on_session_destroyed (_on_destroy )
110126 self ._cleanup_registered = True
@@ -188,7 +204,7 @@ def _set_processed_recording(self):
188204 def _create_main_window (self ):
189205 if self .analyzer is not None :
190206 # prepare the curation data using decoder labels
191- curation_dict = default_curation_dict
207+ curation_dict = deepcopy ( default_curation_dict )
192208 curation_dict ["unit_ids" ] = self .analyzer .unit_ids
193209 if "decoder_label" in self .analyzer .sorting .get_property_keys ():
194210 decoder_labels = self .analyzer .get_sorting_property ("decoder_label" )
@@ -235,19 +251,17 @@ def cleanup(self):
235251
236252 # 1) Release GUI controller references
237253 if self .win is not None :
254+ self .win .controller .analyzer = None
255+ del self .win .controller .analyzer
256+ self .win .controller = None
257+ del self .win .controller
258+ self .win = None
238259 del self .win
239260
240- # 2) Close zarr store explicitly
261+ # 2) Delete the analyzer reference to release memory-mapped files and other resources
241262 if self .analyzer is not None :
242- # Try to close the underlying zarr store
243- zarr_root = getattr (self .analyzer , "sorting_analyzer" , None ) or self .analyzer
244- store = getattr (zarr_root , "_root" , None )
245- if store is not None :
246- inner_store = getattr (store , "store" , None )
247- if inner_store is not None and hasattr (inner_store , "close" ):
248- inner_store .close ()
249- print ("Zarr store closed." )
250263 self .analyzer = None
264+ del self .analyzer
251265 print ("Analyzer resources released." )
252266
253267 # 3) Clear layout references
@@ -263,6 +277,9 @@ def cleanup(self):
263277 gc .collect ()
264278 gc .collect ()
265279
280+ # 5) Force glibc to return freed memory to OS
281+ _malloc_trim ()
282+
266283
267284 final_mem = psutil .virtual_memory ()
268285 current_ram_usage = final_mem .used / (1024 ** 3 )
0 commit comments