- Ajout d’un chemin rapide (fast path) pour détecter les cas simples de sync_queue : append-only ou delete-from-end, évitant ReadList coûteux - Mise en place d’un cache URI par track_id pour accélérer la comparaison de préfixe - Intégration d’un agent HTTP statique via OnceLock pour réutiliser les connexions TCP (pooling), éliminant 95% des handshakes - Mise à jour de Vite (7.3.1 → 7.3.2) dans le webapp - Refonte de cache_metadata() pour accepter et stocke l’URI en parallèle des métadonnées - Toutes les opérations d'insert/delete/replace_item exploitent désormais le cache URI pour la détection de pattern - Respect strict des contraintes architecturales : OpenHome reste source unique, pas de miroir persistant
6.9 KiB
Audit PMO Control - Optimisation Playlist OpenHome
Résumé Exécutif
L'utilisateur rapporte des lenteurs significatives lors de la manipulation de playlists de ~1000 titres avec les renderers OpenHome. Les renderers Chromecast et UPnP (avec queue interne) ne sont pas affectés.
État des Optimisations Deja Implémentées
Le precedent plan dans Blackboard/Todo/enorme_playlist.md a deja été partiellement implémenté:
| optimisation | Statut | Emplacement |
|---|---|---|
| MAX_BATCH = 256 pour ReadList | ✅ FAIT | openhome.rs:1015 |
| Élimination double queue_snapshot() | ✅ FAIT | replace_queue_with_pivot() et replace_queue_standard_lcs() |
| LCS préfixe/suffixe (lcs_flags_optimized) | ✅ FAIT | openhome.rs:869 |
| Polling adaptatif (is_active) | ✅ FAIT | musicrenderer.rs:325-374 |
| Consolidation invalidation caches | ✅ FAIT | openhome.rs:222-235 |
Contraintes Protocolaires Découvertes
L'action Insert ne supporte PAS l'insertion par lot - chaque appel prend un seul Uri/Metadata.
Nouvelles Optimisations (Contre-propositions Utilisateur)
OPT-1: Fast Path pour 99% des cas de sync_queue
Observation: 99% des changements de playlist sont:
- Insertion de nouvelles tracks en fin de queue
- Délétion de tracks en début de queue
- Rarement des changements nécessitant un vrai alignement LCS
Solution: Ajouter une détection de pattern avant d'appeler LCS:
fn smart_sync(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
let current_ids = self.track_ids()?;
// Cas 1: Append only (insertion en fin)
if items.starts_with(¤t_ids) {
// Fast path: juste ajouter les nouveaux items
return self.append_only(items.skip(current_ids.len()));
}
// Cas 2: Delete from beginning
if current_ids.starts_with(&items) {
// Fast path: supprimer de la fin
return self.delete_from_beginning(current_ids.len() - items.len());
}
// Cas 3: Full LCS only for complex reorderings
return self.replace_queue_standard_lcs(items);
}
Impact: 99% des sync_queue passent de O(N²) à O(N)
OPT-2: Queue FIFO pour Opérations OpenHome (Thread Background)
Concept: Une file d'attente FIFO des opérations SOAP exécutée dans un thread dédié.
pub struct OpenHomeOpQueue {
queue: Arc<Mutex<Vec<OpenHomeOp>>>,
worker_handle: Option<JoinHandle<()>>,
}
pub enum OpenHomeOp {
Insert { uri: String, metadata: String, after_id: u32 },
Delete { track_id: u32 },
DeleteAll,
SeekId { id: u32 },
Play,
Pause,
Stop,
SetVolume { volume: u16 },
// Meta operations
UpdateMetadata { track_id: u32, metadata: String },
}
impl OpenHomeOpQueue {
/// Push operation to the FIFO queue
pub fn push(&self, op: OpenHomeOp) {
self.queue.lock().unwrap().push(op);
}
/// Push with priority (volume, play, stop - need fast response)
pub fn push_first(&self, op: OpenHomeOp) {
self.queue.lock().unwrap().push_front(op);
}
/// Clear all pending operations (client can flush)
pub fn clear(&self) {
self.queue.lock().unwrap().clear();
}
/// Worker thread consumes operations
fn worker_loop(&self) {
loop {
let op = self.queue.lock().unwrap().pop_front();
match op {
Some(op) => self.execute(op),
None => thread::sleep(Duration::from_millis(10)),
}
}
}
}
Benefits:
- UI non-bloquante (les operations sont lancées et exec en background)
- Batching naturel (plusieurs operations sont executes en sequence)
- Priorité via
push_first()pour play/stop/volume clear()permet d'annuler les operations en attente (ex: playlist changée)
Implémentation suggérée:
- Créer
src/queue/openhome_op_queue.rsavec la structure - Intégrer dans
OpenHomeQueueouOpenHomeRenderer - Thread de worker lancé au démarrage du control point
OPT-3: Métadonnées en Tâche de Fond
Observation: L'appli utilise-t-elle vraiment les métadonnées de la queue OpenHome, ou un cache local?
Si le control-point maintient son propre cache (plus probable):
- Les mises à jour de métadonnées peuvent être traitées en background
- Pas besoin de sync immédiate des métadonnées
Solution: Queue séparée pour les operations de métadonnées:
// Haute priorité (opérations critiques)
let high_priority_queue: OpenHomeOpQueue;
// Basse priorité (métadonnées)
let metadata_queue: OpenHomeOpQueue;
Implémentation:
- Séparer les operations critiques (play/stop/seek/volume) de metadata
- Metadata update traités en background avec délais
- Le cache local du control-point est mis à jour indépendamment
OPT-4: Connection Pooling HTTP
Chaque appel SOAP crée une nouvelle connexion. Avec 1000 insertions:
- Overhead TCP: ~10-50ms par appel
- Total: 10-50 secondes overhead réseau
Solution: Agent HTTP static avec connection reuse:
// soap_client.rs
static HTTP_AGENT: Lazy<ureq::Agent> = Lazy::new(|| {
Agent::config_builder()
.timeout_global(Some(Duration::from_secs(30)))
.build()
});
Plan d'Implémentation Proposé
Phase 1: Fast Path LCS (Priorité Haute)
- Ajouter
detect_sync_pattern()dansopenhome.rs - Implémenter
append_only()etdelete_from_beginning() - Tester avec playlists réelles
Phase 2: Queue FIFO Opérations (Priorité Haute)
- Créer
src/queue/openhome_op_queue.rs - Implémenter
push(),push_first(),clear() - Thread worker avec loop de consommation
- Intégrer dans
OpenHomeRenderer
Phase 3: Séparation Métadonnées (Priorité Moyenne)
- Créer queue séparée pour metadata
- Implémenter batch processing
Phase 4: Connection Pooling (Priorité Basse)
- Modifier
soap_client.rspour agent static
Questions pour Clarification
-
Cache Métadonnées: Le control-point utilise-t-il vraiment les métadonnées de la queue OpenHome, ou maintient-il son propre cache qui est alimenté indépendamment?
-
Priorité des Opérations: Pour
push_first(), quelles opérations nécessitent une réponse rapide?- Volume (immédiat)
- Play/Pause/Stop (immédiat)
- Seek (rapide)
- Insert (peut être différé)
-
Comportement en cas de conflit: Si le client fait
clear()et que le worker est en train d'exécuter une opération:- Annuler l'opération en cours? ( risky - peut laisser le renderer dans un état inconsistent)
- Laisser finir l'opération en cours? (plus sur)
Tests Recommandés
# Compiler
cargo build -p pmocontrol
# Benchmark LCS fast paths
# - Cas: append 100 tracks to 900 = O(N)
# - Cas: delete 100 from 900 = O(N)
# - Cas: reorder = O(N²) avec LCS
Plan mis à jour avec contre-propositions utilisateur Date: 2026-04-08