Optimize OpenHome playlist operations with caching

This commit introduces caching mechanisms for OpenHome playlist operations to reduce redundant SOAP calls and improve performance. Key changes include:

- Added track IDs caching in OpenHomeQueue to avoid repeated id_array() calls
- Implemented cache invalidation for all playlist modification operations
- Added caching for source XML and source index in OhProductClient
- Updated playlist length, ID retrieval, and track ID fetching to use cached data
- Ensured cache is invalidated on write operations to maintain consistency

The caching strategy uses time-based expiration (1 second for track IDs, 10 minutes for sources) and employs mutex locking for thread safety.
This commit is contained in:
2026-01-31 16:52:03 +01:00
parent fa489f458d
commit 2b6462fdd9
3 changed files with 255 additions and 20 deletions

View File

@@ -1,4 +1,6 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use std::usize;
use quick_xml::escape::escape;
@@ -15,6 +17,55 @@ use crate::queue::{
};
use crate::{DeviceId, DeviceIdentity, RendererInfo};
/// Cache for OpenHome track IDs to avoid redundant SOAP calls
#[derive(Debug)]
struct TrackIdsCache {
/// Cached track IDs
ids: Option<Vec<u32>>,
/// Timestamp of last cache update
last_update: Option<SystemTime>,
}
impl TrackIdsCache {
fn new() -> Self {
Self {
ids: None,
last_update: None,
}
}
/// Check if cache is valid (not expired and has data)
fn is_valid(&self) -> bool {
if let (Some(_), Some(last_update)) = (&self.ids, self.last_update) {
if let Ok(elapsed) = SystemTime::now().duration_since(last_update) {
return elapsed.as_millis() < 1000; // TTL: 1 second
}
}
false
}
/// Get cached IDs if valid
fn get(&self) -> Option<Vec<u32>> {
if self.is_valid() {
self.ids.clone()
} else {
None
}
}
/// Update cache with new IDs
fn set(&mut self, ids: Vec<u32>) {
self.ids = Some(ids);
self.last_update = Some(SystemTime::now());
}
/// Invalidate cache (called on write operations)
fn invalidate(&mut self) {
self.ids = None;
self.last_update = None;
}
}
/// Local mirror of an OpenHome playlist for a single renderer.
#[derive(Clone, Debug)]
pub struct OpenHomeQueue {
@@ -26,6 +77,8 @@ pub struct OpenHomeQueue {
/// Permet de maintenir des métadonnées à jour même si le service OpenHome
/// ne permet pas de les modifier directement.
metadata_cache: HashMap<u32, Option<crate::model::TrackMetadata>>,
/// Cache for track IDs to avoid redundant IdArray SOAP calls
track_ids_cache: Arc<Mutex<TrackIdsCache>>,
}
impl OpenHomeQueue {
@@ -41,6 +94,7 @@ impl OpenHomeQueue {
info_client,
product_client,
metadata_cache: HashMap::new(),
track_ids_cache: Arc::new(Mutex::new(TrackIdsCache::new())),
}
}
@@ -167,6 +221,9 @@ impl OpenHomeQueue {
"Gentle sync completed: preserved playing track as first item (not in new playlist)"
);
// Invalidate cache after playlist modifications
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
@@ -344,6 +401,9 @@ impl OpenHomeQueue {
"Gentle sync completed: double-LCS with pivot (playing track preserved)"
);
// Invalidate cache after playlist modifications
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
@@ -442,6 +502,9 @@ impl OpenHomeQueue {
)));
}
// Invalidate cache after playlist modifications
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
}
@@ -562,7 +625,22 @@ impl QueueBackend for OpenHomeQueue {
/// Return the list of OpenHome track IDs in order.
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
self.ensure_playlist_source_selected()?;
self.playlist_client.id_array()
// Lock the cache for the entire operation to prevent race conditions
let mut cache = self.track_ids_cache.lock().unwrap();
// Check if cache is valid
if let Some(cached_ids) = cache.get() {
return Ok(cached_ids);
}
// Cache miss or expired - fetch from service (keep lock held to prevent concurrent calls)
let ids = self.playlist_client.id_array()?;
// Update cache before releasing lock
cache.set(ids.clone());
Ok(ids)
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
@@ -637,6 +715,8 @@ impl QueueBackend for OpenHomeQueue {
self.ensure_playlist_source_selected()?;
self.playlist_client.stop()?;
}
// Invalidate cache (seek_id modifies playlist state)
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
@@ -659,6 +739,9 @@ impl QueueBackend for OpenHomeQueue {
self.playlist_client.delete_all()?;
self.metadata_cache.clear();
// Invalidate cache after delete_all
self.track_ids_cache.lock().unwrap().invalidate();
if items.is_empty() {
return Ok(());
}
@@ -677,6 +760,9 @@ impl QueueBackend for OpenHomeQueue {
previous_id = new_id;
}
// Invalidate cache after insertions
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
@@ -685,6 +771,8 @@ impl QueueBackend for OpenHomeQueue {
if items.is_empty() {
self.playlist_client.delete_all()?;
self.metadata_cache.clear();
// Invalidate cache after delete_all
self.track_ids_cache.lock().unwrap().invalidate();
return Ok(());
}
@@ -819,6 +907,9 @@ impl QueueBackend for OpenHomeQueue {
self.playlist_client.seek_id(new_id)?;
}
// Invalidate cache after playlist modifications
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
@@ -857,8 +948,13 @@ impl QueueBackend for OpenHomeQueue {
EnqueueMode::ReplaceAll => {
// Replace the entire playlist
self.replace_queue(items, None)?;
return Ok(());
}
}
// Invalidate cache after playlist modifications (except ReplaceAll which already does it)
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
@@ -867,7 +963,10 @@ impl QueueBackend for OpenHomeQueue {
/// Optimized clear_queue: use delete_all() directly instead of replace_queue.
fn clear_queue(&mut self) -> Result<(), ControlPointError> {
self.ensure_playlist_source_selected()?;
self.playlist_client.delete_all()
self.playlist_client.delete_all()?;
// Invalidate cache after clearing playlist
self.track_ids_cache.lock().unwrap().invalidate();
Ok(())
}
/// Optimized is_empty: only fetch track IDs, not the full playlist.