From 4274aacf7642075917aad95c24c1ab6c90f5dae9 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 25 Feb 2026 22:07:50 +0100 Subject: [PATCH 1/2] FlatpakBackend: Introduce Component and flatten that to packages Instead of just having a list with all packages introduce a list of a new Component class. Components are just a container that groups all packages for the same appstream component (e.g. from different flatpak remotes). Then we flatten these packages from components to our package model which we can use as usual. This has the major advantage that getting all packages for a given component (which we do quite often) is now a lookup in constant time instead of O(n) in all packages. It also gives us fast access to a listmodel of unique packages which can be used for category, author, search, etc. views. --- src/Core/Component.vala | 49 ++++++++++++++++++++++++ src/Core/FlatpakBackend.vala | 73 +++++++++++++++++++++++++++--------- src/meson.build | 1 + 3 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 src/Core/Component.vala diff --git a/src/Core/Component.vala b/src/Core/Component.vala new file mode 100644 index 000000000..5808de3c7 --- /dev/null +++ b/src/Core/Component.vala @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class AppCenterCore.Component : Object, ListModel { + public string component_id { get; construct; } + + private Gee.List packages = new Gee.ArrayList (); + + public Component (string component_id) { + Object (component_id: component_id); + } + + public void add_package (Package package) { + if (package in packages) { + return; + } + + packages.add (package); + items_changed (packages.size - 1, 0, 1); + } + + public void remove_package (Package package) { + if (!(package in packages)) { + return; + } + + var index = packages.index_of (package); + + packages.remove_at (index); + + items_changed ((uint) index, 1, 0); + } + + public Type get_item_type () { + return typeof (Package); + } + + public uint get_n_items () { + return (uint) packages.size; + } + + public Object? get_item (uint position) { + return packages.get ((int) position); + } +} diff --git a/src/Core/FlatpakBackend.vala b/src/Core/FlatpakBackend.vala index 3a2ca09d4..3914aa81a 100644 --- a/src/Core/FlatpakBackend.vala +++ b/src/Core/FlatpakBackend.vala @@ -64,6 +64,7 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { private AsyncQueue jobs = new AsyncQueue (); private Thread worker_thread; + private Gee.HashMap component_list; private Gee.HashMap package_list; private AppStream.Pool user_appstream_pool; private AppStream.Pool system_appstream_pool; @@ -75,7 +76,8 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { public Job.Type job_type { get; protected set; } public bool working { public get; protected set; } - private ListStore _packages; + private ListStore components; + private ListModel _packages; private Gtk.SortListModel _sorted_packages; public ListModel packages { get { return _sorted_packages; } } @@ -242,7 +244,9 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { additional_updates = new GLib.ListStore (typeof (Package)); additional_updates.append (runtime_updates); - _packages = new ListStore (typeof (FlatpakPackage)); + components = new ListStore (typeof (Component)); + + _packages = new Gtk.FlattenListModel (components); _packages.items_changed.connect (() => package_list_changed ()); var sorter = new Gtk.StringSorter (new Gtk.PropertyExpression (typeof (Package), null, "name")); @@ -299,6 +303,7 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { system_appstream_pool = new AppStream.Pool (); system_appstream_pool.set_flags (AppStream.PoolFlags.LOAD_OS_CATALOG); + component_list = new Gee.HashMap (null, null); package_list = new Gee.HashMap (null, null); // Monitor the FlatpakInstallation for changes (e.g. adding/removing remotes) @@ -410,19 +415,22 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { } public void notify_package_changed (Package package) { - GLib.ListStore store; + ListModel model; if (package.is_runtime_updates) { - store = additional_updates; + model = additional_updates; } else { - store = _packages; + model = _packages; } - uint pos; - if (store.find (package, out pos)) { - store.items_changed (pos, 1, 1); - } else { - warning ("Package %s not found in the package list", package.name); + for (uint i = 0; i < model.get_n_items (); i++) { + var obj = model.get_item (i); + if (obj == package) { + model.items_changed (i, 1, 1); + return; + } } + + warning ("Package %s not found in the package list", package.name); } private void set_actions_enabled (bool working) { @@ -1333,23 +1341,52 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { if (Thread.self () != worker_thread) { // We are in the main thread so update immediately - update_package_store (removed, added); + update_component_store (removed, added); } else { // We are in the worker thread and changing the package liststore // will trigger signals that update the UI so wrap in Idle to update on the main thread - Idle.add (() => update_package_store (removed, added)); + Idle.add (() => update_component_store (removed, added)); } } - private bool update_package_store (Gee.Collection removed, Gee.Collection added) { - foreach (var package in removed) { - uint pos; - if (_packages.find (package, out pos)) { - _packages.remove (pos); + private bool update_component_store (Gee.Collection removed, Gee.Collection added) { + var new_components = new Gee.HashSet (); + + /* Add added packages to their components, creating new components if necessary */ + foreach (var added_package in added) { + var comp_id = added_package.normalized_component_id; + var comp = component_list[comp_id]; + + if (comp == null) { + comp = new Component (comp_id); + new_components.add (comp); + } + + component_list[comp_id] = comp; + + /* No op if package is already in the component */ + comp.add_package (added_package); + } + + /* Remove removed packages from their components */ + foreach (var removed_package in removed) { + var comp_id = removed_package.normalized_component_id; + component_list[comp_id].remove_package (removed_package); + } + + /* Cleanup empty components */ + if (!removed.is_empty) { + for (int i = (int) components.get_n_items () - 1; i >= 0; i--) { + var component = (Component) components.get_item (i); + if (component.get_n_items () == 0) { + components.remove (i); + component_list.unset (component.component_id); + } } } - _packages.splice (_packages.n_items, 0, added.to_array ()); + /* Add new components */ + components.splice (components.n_items, 0, new_components.to_array ()); return Source.REMOVE; } diff --git a/src/meson.build b/src/meson.build index 4443dd121..47118b833 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,6 +7,7 @@ appcenter_files = files( 'Core/CardUtils.vala', 'Core' / 'CategoryManager.vala', 'Core/ChangeInformation.vala', + 'Core' / 'Component.vala', 'Core/FlatpakBackend.vala', 'Core/HttpClient.vala', 'Core/Job.vala', From b1eaae5d1390bdd638146a1a1f0061cefb000d3b Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 25 Feb 2026 16:27:24 +0100 Subject: [PATCH 2/2] FlatpakBackend: Speedup get_packages_for_component_id From O(n) to O(1) since we now have a hashmap with the component ids. --- src/Core/FlatpakBackend.vala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Core/FlatpakBackend.vala b/src/Core/FlatpakBackend.vala index 3914aa81a..8d2a46979 100644 --- a/src/Core/FlatpakBackend.vala +++ b/src/Core/FlatpakBackend.vala @@ -686,14 +686,17 @@ public class AppCenterCore.FlatpakBackend : Object, Backend { } public Gee.Collection get_packages_for_component_id (string id) { + var normalized_component_id = Utils.normalize_component_id (id); + var component = component_list[normalized_component_id]; var packages = new Gee.ArrayList (); - var suffixed_id = id + ".desktop"; - foreach (var package in package_list.values) { - if (package.component.id == id) { - packages.add (package); - } else if (package.component.id == suffixed_id) { - packages.add (package); - } + + if (component == null) { + return packages; + } + + for (uint i = 0; i < component.get_n_items (); i++) { + var package = (Package) component.get_item (i); + packages.add (package); } return packages;