From c5097e21a06d015c87a6f961abfb76a09c4f706b Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Sun, 27 Sep 2026 13:50:49 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=A9l=C3=A9charger=20la=20pyramide=20du=20wo?= =?UTF-8?q?rker=20sans=20pause,=20garder=20la=20pause=20pour=20le=20rendu?= =?UTF-8?q?=20local=20du=20Pi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- lidar_pipeline/mapserve.py | 10 +++++++--- lidar_pipeline/tests/test_mapserve.py | 26 ++++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 525767f..bb54896 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ - **Pyramide complète générée d'avance** : par défaut `tiles.zoom_cached` stocke TOUS les niveaux jusqu'au natif (`TILE_CACHE_MAX_Z` = `TILE_MAX_NATIVE_Z` 19, `TILE_EVEN_LEVELS` off) et la maintenance de fond les génère d'avance jusqu'à `18@2x` (défaut de `LIDAR_TILE_BACKGROUND_MAX_Z`) ; sur le Pi elle les TÉLÉCHARGE du worker. Carte plafonnée au zoom 19 (`maxZoom`, 1 px écran = 1 px LiDAR, jamais de tuile agrandie). Raison : le rendu à la volée du Pi (~170 ms/tuile, 3 à la fois) donnait 1,5–6 s par écran aux zooms 17–19. File de maintenance bornée (`queue_max`) : chaque dalle garde dans `_bg["incomplete"]` l'indice de sa première tuile refusée faute de place et reprend de là aux scans suivants (avant : le premier scan saturait la file et le reste de la pyramide n'était jamais généré). ~110 000 tuiles @2x pour 3 240 dalles (~5 Go). Stockage réduit toujours possible (`LIDAR_TILE_EVEN_LEVELS=1` → `EvenLevelTileLayer`, niveaux pairs, natif 19 demandé tel quel). - **Mémoire bornée de la carte (Pi, `mem_limit: 1g`)** : `_open_source` (`tiles.py`) met en cache une **copie détachée** de chaque source (une image AVIF ouverte garde son décodeur, ~18 Mo de plus par quadrant 2500²) et compte 4 octets/pixel (PIL stocke le RGB sur 32 bits) ; `Dockerfile.maps` fixe `MALLOC_MMAP_THRESHOLD_=1048576` pour que glibc rende les grands tampons au système. Sans ces deux points, la navigation à fort zoom (niveaux rendus à la volée) montait à ~700 Mo de RSS et le conteneur était tué en boucle par l'OOM killer (502 côté Traefik). - **Carte autonome (Pi sans worker)** : amont (`LIDAR_SOURCE_URL`/`LIDAR_MAPS_URL`) éteint ⇒ la carte reste servie depuis le disque. `_remote_payload` mémorise aussi l'échec (TTL 60 s, délai 10 s : sinon chaque requête repayait le délai réseau et `/api/map/meta` ne répondait plus), puis l'index local (`_build_index`) prend le relais ; coupe-circuits sur les sources (`_SOURCE_OFFLINE`, 60 s) et les tuiles (`_UPSTREAM`, réarmé à expiration) ; source rapatriée datée à sa version amont (`os.utime`) pour que l'index local garde les mêmes dates ; pas de requête amont pour une tuile sans dalle quand l'inventaire vient de l'amont. En cache seule, une tuile périmée reste servie (`no-store`, `X-Tile-Pending`) en attendant la nouvelle. -- **Pyramide téléchargée en tâche de fond** : avec `LIDAR_MAPS_URL`, la maintenance (`_bg_process_one`) TÉLÉCHARGE les tuiles des niveaux stockés depuis l'amont (stat `telechargees`), sans attendre l'affichage ; rendu local en repli si l'amont ne répond pas. +- **Pyramide téléchargée en tâche de fond** : avec `LIDAR_MAPS_URL`, la maintenance (`_bg_process_one`) TÉLÉCHARGE les tuiles des niveaux stockés depuis l'amont (stat `telechargees`), sans attendre l'affichage, et sans pause entre deux téléchargements (le worker encaisse) ; rendu local en repli si l'amont ne répond pas, lui seul suivi de `LIDAR_TILE_BACKGROUND_PAUSE` (ressources du Pi). - **Première apparition des dalles (`_apply_seen`, `index_xyz/.sources_seen.json`)** : date effective d'une source = max(version, première entrée dans l'inventaire). Une dalle écrite avant mais inventoriée après une tuile (TTL de l'index amont, anti-rebond de l'inventaire) périme donc la tuile — sinon trou permanent à ce niveau, sur disque, en mémoire et dans le navigateur (stamp `?v=` inchangé). Registre persistant ; absent avec un cache existant (mise à jour) ⇒ tout est périmé une fois. - **Coût navigateur de la carte (mesuré sous Chrome, Intel Iris Xe)** : le filtre d'assombrissement du fond OSM (`.base-dark`) est posé sur le **conteneur** de la couche, jamais sur chaque tuile — rendu identique (opérations par pixel), processus GPU 92 % → 56 % en déplacement et 99 % → 53 % au zoom molette. Couches LiDAR en `keepBuffer: 1` (tuiles 512 px = 1 Mo décodé chacune). Mesuré sans effet notable : `backdrop-filter` des cartes, `isolation`/`mix-blend-mode` en « normal », `will-change`. Tas JS 2-5 Mo, CPU au repos ~1 %. Décodage AVIF ~12 ms/tuile contre ~5 ms en WebP (compromis assumé : stockage AVIF). - **Encodage AVIF rapide** : `AVIF_SPEED = 9` (`rendering.py`, `tiles.py`, `_SUBTILE_AVIF_SPEED` dans `index.py`) — dalle 5000 × 5000 px encodée en 0,6 s au lieu de 4 s (+3 % de taille, −0,3 dB) ; l'encodage était l'étape la plus longue du rendu d'une couche. diff --git a/lidar_pipeline/mapserve.py b/lidar_pipeline/mapserve.py index a8c6826..3c095c9 100644 --- a/lidar_pipeline/mapserve.py +++ b/lidar_pipeline/mapserve.py @@ -318,7 +318,9 @@ def background_scan(output_dir=None): def _bg_process_one(): """Traite une tuile de la file, sinon une source à rapatrier. Vrai si - un travail a été fait (téléchargement, rendu ou rapatriement). + un travail a été fait : "telechargee" pour une tuile téléchargée de + l'amont (enchaînée sans pause : le worker encaisse), True pour un rendu + local ou un rapatriement de source (suivis de la pause). Tuiles d'abord : légères et visibles tout de suite, elles ne doivent pas attendre des milliers de sources (utiles aux seuls forts zooms rendus à @@ -355,7 +357,7 @@ def _bg_process_one(): tiles_mod.tile_cache_path(OUTPUT_DIR, layer, z, x, y, conf["scale"], conf["fmt"]), data) _bg["stats"]["telechargees"] = _bg["stats"].get("telechargees", 0) + 1 - return True + return "telechargee" # Amont absent ou éteint : rendu local depuis les sources gardées (autonomie). tiles_mod.get_tile(OUTPUT_DIR, layer, z, x, y, conf["scale"], conf["fmt"]) _bg["stats"]["rendues"] += 1 @@ -373,6 +375,8 @@ def _bg_worker(): except Exception as e: # noqa: BLE001 — une tuile n'arrête pas la file logger.warning(f"Maintenance des tuiles : rendu impossible ({e})") rendered = False + if rendered == "telechargee": + continue # téléchargement : aucun calcul local, pas de pause if rendered: _bg["stop"].wait(_bg_conf()["pause"]) # cadence : un rendu, une pause else: @@ -430,7 +434,7 @@ def start_background(): logger.info(f"Maintenance des tuiles active : z ≤ {conf['max_z']} " f"@{conf['scale']}x {conf['fmt']}, scan toutes les " f"{conf['interval']:.0f} s, pause {conf['pause']:.1f} s " - f"entre rendus") + f"entre rendus locaux (téléchargements sans pause)") def stop_background(): diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index 62a68a9..b074a6e 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -1086,7 +1086,7 @@ def test_background_downloads_tiles_from_upstream(tmp_path, monkeypatch): z = 13 x, y = _tile_of_cell(1054, 6882, z) mapserve._bg_enqueue([("aspect", z, x, y)]) - assert mapserve._bg_process_one() is True + assert mapserve._bg_process_one() == "telechargee" assert calls == [f"tiles/aspect/{z}/{x}/{y}@2x.avif"] cache = tiles.tile_cache_path(tmp_path, "aspect", z, x, y, 2, "avif") assert cache.read_bytes() == AVIF @@ -1096,6 +1096,28 @@ def test_background_downloads_tiles_from_upstream(tmp_path, monkeypatch): assert state == "fresh" and data == AVIF +def test_background_downloads_without_pause(monkeypatch): + """Tuiles téléchargées de l'amont enchaînées sans pause ; rendu local + et rapatriement de source gardent la pause.""" + import lidar_pipeline.mapserve as mapserve + results = iter(["telechargee", "telechargee", True]) + waits = [] + + class Stop: + def is_set(self): + return len(waits) >= 1 + + def wait(self, delay): + waits.append(delay) + + monkeypatch.setattr(mapserve, "_bg", {**mapserve._bg, "stop": Stop()}) + monkeypatch.setattr(mapserve, "_bg_process_one", lambda: next(results)) + monkeypatch.setenv("LIDAR_TILE_BACKGROUND_PAUSE", "0.2") + monkeypatch.setattr(mapserve.os, "nice", lambda n: 0) + mapserve._bg_worker() + assert waits == [0.2] + + def test_background_renders_locally_when_upstream_down(tmp_path, monkeypatch): """Amont éteint : la maintenance rend la tuile localement (autonomie).""" import lidar_pipeline.mapserve as mapserve @@ -1186,7 +1208,7 @@ def test_background_tiles_before_sources(tmp_path, monkeypatch): z = 13 x, y = _tile_of_cell(1054, 6882, z) mapserve._bg_enqueue([("aspect", z, x, y)]) - assert mapserve._bg_process_one() is True + assert mapserve._bg_process_one() == "telechargee" assert fetched == [] and mapserve._bg["stats"]["telechargees"] == 1 assert mapserve._bg_process_one() is True assert fetched == [1]