Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid memory leaks with View

Tags:

With the following program:

from traits.api import HasTraits, Int, Instance
from traitsui.api import View

class NewView(View):
    def __del__(self):
        print('deleting NewView')

class A(HasTraits):
    new_view = Instance(NewView)
    def __del__(self):
        print('deleting {}'.format(self))

    a = Int

    def default_traits_view(self):
        new_view = NewView('a')
        return new_view

running

a = A()
del(a)

returns

 deleting <__main__.A object at 0x12a016a70>

as it should.

If I do

a = A()
a.configure_traits()

and after closing the dialog:

del(a)

I have the same kind of message:

deleting <__main__.A object at 0x12a016650>

with no mention of the NewView being deleted.

In geneal, what are the good practices to avoid memory leaks with Traits and TraitsUI?

like image 625
Yves Surrel Avatar asked Nov 28 '16 08:11

Yves Surrel


1 Answers

What's going on here is that the NewView object is involved in a reference cycle, and the objects in that cycle don't get automatically collected as part of CPython's primary reference-counting-based object deallocation mechanism. However, they should be eventually collected as part of CPython's cyclic garbage collector, or you can force that collection by doing a gc.collect(), so there should be no actual long-term memory leak here.

Ironically, attempting to detect that eventual collection by adding a __del__ method to NewView hinders the process, since it renders the NewView object uncollectable: at least in Python 2, Python won't try to collect cycles containing objects that have __del__ methods. See the gc docs for details. (Python 3 is somewhat cleverer here, thanks to the changes outlined in PEP 442.) So with the __del__ method, using Python 2, there will indeed be a slow memory leak over time. The solution is to remove the __del__ method.

Here's a graph showing the reference cycle (actually, this shows the whole strongly-connected component of the object graph containing the NewView object): nodes are the objects involved, and arrows go from referrers to referents. In the bottom right portion of the graph, you see that the NewView object has a reference to its top-level Group (via the content attribute), and that Group object has a reference back to the original view (the container attribute). There are similar cycles going on elsewhere in the view.

NewView reference cycle

It's probably worth opening a feature request on the Traits UI tracker: in theory, it should be possible to break the reference cycles manually when the view is no longer needed, though in practice that might require significant reworking of the Traits UI source.

Here's some code that demonstrates that (with the __del__ methods removed) a call to gc.collect does collect the NewView object: it stores a weak reference to the view on the A instance, with a callback that reports when that view is garbage collected.

from traits.api import HasTraits, Int, Instance
from traitsui.api import View

import gc
import weakref

class NewView(View):
    pass


def report_collection(ref):
    print("NewView object has been collected")


class A(HasTraits):
    a = Int

    def default_traits_view(self):
        new_view = NewView('a')
        self.view_ref = weakref.ref(new_view, report_collection)
        return new_view


def open_view():
    a = A()
    a.configure_traits()
    print("Collecting cyclic garbage")
    gc.collect()
    print("Cyclic garbage collection complete")

On my machine, here's what I see when open_view is called:

>>> open_view()
Collecting cyclic garbage
NewView object has been collected
Cyclic garbage collection complete
like image 181
Mark Dickinson Avatar answered Sep 25 '22 16:09

Mark Dickinson