From 9b9596f56818313e2414c68fad16dc214f302d2d Mon Sep 17 00:00:00 2001 From: Alexander K Date: Sun, 13 Apr 2025 23:57:56 +0200 Subject: [PATCH 1/3] Optimize package search --- usr/lib/linuxmint/mintinstall/mintinstall.py | 120 ++++++++++++++----- 1 file changed, 89 insertions(+), 31 deletions(-) diff --git a/usr/lib/linuxmint/mintinstall/mintinstall.py b/usr/lib/linuxmint/mintinstall/mintinstall.py index 03bbc7a..4e49ec4 100755 --- a/usr/lib/linuxmint/mintinstall/mintinstall.py +++ b/usr/lib/linuxmint/mintinstall/mintinstall.py @@ -19,6 +19,7 @@ from pathlib import Path import traceback from operator import attrgetter +from collections import namedtuple import gi gi.require_version('Gtk', '3.0') @@ -611,6 +612,8 @@ def __init__(self, category): self.add(box) +PkgInfoSearchCache = namedtuple('PkgInfoSearchCache', ['name', 'display_name', 'keywords', 'summary', 'description']) + class Application(Gtk.Application): (ACTION_TAB, PROGRESS_TAB, SPINNER_TAB) = list(range(3)) @@ -663,6 +666,7 @@ def __init__(self): self.installer_pulse_timer = 0 self.search_changed_timer = 0 self.search_idle_timer = 0 + self.generate_search_cache_idle_timer = 0 self.action_button_signal_id = 0 self.launch_button_signal_id = 0 @@ -1052,6 +1056,9 @@ def on_appstream_changed(self, installer): if self.banner_tile is not None: self.banner_tile.repopulate_tile() + GLib.idle_add(self.pregenerate_search_cache) + + def on_installer_ready(self): self.page_stack.set_visible_child_name(self.PAGE_LOADING) try: @@ -1072,10 +1079,14 @@ def on_installer_ready(self): # Can take some time, don't block for it (these are categorizing packages based on apt info, not our listings) GLib.idle_add(self.process_unmatched_packages) + if not self.installer.have_flatpak: + GLib.idle_add(self.pregenerate_search_cache) + housekeeping.run() self.refresh_cache_menuitem.set_sensitive(True) self.print_startup_time() + except Exception as e: print("Loading error: %s" % e) traceback.print_tb(e.__traceback__) @@ -1397,11 +1408,11 @@ def on_process_exited(proc, result): # Add a callback when we exit mintsources p.wait_async(None, on_process_exited) - def should_show_pkginfo(self, pkginfo): + def should_show_pkginfo(self, pkginfo, allow_unverified_flatpaks): if pkginfo.pkg_hash.startswith("apt"): return True - if not self.settings.get_boolean(prefs.ALLOW_UNVERIFIED_FLATPAKS): + if not allow_unverified_flatpaks: return pkginfo.verified return pkginfo.refid.startswith("app/") @@ -1753,6 +1764,10 @@ def on_search_changed(self, searchentry): self.show_category(self.current_category) elif terms != "" and len(terms) >= 3: self.show_search_results(terms) + elif terms == "": + page = self.page_stack.get_visible_child_name() + if page == self.PAGE_LIST or page == self.PAGE_SEARCHING: + self.go_back_action() self.search_changed_timer = 0 return False @@ -2185,6 +2200,11 @@ def show_active_tasks(self): def on_back_button_clicked(self, button): self.go_back_action() + def cancel_running_search(self): + if self.search_idle_timer > 0: + GLib.source_remove(self.search_idle_timer) + self.search_idle_timer = 0 + def go_back_action(self): XApp.set_window_progress(self.main_window, 0) self.stop_progress_pulse() @@ -2202,6 +2222,8 @@ def go_back_action(self): self.installer.cancel_task(self.current_task) self.current_task = None + self.cancel_running_search() + if self.page_stack.get_visible_child_name() == self.PAGE_PREFS: self.search_tool_item.set_sensitive(True) @@ -2304,6 +2326,42 @@ def get_application_icon(self, pkginfo, size): return imaging.get_icon(icon_string, size) + def update_package_search_cache(self, pkginfo, search_in_description): + if not hasattr(pkginfo, "search_cache"): + pkginfo.search_cache = PkgInfoSearchCache( + name=pkginfo.name.upper(), + display_name=pkginfo.get_display_name().upper(), + keywords=pkginfo.get_keywords().upper(), + summary=pkginfo.get_summary().upper(), + description=None + if not search_in_description + else self.installer.get_description(pkginfo, for_search=True).upper() + ) + + # installer.get_description() is very slow, so we only fetch it if it's required + if search_in_description and pkginfo.search_cache.description is None: + description = self.installer.get_description(pkginfo, for_search=True).upper() + pkginfo.search_cache = pkginfo.search_cache._replace(description=description) + + def pregenerate_search_cache(self): + if self.generate_search_cache_idle_timer > 0: + GLib.source_remove(self.generate_search_cache_idle_timer) + self.generate_search_cache_idle_timer = 0 + + search_in_description = self.settings.get_boolean(prefs.SEARCH_IN_DESCRIPTION) + pkginfos = self.installer.cache.values() + + def generate_package_cache(pkginfos_iter): + try: + pkginfo = next(pkginfos_iter) + self.update_package_search_cache(pkginfo, search_in_description) + return True + except StopIteration: + self.generate_search_cache_idle_timer = 0 + return False + + self.generate_search_cache_idle_timer = GLib.idle_add(generate_package_cache, iter(pkginfos)) + @print_timing def show_search_results(self, terms): if not self.gui_ready: @@ -2339,53 +2397,65 @@ def show_search_results(self, terms): searched_packages = [] - if self.search_idle_timer > 0: - GLib.source_remove(self.search_idle_timer) - self.search_idle_timer = 0 + self.cancel_running_search() search_in_summary = self.settings.get_boolean(prefs.SEARCH_IN_SUMMARY) search_in_description = self.settings.get_boolean(prefs.SEARCH_IN_DESCRIPTION) package_type_preference = self.settings.get_string(prefs.PACKAGE_TYPE_PREFERENCE) hidden_packages = set() + allow_unverified_flatpaks = self.settings.get_boolean(prefs.ALLOW_UNVERIFIED_FLATPAKS) def idle_search_one_package(pkginfos): try: - pkginfo = pkginfos.pop(0) - except IndexError: + pkginfo = next(pkginfos) + except StopIteration: self.search_idle_timer = 0 + + if package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_APT: + results = [p for p in searched_packages if not (p.pkg_hash.startswith("f") and p.name in hidden_packages)] + elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK: + results = [p for p in searched_packages if not (p.pkg_hash.startswith("a") and p.name in hidden_packages)] + else: + results = searched_packages + + GLib.idle_add(self.on_search_results_complete, results) return False flatpak = pkginfo.pkg_hash.startswith("f") is_match = False while True: - if not self.should_show_pkginfo(pkginfo): + if not self.should_show_pkginfo(pkginfo, allow_unverified_flatpaks): break - if all(piece in pkginfo.name.upper() for piece in termsSplit): + self.update_package_search_cache(pkginfo, search_in_description) + + if all(piece in pkginfo.search_cache.name for piece in termsSplit): is_match = True pkginfo.search_tier = 0 break + # pkginfo.name for flatpaks is their id (org.foo.BarMaker), which # may not actually contain the app's name. In this case their display # names are better. The 'name' is still checked first above, because # it's static - get_display_name() may involve a lookup with appstream. - if flatpak and all(piece in pkginfo.get_display_name().upper() for piece in termsSplit): + if flatpak and all(piece in pkginfo.search_cache.display_name for piece in termsSplit): is_match = True pkginfo.search_tier = 0 break - if termsUpper in pkginfo.get_keywords().upper(): + if termsUpper in pkginfo.search_cache.keywords: is_match = True pkginfo.search_tier = 50 break - if (search_in_summary and termsUpper in pkginfo.get_summary().upper()): + if (search_in_summary and termsUpper in pkginfo.search_cache.summary): is_match = True pkginfo.search_tier = 100 break - if(search_in_description and termsUpper in self.installer.get_description(pkginfo).upper()): + + if (search_in_description and termsUpper in pkginfo.search_cache.description): is_match = True pkginfo.search_tier = 200 break @@ -2398,23 +2468,9 @@ def idle_search_one_package(pkginfos): elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK and flatpak: hidden_packages.add(DEB_EQUIVS.get(pkginfo.name)) - # Repeat until empty - if len(pkginfos) > 0: - return True - - self.search_idle_timer = 0 - - if package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_APT: - results = [p for p in searched_packages if not (p.pkg_hash.startswith("f") and p.name in hidden_packages)] - elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK: - results = [p for p in searched_packages if not (p.pkg_hash.startswith("a") and p.name in hidden_packages)] - else: - results = searched_packages - - GLib.idle_add(self.on_search_results_complete, results) - return False + return True - self.search_idle_timer = GLib.idle_add(idle_search_one_package, list(listing)) + self.search_idle_timer = GLib.idle_add(idle_search_one_package, iter(listing)) def on_search_results_complete(self, results): self.page_stack.set_visible_child_name(self.PAGE_LIST) @@ -2485,6 +2541,7 @@ def sort_packages(self, pkgs, key_func): def show_packages(self, pkginfos, from_search=False): self.stop_slideshow_timer() + allow_unverified_flatpaks = self.settings.get_boolean(prefs.ALLOW_UNVERIFIED_FLATPAKS) if self.one_package_idle_timer > 0: GLib.source_remove(self.one_package_idle_timer) @@ -2521,7 +2578,7 @@ def show_packages(self, pkginfos, from_search=False): apps = [info for info in pkginfos] # should_show_pkginfo was applied during search matching apps = self.sort_packages(apps, attrgetter("unverified", "search_tier", "score_desc", "name")) else: - apps = [info for info in pkginfos if self.should_show_pkginfo(info)] + apps = [info for info in pkginfos if self.should_show_pkginfo(info, allow_unverified_flatpaks)] apps = self.sort_packages(apps, attrgetter("unverified", "score_desc", "name")) apps = apps[0:201] @@ -2851,10 +2908,11 @@ def on_package_type_button_clicked(self, button, pkginfo): self.show_package(pkginfo, self.previous_page) def get_flatpak_for_deb(self, pkginfo): + allow_unverified_flatpaks = self.settings.get_boolean(prefs.ALLOW_UNVERIFIED_FLATPAKS) try: fp_name = FLATPAK_EQUIVS[pkginfo.name] flatpak_pkginfo = self.installer.find_pkginfo(fp_name, installer.PKG_TYPE_FLATPAK) - if self.should_show_pkginfo(flatpak_pkginfo): + if self.should_show_pkginfo(flatpak_pkginfo, allow_unverified_flatpaks): return flatpak_pkginfo except: return None From de6078bbc9451833a50a0237ba9c629eeccf1f97 Mon Sep 17 00:00:00 2001 From: Alexander K Date: Sat, 31 May 2025 15:33:42 +0200 Subject: [PATCH 2/3] update progress screens --- resources/mintinstall.glade | 30 +++++++------ usr/lib/linuxmint/mintinstall/mintinstall.py | 45 +++++++++++++++++--- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/resources/mintinstall.glade b/resources/mintinstall.glade index 66c89b7..136757b 100644 --- a/resources/mintinstall.glade +++ b/resources/mintinstall.glade @@ -174,11 +174,14 @@ vertical 10 - + True False - image-loading-symbolic - 6 + Loading, please wait + + + + False @@ -187,14 +190,13 @@ - + True + True False - Loading, please wait - - - - + 50 + 50 + center False @@ -1957,11 +1959,13 @@ - - 50 - True + + 200 False - True + 12 + True + center + 10 False diff --git a/usr/lib/linuxmint/mintinstall/mintinstall.py b/usr/lib/linuxmint/mintinstall/mintinstall.py index 4e49ec4..398b078 100755 --- a/usr/lib/linuxmint/mintinstall/mintinstall.py +++ b/usr/lib/linuxmint/mintinstall/mintinstall.py @@ -244,9 +244,9 @@ def installer_progress(self, pkginfo, progress, estimating, status_text=None): self.spinner.show() class SaneProgressBar(Gtk.DrawingArea): - def __init__(self): - super(Gtk.DrawingArea, self).__init__(width_request=-1, - height_request=8, + def __init__(self, width=-1, height=8): + super(Gtk.DrawingArea, self).__init__(width_request=width, + height_request=height, margin_top=1, #??? to align better with the stars and count hexpand=True, valign=Gtk.Align.CENTER, @@ -692,7 +692,7 @@ def do_activate(self): def _init_installer_thread(self): if self.installer.init_sync(): - GLib.idle_add(self.on_installer_ready) + self.on_installer_ready() else: self.page_stack.set_visible_child_name(self.PAGE_GENERATING_CACHE) self.installer.init(self.on_installer_ready) @@ -860,6 +860,12 @@ def create_window(self, starting_page): self.progress_box.pack_start(self.progress_label, False, False, 0) self.progress_label.show() + # self.progress_bar = self.builder.get_object('search_progress_bar') + box_searching = self.builder.get_object('search_progress_bar') + self.progress_bar = SaneProgressBar(-1, 12) + box_searching.pack_start(self.progress_bar, True, True, 0) + self.progress_bar.show() + box_reviews = self.builder.get_object("box_reviews") def list_header_func(row, before, user_data=None): @@ -1060,7 +1066,10 @@ def on_appstream_changed(self, installer): def on_installer_ready(self): - self.page_stack.set_visible_child_name(self.PAGE_LOADING) + def set_loading_page(): + self.page_stack.set_visible_child_name(self.PAGE_LOADING) + GLib.idle_add(set_loading_page) + try: self.process_matching_packages() @@ -2406,11 +2415,15 @@ def show_search_results(self, terms): hidden_packages = set() allow_unverified_flatpaks = self.settings.get_boolean(prefs.ALLOW_UNVERIFIED_FLATPAKS) - def idle_search_one_package(pkginfos): + list_size = len(listing) + self.search_progress = 0 + + def idle_search_one_package(pkginfos, list_size): try: pkginfo = next(pkginfos) except StopIteration: self.search_idle_timer = 0 + self.search_progress = 0 if package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_APT: results = [p for p in searched_packages if not (p.pkg_hash.startswith("f") and p.name in hidden_packages)] @@ -2421,6 +2434,12 @@ def idle_search_one_package(pkginfos): GLib.idle_add(self.on_search_results_complete, results) return False + except RuntimeError: # dictionary changed size during iteration + self.search_idle_timer = 0 + self.search_progress = 0 + + self.go_back_action() + return False flatpak = pkginfo.pkg_hash.startswith("f") is_match = False @@ -2468,9 +2487,21 @@ def idle_search_one_package(pkginfos): elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK and flatpak: hidden_packages.add(DEB_EQUIVS.get(pkginfo.name)) + self.search_progress = self.search_progress + 1 + self.update_progress(self.search_progress / list_size) + return True - self.search_idle_timer = GLib.idle_add(idle_search_one_package, iter(listing)) + self.search_idle_timer = GLib.idle_add(idle_search_one_package, iter(listing), list_size) + + def update_progress(self, progress): + progress = max(0.0, min(1.0, progress)) + + def update_progress_ui(): + self.progress_bar.set_fraction(progress) + return False + + GLib.idle_add(update_progress_ui) def on_search_results_complete(self, results): self.page_stack.set_visible_child_name(self.PAGE_LIST) From c33b13d58781afbadd73fd7d3249032941c792a3 Mon Sep 17 00:00:00 2001 From: Alexander K Date: Sun, 1 Jun 2025 13:20:25 +0200 Subject: [PATCH 3/3] improve package search speed using CooperativeIterator --- usr/lib/linuxmint/mintinstall/mintinstall.py | 146 ++++++++++++++----- 1 file changed, 108 insertions(+), 38 deletions(-) diff --git a/usr/lib/linuxmint/mintinstall/mintinstall.py b/usr/lib/linuxmint/mintinstall/mintinstall.py index 398b078..de61b80 100755 --- a/usr/lib/linuxmint/mintinstall/mintinstall.py +++ b/usr/lib/linuxmint/mintinstall/mintinstall.py @@ -614,6 +614,69 @@ def __init__(self, category): PkgInfoSearchCache = namedtuple('PkgInfoSearchCache', ['name', 'display_name', 'keywords', 'summary', 'description']) + +class CooperativeIterator: + """ + Iterates over items cooperatively within the GLib main loop, yielding periodically to keep the UI responsive. + """ + def __init__(self, iterable, on_per_item, *, on_progress=None, on_finish=None, on_error=None, max_duration_ms=16, **kwargs): + self._size = len(iterable) if hasattr(iterable, '__len__') else None + self._iterator = iter(iterable) + self._on_per_item = on_per_item + self._on_progress = on_progress + self._on_finish = on_finish + self._on_error = on_error + self._kwargs = kwargs + self._max_duration = max_duration_ms / 1000.0 + self._cancelled = False + + def run(self): + self._start_time = time.monotonic() + self._current_index = 0 + GLib.idle_add(self._process) + + def _process(self): + if self._cancelled: + return False + + start_time = time.monotonic() + + try: + while not self._cancelled: + item = next(self._iterator) + + self._on_per_item(item, **self._kwargs) + + self._current_index += 1 + if self._on_progress and self._size is not None: + self._on_progress(self._current_index / self._size) + + if (time.monotonic() - start_time) >= self._max_duration: + return True + + except StopIteration: + if os.getenv("DEBUG", False): + elapsed_time = time.monotonic() - self._start_time + print(f"CooperativeIterator: Finished processing {self._on_per_item.__name__} in {elapsed_time:.3f} seconds.") + + if self._on_finish: + try: + self._on_finish(**self._kwargs) + except Exception as e: + print(f"CooperativeIterator: Error in on_finish: {e}") + return False + + except RuntimeError: + if self._on_error: + try: + self._on_error() + except Exception as e: + print(f"CooperativeIterator: Error in on_error: {e}") + return False + + def cancel(self): + self._cancelled = True + class Application(Gtk.Application): (ACTION_TAB, PROGRESS_TAB, SPINNER_TAB) = list(range(3)) @@ -665,7 +728,7 @@ def __init__(self): self.one_package_idle_timer = 0 self.installer_pulse_timer = 0 self.search_changed_timer = 0 - self.search_idle_timer = 0 + self.search_iterator = None self.generate_search_cache_idle_timer = 0 self.action_button_signal_id = 0 @@ -2210,9 +2273,9 @@ def on_back_button_clicked(self, button): self.go_back_action() def cancel_running_search(self): - if self.search_idle_timer > 0: - GLib.source_remove(self.search_idle_timer) - self.search_idle_timer = 0 + if self.search_iterator: + self.search_iterator.cancel() + self.search_iterator = None def go_back_action(self): XApp.set_window_progress(self.main_window, 0) @@ -2412,35 +2475,34 @@ def show_search_results(self, terms): search_in_description = self.settings.get_boolean(prefs.SEARCH_IN_DESCRIPTION) package_type_preference = self.settings.get_string(prefs.PACKAGE_TYPE_PREFERENCE) - hidden_packages = set() allow_unverified_flatpaks = self.settings.get_boolean(prefs.ALLOW_UNVERIFIED_FLATPAKS) - list_size = len(listing) - self.search_progress = 0 - - def idle_search_one_package(pkginfos, list_size): - try: - pkginfo = next(pkginfos) - except StopIteration: - self.search_idle_timer = 0 - self.search_progress = 0 + def on_finish(list_size, searched_packages, hidden_packages, package_type_preference, **kwargs): + self.search_iterator = None - if package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_APT: - results = [p for p in searched_packages if not (p.pkg_hash.startswith("f") and p.name in hidden_packages)] - elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK: - results = [p for p in searched_packages if not (p.pkg_hash.startswith("a") and p.name in hidden_packages)] - else: - results = searched_packages + if package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_APT: + results = [p for p in searched_packages if not (p.pkg_hash.startswith("f") and p.name in hidden_packages)] + elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK: + results = [p for p in searched_packages if not (p.pkg_hash.startswith("a") and p.name in hidden_packages)] + else: + results = searched_packages + self.on_search_results_complete(results) - GLib.idle_add(self.on_search_results_complete, results) - return False - except RuntimeError: # dictionary changed size during iteration - self.search_idle_timer = 0 - self.search_progress = 0 + def on_error(): + self.search_iterator = None - self.go_back_action() - return False + self.go_back_action() + def search_one_package( + pkginfo, + list_size, + searched_packages, + hidden_packages, + allow_unverified_flatpaks, + package_type_preference, + search_in_summary, + search_in_description + ): flatpak = pkginfo.pkg_hash.startswith("f") is_match = False @@ -2487,21 +2549,29 @@ def idle_search_one_package(pkginfos, list_size): elif package_type_preference == prefs.PACKAGE_TYPE_PREFERENCE_FLATPAK and flatpak: hidden_packages.add(DEB_EQUIVS.get(pkginfo.name)) - self.search_progress = self.search_progress + 1 - self.update_progress(self.search_progress / list_size) - - return True + def on_progress(progress): + self.update_progress(progress) + + self.search_iterator = CooperativeIterator( + listing, + search_one_package, + on_finish=on_finish, + on_error=on_error, + on_progress=on_progress, + list_size=len(listing), + searched_packages=[], + hidden_packages=set(), + allow_unverified_flatpaks=allow_unverified_flatpaks, + package_type_preference=package_type_preference, + search_in_summary=search_in_summary, + search_in_description=search_in_description + ) + self.search_iterator.run() - self.search_idle_timer = GLib.idle_add(idle_search_one_package, iter(listing), list_size) def update_progress(self, progress): progress = max(0.0, min(1.0, progress)) - - def update_progress_ui(): - self.progress_bar.set_fraction(progress) - return False - - GLib.idle_add(update_progress_ui) + self.progress_bar.set_fraction(progress) def on_search_results_complete(self, results): self.page_stack.set_visible_child_name(self.PAGE_LIST)