Files
pmomusic/.kilo/plans/1775631632458-happy-cactus.md
Eric Coissac c128120697 🚀 Optimisations OpenHome : fast path LCS, connection pooling & metadata cache
- 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
2026-04-09 08:40:10 +02:00

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(&current_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:

  1. Créer src/queue/openhome_op_queue.rs avec la structure
  2. Intégrer dans OpenHomeQueue ou OpenHomeRenderer
  3. 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:

  1. Séparer les operations critiques (play/stop/seek/volume) de metadata
  2. Metadata update traités en background avec délais
  3. 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)

  1. Ajouter detect_sync_pattern() dans openhome.rs
  2. Implémenter append_only() et delete_from_beginning()
  3. Tester avec playlists réelles

Phase 2: Queue FIFO Opérations (Priorité Haute)

  1. Créer src/queue/openhome_op_queue.rs
  2. Implémenter push(), push_first(), clear()
  3. Thread worker avec loop de consommation
  4. Intégrer dans OpenHomeRenderer

Phase 3: Séparation Métadonnées (Priorité Moyenne)

  1. Créer queue séparée pour metadata
  2. Implémenter batch processing

Phase 4: Connection Pooling (Priorité Basse)

  1. Modifier soap_client.rs pour agent static

Questions pour Clarification

  1. 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?

  2. 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é)
  3. 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