Télécharger la pyramide du worker sans pause, garder la pause pour le rendu local du Pi

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 13:50:49 +02:00
parent 6e1b26986c
commit c5097e21a0
3 changed files with 32 additions and 6 deletions

View File

@ -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.

View File

@ -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():

View File

@ -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]