File size: 4,796 Bytes
1c730d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 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. |