Improve initial directory listing latency

Avoid synchronous child counts during item construction, use extension-based MIME detection for initial scans, and defer expensive MIME icon resolution by using generic file icons for ordinary files. Document the local xdg-desktop-portal FileChooser workaround for COSMIC portal crashes.
This commit is contained in:
Votre Nom 2026-05-05 08:00:08 +02:00
parent f0538190d9
commit 338354c4d0
2 changed files with 118 additions and 32 deletions

View file

@ -0,0 +1,78 @@
# Local performance and portal notes
Date: 2026-05-05
This repository carries a local patch aimed at improving initial directory
display latency in large folders, plus a system-level workaround for a COSMIC
portal FileChooser crash observed with Firefox and Chromium.
## Directory listing latency
The slow path was the initial construction of `Item` values in `src/tab.rs`.
On a test folder with about 2000 entries, raw filesystem enumeration and stat
calls completed in a few milliseconds, while COSMIC Files took multiple seconds
before showing the directory.
The local patch keeps initial item construction cheap:
- directory child counts are no longer computed synchronously in
`item_from_entry` and `item_from_gvfs_info`;
- initial MIME detection uses extension-based `mime_guess`;
- regular files use a generic file icon during the initial scan instead of
resolving the full MIME icon immediately.
This keeps folders and `.desktop` files special-cased, while avoiding expensive
per-file work for ordinary files during first paint.
Measured locally during investigation:
- before the MIME/icon changes: about 3.1 seconds for `~/Téléchargements`;
- after avoiding full MIME icon resolution during scan: below the temporary
100 ms perf-log threshold for the same folder.
## File chooser portal workaround
Firefox and Chromium were failing to open `Save As` on the first attempt because
`xdg-desktop-portal-cosmic` crashed while handling `FileChooser`.
Logs showed:
```text
Backend call failed: Remote peer disconnected
xdg-desktop-portal-cosmic ... status=11/SEGV
```
The working local system workaround is to remove
`org.freedesktop.impl.portal.FileChooser` from:
```text
/usr/share/xdg-desktop-portal/portals/cosmic.portal
```
so the file chooser falls back to GTK. The resulting local `cosmic.portal`
keeps COSMIC for the other interfaces:
```ini
[portal]
DBusName=org.freedesktop.impl.portal.desktop.cosmic
Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.ScreenCast
UseIn=COSMIC
```
Backups created locally:
```text
/usr/share/xdg-desktop-portal/portals/cosmic.portal.bak-codex-20260505-filechooser
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505
/usr/share/xdg-desktop-portal/cosmic-portals.conf.bak-codex-20260505-2
```
After editing portal files, restart:
```sh
systemctl --user restart xdg-desktop-portal.service xdg-desktop-portal-gtk.service
```
Package updates may overwrite `/usr/share/xdg-desktop-portal/portals/cosmic.portal`.
If `Save As` starts needing two attempts again, re-check that `FileChooser` has
not been reintroduced in `cosmic.portal`.

View file

@ -302,6 +302,26 @@ pub fn folder_icon_symbolic(path: &PathBuf, icon_size: u16) -> widget::icon::Han
.handle()
}
fn generic_file_icons(
sizes: IconSizes,
) -> (
widget::icon::Handle,
widget::icon::Handle,
widget::icon::Handle,
) {
(
widget::icon::from_name("text-x-generic")
.size(sizes.grid())
.handle(),
widget::icon::from_name("text-x-generic")
.size(sizes.list())
.handle(),
widget::icon::from_name("text-x-generic")
.size(sizes.list_condensed())
.handle(),
)
}
//TODO: replace with Path::has_trailing_sep when stable
fn has_trailing_sep(path: &Path) -> bool {
path.as_os_str()
@ -665,9 +685,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
folder_icon(&path, sizes.list_condensed()),
)
} else {
// ALWAYS assume we're remote for mime guessing here, since gvfs reading can be expensive
// @todo - expose this as a config option?
let mime = mime_for_path(&path, None, true);
// Keep the initial directory scan cheap. Opening files still
// recalculates MIME from the real path before launching apps.
let mime = mime_guess::from_path(&path).first_or_octet_stream();
//TODO: clean this up, implement for trash
let icon_name_opt = if mime == "application/x-desktop" {
@ -684,28 +704,21 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
desktop_icon_handle(&icon_name, sizes.list_condensed()),
)
} else {
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
generic_file_icons(sizes);
(
mime.clone(),
mime_icon(mime.clone(), sizes.grid()),
mime_icon(mime.clone(), sizes.list()),
mime_icon(mime, sizes.list_condensed()),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
)
}
};
let mut children_opt = None;
let children_opt = None;
let mut dir_size = DirSize::NotDirectory;
if is_dir && !remote {
dir_size = DirSize::Calculating(Controller::default());
//TODO: calculate children in the background (and make it cancellable?)
match fs::read_dir(&path) {
Ok(entries) => {
children_opt = Some(entries.count());
}
Err(err) => {
log::warn!("failed to read directory {}: {}", path.display(), err);
}
}
}
let display_name = display_name_for_file(&path, &file_info.display_name(), false, is_desktop);
@ -807,7 +820,9 @@ pub fn item_from_entry(
folder_icon(&path, sizes.list_condensed()),
)
} else {
let mime = mime_for_path(&path, Some(&metadata), remote);
// Keep the initial directory scan cheap. Opening files still
// recalculates MIME from the real path before launching apps.
let mime = mime_guess::from_path(&path).first_or_octet_stream();
//TODO: clean this up, implement for trash
let icon_name_opt = if mime == "application/x-desktop" {
is_desktop = true;
@ -823,28 +838,21 @@ pub fn item_from_entry(
desktop_icon_handle(&icon_name, sizes.list_condensed()),
)
} else {
let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
generic_file_icons(sizes);
(
mime.clone(),
mime_icon(mime.clone(), sizes.grid()),
mime_icon(mime.clone(), sizes.list()),
mime_icon(mime, sizes.list_condensed()),
mime,
icon_handle_grid,
icon_handle_list,
icon_handle_list_condensed,
)
}
};
let mut children_opt = None;
let children_opt = None;
let mut dir_size = DirSize::NotDirectory;
if metadata.is_dir() && !remote {
dir_size = DirSize::Calculating(Controller::default());
//TODO: calculate children in the background (and make it cancellable?)
match fs::read_dir(&path) {
Ok(entries) => {
children_opt = Some(entries.count());
}
Err(err) => {
log::warn!("failed to read directory {}: {}", path.display(), err);
}
}
}
let display_name = display_name_for_file(&path, &name, is_gvfs, is_desktop);