# 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:

```rust
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é.

```rust
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:

```rust
// 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:

```rust
// 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

```bash
# 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*