Memory/space leak when using Gtk.Stack (?)

I have a program that uses Gtk.Stack to display the different views of the application. It works as intended, except that switching views makes RAM memory usage grow and grow unexpectedly, even though the last view is always removed from the stack, so that there is only one at a time. if you leave the application alone without interacting with it, the accumulated memory is eventually released (one or two hours later).

I noticed that the gtk4-demo behaves similarly. Just like my application, it gets to the top of the RAM usage charts after a couple of minutes of interacting with it.

System info

  • Linux 6.9.10 (x86_64)
  • GTK 4.14.2

Reproducer mini-app

I wrote a mini app that reproduces the problem.

Video 1: Reproducer application

The source code looks as follows:

"""GtkStack memory leak? Using a custom ImageView."""

import random
import sys
import gi

gi.require_version("Gtk", "4.0")

from gi.repository import Gio, Gtk


# CONSTANTS

IMAGES = [
    "img/01.png",
    "img/02.png",
    "img/03.png",
]


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.present()

    def on_quit(self, action, param):
        self.quit()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    header_bar = Gtk.Template.Child("header-bar")
    title_label = Gtk.Template.Child("title-label")
    view_stack = Gtk.Template.Child("view-stack")

    def __init__(self, app):
        """Initialize the window and set the given app, a
        Gtk.Application, as its manager."""
        super().__init__()
        self.page_count = 1  # Count pages added to the stack.

        # Show image view.
        view = ImageView(self, random.choice(IMAGES))
        self.show_view(view)

    def show_view(self, view, transition=Gtk.StackTransitionType.SLIDE_LEFT):
        """Add the given view to the stack and show it."""
        # Get old view.
        old_view = self.view_stack.get_visible_child()

        # Add new view.
        view_name = "page-{}".format(self.page_count)
        self.view_stack.add_named(view, view_name)
        self.title_label.set_text("Stack Page {}".format(self.page_count))

        # Show new view.
        self.view_stack.set_transition_type(transition)
        self.view_stack.set_visible_child_name(view_name)

        # Remove old view (if any).
        if old_view:
            self.view_stack.remove(old_view)

        # Update page counter.
        self.page_count += 1


@Gtk.Template(filename="image-view.ui")
class ImageView(Gtk.Box):
    """The widget that displays a random image and a button to change it."""
    __gtype_name__ = "ImageView"

    image = Gtk.Template.Child("image")
    next_button = Gtk.Template.Child("next-button")

    def __init__(self, window, image_path):
        super().__init__()
        self.window = window
        self.image.set_from_file(image_path)

    @Gtk.Template.Callback()
    def on_next_button_clicked(self, button):
        """Handle the given button's clicked signal."""
        view = ImageView(self.window, random.choice(IMAGES))
        self.window.show_view(view)


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

This is the XML UI templates:

app-window.ui:

<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.10.3 -->
<interface>
  <!-- interface-name app-window.ui -->
  <requires lib="gtk" version="4.6"/>
  <template class="AppWindow" parent="GtkApplicationWindow">
    <property name="child">
      <object class="GtkStack" id="view-stack"/>
    </property>
    <property name="default-height">300</property>
    <property name="default-width">300</property>
    <child type="titlebar">
      <object class="GtkHeaderBar" id="header-bar">
        <child type="start">
          <object class="GtkBox" id="actions-box"/>
        </child>
        <child type="title">
          <object class="GtkBox">
            <property name="orientation">vertical</property>
            <property name="valign">center</property>
            <child>
              <object class="GtkLabel">
                <property name="justify">center</property>
                <property name="label">&lt;b&gt;MyMemoryLeak&lt;/b&gt;</property>
                <property name="use-markup">True</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="title-label">
                <property name="css-classes">subtitle</property>
                <property name="ellipsize">middle</property>
                <property name="justify">center</property>
                <property name="label">Title of View</property>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

image-view.ui

<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.10.3 -->
<interface>
  <!-- interface-name image-view.ui -->
  <requires lib="gtk" version="4.6"/>
  <template class="ImageView" parent="GtkBox">
    <property name="margin-bottom">12</property>
    <property name="margin-end">12</property>
    <property name="margin-start">12</property>
    <property name="margin-top">12</property>
    <property name="orientation">vertical</property>
    <property name="spacing">6</property>
    <child>
      <object class="GtkImage" id="image">
        <property name="vexpand">True</property>
      </object>
    </child>
    <child>
      <object class="GtkButton" id="next-button">
        <property name="label">Next</property>
        <signal name="clicked" handler="on_next_button_clicked"/>
      </object>
    </child>
  </template>
</interface>

The images:

01
02
03

And just in case you use Guix, a manifest to create a shell with the necessary dependencies:

#| GNU Guix manifest

This file is a GNU Guix manifest file. It can be used with GNU Guix to
create a profile or an environment to work on the project. |#

(use-modules (gnu packages))


(define DEV_PACKAGES
  (list "coreutils"
        "python-objgraph"))

(define PRODUCTION_PACKAGES
  (list "adwaita-icon-theme"
        "dbus"
        "gtk"
        "python"))

(specifications->manifest
 (append DEV_PACKAGES
         PRODUCTION_PACKAGES))

Debugging & Profiling info

TODO: Use any of the procedures described in PyGObject · Debugging & Profiling section.

Possible causes

I suspect that instances of ImageView are not being destroyed (their destroy signal is not emitted, apparently). Although inspecting the application tree with Ctrl+Shift+I doesn’t seem to show any accumulation of those kinds of objects in the Gtk.Stack,

Video 2: Application tree inspection

Additional information

In this particular mini-app, I noticed that the leak disappears if I define the signal handlers as functions instead of methods:

Video 3: Leak fixer

import random
import sys
import gi

gi.require_version("Gtk", "4.0")

from gi.repository import Gio, Gtk


# CONSTANTS

IMAGES = [
    "img/01.png",
    "img/02.png",
    "img/03.png",
]


# EVENT HANDLERS

def on_next_button_clicked(button, window):
    """Handle the given button's clicked signal and update window."""
    view = ImageView(window, random.choice(IMAGES))
    window.show_view(view)


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.present()

    def on_quit(self, action, param):
        self.quit()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    header_bar = Gtk.Template.Child("header-bar")
    title_label = Gtk.Template.Child("title-label")
    view_stack = Gtk.Template.Child("view-stack")

    def __init__(self, app):
        """Initialize the window and set the given app, a
        Gtk.Application, as its manager."""
        super().__init__()
        self.page_count = 1  # Count pages added to the stack.

        # Show image view.
        view = ImageView(self, random.choice(IMAGES))
        self.show_view(view)

    def show_view(self, view, transition=Gtk.StackTransitionType.SLIDE_LEFT):
        """Add the given view to the stack and show it."""
        # Get old view.
        old_view = self.view_stack.get_visible_child()

        # Add new view.
        view_name = "page-{}".format(self.page_count)
        self.view_stack.add_named(view, view_name)
        self.title_label.set_text("Stack Page {}".format(self.page_count))

        # Show new view.
        self.view_stack.set_transition_type(transition)
        self.view_stack.set_visible_child_name(view_name)

        # Remove old view (if any).
        if old_view:
            self.view_stack.remove(old_view)

        # Update page counter.
        self.page_count += 1


@Gtk.Template(filename="image-view-no-callbacks.ui")
class ImageView(Gtk.Box):
    """The widget that displays a random image and a button to change it."""
    __gtype_name__ = "ImageView"

    image = Gtk.Template.Child("image")
    next_button = Gtk.Template.Child("next-button")

    def __init__(self, window, image_path):
        super().__init__()
        self.window = window
        self.image.set_from_file(image_path)

        # Connect button to callback function instead of callback method.
        self.next_button.connect("clicked", on_next_button_clicked, window)


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

But I don’t see why I should do that, and also, doing so in the target app that I want to fix doesn’t seem to make any difference.

Finally

I’d really appreciate if you could point out what I’m doing wrong here.

Thanks in advance.

Hi,

Which demo in particular? How do you observe / measure the RAM increase?

Note that if there is no memory pressure, the OS may not reclaim immediately the memory left by disposed widgets.

Back to Python: connecting to signals with methods may create circular references on the object, so widgets won’t be freed immediately on dispose, but later (can be very late) by Python’s garbage collector.

You can print some logs to know when GTK/GObject disposes the widget:

        view = ImageView(window, random.choice(IMAGES))
        view.weak_ref(print, "ImageView disposed")

Also, you can log when Python collects the widget. At this point, it should be freed from the memory:

class ImageView(Gtk.Box):
   ...
    def __del__(self):
        print("ImageView garbage-collected")

(Keep in mind the RAM won’t decrease at this point unless the OS reclaims it.)

You can try to avoid circular references by manually disconnecting the callback, and check if that improves the RAM usage:

        if old_view:
            old_view.next_button.disconnect_by_func(self.on_next_button_clicked)
            self.view_stack.remove(old_view)

Hi, @gwillems , thanks for taking a look.

The following are the ones that use more RAM that takes at least one hour to be freed (★ indicates more RAM usage). To check RAM usage I use the top program:

  • Primary Menu → Inspector
  • Demo: Benchmark → Fishbowl ★
  • Demo: Benchmark → Scrolling ★
  • Demo: Clipboard → File (open file chooser)
  • Demo: Gestures
  • Demo: Images
  • Demo: List Box → Complex ★
  • Demo: Menu ★
  • Demo: OpenGL → Shadertoy
  • Demo: Overlay → Interactive Overlay
  • Demo: Pickers → Font (pick another font) ★
  • Demo: Sliding Puzzle ★
  • Demo: Text View → Markup
  • Demo: Video Player (incluso se incrementa al repetir apertura) ★

Oh, I was hoping I didn’t have to do that, but I’ll check and see if it makes any difference.

Thanks again.

Well, trying to disconnect handlers manually with disconnect_by_func I get an error:

line 90, in show_view
    old_view.next_button.disconnect_by_func(
TypeError: nothing connected to <bound method ImageView.on_next_button_clicked of <__main__.ImageView object at 0x7f7bff8ecd40 (ImageView at 0x5dbaa90)>>

By the way, where does disconnect_by_func come from? I only see disconnect and signal_handlers_disconnect_by_func in GObject API reference, and both give me the impression they are not supposed to be used in Python scripting because of the paragraph that reads:

This function is not directly available to language bindings.

The same says in the documentation of weak_ref.

Better search from the PyGObject doc at https://pygobject.gnome.org/ , instead of the C APIs:

A few things are indeed Python specific.

1 Like
TypeError: nothing connected to <bound method ImageView.on_next_button_clicked of <__main__.ImageView object at 0x7f7bff8ecd40 (ImageView at 0x5dbaa90)>>

hmm… have you connected on_next_button_clicked or self.on_next_button_clicked ?
(I assumed it was the method)

Ideally we should not have to do that, but the way Python garbage collector works sometimes forces us to manually break circular references, to avoid dangling resources.

In C, we can use the gobject’s dispose method to properly cleanup signals, but it’s not doable in Python. It’s a known issue, I don’t know it something can be done automatically here.

Yeah, to the method. That is, the click signal of the ImageView.next_button is connected as in the original code: from image-view.ui to the decorated ImageView.on_next_button_clicked callback method.

Then, I tried disconnecting it as follows in AppWindow.show_view:

# Remove old view (if any).
if old_view:
    old_view.next_button.disconnect_by_func(
        old_view.on_next_button_clicked
    )
    self.view_stack.remove(old_view)

disconnect_by_func() should work fine if connect() was manually used to setup the callback.

If you used an *.ui file + template + decorators, there may be some wrapping around the callback due to the internal template mechanism, which may explain why the on_next_button_clicked wasn’t found by disconnect, i.e. something is called inbetween.
I’m not sure how to manage that case…

1 Like

Yes, that way, the handlers can be disconnected, thanks.

So now I can confirm that disconnecting the handlers manually also repairs the unexpected memory usage (at least in the reproducer miniapp). It seems to break the circular references and the old_view is garbage collected when removed from the stack (i.e. the old_view’s __del__ method is called, and the message ImageView garbage-collected is printed).

The following are the two versions of app.py that fix the leak:

app-manual-connection-to-callback-function-instead-of-method.py

import random
import sys
import gi

gi.require_version("Gtk", "4.0")

from gi.repository import Gio, Gtk


# CONSTANTS

IMAGES = [
    "img/01.png",
    "img/02.png",
    "img/03.png",
]


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.present()

    def on_quit(self, action, param):
        self.quit()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    header_bar = Gtk.Template.Child("header-bar")
    title_label = Gtk.Template.Child("title-label")
    view_stack = Gtk.Template.Child("view-stack")

    def __init__(self, app):
        """Initialize the window and set the given app, a
        Gtk.Application, as its manager."""
        super().__init__()
        self.page_count = 1  # Count pages added to the stack.

        # Show image view.
        view = ImageView(self, random.choice(IMAGES))
        self.show_view(view)

    def show_view(self, view, transition=Gtk.StackTransitionType.SLIDE_LEFT):
        """Add the given view to the stack and show it."""
        # Get old view.
        old_view = self.view_stack.get_visible_child()

        # Add new view.
        view_name = "page-{}".format(self.page_count)
        self.view_stack.add_named(view, view_name)
        self.title_label.set_text("Stack Page {}".format(self.page_count))

        # Show new view.
        self.view_stack.set_transition_type(transition)
        self.view_stack.set_visible_child_name(view_name)

        # Remove old view (if any).
        if old_view:
            self.view_stack.remove(old_view)

        # Update page counter.
        self.page_count += 1


@Gtk.Template(filename="image-view-no-callbacks.ui")
class ImageView(Gtk.Box):
    """The widget that displays a random image and a button to change it."""
    __gtype_name__ = "ImageView"

    image = Gtk.Template.Child("image")
    next_button = Gtk.Template.Child("next-button")

    def __init__(self, window, image_path):
        super().__init__()
        self.window = window
        self.image.set_from_file(image_path)

        # Connect button to callback function instead of callback method.
        self.next_button.connect("clicked", on_next_button_clicked, window)

    def __del__(self):
        print("ImageView garbage-collected")


# SIGNAL HANDLERS

def on_next_button_clicked(button, window):
    """Handle the given button's clicked signal and update window."""
    view = ImageView(window, random.choice(IMAGES))
    window.show_view(view)


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

app-disconnect-callback-methods-manually-before-removing-old-view.py

import random
import sys
import gi

gi.require_version("Gdk", "4.0")
gi.require_version("Gtk", "4.0")

from gi.repository import Gio, GObject, Gtk


# CONSTANTS

IMAGES = [
    "img/01.png",
    "img/02.png",
    "img/03.png",
]


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.present()

    def on_quit(self, action, param):
        self.quit()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    header_bar = Gtk.Template.Child("header-bar")
    title_label = Gtk.Template.Child("title-label")
    view_stack = Gtk.Template.Child("view-stack")

    def __init__(self, app):
        """Initialize the window and set the given app, a
        Gtk.Application, as its manager."""
        super().__init__()
        self.page_count = 1  # Count pages added to the stack.

        # Show image view.
        view = ImageView(self, random.choice(IMAGES))
        self.show_view(view)

    def show_view(self, view, transition=Gtk.StackTransitionType.SLIDE_LEFT):
        """Add the given view to the stack and show it."""
        # Get old view.
        old_view = self.view_stack.get_visible_child()

        # Add new view.
        view_name = "page-{}".format(self.page_count)
        self.view_stack.add_named(view, view_name)
        self.title_label.set_text("Stack Page {}".format(self.page_count))

        # Show new view.
        self.view_stack.set_transition_type(transition)
        self.view_stack.set_visible_child_name(view_name)

        # Remove old view (if any).
        if old_view:
            old_view.disconnect_handlers()
            self.view_stack.remove(old_view)

        # Update page counter.
        self.page_count += 1


@Gtk.Template(filename="image-view.ui")
class ImageView(Gtk.Box):
    """The widget that displays a random image and a button to change it."""
    __gtype_name__ = "ImageView"

    image = Gtk.Template.Child("image")
    next_button = Gtk.Template.Child("next-button")

    def __init__(self, window, image_path):
        super().__init__()
        self.window = window
        self.image.set_from_file(image_path)

        # Connect signals to handlers.
        self.next_button.connect("clicked", self.on_next_button_clicked)

    def __del__(self):
        print("ImageView garbage-collected")

    def on_next_button_clicked(self, button):
        """Handle the given button's clicked signal."""
        view = ImageView(self.window, random.choice(IMAGES))
        self.window.show_view(view)

    def disconnect_handlers(self):
        "Disconnect manually connected handlers to break circular references."
        self.next_button.disconnect_by_func(self.on_next_button_clicked)
        print("Old ImageView: Signal handlers disconnected.")


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

Now I have to check whether disconnecting handlers does the trick in my real application.

Thanks for all the help so far, @gwillems .

1 Like

For what it’s worth, here are debugging sessions with Python pdb and some object graphs of the original miniapp, which leaks memory, and the modified app that defines the clicked signal handler as a function, which fixes the leak in this case (please mind the misleading variable names in some places).

Debugging info: app.py

❯ python3 -m pdb app.py
> ./app.py(1)<module>()
(Pdb) import sys
(Pdb) r

(APP STARTS, I BROWSE 10 VIEWS AND QUIT APP)

--Return--
> ./app.py(108)<module>()->None
-> sys.exit(exit_status)
(Pdb) import gc
(Pdb) gc.collect()
92
(Pdb) gc.collect()
0
(Pdb) import objgraph
(Pdb) views_tracked_by_gc = objgraph.by_type("ImageView")
(Pdb) len(views_tracked_by_gc)
10
(Pdb) leaked_views = objgraph.get_leaking_objects(views_tracked_by_gc)
(Pdb) len(leaked_views)
10
(Pdb) import random
(Pdb) random_leaked_view = random.choice(leaked_views)
(Pdb) random_leaked_view
<__main__.ImageView object at 0x7ff6ac645240 (ImageView at 0x2da573b0)>
(Pdb) sys.getrefcount(random_leaked_view)
5
(Pdb) objgraph.show_chain(objgraph.find_backref_chain(random_leaked_view, objgraph.is_proper_module), filename="/tmp/app-random-leaked-view.png")
Graph written to /tmp/objgraph-cz0eglb4.dot (3 nodes)
Image generated as /tmp/app-random-leaked-view.png
(Pdb) objgraph.show_backrefs([random_leaked_view], max_depth=10, filename="/tmp/app-random-leaked-view-depth-10.png")
Graph written to /tmp/objgraph-7u47m7xb.dot (69 nodes)
Image generated as /tmp/app-random-leaked-view-depth-10.png
(Pdb)

Graphs of randomly selected “leaked” view:

app-random-leaked-view

Debugging info: Modified app.py with callback function

❯ python3 -m pdb app-manual-connection-to-callback-function.py
> ./app-manual-connection-to-callback-function.py(1)<module>()
(Pdb) import sys
(Pdb) r

(APP STARTS, I BROWSE 10 VIEWS AND QUIT APP)

ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
ImageView garbage-collected
--Return--
> ./app-manual-connection-to-callback-function.py(127)<module>()->None
-> sys.exit(exit_status)
(Pdb) import gc
(Pdb) gc.collect()
92
(Pdb) gc.collect()
0
(Pdb) import objgraph
(Pdb) views_tracked_by_gc = objgraph.by_type("ImageView")
(Pdb) len(views_tracked_by_gc)
1
(Pdb) leaked_views = objgraph.get_leaking_objects(views_tracked_by_gc)
(Pdb) len(leaked_views)
1
(Pdb) leaked_view = leaked_views[0]
(Pdb) leaked_view
<__main__.ImageView object at 0x7f6b62935240 (ImageView at 0x3205ec0)>
(Pdb) sys.getrefcount(leaked_view)
5
(Pdb) objgraph.show_chain(objgraph.find_backref_chain(leaked_view, objgraph.is_proper_module), filename="/tmp/app-with-callback-functions-leaked-view.png")
Graph written to /tmp/objgraph-d99bzl3b.dot (3 nodes)
Image generated as /tmp/app-with-callback-functions-leaked-view.png
(Pdb) objgraph.show_backrefs([leaked_view], max_depth=10, filename="/tmp/app-with-callback-functions-leaked-view-depth-10.png")
Graph written to /tmp/objgraph-1rwt8bvt.dot (47 nodes)
Image generated as /tmp/app-with-callback-functions-leaked-view-depth-10.png
(Pdb)

app-with-callback-functions-leaked-view

1 Like

After modifying most of the source code of my real app, I could get rid of most of the memory problem. But it seems disconnecting signal handlers is not enough when using the Gtk.Video widget and custom widgets with custom signals and complex inheritance.

The following is a variant of the replicator mini-app that reproduces the problem with Gtk.Video:

import random
import sys
import gi

gi.require_version("Gtk", "4.0")

from gi.repository import Gio, Gtk


# CONSTANTS

SOUNDS = [
    "snd/01.oga",
    "snd/02.oga",
    "snd/03.oga",
]


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.present()

    def on_quit(self, action, param):
        self.quit()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    header_bar = Gtk.Template.Child("header-bar")
    title_label = Gtk.Template.Child("title-label")
    view_stack = Gtk.Template.Child("view-stack")

    def __init__(self, app):
        """Initialize the window and set the given app, a
        Gtk.Application, as its manager."""
        super().__init__()
        self.page_count = 1  # Count pages added to the stack.

        # Show player view.
        view = PlayerView(random.choice(SOUNDS))
        self.show_view(view)

    def show_view(self, view, transition=Gtk.StackTransitionType.SLIDE_LEFT):
        """Add the given view to the stack and show it."""
        # Get old view.
        old_view = self.view_stack.get_visible_child()

        # Add new view.
        view_name = "page-{}".format(self.page_count)
        self.view_stack.add_named(view, view_name)
        self.title_label.set_text("Stack Page {}".format(self.page_count))

        # Show new view.
        self.view_stack.set_transition_type(transition)
        self.view_stack.set_visible_child_name(view_name)

        # Remove old view (if any).
        if old_view:
            old_view.disconnect_signal_handlers()
            self.view_stack.remove(old_view)

        # Update page counter.
        self.page_count += 1


@Gtk.Template(filename="player-view.ui")
class PlayerView(Gtk.Box):
    """The widget that displays a media player and a button to change it."""
    __gtype_name__ = "PlayerView"

    player = Gtk.Template.Child("player")
    next_button = Gtk.Template.Child("next-button")

    def __init__(self, media_path):
        super().__init__()
        self.player.set_filename(media_path)

        # XXX: Connect signal handlers manually.
        self.next_button.connect(
            "clicked",
            self.on_next_button_clicked
        )

    # XXX: Disconnect signal handlers to ensure garbage collection of self.
    def disconnect_signal_handlers(self):
        self.next_button.disconnect_by_func(
            self.on_next_button_clicked
        )

    def on_next_button_clicked(self, button):
        """Handle the given button's clicked signal."""
        view = PlayerView(random.choice(SOUNDS))
        window = self.get_root()
        window.show_view(view)


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

The comments on another post I found on GNOME Discourse about a similar (the same?) problem in JS are not very encouraging:

Hm… but I don’t see obvious Python<->GStreamer references in your code. Not sure if it’s a GStreamer internal issue or again something Python-specific…

Does it help to set self.player = None in your disconnect_signal_handlers() function? That may help breaking some more circular references.

Ah, and a small trick: you can try to set

gc.set_threshold(5)

to force Python’s garbage collector to run more often (useful for debugging).

Unfortunately, it doesn’t help. I had tried that too, with no luck, in the other problematic views where disconnecting signal handlers is not enough.

But thanks for the gc trick :slight_smile:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.