Skip to content
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

Parameter Refs leave dangling Watchers that block garbage collection #963

Open
tbronson opened this issue Aug 15, 2024 · 0 comments
Open

Comments

@tbronson
Copy link

ALL software version info

Python 3.12
Param 2.1.0

Description of expected behavior and the observed behavior

When you create a reference to another parameter, a hardref cycle is formed between the two that remains unbroken, causing all relevant objects to never be collected, until all of them no longer have an anchoring ref. Further, clobbering the reference by setting the Parameter to a non-referenced value does not remove the Watcher keeping the objects alive.

Complete, minimal, self-contained example code that reproduces the issue

import param
import weakref
import gc

class View(param.Parameterized):
    n1 = param.Number(allow_refs=True)
    
    def _clear_refs(self):
        param_private = self._param__private
        for name in param_private.refs:
            if name in param_private.async_refs:
                param_private.async_refs.pop(name).cancel()
        for _, watcher in param_private.ref_watchers:
            dep_obj = watcher.cls if watcher.inst is None else watcher.inst
            dep_obj.param.unwatch(watcher)
        self._param__private.ref_watchers = []
        self._param__private.refs = {}

class Parent(param.Parameterized):
    n1 = param.Number()    

class ViewFactory:

    def __init__(self) -> None:
        self._view = weakref.WeakValueDictionary()
    
    @property
    def view(self):
        if 'view' not in self._view:
            t = Parent()
            self._view['view'] = t
        else:
            t = self._view['view']
        return t

fact = ViewFactory()
view1 = View(n1=fact.view.param.n1)
view2 = View(n1=fact.view.param.n1)
weakref.finalize(view1, lambda: print('finalized view1'))
weakref.finalize(view2, lambda: print('finalized view2'))
print(view1.n1)
print(view2.n1)
fact.view.n1 = 1
print(view1.n1)
print(view2.n1)
# let's clobber view1's ref
view1.n1 = 0
fact.view.n1 = 2
print(view1.n1)
print(view2.n1)
# hold a view1 weakref so we can access after del
ref = weakref.WeakValueDictionary(view1=view1)
del view1
gc.collect()
# nothing
# let's manually clear out the references
ref['view1']._clear_refs()
gc.collect()
# now it should be finalized
del view2
gc.collect()
# removal of the last referring object will break the hardref cycle and finally cause them all to be collected.... but if at least 1 still exists during collection, the problem will grow unbounded
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant