Guilherme Silberfarb Costa commited on
Commit
9c95fcc
·
1 Parent(s): a484189

Speed up model search warmup and first map load

Browse files
backend/app/core/map_layers.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import json
4
  from html import escape
5
  from pathlib import Path
6
- from threading import Lock
7
  from typing import Any
8
 
9
  import folium
@@ -19,6 +19,8 @@ _SIMPLIFY_TOLERANCE = 0.00005
19
  _BAIRROS_CACHE_LOCK = Lock()
20
  _BAIRROS_GEOJSON_CACHE: dict[str, Any] | None = None
21
  _BAIRROS_SOURCE_SIGNATURE: tuple[tuple[str, int, int], ...] | None = None
 
 
22
 
23
 
24
  def _assinatura_bairros_source() -> tuple[tuple[str, int, int], ...] | None:
@@ -38,61 +40,84 @@ def _assinatura_bairros_source() -> tuple[tuple[str, int, int], ...] | None:
38
 
39
  def _carregar_bairros_geojson() -> dict[str, Any] | None:
40
  global _BAIRROS_GEOJSON_CACHE, _BAIRROS_SOURCE_SIGNATURE
 
41
  assinatura = _assinatura_bairros_source()
42
  if assinatura is None or not _BAIRROS_SHP_PATH.exists():
43
  return None
44
 
45
- with _BAIRROS_CACHE_LOCK:
46
- if _BAIRROS_SOURCE_SIGNATURE == assinatura and _BAIRROS_GEOJSON_CACHE is not None:
47
- return _BAIRROS_GEOJSON_CACHE
48
-
49
-
50
- geojson = None
51
-
52
- try:
53
- import geopandas as gpd
54
- except Exception:
55
- gpd = None
 
 
 
 
 
 
56
 
57
- if gpd is not None:
58
  try:
59
- gdf = gpd.read_file(_BAIRROS_SHP_PATH, engine="fiona")
60
- if gdf is not None and not gdf.empty:
61
- if gdf.crs is not None:
62
- gdf = gdf.to_crs("EPSG:4326")
63
- campos = ["geometry"]
64
- for campo in _TOOLTIP_FIELDS:
65
- if campo in gdf.columns:
66
- campos.insert(0, campo)
67
- break
68
- gdf = gdf.loc[:, campos].copy()
69
- if _SIMPLIFY_TOLERANCE > 0:
70
- gdf["geometry"] = gdf.geometry.simplify(_SIMPLIFY_TOLERANCE, preserve_topology=True)
71
- geojson = json.loads(gdf.to_json(drop_id=True))
72
- except Exception as exc:
73
- append_runtime_log(f"[mesa] bairros: falha no geopandas para camada de bairros: {exc}")
74
-
75
- if geojson is None:
76
- try:
77
- geojson = load_vector_geojson(
78
- _BAIRROS_SHP_PATH,
79
- target_crs="EPSG:4326",
80
- property_fields=_TOOLTIP_FIELDS,
81
- simplify_tolerance=_SIMPLIFY_TOLERANCE,
82
- )
83
- append_runtime_log("[mesa] bairros: usando fallback leve para camada de bairros")
84
- except Exception as exc:
85
- append_runtime_log(f"[mesa] bairros: fallback de bairros falhou: {exc}")
86
  geojson = None
87
 
88
- with _BAIRROS_CACHE_LOCK:
89
- if geojson is not None:
90
- _BAIRROS_GEOJSON_CACHE = geojson
91
- _BAIRROS_SOURCE_SIGNATURE = assinatura
92
- else:
93
- _BAIRROS_GEOJSON_CACHE = None
94
- _BAIRROS_SOURCE_SIGNATURE = None
95
- return geojson
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
 
98
  def get_bairros_geojson() -> dict[str, Any] | None:
 
3
  import json
4
  from html import escape
5
  from pathlib import Path
6
+ from threading import Event, Lock
7
  from typing import Any
8
 
9
  import folium
 
19
  _BAIRROS_CACHE_LOCK = Lock()
20
  _BAIRROS_GEOJSON_CACHE: dict[str, Any] | None = None
21
  _BAIRROS_SOURCE_SIGNATURE: tuple[tuple[str, int, int], ...] | None = None
22
+ _BAIRROS_INFLIGHT_SIGNATURE: tuple[tuple[str, int, int], ...] | None = None
23
+ _BAIRROS_INFLIGHT_EVENT: Event | None = None
24
 
25
 
26
  def _assinatura_bairros_source() -> tuple[tuple[str, int, int], ...] | None:
 
40
 
41
  def _carregar_bairros_geojson() -> dict[str, Any] | None:
42
  global _BAIRROS_GEOJSON_CACHE, _BAIRROS_SOURCE_SIGNATURE
43
+ global _BAIRROS_INFLIGHT_SIGNATURE, _BAIRROS_INFLIGHT_EVENT
44
  assinatura = _assinatura_bairros_source()
45
  if assinatura is None or not _BAIRROS_SHP_PATH.exists():
46
  return None
47
 
48
+ while True:
49
+ with _BAIRROS_CACHE_LOCK:
50
+ if _BAIRROS_SOURCE_SIGNATURE == assinatura and _BAIRROS_GEOJSON_CACHE is not None:
51
+ return _BAIRROS_GEOJSON_CACHE
52
+
53
+ if _BAIRROS_INFLIGHT_SIGNATURE == assinatura and _BAIRROS_INFLIGHT_EVENT is not None:
54
+ inflight = _BAIRROS_INFLIGHT_EVENT
55
+ is_builder = False
56
+ else:
57
+ inflight = Event()
58
+ _BAIRROS_INFLIGHT_SIGNATURE = assinatura
59
+ _BAIRROS_INFLIGHT_EVENT = inflight
60
+ is_builder = True
61
+
62
+ if not is_builder:
63
+ inflight.wait()
64
+ continue
65
 
 
66
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  geojson = None
68
 
69
+ try:
70
+ import geopandas as gpd
71
+ except Exception:
72
+ gpd = None
73
+
74
+ if gpd is not None:
75
+ try:
76
+ gdf = gpd.read_file(_BAIRROS_SHP_PATH, engine="fiona")
77
+ if gdf is not None and not gdf.empty:
78
+ if gdf.crs is not None:
79
+ gdf = gdf.to_crs("EPSG:4326")
80
+ campos = ["geometry"]
81
+ for campo in _TOOLTIP_FIELDS:
82
+ if campo in gdf.columns:
83
+ campos.insert(0, campo)
84
+ break
85
+ gdf = gdf.loc[:, campos].copy()
86
+ if _SIMPLIFY_TOLERANCE > 0:
87
+ gdf["geometry"] = gdf.geometry.simplify(_SIMPLIFY_TOLERANCE, preserve_topology=True)
88
+ geojson = json.loads(gdf.to_json(drop_id=True))
89
+ except Exception as exc:
90
+ append_runtime_log(f"[mesa] bairros: falha no geopandas para camada de bairros: {exc}")
91
+
92
+ if geojson is None:
93
+ try:
94
+ geojson = load_vector_geojson(
95
+ _BAIRROS_SHP_PATH,
96
+ target_crs="EPSG:4326",
97
+ property_fields=_TOOLTIP_FIELDS,
98
+ simplify_tolerance=_SIMPLIFY_TOLERANCE,
99
+ )
100
+ append_runtime_log("[mesa] bairros: usando fallback leve para camada de bairros")
101
+ except Exception as exc:
102
+ append_runtime_log(f"[mesa] bairros: fallback de bairros falhou: {exc}")
103
+ geojson = None
104
+
105
+ with _BAIRROS_CACHE_LOCK:
106
+ if geojson is not None:
107
+ _BAIRROS_GEOJSON_CACHE = geojson
108
+ _BAIRROS_SOURCE_SIGNATURE = assinatura
109
+ else:
110
+ _BAIRROS_GEOJSON_CACHE = None
111
+ _BAIRROS_SOURCE_SIGNATURE = None
112
+ return geojson
113
+ finally:
114
+ with _BAIRROS_CACHE_LOCK:
115
+ waiter = _BAIRROS_INFLIGHT_EVENT if _BAIRROS_INFLIGHT_SIGNATURE == assinatura else None
116
+ if _BAIRROS_INFLIGHT_SIGNATURE == assinatura:
117
+ _BAIRROS_INFLIGHT_SIGNATURE = None
118
+ _BAIRROS_INFLIGHT_EVENT = None
119
+ if waiter is not None:
120
+ waiter.set()
121
 
122
 
123
  def get_bairros_geojson() -> dict[str, Any] | None:
backend/app/main.py CHANGED
@@ -9,7 +9,7 @@ from fastapi.staticfiles import StaticFiles
9
 
10
  from app.api import auth, elaboracao, health, logs, pesquisa, repositorio, session, trabalhos_tecnicos, visualizacao
11
  from app.runtime_paths import resolve_frontend_dist_dir
12
- from app.services import auth_service
13
 
14
  app = FastAPI(
15
  title="MESA Frame API",
@@ -57,6 +57,12 @@ app.include_router(trabalhos_tecnicos.router)
57
  app.include_router(logs.router)
58
 
59
 
 
 
 
 
 
 
60
  def _mount_frontend_if_exists() -> None:
61
  frontend_dist = resolve_frontend_dist_dir()
62
  index_file = frontend_dist / "index.html"
 
9
 
10
  from app.api import auth, elaboracao, health, logs, pesquisa, repositorio, session, trabalhos_tecnicos, visualizacao
11
  from app.runtime_paths import resolve_frontend_dist_dir
12
+ from app.services import auth_service, visualizacao_service
13
 
14
  app = FastAPI(
15
  title="MESA Frame API",
 
57
  app.include_router(logs.router)
58
 
59
 
60
+ @app.on_event("startup")
61
+ async def schedule_app_support_warmup() -> None:
62
+ # Keep startup/login responsive and warm shared caches shortly after readiness.
63
+ visualizacao_service.schedule_visualizacao_support_warmup(delay_seconds=1.0)
64
+
65
+
66
  def _mount_frontend_if_exists() -> None:
67
  frontend_dist = resolve_frontend_dist_dir()
68
  index_file = frontend_dist / "index.html"
backend/app/services/pesquisa_service.py CHANGED
@@ -9,7 +9,7 @@ import unicodedata
9
  from dataclasses import dataclass
10
  from datetime import date, datetime
11
  from pathlib import Path
12
- from threading import Lock
13
  from typing import Any
14
 
15
  import folium
@@ -231,6 +231,7 @@ _ADMIN_FONTES_SESSION: dict[str, list[str]] = {}
231
  _CATALOGO_VIAS_CACHE: list[dict[str, Any]] | None = None
232
  _FAMILIAS_VERSOES_CACHE: dict[str, list[dict[str, Any]]] | None = None
233
  _FAMILIAS_VERSOES_SIGNATURE: tuple[str, tuple[tuple[str, int, int], ...]] | None = None
 
234
 
235
 
236
  def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
@@ -601,12 +602,11 @@ def _montar_familias_versoes_modelos(caminhos_modelo: list[Path]) -> dict[str, l
601
  familias: dict[str, list[dict[str, Any]]] = {}
602
 
603
  for caminho in caminhos_modelo:
604
- resumo = _carregar_resumo_com_cache(caminho)
605
  info = _extrair_info_versao_modelo(
606
  {
607
- "id": resumo.get("id") or caminho.stem,
608
  "arquivo": caminho.name,
609
- "nome_modelo": resumo.get("nome_modelo") or caminho.stem,
610
  }
611
  )
612
  if info is None:
@@ -618,7 +618,7 @@ def _montar_familias_versoes_modelos(caminhos_modelo: list[Path]) -> dict[str, l
618
  "sufixo": sufixo,
619
  "id": modelo_id,
620
  "arquivo": caminho.name,
621
- "nome_modelo": str(resumo.get("nome_modelo") or modelo_id).strip() or modelo_id,
622
  }
623
  )
624
 
@@ -646,26 +646,42 @@ def obter_familias_versoes_modelos_cache(caminhos_modelo: list[Path] | None = No
646
  caminhos = list(caminhos_modelo) if caminhos_modelo is not None else list(resolved.modelos_dir.glob("*.dai"))
647
  assinatura = (resolved.signature, _assinatura_modelos_familias(caminhos))
648
 
649
- with _CACHE_LOCK:
650
- if _FAMILIAS_VERSOES_SIGNATURE == assinatura and isinstance(_FAMILIAS_VERSOES_CACHE, dict):
651
- return {
652
- chave: [dict(item) for item in itens]
653
- for chave, itens in _FAMILIAS_VERSOES_CACHE.items()
654
- }
 
655
 
656
- familias = _montar_familias_versoes_modelos(caminhos)
 
 
 
 
 
 
657
 
658
- with _CACHE_LOCK:
659
- _FAMILIAS_VERSOES_SIGNATURE = assinatura
660
- _FAMILIAS_VERSOES_CACHE = {
661
- chave: [dict(item) for item in itens]
662
- for chave, itens in familias.items()
663
- }
 
 
 
 
 
 
 
 
 
 
 
 
664
 
665
- return {
666
- chave: [dict(item) for item in itens]
667
- for chave, itens in familias.items()
668
- }
669
 
670
 
671
  def _agrupar_nomes_modelo_por_familia(values: list[Any]) -> dict[str, list[str]]:
 
9
  from dataclasses import dataclass
10
  from datetime import date, datetime
11
  from pathlib import Path
12
+ from threading import Event, Lock
13
  from typing import Any
14
 
15
  import folium
 
231
  _CATALOGO_VIAS_CACHE: list[dict[str, Any]] | None = None
232
  _FAMILIAS_VERSOES_CACHE: dict[str, list[dict[str, Any]]] | None = None
233
  _FAMILIAS_VERSOES_SIGNATURE: tuple[str, tuple[tuple[str, int, int], ...]] | None = None
234
+ _FAMILIAS_VERSOES_INFLIGHT: dict[tuple[str, tuple[tuple[str, int, int], ...]], Event] = {}
235
 
236
 
237
  def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
 
602
  familias: dict[str, list[dict[str, Any]]] = {}
603
 
604
  for caminho in caminhos_modelo:
 
605
  info = _extrair_info_versao_modelo(
606
  {
607
+ "id": caminho.stem,
608
  "arquivo": caminho.name,
609
+ "nome_modelo": caminho.stem,
610
  }
611
  )
612
  if info is None:
 
618
  "sufixo": sufixo,
619
  "id": modelo_id,
620
  "arquivo": caminho.name,
621
+ "nome_modelo": modelo_id,
622
  }
623
  )
624
 
 
646
  caminhos = list(caminhos_modelo) if caminhos_modelo is not None else list(resolved.modelos_dir.glob("*.dai"))
647
  assinatura = (resolved.signature, _assinatura_modelos_familias(caminhos))
648
 
649
+ while True:
650
+ with _CACHE_LOCK:
651
+ if _FAMILIAS_VERSOES_SIGNATURE == assinatura and isinstance(_FAMILIAS_VERSOES_CACHE, dict):
652
+ return {
653
+ chave: [dict(item) for item in itens]
654
+ for chave, itens in _FAMILIAS_VERSOES_CACHE.items()
655
+ }
656
 
657
+ inflight = _FAMILIAS_VERSOES_INFLIGHT.get(assinatura)
658
+ if inflight is None:
659
+ inflight = Event()
660
+ _FAMILIAS_VERSOES_INFLIGHT[assinatura] = inflight
661
+ is_builder = True
662
+ else:
663
+ is_builder = False
664
 
665
+ if is_builder:
666
+ try:
667
+ familias = _montar_familias_versoes_modelos(caminhos)
668
+ with _CACHE_LOCK:
669
+ _FAMILIAS_VERSOES_SIGNATURE = assinatura
670
+ _FAMILIAS_VERSOES_CACHE = {
671
+ chave: [dict(item) for item in itens]
672
+ for chave, itens in familias.items()
673
+ }
674
+ return {
675
+ chave: [dict(item) for item in itens]
676
+ for chave, itens in familias.items()
677
+ }
678
+ finally:
679
+ with _CACHE_LOCK:
680
+ waiter = _FAMILIAS_VERSOES_INFLIGHT.pop(assinatura, None)
681
+ if waiter is not None:
682
+ waiter.set()
683
 
684
+ inflight.wait()
 
 
 
685
 
686
 
687
  def _agrupar_nomes_modelo_por_familia(values: list[Any]) -> dict[str, list[str]]:
backend/app/services/visualizacao_service.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  from pathlib import Path
 
4
  from threading import Lock, Thread
5
  from typing import Any
6
 
@@ -31,6 +32,7 @@ from app.core.elaboracao.core import (
31
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
32
  from app.core.visualizacao.map_payload import build_leaflet_payload, build_visualizacao_map_payload
33
  from app.models.session import SessionState
 
34
  from app.services import model_repository, pesquisa_service, trabalhos_tecnicos_service
35
  from app.services.equacao_service import build_equacoes_payload, exportar_planilha_equacao
36
  from app.services.knn_avaliacao_service import estimar_valor_knn_avaliacao
@@ -421,7 +423,7 @@ def listar_modelos_repositorio() -> dict[str, Any]:
421
  return sanitize_value(model_repository.list_repository_models())
422
 
423
 
424
- def _warm_visualizacao_support_caches_async() -> None:
425
  global _MAP_SUPPORT_WARMUP_SIGNATURE
426
  try:
427
  signature = model_repository.resolve_model_repository().signature
@@ -434,16 +436,19 @@ def _warm_visualizacao_support_caches_async() -> None:
434
  _MAP_SUPPORT_WARMUP_SIGNATURE = signature
435
 
436
  def _worker() -> None:
 
 
 
437
  try:
438
  get_bairros_geojson()
439
- except Exception:
440
- pass
441
  try:
442
  pesquisa_service.obter_familias_versoes_modelos_cache()
443
- except Exception:
444
- pass
445
 
446
- Thread(target=_worker, name="mesa-visualizacao-map-warmup", daemon=True).start()
447
 
448
 
449
  def carregar_modelo_repositorio(session: SessionState, modelo_id: str) -> dict[str, Any]:
@@ -473,7 +478,6 @@ def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, An
473
  session.dados_visualizacao = None
474
  session.avaliacoes_visualizacao = []
475
  session.visualizacao_cache = {}
476
- _warm_visualizacao_support_caches_async()
477
 
478
  nome_modelo = Path(caminho_arquivo).stem
479
  badge_html = viz_app._formatar_badge_completo(pacote, nome_modelo=nome_modelo)
 
1
  from __future__ import annotations
2
 
3
  from pathlib import Path
4
+ from time import sleep
5
  from threading import Lock, Thread
6
  from typing import Any
7
 
 
32
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
33
  from app.core.visualizacao.map_payload import build_leaflet_payload, build_visualizacao_map_payload
34
  from app.models.session import SessionState
35
+ from app.runtime_log import append_runtime_log
36
  from app.services import model_repository, pesquisa_service, trabalhos_tecnicos_service
37
  from app.services.equacao_service import build_equacoes_payload, exportar_planilha_equacao
38
  from app.services.knn_avaliacao_service import estimar_valor_knn_avaliacao
 
423
  return sanitize_value(model_repository.list_repository_models())
424
 
425
 
426
+ def schedule_visualizacao_support_warmup(*, delay_seconds: float = 0.0) -> None:
427
  global _MAP_SUPPORT_WARMUP_SIGNATURE
428
  try:
429
  signature = model_repository.resolve_model_repository().signature
 
436
  _MAP_SUPPORT_WARMUP_SIGNATURE = signature
437
 
438
  def _worker() -> None:
439
+ delay = max(0.0, float(delay_seconds))
440
+ if delay > 0:
441
+ sleep(delay)
442
  try:
443
  get_bairros_geojson()
444
+ except Exception as exc:
445
+ append_runtime_log(f"[mesa] warmup: falha ao aquecer bairros: {exc}")
446
  try:
447
  pesquisa_service.obter_familias_versoes_modelos_cache()
448
+ except Exception as exc:
449
+ append_runtime_log(f"[mesa] warmup: falha ao aquecer familias de modelos: {exc}")
450
 
451
+ Thread(target=_worker, name="mesa-visualizacao-support-warmup", daemon=True).start()
452
 
453
 
454
  def carregar_modelo_repositorio(session: SessionState, modelo_id: str) -> dict[str, Any]:
 
478
  session.dados_visualizacao = None
479
  session.avaliacoes_visualizacao = []
480
  session.visualizacao_cache = {}
 
481
 
482
  nome_modelo = Path(caminho_arquivo).stem
483
  badge_html = viz_app._formatar_badge_completo(pacote, nome_modelo=nome_modelo)