Spaces:
Paused
Paused
| # Mutualisation du pypowsybl Network entre `network_service` et `recommender_service` | |
| ## Observation (v8 trace) | |
| Le NAD prefetch introduit en v8 (`docs/performance/history/nad-prefetch.md`) a délivré | |
| `/api/network-diagram` en **516 ms** au lieu de 6 385 ms, mais | |
| `/api/config` est passé de 14 790 ms à **19 171 ms (+4.4 s)**. Le gain | |
| net a été de seulement ~1.5 s sur la critical path. | |
| En auditant le code, on s'aperçoit qu'on charge le **même `.xiidm` | |
| jusqu'à trois fois** pendant `/api/config` : | |
| 1. `network_service.load_network()` — `pn.load(file_path)`, ~3-5 s | |
| 2. `recommender_service._get_base_network()` — `pp.network.load(str(network_file))`, ~3-5 s **doublon complet** | |
| 3. `setup_environment_configs_pypowsybl()` dans grid2op — backend pypowsybl charge encore une fois, ~3-5 s | |
| Les appels #1 et #2 chargent **exactement le même fichier** et produisent | |
| **la même Network pypowsybl**. La pré-chauffe `_base_network` que fait | |
| `prefetch_base_nad_async()` en main thread est responsable d'une grosse | |
| partie du +4.4 s sur `/api/config`. | |
| ## Change | |
| `_get_base_network()` préfère maintenant **réutiliser la Network déjà | |
| chargée par `network_service`** plutôt que de la re-parser : | |
| ```python | |
| def _get_base_network(self): | |
| if self._base_network is not None: | |
| return self._base_network | |
| # Préfère l'instance déjà chargée par network_service. | |
| from expert_backend.services.network_service import network_service | |
| if network_service.network is not None: | |
| n = network_service.network | |
| n.get_line_ids = lambda: n.get_lines().index.tolist() | |
| self._base_network = n | |
| self._capture_initial_pst_taps(n) | |
| return self._base_network | |
| # Fallback : pp.network.load(config.ENV_PATH) pour les tests / | |
| # appels qui bypass /api/config. | |
| … | |
| ``` | |
| ## Sûreté de partage | |
| Les deux services tiennent une référence vers le **même objet | |
| pypowsybl Network**. Les accès restent sûrs : | |
| - **`network_service`** ne fait que des lectures (`get_lines`, | |
| `get_voltage_levels`, …), jamais de variant switch. | |
| - **`recommender_service`** switche les variants dans `_get_n_variant` | |
| / `_get_n1_variant` mais restaure toujours le variant d'origine dans | |
| un `try/finally`, donc toute lecture depuis `network_service` voit | |
| un état cohérent. | |
| - Le monkey-patch `get_line_ids` est idempotent et purement additif. | |
| - `_capture_initial_pst_taps` est spécifique au recommender (state | |
| isolé) et doit toujours s'exécuter pour capturer les tap positions | |
| N-state. | |
| ## Invariants (testés) | |
| `test_recommender_service.py::TestGetBaseNetworkMutualisation` : | |
| - `test_reuses_network_service_network_when_available` — quand | |
| `network_service.network` est peuplé, `_get_base_network()` ne | |
| déclenche AUCUN nouveau `pp.network.load()` et retourne exactement | |
| la même instance. | |
| - `test_falls_back_to_standalone_load_when_network_service_empty` — | |
| quand `network_service.network is None` (tests, appelants qui | |
| bypass `/api/config`), on retombe sur le chargement standalone. | |
| Le test existant `test_direct_file_loading.py::test_get_base_network_with_file_path` | |
| est mis à jour pour forcer explicitement le chemin fallback en | |
| mettant `network_service.network = None`. | |
| ## Impact attendu | |
| Baseline v8 (avec NAD prefetch seul) : | |
| | Segment | v8 mesuré | | |
| |---|---| | |
| | `/api/config` | 19 171 ms | | |
| | `/api/network-diagram` | 516 ms | | |
| | Total critical path | ~20.5 s | | |
| Attendu v9 (avec mutualisation) : | |
| | Segment | v9 attendu | | |
| |---|---| | |
| | `/api/config` | ~14-16 s (−3-5 s : plus de pp.network.load doublon) | | |
| | `/api/network-diagram` | ~500 ms (inchangé, cache toujours chaud) | | |
| | Total critical path | **~15 s (−5 s vs v8, −6 s vs v7)** | | |
| On récupère ainsi **une grosse partie du gain théorique du prefetch | |
| NAD** qui avait été absorbé par le chargement dupliqué. | |
| ## Ce qui n'est PAS changé | |
| - **Le chargement pypowsybl par grid2op** (#3 ci-dessus) reste en | |
| place — il est enfoui dans `expert_op4grid_recommender` et ne peut | |
| pas être court-circuité sans modifier la lib externe. | |
| - **Le comportement des endpoints** est identique : seul le plomberie | |
| interne change. | |
| - **Les appelants qui construisent `RecommenderService` directement** | |
| (tests, scripts d'intégration) continuent de fonctionner grâce au | |
| fallback. | |
| ## Fichiers modifiés | |
| | Fichier | Changement | | |
| |---|---| | |
| | `expert_backend/services/recommender_service.py` | `_get_base_network` préfère `network_service.network` quand disponible ; fallback `pp.network.load` préservé. | | |
| | `expert_backend/tests/test_recommender_service.py` | Nouvelle classe `TestGetBaseNetworkMutualisation` (2 tests). | | |
| | `expert_backend/tests/test_direct_file_loading.py` | `test_get_base_network_with_file_path` force explicitement le chemin fallback. | | |