From 3abe025a80e9b53d23d1116b79bcd4087a595815 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 26 May 2026 11:38:42 -0700 Subject: [PATCH] Better performance when bulk populating directory lists. --- borgmatic/actions/browse/panels.py | 107 ++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 26 deletions(-) diff --git a/borgmatic/actions/browse/panels.py b/borgmatic/actions/browse/panels.py index da3a2535..3e97939e 100644 --- a/borgmatic/actions/browse/panels.py +++ b/borgmatic/actions/browse/panels.py @@ -96,6 +96,38 @@ class Archives_list(textual.widgets.OptionList): self.highlighted_option_changed = True +def get_relative_archive_path_components(archive_path, current_directory_path_components): + ''' + Given an Archive_path instance and a tuple of path components for the currently browsed + directory, get the path components for the archive path relative to that directory. + + If the archive path is not actually relative to that directory, return None. + ''' + archive_path_components = archive_path.file_path.split(os.path.sep) + + if not current_directory_path_components: + return archive_path_components + + # If the loaded path doesn't match this directory list's own path, then we don't care about + # it for purposes of displaying this particular directory. + if tuple(archive_path_components[: len(current_directory_path_components)]) != current_directory_path_components: + return None + + # Strip off the portion of the archive path that matches the directory list's own path. + return archive_path_components[len(current_directory_path_components):] + + +def make_directory_list_option(archive_path, archive_path_components): + pieces = ( + borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get( + archive_path.path_type if len(archive_path_components) == 1 else 'd', '❓' + ), + archive_path_components[0], + ) + (('→', archive_path.link_target) if archive_path.link_target else ()) + + return textual.widgets.option_list.Option(' '.join(pieces), id=archive_path_components[0]) + + def add_archive_path( directory_list, config, @@ -103,37 +135,56 @@ def add_archive_path( archive_name, archive_path, ): - archive_path_components = archive_path.file_path.split(os.path.sep) + archive_path_components = get_relative_archive_path_components(archive_path, directory_list.path_components) - if directory_list.path_components: - # If the loaded path doesn't match this directory list's own path, then we don't care about - # it for purposes of displaying this particular directory. - if tuple(archive_path_components[: len(directory_list.path_components)]) != directory_list.path_components: - return - - # Strip off the portion of the archive path that matches the directory list's own path. - archive_path_components = archive_path_components[len(directory_list.path_components):] - - base_path = archive_path_components[0] + if archive_path_components is None: + return # If the option is already in the list, bail. with contextlib.suppress(textual.widgets.option_list.OptionDoesNotExist): - directory_list.get_option(option_id=base_path) + directory_list.get_option(option_id=archive_path_components[0]) return - pieces = ( - borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get( - archive_path.path_type if len(archive_path_components) == 1 else 'd', '❓' - ), - base_path, - ) + (('→', archive_path.link_target) if archive_path.link_target else ()) highlighted_option = directory_list.highlighted_option sorted_options = sorted( - [ + ( *directory_list.options, - textual.widgets.option_list.Option(' '.join(pieces), id=base_path), - ], + make_directory_list_option(archive_path, archive_path_components), + ), + key=lambda option: ((option.id == 'loading-indicator'), option.prompt), + ) + + directory_list.set_options(sorted_options) + directory_list.highlighted = ( + directory_list.get_option_index(highlighted_option.id) + if highlighted_option and directory_list.highlighted_option_changed + else 0 + ) + + +def bulk_add_archive_paths( + directory_list, + config, + repository, + archive_name, + archive_paths, +): + highlighted_option = directory_list.highlighted_option + + sorted_options = sorted( + ( + *directory_list.options, + *( + make_directory_list_option(archive_path, archive_path_components) + for archive_path in archive_paths + for archive_path_components in (get_relative_archive_path_components( + archive_path, + directory_list.path_components, + ),) + if archive_path_components is not None + ), + ), key=lambda option: ((option.id == 'loading-indicator'), option.prompt), ) @@ -184,11 +235,15 @@ class Directory_list(textual.widgets.OptionList): self.timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self) if self.path_components: - # FIXME: For performance reasons, maybe add these in bulk instead of in a loop one at a time. - for archive_path in borgmatic.actions.browse.workers.get_paths( - self.path_loaded.path_hierarchy, self.path_components - ): - self.on_archive_path_loaded(archive_path) + bulk_add_archive_paths( + directory_list=self, + config=self.config, + repository=self.repository, + archive_name=self.archive_name, + archive_paths=borgmatic.actions.browse.workers.get_paths( + self.path_loaded.path_hierarchy, self.path_components + ), + ) else: borgmatic.actions.browse.workers.load_archive_files( self.app,