Skip to content

Commit 024e48f

Browse files
committed
Bugfix: Cache volumes to prevent timeout at start
During init the frontend calls `list_locations()` 5× within milliseconds(1× `listVolumes` + 4× `findContainingVolume`). Each call does expensive NSFileManager enumeration + icon fetching via `spawn_blocking`, easily exceeding the 2-second backend timeout under concurrent load. - Add a 5-second TTL cache around `list_locations()` in `volumes/mod.rs` - Invalidate the cache on volume mount/unmount in `watcher.rs`
1 parent 90a4b5a commit 024e48f

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

apps/desktop/src-tauri/src/volumes/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub mod watcher;
1212
use serde::{Deserialize, Serialize};
1313
use std::collections::HashSet;
1414
use std::path::Path;
15+
use std::sync::Mutex;
16+
use std::time::Instant;
1517

1618
/// Category of a location item.
1719
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -88,8 +90,30 @@ fn get_fs_type(path: &str) -> Option<String> {
8890
String::from_utf8(name_bytes).ok()
8991
}
9092

93+
/// Short-lived cache for `list_locations()`. During app init the frontend calls
94+
/// `list_volumes` + `find_containing_volume` × N tabs within milliseconds — each
95+
/// one calling `list_locations()` which does expensive NSFileManager + icon work.
96+
/// Caching the result for a few seconds collapses all of those into a single real call.
97+
static LOCATIONS_CACHE: Mutex<Option<(Instant, Vec<LocationInfo>)>> = Mutex::new(None);
98+
const LOCATIONS_CACHE_TTL_SECS: u64 = 5;
99+
100+
/// Invalidate the locations cache. Called by the volume watcher on mount/unmount.
101+
pub fn invalidate_locations_cache() {
102+
*LOCATIONS_CACHE.lock().unwrap() = None;
103+
}
104+
91105
/// Get all locations organized by category, deduplicated.
92106
pub fn list_locations() -> Vec<LocationInfo> {
107+
if let Some((ts, cached)) = LOCATIONS_CACHE.lock().unwrap().as_ref()
108+
&& ts.elapsed().as_secs() < LOCATIONS_CACHE_TTL_SECS {
109+
return cached.clone();
110+
}
111+
let result = list_locations_uncached();
112+
*LOCATIONS_CACHE.lock().unwrap() = Some((Instant::now(), result.clone()));
113+
result
114+
}
115+
116+
fn list_locations_uncached() -> Vec<LocationInfo> {
93117
let mut locations = Vec::new();
94118
let mut seen_paths: HashSet<String> = HashSet::new();
95119

apps/desktop/src-tauri/src/volumes/watcher.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,19 @@ fn check_for_volume_changes() {
114114
Err(_) => return,
115115
};
116116

117-
// Find newly mounted volumes
118-
for path in current_volumes.difference(&known_guard) {
117+
let mounted: Vec<_> = current_volumes.difference(&known_guard).cloned().collect();
118+
let unmounted: Vec<_> = known_guard.difference(&current_volumes).cloned().collect();
119+
120+
if !mounted.is_empty() || !unmounted.is_empty() {
121+
super::invalidate_locations_cache();
122+
}
123+
124+
for path in &mounted {
119125
debug!("Volume mounted: {}", path);
120126
emit_volume_mounted(path);
121127
}
122128

123-
// Find unmounted volumes
124-
for path in known_guard.difference(&current_volumes) {
129+
for path in &unmounted {
125130
debug!("Volume unmounted: {}", path);
126131
emit_volume_unmounted(path);
127132
}

0 commit comments

Comments
 (0)