I recently realized a program of mine has memory leaks. Upon debugging, I narrowed it down to an issue while closing tabs using AdwTabView
. I have created a small demo terminal app that can reproduce this issue.
Terminal tabs can be closed in three ways:
- User closes the tab directly (clicks on the “X” button on the tab)
- User closes the window, then all tabs are closed
- The process running in the terminal exits, which causes a chain reaction to close the tab for that terminal
- Context for this case:
- There is an intermediary component between
AdwTabPage
andTerminal
:TerminalTab
.TerminalTab
listens to thechild_exited
event emitted byTerminal
and emits aclose_request
event of its own when that happens - Upon adding a
TerminalTab
toAdwTabView
(withadw_tab_view_add_page
), I listen for theclose_request
eventTerminalTab
may emit - The callback for the
close_request
event gets the page for theTerminalTab
that emitted the event (withadw_tab_view_get_page
) and callsadw_tab_view_close_page
- There is an intermediary component between
- Context for this case:
Scenarios 1 and 2 work flawlessly. Upon closing a tab or the window, both dispose, and Vala’s “destructor” methods are called.
Scenario 3, however, requires me to call terminal_tab.unparent
(line 118) in the close_request
callback. Not calling unparent causes a leak; the terminal tab, and terminal, consequently, are never free’d.
Demo Source
class MyTerminal : Vte.Terminal {
public MyTerminal () {
Object ();
this.spawn_async (Vte.PtyFlags.DEFAULT,
null,
{ "zsh" },
null,
0,
null,
-1,
null,
null);
}
~MyTerminal () {
message ("MyTerminal destroyed");
}
public override void dispose () {
message ("MyTerminal dispose");
base.dispose ();
}
}
class MyTerminalTab : Gtk.Box {
private MyTerminal terminal;
public signal void close_request ();
public MyTerminalTab () {
Object (orientation: Gtk.Orientation.VERTICAL, spacing: 0);
this.terminal = new MyTerminal ();
this.append (this.terminal);
this.terminal.child_exited.connect (this.on_child_exited);
}
private void on_child_exited (int code) {
message ("Child exited with code %d", code);
this.close_request.emit ();
}
~MyTerminalTab () {
message ("MyTerminalTab destroyed");
}
public override void dispose () {
message ("MyTerminalTab dispose");
// This causes terminal to emit child_exited. Without this call,
// terminal is still disposed and destroyed, but it does not emit the
// signal.
// this.terminal = null;
// This changes nothing (no signal emited nor any error thrown)
// this.remove (this.terminal);
base.dispose ();
}
}
class SimpleWindow : Adw.ApplicationWindow {
private Adw.TabView tab_view;
private Adw.TabBar tab_bar;
construct {
var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
var b = new Gtk.Button () {
icon_name = "list-add-symbolic",
action_name = "win.new-tab",
};
this.tab_view = new Adw.TabView ();
this.tab_bar = new Adw.TabBar () {
view = this.tab_view,
autohide = false,
};
var hb = new Adw.HeaderBar () {
show_end_title_buttons = true,
show_start_title_buttons = true,
};
hb.pack_end (b);
box.append (hb);
box.append (this.tab_bar);
box.append (this.tab_view);
this.content = box;
ActionEntry[] entries = {
{ "new-tab", this.add_tab },
};
this.add_action_entries (entries, this);
}
private void add_tab () {
var tt = new MyTerminalTab ();
tt.close_request.connect (this.on_tt_close_request);
this.tab_view.append (tt);
}
private void on_tt_close_request (MyTerminalTab terminal_tab) {
message ("MyTerminalTab emitted a close request");
var page = this.tab_view.get_page (terminal_tab);
if (page != null) {
message ("Closing page");
this.tab_view.close_page (page);
// [QUESTION] why is it that if I don't call unparent here, Adw.TabPage
// keeps a referece to the terminal tab, causing it never to be free'd?
terminal_tab.unparent ();
}
}
}
static int main (string[] args) {
var app = new Adw.Application ("com.raggesilver.Test",
GLib.ApplicationFlags.DEFAULT_FLAGS);
app.activate.connect ((application) => {
var w = new SimpleWindow () { application = application as Adw.Application };
w.present ();
});
return app.run (args);
}
meson.build (not required)
project('vala-memory', ['c', 'vala'],
version: '0.0.1',
meson_version: '>= 0.50.0',
default_options: [ 'warning_level=2',
],
)
vala_memory_sources = files(
'adw-tab-test.vala'
)
vala_memory_deps = [
dependency('gtk4', version: '>= 4.10.0'),
dependency('libadwaita-1', version: '>= 1.3'),
dependency('vte-2.91-gtk4', version: '>= 0.71.0'),
]
executable('adw-tab-test', vala_memory_sources,
vala_args: '--target-glib=2.50', dependencies: vala_memory_deps,
install: true,
)
Build instructions:
valac --pkg=gtk4 --pkg=libadwaita-1 --pkg=vte-2.91-gtk4 adw-tab-test.vala
I have also created a snippet in GitLab with the source and compilation instructions for this demo AdwTabPage closing issue ($5873) · Snippets · Paulo Queiroz / Black Box · GitLab
Am I really supposed to call unparent
here? Is this a bug, or am I doing something wrong?
TIA