# #86 — Optimisation globale des performances (phase 3) > **Statut :** ✅ livré | **Zone :** backend (`indexer.py`, `mutations.py`) > **Prérequis déjà livrés :** BUG-033 (recherche via inverted index), BUG-040 (PDF lazy), > BUG-025 (caps regex). ## Contexte La roadmap #86 demandait : recherche simple + tool IA via l'inverted index, indexation incrémentale + scan différentiel au démarrage, extraction PDF/excalidraw différée et caps CPU sur les opérations regex. Trois de ces cinq points étaient déjà couverts par des correctifs antérieurs ; cette livraison ferme les deux points restants. ## Ce qui était déjà livré (rappel) | Point #86 | Livré par | État | |---|---|---| | Recherche simple + `search_fulltext` via inverted index | BUG-033 | `search()` récupère ses candidats via l'inverted index (intersection + expansion de préfixes), repli scan pendant la construction | | Extraction PDF différée | BUG-040 | `_scan_vault` ne lit que les métadonnées ; `enrich_pdf_texts()` extrait après index | | Caps CPU regex | BUG-025 | `validate_regex` (longueur ≤ 500, rejet quantificateurs imbriqués), contenu tronqué à 200 kio, matchs plafonnés à 1 000 | ## Livré ici ### 1. Scan différentiel au démarrage (`backend/indexer.py`) - `_scan_vault(..., previous_files)` : quand un snapshot `{relative_path: file_info}` est fourni, toute entrée dont `size` **et** `modified` sont inchangés est réutilisée sans lecture disque ni re-parse (copie du dict, tags recomptés, wikilinks ré-enregistrés depuis le contenu caché pour reconstruire l'index de backlinks). - `build_index()` capture le snapshot précédent avant le `clear()` et le transmet à chaque scan de vault (via `functools.partial` pour l'executor) ; `reload_single_vault()` fait de même pour son vault. Seuls le `os.walk` + `stat` (bon marché) tournent à chaque passe ; le retour inclut `reused` (hits différentiels, loggé par vault). - Premier démarrage (aucun snapshot) : comportement identique à avant. ### 2. Extraction excalidraw différée (`backend/indexer.py`) - Le scan ne lit plus les `.excalidraw` / `.excalidraw.md` : titre dérivé du nom de fichier, `content` vide, flag `excalidraw_text_pending` (plus de décompression lz-string pendant le scan). - `enrich_pdf_texts()` traite désormais les deux flags (`pdf` + `excalidraw`) via `_read_excalidraw_indexable_text()` exécuté dans l'executor, avec notification du hook d'index incrémental comme pour les PDF. Nom conservé pour compatibilité (tests, `main.py`, `reload_index`, `reload_single_vault` inchangés côté appel). - Le chemin incrémental fichier-à-fichier (`_index_single_file_sync`, watcher) reste immédiat : un seul fichier ne justifie pas le différé. ### 3. Garde-fou taille sur `replace_in_files` (`backend/services/mutations.py`) - Nouvelle constante `MAX_REPLACE_FILE_BYTES` (5 Mio) : tout fichier dépassant le plafond est sauté (warning loggé) au lieu d'être lu intégralement puis balayé par le pattern utilisateur. Complète les caps BUG-025 (qui couvrent la recherche, pas le remplacement qui opère sur le contenu disque complet par nature). ## Tests `tests/test_perf_phase3.py` (9 tests) : différé excalidraw au scan (`.excalidraw` + `.excalidraw.md`), remplissage par l'enrichissement, `reused == 0` au premier scan, réutilisation à l'identique, re-parse du fichier modifié, ajout/suppression, skip `replace` sur fichier surdimensionné + cas passant nominal. ## Limites connues - Pas de persistance disque de l'index : le différentiel joue sur les rebuilds dans le même processus (`reload_index`, `reload_single_vault`), pas entre deux redémarrages (persistance = #85, phase 2). - La comparaison `size + mtime` ne détecte pas une modification qui conserverait taille et mtime à la milliseconde près (cas pathologique, le watcher temps réel couvre les modifications en cours d'exécution).