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"><b>MyMemoryLeak</b></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:
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.