perf: separate library DB connection for download post-processing

Add a second LibraryDatabase connection to OfflineCacheState dedicated
to download post-processing writes. SQLite WAL mode natively handles
concurrent reads (UI via main connection) and writes (downloads via
this separate connection), eliminating app-level mutex contention that
caused progressive UI lag during bulk downloads.

Init after library.init_at() in both activate_session and
activate_offline_session. Teardown before main offline_cache teardown.
This commit is contained in:
vicrodh committed 2026-02-17 11:02:02 -06:00
1 parent 644571f82a
commit 76cf401a3a
3 files changed
+26 -4

No files matched your search

+2 -4
View File
@@ -6549,7 +6549,6 @@ pub async fn v2_cache_track_for_offline(
sample_rate: Option<f64>,
state: State<'_, AppState>,
cache_state: State<'_, OfflineCacheState>,
library_state: State<'_, crate::library::LibraryState>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
log::info!("Command: v2_cache_track_for_offline {} - {} by {}", track_id, title, artist);
@@ -6581,7 +6580,7 @@ pub async fn v2_cache_track_for_offline(
cache_state.fetcher.clone(),
cache_state.db.clone(),
cache_state.get_cache_path(),
library_state.db.clone(),
cache_state.library_db.clone(),
app_handle.clone(),
cache_state.cache_semaphore.clone(),
);
@@ -6608,7 +6607,6 @@ pub async fn v2_cache_tracks_batch_for_offline(
tracks: Vec<BatchTrackInfo>,
state: State<'_, AppState>,
cache_state: State<'_, OfflineCacheState>,
library_state: State<'_, crate::library::LibraryState>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
log::info!("Command: v2_cache_tracks_batch_for_offline ({} tracks)", tracks.len());
@@ -6652,7 +6650,7 @@ pub async fn v2_cache_tracks_batch_for_offline(
cache_state.fetcher.clone(),
cache_state.db.clone(),
cache_state.get_cache_path(),
library_state.db.clone(),
cache_state.library_db.clone(),
app_handle.clone(),
cache_state.cache_semaphore.clone(),
);
+22
View File
@@ -135,6 +135,9 @@ pub struct OfflineCacheState {
/// Cache limit in bytes (None = unlimited)
pub limit_bytes: Arc<Mutex<Option<u64>>>,
pub cache_semaphore: Arc<Semaphore>,
/// Separate library DB connection for download post-processing writes.
/// This avoids contending with the main library DB mutex used by UI queries.
pub library_db: Arc<Mutex<Option<qbz_library::LibraryDatabase>>>,
}
impl OfflineCacheState {
@@ -165,6 +168,7 @@ impl OfflineCacheState {
cache_dir: Arc::new(RwLock::new(cache_dir.clone())),
limit_bytes: Arc::new(Mutex::new(default_limit)),
cache_semaphore: Arc::new(Semaphore::new(3)),
library_db: Arc::new(Mutex::new(None)),
};
log::info!("Offline cache initialized at: {:?}", cache_dir);
@@ -183,6 +187,7 @@ impl OfflineCacheState {
cache_dir: Arc::new(RwLock::new(cache_dir)),
limit_bytes: Arc::new(Mutex::new(Some(2 * 1024 * 1024 * 1024u64))),
cache_semaphore: Arc::new(Semaphore::new(3)),
library_db: Arc::new(Mutex::new(None)),
}
}
@@ -206,7 +211,24 @@ impl OfflineCacheState {
Ok(())
}
/// Open a separate library DB connection for download post-processing.
/// Must be called after library.init_at() so the schema exists.
pub async fn init_library_connection(&self, data_dir: &std::path::Path) -> Result<(), String> {
let db_path = data_dir.join("library.db");
let lib_db = qbz_library::LibraryDatabase::open(&db_path)
.map_err(|e| format!("Failed to open download library connection: {}", e))?;
let mut guard = self.library_db.lock().await;
*guard = Some(lib_db);
log::info!("Offline cache: separate library DB connection opened at {:?}", db_path);
Ok(())
}
pub async fn teardown(&self) {
// Close library connection first (before main teardown)
{
let mut lib_guard = self.library_db.lock().await;
*lib_guard = None;
}
let mut guard = self.db.lock().await;
*guard = None;
}
+2
View File
@@ -134,6 +134,7 @@ pub async fn activate_session(app: &tauri::AppHandle, user_id: u64) -> Result<()
// Cache-dir stores
offline_cache.init_at(&cache_dir).await?;
offline_cache.init_library_connection(&data_dir).await?;
lyrics.init_at(&cache_dir).await?;
// Run deferred subscription purge check
@@ -313,6 +314,7 @@ pub async fn activate_offline_session(app: &tauri::AppHandle) -> Result<(), Stri
library.init_at(&data_dir).await?;
offline.init_at(&data_dir)?;
offline_cache.init_at(&cache_dir).await?;
offline_cache.init_library_connection(&data_dir).await?;
audio_settings.init_at(&data_dir)?;
playback_prefs.init_at(&data_dir)?;