Performance: Switch browse action to list archive files only once per archive.

This commit is contained in:
Dan Helfman
2026-05-25 16:27:36 -07:00
parent 4b8a6d1a34
commit ea0ed8345a
6 changed files with 191 additions and 68 deletions
+4 -12
View File
@@ -45,12 +45,9 @@ def get_repository_archives(config, repository):
)
def get_archive_files(config, repository, archive_name, list_path=None):
def get_archive_files(config, repository, archive_name):
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
if list_path:
logger.info(f'Listing archive {archive_name} at path {list_path}')
else:
logger.info(f'Listing archive {archive_name}')
logger.info(f'Listing archive {archive_name}')
global_arguments = argparse.Namespace()
local_path = config.get('local_path', 'borg')
@@ -60,10 +57,8 @@ def get_archive_files(config, repository, archive_name, list_path=None):
return (
(
path_data['type']
if os.path.join(list_path, base_path) == path_data['path']
else 'd',
base_path,
path_data['type'],
path_data['path'],
path_data.get('linktarget'),
)
for path_data in borgmatic.borg.list.capture_archive_listing(
@@ -72,12 +67,9 @@ def get_archive_files(config, repository, archive_name, list_path=None):
config,
local_borg_version,
global_arguments,
list_paths=(rf're:^{list_path}/[^/]+',) if list_path else (r're:^[^/]+',),
local_path=local_path,
remote_path=remote_path,
)
for base_path in (os.path.relpath(path_data['path'], list_path).split(os.path.sep)[0],)
if base_path not in seen and not seen.add(base_path)
)
+5 -2
View File
@@ -43,6 +43,7 @@ def make_next_panel(focused_panel, option_id):
focused_panel.config,
focused_panel.repository,
focused_panel.archive_name,
path_loaded=focused_panel.path_loaded,
path_components=(*focused_panel.path_components, option_id),
)
elif option.prompt.startswith(
@@ -118,7 +119,7 @@ class Carousel(textual.containers.Horizontal):
self.focused_panel.styles.display = 'none'
self.focused_panel = next_panel
self.focused_panel.focus()
self.refresh(recompose=True)
self.mount(self.focused_panel)
def on_option_list_option_highlighted(self, event):
'''
@@ -133,7 +134,9 @@ class Carousel(textual.containers.Horizontal):
An option has been selected, so advance to the next panel—unless the option selected is
"..", in which case go to the previous panel.
'''
if event.option_list != self.focused_panel or event.option_id == 'loading-indicator': # pragma: no cover
if (
event.option_list != self.focused_panel or event.option_id == 'loading-indicator'
): # pragma: no cover
return
if event.option_id == '..':
+4
View File
@@ -7,6 +7,10 @@ import textual.widgets.option_list
LOADING_DOT_INTERVAL_SECONDS = 0.3
import logging
logger = logging.getLogger('__name__')
def update_inline_loading_indicator(widget):
if isinstance(widget, textual.widgets.OptionList):
with contextlib.suppress(textual.widgets.option_list.OptionDoesNotExist):
+98 -7
View File
@@ -1,3 +1,5 @@
import contextlib
import logging
import os
import textual.binding
@@ -7,6 +9,10 @@ import borgmatic.actions.browse.loading
import borgmatic.actions.browse.paths
import borgmatic.actions.browse.workers
logger = logging.getLogger('__name__')
OPTION_LIST_BINDINGS = (
*textual.widgets.OptionList.BINDINGS,
textual.binding.Binding(
@@ -90,10 +96,59 @@ class Archives_list(textual.widgets.OptionList):
self.highlighted_option_changed = True
def add_archive_path(
directory_list,
config,
repository,
archive_name,
archive_path,
):
archive_path_components = archive_path.file_path.split(os.path.sep)
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 the option is already in the list, bail.
with contextlib.suppress(textual.widgets.option_list.OptionDoesNotExist):
directory_list.get_option(option_id=base_path)
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),
],
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
)
class Directory_list(textual.widgets.OptionList):
BINDINGS = OPTION_LIST_BINDINGS
def __init__(self, config, repository, archive_name, path_components=None):
def __init__(self, config, repository, archive_name, path_loaded=None, path_components=None):
self.config = config
self.repository = repository
self.archive_name = archive_name
@@ -101,6 +156,7 @@ class Directory_list(textual.widgets.OptionList):
self.highlighted_option_changed = False
super().__init__(classes='panel')
self.border_title = ' '.join(
(
borgmatic.actions.browse.paths.PATH_TYPE_ICONS[
@@ -112,17 +168,52 @@ class Directory_list(textual.widgets.OptionList):
)
)
timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
# FIXME: This isn't working when loading is still underway??? I don't see a ".."
if self.path_components:
self.add_option(
textual.widgets.option_list.Option(
f'{borgmatic.actions.browse.paths.PATH_TYPE_ICONS[borgmatic.actions.browse.paths.Path_type.DIRECTORY.value]} ..',
id='..',
),
)
borgmatic.actions.browse.workers.add_archive_files(
self.app,
self.path_loaded = path_loaded or borgmatic.actions.browse.workers.Archive_path_loaded(
self, 'archive path loaded'
)
if not self.path_loaded.complete:
# FIXME: After going back to a previous panel, the loading indicator is no longer animating.
self.timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
if self.path_components:
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)
else:
borgmatic.actions.browse.workers.load_archive_files(
self.app,
directory_list=self,
config=self.config,
repository=self.repository,
archive_name=self.archive_name,
)
def on_mount(self):
self.path_loaded.subscribe(self, self.on_archive_path_loaded)
def on_archive_path_loaded(self, data):
if data is borgmatic.actions.browse.workers.LOADING_DONE:
self.timer.stop()
self.remove_option('loading-indicator')
return
add_archive_path(
directory_list=self,
config=self.config,
repository=self.repository,
archive_name=self.archive_name,
list_path=os.path.sep.join(self.path_components),
root_directory=not bool(self.path_components),
timer=timer,
archive_path=data,
)
def on_option_list_option_highlighted(self, event):
+63 -42
View File
@@ -1,10 +1,19 @@
import collections
import contextlib
import logging
import os
import rich.syntax
import textual
import textual.signal
import textual.widgets.option_list
import borgmatic.actions.browse.archive
logger = logging.getLogger('__name__')
@textual.work(thread=True)
def add_repository_archives(browse_app, archives_list, config, repository, timer):
archives_data = borgmatic.actions.browse.archive.get_repository_archives(config, repository)
@@ -36,50 +45,62 @@ def add_repository_archives(browse_app, archives_list, config, repository, timer
browse_app.call_from_thread(timer.stop)
Archive_path = collections.namedtuple(
'Archive_path',
('path_type', 'file_path', 'link_target'),
)
def record_path(archive_path, hierarchy, path_components):
if len(path_components) == 1:
hierarchy[path_components[0]] = {} if archive_path.path_type == 'd' else archive_path
return
record_path(archive_path, hierarchy.setdefault(path_components[0], {}), path_components[1:])
def get_paths(hierarchy, path_components, full_path_components=None):
if full_path_components is None:
full_path_components = path_components
if len(path_components) == 1:
return (
archive_path
if isinstance(archive_path, Archive_path)
else Archive_path('d', os.path.join(*full_path_components, component), '')
for component, archive_path in hierarchy[path_components[0]].items()
)
return get_paths(hierarchy[path_components[0]], path_components[1:], full_path_components)
LOADING_DONE = object()
class Archive_path_loaded(textual.signal.Signal):
def __init__(self, owner, name):
self.path_hierarchy = {}
self.complete = False
super().__init__(owner, name)
def publish(self, data):
super().publish(data)
if data is LOADING_DONE:
self.complete = True
else:
record_path(data, self.path_hierarchy, data.file_path.split(os.path.sep))
@textual.work(thread=True)
def add_archive_files(
browse_app, directory_list, config, repository, archive_name, list_path, root_directory, timer
):
file_type_paths = borgmatic.actions.browse.archive.get_archive_files(
config, repository, archive_name, list_path
)
loading_option = directory_list.get_option('loading-indicator')
def load_archive_files(browse_app, directory_list, config, repository, archive_name):
for path_type, file_path, link_target in borgmatic.actions.browse.archive.get_archive_files(
config, repository, archive_name
):
directory_list.path_loaded.publish(Archive_path(path_type, file_path, link_target))
if not root_directory:
browse_app.call_from_thread(directory_list.remove_option, 'loading-indicator')
browse_app.call_from_thread(
directory_list.add_options,
(
textual.widgets.option_list.Option(
f'{borgmatic.actions.browse.paths.PATH_TYPE_ICONS[borgmatic.actions.browse.paths.Path_type.DIRECTORY.value]} ..',
id='..',
),
loading_option,
),
)
for path_type, file_path, link_target in file_type_paths:
pieces = (
borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, ''),
file_path,
) + (('', link_target) if link_target else ())
highlighted_option = directory_list.highlighted_option
sorted_options = sorted(
[
*directory_list.options,
textual.widgets.option_list.Option(' '.join(pieces), id=file_path),
],
key=lambda option: ((option.id == 'loading-indicator'), option.prompt),
)
browse_app.call_from_thread(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
)
browse_app.call_from_thread(timer.stop)
browse_app.call_from_thread(directory_list.remove_option, 'loading-indicator')
directory_list.path_loaded.publish(LOADING_DONE)
@textual.work(thread=True)
@@ -96,7 +96,10 @@ def test_make_next_panel_with_non_root_directory_list_and_selected_directory_opt
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive', path_components=('etc',),
config,
config['repositories'][0],
'archive',
path_components=('etc',),
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📁 borgmatic'))
@@ -123,7 +126,9 @@ def test_make_next_panel_with_root_directory_list_and_selected_file_option_retur
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive'
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📄 config.yaml'))
flexmock(focused_panel).should_receive('get_option').and_return(
flexmock(prompt='📄 config.yaml')
)
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml')
@@ -146,9 +151,14 @@ def test_make_next_panel_with_non_root_directory_list_and_selected_file_option_r
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive', path_components=('etc', 'borgmatic'),
config,
config['repositories'][0],
'archive',
path_components=('etc', 'borgmatic'),
)
flexmock(focused_panel).should_receive('get_option').and_return(
flexmock(prompt='📄 config.yaml')
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📄 config.yaml'))
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml')
@@ -174,7 +184,9 @@ def test_make_next_panel_with_directory_list_and_unsupported_selected_option_ret
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive'
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt=f'{icon} config.yaml'))
flexmock(focused_panel).should_receive('get_option').and_return(
flexmock(prompt=f'{icon} config.yaml')
)
assert module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml') is None