From 3f45e0e34bb06a45f4eda7703afe84584b19dabd Mon Sep 17 00:00:00 2001 From: Lewis Goddard Date: Mon, 27 Jul 2026 04:22:19 +0100 Subject: [PATCH 1/2] Screenshot: don't leak every screenshot into the Granite.Settings singleton set_branding() connected a lambda to the process-lifetime Granite.Settings singleton that captured its local `package` parameter. Capturing a local makes Vala emit g_signal_connect_data() with a heap closure block holding strong refs to both the Screenshot widget and the Package, instead of the g_signal_connect_object() it emits when only `this` is captured. Nothing ever disconnected, so every Screenshot ever displayed stayed alive for the life of the process along with its decoded image data. Connect in construct(), where no local is in scope to capture, and keep the colour in a field. This also fixes set_branding() stacking an additional permanent handler on each call. --- src/Widgets/Screenshot.vala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Widgets/Screenshot.vala b/src/Widgets/Screenshot.vala index 549fed73d..59e06f80b 100644 --- a/src/Widgets/Screenshot.vala +++ b/src/Widgets/Screenshot.vala @@ -14,6 +14,7 @@ public class AppCenter.Screenshot : Granite.Bin { private static Gee.HashMap? providers; private Gtk.Picture picture; + private string? accent_color; class construct { set_css_name ("screenshot"); @@ -40,16 +41,20 @@ public class AppCenter.Screenshot : Granite.Bin { add_css_class (Granite.CssClass.CARD); bind_property ("caption", label, "label"); - } - - public void set_branding (AppCenterCore.Package package) { - set_accent_color (package.get_color_primary ()); + // Only capture `this` here: capturing a local would make Vala use + // g_signal_connect_data (), which refs us into the process-lifetime + // Granite.Settings singleton and leaks every screenshot ever shown Granite.Settings.get_default ().notify["prefers-color-scheme"].connect (() => { - set_accent_color (package.get_color_primary ()); + set_accent_color (accent_color); }); } + public void set_branding (AppCenterCore.Package package) { + accent_color = package.get_color_primary (); + set_accent_color (accent_color); + } + private void set_accent_color (string? color) { if (color == null) { /* We automatically use the accent color */ From 061825f59a4dda1cd217888249fb8ddf5bb58b04 Mon Sep 17 00:00:00 2001 From: Lewis Goddard Date: Mon, 27 Jul 2026 07:59:54 +0100 Subject: [PATCH 2/2] =?UTF-8?q?AppInfoView:=20break=20view=E2=86=94child?= =?UTF-8?q?=20reference=20cycle,=20free=20memory=20on=20window=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The motion-controller enter/leave handlers and the scroll value-changed handler captured construct-locals, so Vala emitted g_signal_connect_data() with a strong reference back to the view. Because the view owns those child widgets, that closed a reference cycle: every AppInfoView (and all of its screenshots) stayed alive after being popped from the navigation view, so browsing leaked hundreds of MB that were never reclaimed. Promote the captured locals to fields so the handlers capture only `this` and Vala emits the weak g_signal_connect_object(), letting the view finalize on navigation. Also malloc_trim() once the window is destroyed so closing it returns the freed browsing memory to the OS. --- src/MainWindow.vala | 8 +++++++- src/Views/AppInfoView.vala | 16 +++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 8a2fed375..c944bf766 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -14,6 +14,9 @@ * with this program. If not, see http://www.gnu.org/licenses/. */ +[CCode (cheader_filename = "malloc.h")] +extern int malloc_trim (size_t pad); + public class AppCenter.MainWindow : Gtk.ApplicationWindow { public const string ACTION_PREFIX = "win."; public const string ACTION_SHOW_PACKAGE = "show-package"; @@ -156,7 +159,10 @@ public class AppCenter.MainWindow : Gtk.ApplicationWindow { // We have to wrap in Idle otherwise we crash because libportal hasn't unexported us yet. ((AppCenter.App) application).request_background.begin (() => - Idle.add_once (() => destroy ()) + Idle.add_once (() => { + destroy (); + Idle.add_once (() => malloc_trim (0)); + }) ); return true; diff --git a/src/Views/AppInfoView.vala b/src/Views/AppInfoView.vala index 04a1ae261..adeb37b03 100644 --- a/src/Views/AppInfoView.vala +++ b/src/Views/AppInfoView.vala @@ -45,6 +45,12 @@ public class AppCenter.Views.AppInfoView : Adw.NavigationPage { private Gtk.Revealer oars_flowbox_revealer; private Gtk.Revealer uninstall_button_revealer; + private Gtk.Revealer title_revealer; + private Gtk.Box header; + private Gtk.ScrolledWindow scrolled; + private Gtk.Revealer screenshot_arrow_revealer_p; + private Gtk.Revealer screenshot_arrow_revealer_n; + private bool is_runtime_warning_shown = false; private bool permissions_shown = false; @@ -75,7 +81,7 @@ public class AppCenter.Views.AppInfoView : Adw.NavigationPage { title_widget.append (title_label); title_widget.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); - var title_revealer = new Gtk.Revealer () { + title_revealer = new Gtk.Revealer () { child = title_widget, transition_type = CROSSFADE }; @@ -192,7 +198,7 @@ public class AppCenter.Views.AppInfoView : Adw.NavigationPage { maximum_size = MAX_WIDTH }; - var header = new Gtk.Box (HORIZONTAL, 0) { + header = new Gtk.Box (HORIZONTAL, 0) { hexpand = true }; header.append (header_clamp); @@ -555,14 +561,14 @@ public class AppCenter.Views.AppInfoView : Adw.NavigationPage { } }); - var screenshot_arrow_revealer_p = new Gtk.Revealer () { + screenshot_arrow_revealer_p = new Gtk.Revealer () { child = screenshot_previous, halign = Gtk.Align.START, valign = Gtk.Align.CENTER, transition_type = Gtk.RevealerTransitionType.CROSSFADE }; - var screenshot_arrow_revealer_n = new Gtk.Revealer () { + screenshot_arrow_revealer_n = new Gtk.Revealer () { child = screenshot_next, halign = Gtk.Align.END, valign = Gtk.Align.CENTER, @@ -673,7 +679,7 @@ public class AppCenter.Views.AppInfoView : Adw.NavigationPage { box.append (body_clamp); box.append (new AuthorView (package, MAX_WIDTH)); - var scrolled = new Gtk.ScrolledWindow () { + scrolled = new Gtk.ScrolledWindow () { child = box, hscrollbar_policy = Gtk.PolicyType.NEVER, hexpand = true,