From 2f262abfe8dc5ac8fe484c248a3eaccefdbeb481 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Wed, 10 Jun 2026 09:19:54 +0200 Subject: [PATCH] Add public query performance audit --- ...it-public-query-performance-bottlenecks.md | 134 ++++++ docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md | 402 ++++++++++++++++++ 2 files changed, 536 insertions(+) create mode 100644 ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md create mode 100644 docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md diff --git a/ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md b/ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md new file mode 100644 index 0000000..9abb832 --- /dev/null +++ b/ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md @@ -0,0 +1,134 @@ +--- +id: TASK-214 +title: Audit public query performance bottlenecks +status: done +type: research +team: Analista +supporting_teams: + - Frontend Senior + - Backend Senior + - Arquitecto de Base de Datos +roadmap_item: foundation +priority: high +--- + +# TASK-214 - Audit public query performance bottlenecks + +## Goal + +Auditar los posibles cuellos de botella de las consultas publicas de HLL Vietnam y producir un analisis tecnico accionable para futuras optimizaciones, sin implementar cambios funcionales todavia. + +## Context + +El ranking anual ya sufrio un cuello de botella grave y quedo corregido al sacar `initialize_rcon_materialized_storage()` del path publico de lectura anual en PostgreSQL. Tras ese fix, las latencias confirmadas de API de ranking son bajas, pero la UI publica aun puede percibirse lenta o quedarse temporalmente en estados de carga. + +La regla arquitectonica objetivo de esta auditoria es explicita: + +- los endpoints publicos deben leer read models propios en PostgreSQL +- no deben consultar RCON directo +- no deben consultar scoreboard publico +- no deben recalcular rankings en runtime +- no deben escanear `rcon_match_player_stats` salvo en procesos internos +- no deben inicializar o migrar storage dentro de request publico +- no deben bloquearse por health checks no necesarios + +Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution. + +## Steps + +1. Revisar primero la documentacion base, las rutas publicas y los frontends que las consumen. +2. Auditar endpoints publicos, fallbacks, read models, polling, cargas secuenciales y riesgos de runtime. +3. Redactar el informe tecnico en `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md`. +4. Cerrar esta task en `ai/tasks/done/` documentando hallazgos, validacion y exclusiones. + +## Files to Read First + +- `AGENTS.md` +- `ai/repo-context.md` +- `ai/architecture-index.md` +- `docs/decisions.md` +- `backend/app/routes.py` +- `backend/app/payloads.py` + +## Expected Files to Modify + +- `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md` +- `ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md` + +## Constraints + +- Keep the change minimal. +- Preserve HLL Vietnam project identity. +- Do not introduce unnecessary frameworks or dependencies. +- Do not implement backend functionality unless the task explicitly requires it. +- Do not expand Elo/MMR, historical workers or RCON server #03 handling unless the task explicitly requires it. +- Do not overwrite repository-specific context with generic platform template text. +- No ejecutar `ai-platform run`. +- No implementar optimizaciones funcionales todavia. +- No cambiar comportamiento de endpoints. +- No tocar frontend funcional. +- No tocar backend funcional. +- No tocar tests. +- No tocar assets de armas. +- No tocar SVGs. +- No modificar imagenes fisicas. +- No reactivar Elo/MMR. +- No reintroducir Comunidad Hispana #03. +- No hacer `push`. + +## Validation + +Before completing the task ensure: + +- `git status --short --untracked-files=all` +- `git diff --name-only` +- confirmar que solo se crean o modifican: + - `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md` + - `ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md` +- no unrelated files were modified +- documentation remains consistent with the repository state + +## Outcome + +Archivos creados: + +- `docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md` +- `ai/tasks/done/TASK-214-audit-public-query-performance-bottlenecks.md` + +Resumen de hallazgos: + +- `ranking.js` y `stats.js` bloquean carga principal detras de `/health`. +- `ranking.js` no protege request race ni estados de carga obsoletos. +- `/api/ranking` weekly/monthly conserva fallback runtime sobre tablas materializadas. +- search y profile de `stats` pueden caer a runtime si faltan read models dedicados. +- `historico.js` ya muestra un patron frontend mas sano: cache, deduplicacion y `requestId`. +- `/api/current-match` sigue consultando RCON directo en request publico. +- `partida-actual.js` hace polling agresivo a tres endpoints en paralelo. +- varias queries runtime filtran con `COALESCE(CAST(... AS TEXT))`, con riesgo de degradar indices. + +Validacion ejecutada: + +- lectura de contexto base: `AGENTS.md`, `ai/repo-context.md`, `ai/architecture-index.md`, `docs/decisions.md` +- lectura de endpoints y builders: `backend/app/routes.py`, `backend/app/payloads.py` +- lectura de modulos clave: `backend/app/rcon_annual_rankings.py`, `backend/app/rcon_historical_leaderboards.py`, `backend/app/rcon_historical_player_stats.py`, `backend/app/rcon_historical_read_model.py`, `backend/app/postgres_rcon_storage.py`, `backend/app/historical_runner.py`, `backend/app/main.py`, `backend/app/config.py` +- lectura de frontends clave: `frontend/assets/js/ranking.js`, `stats.js`, `historico.js`, `historico-partida.js`, `partida-actual.js`, `main.js` +- lectura de paginas clave: `frontend/ranking.html`, `stats.html`, `historico.html`, `partida-actual.html` +- lectura de tests relevantes: `backend/tests/test_annual_ranking_payload.py`, `test_rcon_materialization_pipeline.py`, `test_historical_snapshot_refresh.py` +- validacion de alcance con `git status --short --untracked-files=all` y `git diff --name-only` + +Confirmaciones de exclusiones: + +- no se toco codigo funcional de frontend ni backend +- no se tocaron tests +- no se tocaron endpoints ni contratos +- no se tocaron assets de armas, SVGs ni imagenes fisicas +- no se reactivo Elo/MMR +- no se reintrodujo Comunidad Hispana #03 +- no se ejecuto `ai-platform run` +- no se hizo `push` + +## Change Budget + +- Prefer fewer than 5 modified files. +- Prefer changes under 200 lines when feasible. +- Split the work into follow-up tasks if limits are exceeded. diff --git a/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md b/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md new file mode 100644 index 0000000..8e09641 --- /dev/null +++ b/docs/PERFORMANCE_PUBLIC_QUERY_AUDIT.md @@ -0,0 +1,402 @@ +# Performance Public Query Audit + +## Resumen Ejecutivo + +Esta auditoria revisa las rutas publicas que alimentan `ranking`, `stats`, `historico`, `historico-partida`, `partida-actual` e `index`, con foco en latencia percibida por UI y en el cumplimiento de la regla arquitectonica de leer read models publicos propios en PostgreSQL. + +Conclusiones principales: + +- El backend de `ranking` ya no muestra el cuello de botella grave del ranking anual. La evidencia mas fuerte es el test `backend/tests/test_annual_ranking_payload.py`, que confirma que la lectura anual en PostgreSQL ya no inicializa storage en request publico. +- El cuello de botella visible actual mas claro esta en frontend: `frontend/assets/js/ranking.js` y `frontend/assets/js/stats.js` bloquean la carga principal detras de `/health`, y `ranking.js` no tiene proteccion contra request race ni limpieza robusta del estado de carga. +- `historico.js` es hoy la referencia mas sana del frontend publico: usa snapshots, cache TTL, deduplicacion de peticiones y `requestId` para ignorar respuestas obsoletas. +- `partida-actual.js` no bloquea por `/health`, pero hace polling agresivo y paralelo a tres endpoints (`/api/current-match`, `/api/current-match/kills`, `/api/current-match/players`) sin `AbortController`, con intervalos de 1.5 s y 3 s que pueden amplificar carga y re-render innecesario. +- En backend siguen existiendo fallbacks runtime publicos sobre tablas materializadas grandes para `ranking`, `stats search` y `stats player profile`. Son mejores que consultar RCON directo, pero siguen rompiendo la meta de servir lecturas publicas desde read models dedicados. +- Las queries runtime de leaderboard y player stats usan patrones como `COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT))` y agregaciones sobre `rcon_match_player_stats`, lo que aumenta riesgo de scans y de uso parcial de indices. +- `current-match` sigue consultando RCON directo en request publico cuando hay target confiable. Eso contradice la regla objetivo de esta auditoria y debe tratarse como deuda arquitectonica explicita aunque hoy sea un requisito funcional de la pagina live. + +## Mapa de Arquitectura de Lectura Publica + +Flujo esperado: + +1. Procesos internos leen RCON, AdminLog o scoreboard publico cuando aplica. +2. Procesos internos refrescan snapshots y read models en PostgreSQL. +3. Endpoints publicos leen solo read models publicos, sin recalculo runtime ni inicializacion de storage. +4. Frontend pide datos principales sin bloquearlos por checks tecnicos no esenciales. + +Flujo observado: + +- `ranking` y `stats` mezclan frontend secuencial con backend que aun puede caer a runtime sobre tablas materializadas si falta snapshot o read model. +- `historico` ya prioriza snapshots y fallback controlado. +- `current-match` expone una excepcion relevante: consulta RCON directo en la ruta publica cuando encuentra target valido. + +## Inventario de Endpoints Publicos + +| Ruta | Frontend | Builder backend | Read model esperado | Lectura observada | Fallback/runtime | Riesgo | +|---|---|---|---|---|---|---| +| `/api/ranking` | `frontend/assets/js/ranking.js` | `build_global_ranking_payload()` | `ranking_snapshots`, `ranking_snapshot_items`, `rcon_annual_ranking_snapshots`, `rcon_annual_ranking_snapshot_items` | Annual lee snapshot anual; weekly/monthly leen snapshot y pueden caer a `list_rcon_materialized_leaderboard()` | Si `HLL_BACKEND_RANKING_RUNTIME_FALLBACK_ENABLED=true`, si | P0 | +| `/api/stats/players/search` | `frontend/assets/js/stats.js` | `build_stats_player_search_payload()` | `player_search_index` | Primero `player_search_index`, si falla cae a runtime contra `rcon_match_player_stats` + `rcon_materialized_matches` | Si | P1 | +| `/api/stats/players/{player_id}` | `frontend/assets/js/stats.js` | `build_stats_player_profile_payload()` | `player_period_stats` | Primero `player_period_stats`, si falta cae a runtime y ademas calcula weekly/monthly ranking en caliente | Si | P1 | +| `/api/stats/rankings/annual` | `frontend/assets/js/stats.js` | `build_annual_ranking_snapshot_payload()` | `rcon_annual_ranking_snapshots`, `rcon_annual_ranking_snapshot_items` | Snapshot anual | No recalcula ranking en lectura | P2 | +| `/api/historical/snapshots/leaderboard` | `frontend/assets/js/historico.js` | `build_rcon_materialized_leaderboard_snapshot_payload()` en modo rcon | Snapshot historico publico equivalente | En modo rcon el nombre dice snapshot, pero sirve runtime fast path sobre materialized leaderboard | Si, por definicion del endpoint en modo rcon | P1 | +| `/api/historical/snapshots/recent-matches` | `frontend/assets/js/historico.js` | `build_recent_historical_matches_snapshot_payload()` | Snapshot publico de recent matches | Segun source kind puede usar RCON read model o fallback publico | Si | P2 | +| `/api/historical/matches/detail` | `frontend/assets/js/historico-partida.js` | `build_historical_match_detail_payload()` | Read model detalle de partida | Intenta `get_rcon_historical_match_detail()`, luego fallback a storage historico publico | Si | P2 | +| `/api/current-match` | `frontend/assets/js/partida-actual.js` | `build_current_match_payload()` | Read model live propio | Primero intenta `_query_current_match_rcon_sample()` directo; luego fallback a `/api/servers` snapshot | Si, y toca RCON directo | P0 | +| `/api/current-match/kills` | `frontend/assets/js/partida-actual.js` | `build_current_match_kill_feed_payload()` | Read model live propio de kill feed | `list_current_match_kill_feed()` desde evidencia AdminLog materializada | No se observo fallback a scoreboard | P1 | +| `/api/current-match/players` | `frontend/assets/js/partida-actual.js` | `build_current_match_player_stats_payload()` | Read model live propio de player stats | `list_current_match_player_stats()` desde evidencia AdminLog materializada | No se observo fallback a scoreboard | P1 | +| `/api/servers` | `frontend/assets/js/main.js`, fallback de current-match | `build_servers_payload()` | Snapshot live de servidores | Snapshot local/live payload | Sin fallback costoso visible | P2 | +| `/health` | `frontend/assets/js/main.js`, `ranking.js`, `stats.js` | `build_health_payload()` | N/A | Check tecnico | No aplica | P1 por bloqueo UI, no por backend | + +Notas: + +- El endpoint equivalente real al pedido como `/api/current-match/player-stats` es `/api/current-match/players`. +- El equivalente real de `historico` no es una sola ruta `/api/historico`; el frontend consume varias rutas bajo `/api/historical/...`. + +## Analisis Frontend + +### Ranking + +Archivo: `frontend/assets/js/ranking.js` + +Hallazgos: + +- La carga inicial depende de `refreshBackendHealth()` y solo despues llama `loadRanking()`. +- Si `/health` tarda, falla o llega fuera de orden respecto a cambios del usuario, la UI puede permanecer en `Cargando ranking global...` o en estado offline aunque `/api/ranking` sea rapido. +- No hay `AbortController` ni `currentRequestId`. +- Cada cambio de filtro dispara `loadRanking()` sin proteccion contra respuestas antiguas. +- No hay `finally` dedicado para limpiar loading o reactivar controles. +- `clearRankingSurface()` vacia tabla y meta antes de cada request, amplificando el parpadeo de UI. + +Impacto: + +- Alto en latencia percibida. +- Alto en riesgo de estado obsoleto. + +### Stats + +Archivo: `frontend/assets/js/stats.js` + +Hallazgos: + +- Repite el patron de esperar `/health` antes de cargar el ranking anual. +- `searchPlayers()` hace una sola request, pero cualquier error marca backend offline globalmente. +- `loadPlayerProfile()` usa `Promise.allSettled()` para semanal y mensual, lo cual es correcto, pero sigue dependiendo del flag global `isBackendOnline`. +- No hay cancelacion de busquedas sucesivas ni de perfiles sucesivos. + +Impacto: + +- Alto en UX. +- Medio en carga backend. + +### Historico + +Archivo: `frontend/assets/js/historico.js` + +Fortalezas observadas: + +- Usa `activeServerRequestId` y `activeLeaderboardRequestId`. +- Cachea snapshots con TTL. +- Deduplica peticiones en `pendingRequestCache`. +- Hidrata desde cache y luego refresca. +- No bloquea la carga principal detras de `/health`. + +Riesgos residuales: + +- Mucho uso de `innerHTML` completo en bloques grandes. +- El endpoint llamado como `snapshots/leaderboard` en modo rcon puede ser runtime fast path, lo que hace que la UI se vea sana aunque backend no este sirviendo un snapshot real. + +### Historico Partida + +Archivo: `frontend/assets/js/historico-partida.js` + +Hallazgos: + +- Hace una sola request principal de detalle. +- No hay `/health` previo. +- El costo fuerte parece mas de render DOM que de orchestration. + +Riesgo: + +- Bajo a medio. + +### Partida Actual + +Archivo: `frontend/assets/js/partida-actual.js` + +Hallazgos: + +- Polling en tres loops independientes: + - current match cada 30 s + - kills cada 1.5 s + - players cada 3 s +- Hay guardas `*_RefreshInFlight`, pero no `AbortController`. +- Si el usuario abandona la pagina sin descargar el documento, el polling sigue hasta destruir el contexto. +- `current-match` re-renderiza bloques completos con `innerHTML`. +- `kill feed` y `player stats` reducen re-render con `visibleSignature`, lo cual ayuda. + +Riesgo: + +- Alto para carga sostenida. +- Alto si la base live comparte recursos con lecturas publicas. + +### Landing + +Archivo: `frontend/assets/js/main.js` + +Hallazgos: + +- `fetchHealth()` es paralelo a `hydrateTrailer()` y `refreshServers()`, no bloqueante. +- El polling de servidores es cada 300 s y tiene guardas `serverRefreshInFlight`. + +Riesgo: + +- Bajo. + +## Analisis Backend + +### Confirmaciones positivas + +- `backend/tests/test_annual_ranking_payload.py` confirma que `get_annual_ranking_snapshot()` no llama `initialize_rcon_materialized_storage()` en lectura PostgreSQL. Ese fix elimina el cuello de botella mas grave ya conocido del ranking anual. +- `routes.py` separa parseo/validacion de parametros y builders por endpoint de forma clara. +- `main.py` ya serializa `date` y `datetime` para no abortar respuestas por JSON. + +### Riesgos de backend por area + +#### Ranking publico + +Archivos: `backend/app/payloads.py`, `backend/app/rcon_historical_leaderboards.py`, `backend/app/rcon_annual_rankings.py` + +Hallazgos: + +- Weekly/monthly `build_global_ranking_payload()` puede caer a `list_rcon_materialized_leaderboard()` si falta snapshot y el flag runtime fallback sigue activo. +- `list_rcon_materialized_leaderboard()` hace agregacion runtime sobre `rcon_match_player_stats` + `rcon_materialized_matches`. +- Las ventanas usan filtros sobre `COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT))`, lo que puede degradar indices y elevar costo. + +Riesgo: + +- El endpoint publico sigue siendo rapido hoy en muchos casos, pero no esta totalmente blindado contra crecimiento de datos. + +#### Stats search + +Archivo: `backend/app/rcon_historical_player_stats.py` + +Hallazgos: + +- `search_rcon_materialized_players()` usa `player_search_index` primero, bien. +- Si el read model falla o falta, cae a runtime contra tablas grandes. +- La busqueda runtime usa `LOWER(COALESCE(stats.player_name,'')) LIKE LOWER(?)`, agregacion por jugador y luego lookups adicionales de nombres y `servers_seen`. + +Riesgo: + +- Posible scan costoso y plan poco estable al crecer el historico. + +#### Stats player profile + +Archivo: `backend/app/rcon_historical_player_stats.py` + +Hallazgos: + +- `get_rcon_materialized_player_stats()` usa `player_period_stats` primero, bien. +- Si falta, cae a runtime y ademas consulta ranking semanal y mensual del jugador contra tablas materializadas. +- La ruta runtime agrega sobre stats, busca source range y ranking semanal/mensual en la misma lectura. + +Riesgo: + +- Latencia alta en cold path o con crecimiento de tabla. + +#### Historical endpoints + +Archivo: `backend/app/payloads.py` + +Hallazgos: + +- `build_recent_historical_matches_payload()` y `build_historical_match_detail_payload()` permiten fallback a storage publico cuando el read model RCON no cubre el caso. +- El test `test_public_scoreboard_fallback_used_only_without_rcon_activity` confirma que el fallback sigue activo y esperado. + +Riesgo: + +- Correcto como compatibilidad, pero dificulta garantizar tiempos uniformes y pureza de read model publico. + +#### Current match + +Archivo: `backend/app/payloads.py` + +Hallazgos: + +- `build_current_match_payload()` intenta `_query_current_match_rcon_sample()` en request publico. +- Solo si falla usa snapshot de `/api/servers`. + +Riesgo: + +- Este es el incumplimiento arquitectonico mas claro: request publico consultando RCON directo. + +#### Conexiones y cold path + +Archivos: `backend/app/postgres_rcon_storage.py`, `backend/app/config.py` + +Hallazgos: + +- Se usa `psycopg.connect(...)` por contexto, no se observa pool reutilizable. +- No se observan migrations/initializers pesados dentro de rutas publicas, salvo funciones de read model que aun llaman wrappers de initialize en algunas rutas runtime. + +Riesgo: + +- Medio. La ausencia de pooling puede no ser critica hoy, pero empeora cold starts y bursts cortos. + +## Analisis PostgreSQL / Read Models + +### Tablas publicas esperadas + +- `player_search_index` +- `player_period_stats` +- `ranking_snapshots` +- `ranking_snapshot_items` +- `rcon_annual_ranking_snapshots` +- `rcon_annual_ranking_snapshot_items` + +### Tablas grandes o candidatas a crecimiento + +- `rcon_materialized_matches` +- `rcon_match_player_stats` + +### Indices confirmados por codigo + +- `idx_ranking_snapshots_lookup` +- `idx_ranking_snapshot_items_snapshot` +- `idx_ranking_snapshot_items_player` +- `idx_rcon_annual_ranking_snapshots_year` +- `idx_rcon_annual_ranking_snapshots_status` +- `idx_player_search_index_name` +- `idx_player_search_index_last_seen` +- `idx_player_search_index_player` +- `idx_player_period_stats_player_period_server` +- `idx_player_period_stats_server_period` +- `idx_player_period_stats_last_seen` +- `idx_player_period_stats_updated` +- `idx_rcon_materialized_matches_recent` +- indices textuales sobre `COALESCE(CAST(ended_at AS TEXT), CAST(started_at AS TEXT))` +- `idx_rcon_match_player_stats_match` +- `idx_rcon_match_player_stats_player_id_match` + +### Riesgos de SQL + +- Varios paths runtime filtran por `COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT)) >= ?`. +- Ese patron tiende a forzar expresiones calculadas en filtro, incluso aunque existan indices funcionales; es un area clara para `EXPLAIN ANALYZE`. +- `LIKE` sobre `LOWER(player_name)` puede degradarse sin estrategia de indice orientada a busqueda parcial. +- Agregaciones publicas sobre `COUNT(DISTINCT stats.match_key)` y `SUM(...)` en tablas grandes deben salir de snapshots o read models, no del path web. + +### Queries candidatas a EXPLAIN ANALYZE + +- Runtime fallback de `list_rcon_materialized_leaderboard()` +- Runtime fallback de `_search_rcon_materialized_players_runtime()` +- Runtime fallback de `_get_rcon_materialized_player_stats_runtime()` +- Lectura de `get_latest_ranking_snapshot()` +- Lectura de `get_annual_ranking_snapshot()` +- Lectura de `player_search_index` por `server_id + normalized_player_name` +- Lectura de `player_period_stats` por `player_id + period_type + server_id` + +### Locks y refresh + +- Los runners usan `backend_writer_lock` para procesos internos, lo cual ayuda a writers. +- Aun asi, refreshes completos de snapshots y read models pueden competir por IO con lecturas publicas si comparten la misma base y no se mide el impacto. +- La auditoria no encontro evidencia directa de locks de lectura publica, pero si suficiente motivo para instrumentarlos. + +## Riesgos de Fallback / Runtime + +Principio deseado: + +- Publico: solo read models publicos. +- Interno: materializacion y recalculo. + +Desviaciones observadas: + +- `/api/ranking` weekly/monthly puede recalcular agregados runtime sobre tablas materializadas. +- `/api/stats/players/search` puede escanear runtime si falta `player_search_index`. +- `/api/stats/players/{player_id}` puede recalcular runtime si falta `player_period_stats`. +- `/api/current-match` consulta RCON directo en request publico. +- `historico` snapshot en modo rcon puede servir runtime fast path en lugar de snapshot real. + +## Observabilidad Recomendada + +Medicion backend por request: + +- `endpoint` +- `status_code` +- `total_duration_ms` +- `db_duration_ms` +- `payload_build_duration_ms` +- `query_count` +- `read_model` +- `fallback_used` +- `fallback_reason` +- `snapshot_status` +- `payload_bytes` +- `timeframe` +- `server_id` +- `metric` +- `limit` + +Medicion frontend por vista: + +- `page` +- `request_started_at` +- `response_received_at` +- `render_completed_at` +- `perceived_latency_ms` +- `health_blocked_main_data` boolean +- `request_aborted` +- `request_superseded` +- `render_error` +- `stale_response_ignored` + +Puntos concretos: + +- `performance.mark()` / `performance.measure()` en `ranking.js`, `stats.js`, `partida-actual.js` +- Logging estructurado por endpoint en `main.py` o wrapper de builders +- Incluir `read_model`, `fallback_used`, `snapshot_status` y `payload_bytes` en la respuesta o en logs +- Contador de queries por request en paths publicos costosos + +## Matriz de Hallazgos Priorizados + +| Pri | Hallazgo | Impacto | Evidencia | Archivo | Propuesta | Riesgo | Validacion | +|---|---|---|---|---|---|---|---| +| P0 | `ranking.js` bloquea la carga principal detras de `/health` y no protege carreras | UI lenta o bloqueada aunque `/api/ranking` responda en <200 ms | Flujo `refreshBackendHealth() -> loadRanking()` y ausencia de `AbortController/currentRequestId` | `frontend/assets/js/ranking.js` | Separar health del dato principal y usar cancelacion o requestId | Bajo | medir `response_received_at -> render_completed_at` | +| P0 | `/api/current-match` consulta RCON directo en request publico | Rompe la regla arquitectonica y puede introducir latencia o fragilidad externa | `_query_current_match_rcon_sample()` se ejecuta antes del fallback a snapshot | `backend/app/payloads.py` | Crear read model live publico y mover la consulta RCON al runner | Medio | endpoint debe seguir respondiendo sin tocar RCON | +| P1 | `/api/ranking` weekly/monthly mantiene fallback runtime sobre tablas materializadas | Riesgo de latencia creciente y scans | `build_global_ranking_payload()` cae a `list_rcon_materialized_leaderboard()` | `backend/app/payloads.py` | Desactivar fallback runtime en publico tras completar snapshot coverage | Medio | requests solo con `read_model=ranking-snapshot` | +| P1 | `stats.js` tambien bloquea por `/health` el ranking anual inicial | Latencia percibida innecesaria | `refreshBackendHealth()` llama `loadAnnualRanking()` despues de `/health` | `frontend/assets/js/stats.js` | Cargar anual directo y tratar `/health` como señal secundaria | Bajo | anual visible sin depender de health | +| P1 | Search de jugadores puede caer a runtime costoso | Picos de latencia en busqueda | `search_rcon_materialized_players()` con fallback runtime | `backend/app/rcon_historical_player_stats.py` | Endurecer cobertura de `player_search_index` y alertar si falta | Bajo | `fallback_used=false` sostenido | +| P1 | Perfil de jugador puede caer a runtime y recomponer rankings semanales/mensuales | Respuestas lentas y carga de DB | `get_rcon_materialized_player_stats()` | `backend/app/rcon_historical_player_stats.py` | Exigir `player_period_stats` actualizado antes de publicar | Medio | `read_model=player-period-stats` | +| P1 | Endpoints snapshot historicos en modo rcon no siempre son snapshots reales | Ambiguedad operativa y mediciones engañosas | `build_rcon_materialized_leaderboard_snapshot_payload()` declara snapshot pero usa runtime materialized fast path | `backend/app/rcon_historical_leaderboards.py` | Renombrar politica o servir snapshot real | Medio | source/generation policy coherentes | +| P2 | Polling de current match demasiado agresivo | Carga sostenida y re-render frecuente | intervalos de 1.5 s / 3 s / 30 s | `frontend/assets/js/partida-actual.js` | Consolidar polling o usar fan-out backend/cache | Medio | bajar requests por minuto | +| P2 | Paths runtime usan `COALESCE(CAST(... AS TEXT))` en filtros temporales | Planes menos eficientes | multiples queries runtime en leaderboard y player stats | `backend/app/rcon_historical_leaderboards.py`, `backend/app/rcon_historical_player_stats.py` | Revisar predicados e indices funcionales con EXPLAIN | Medio | comparar buffers/scan time | +| P3 | No se observa pooling PostgreSQL reutilizable | Mayor costo de cold path y bursts | `psycopg.connect()` por contexto | `backend/app/postgres_rcon_storage.py` | Evaluar pool ligero cuando el resto del path este estabilizado | Medio | medir connect time y throughput | + +## Plan de Tasks Recomendado + +1. `TASK-215-decouple-public-frontend-data-load-from-health-checks` + Alcance: `ranking.js`, `stats.js`. + Objetivo: no bloquear datos principales por `/health`. + +2. `TASK-216-add-request-race-protection-to-public-ranking-and-stats` + Alcance: `ranking.js`, `stats.js`. + Objetivo: `AbortController` o `currentRequestId`, cleanup robusto de loading. + +3. `TASK-217-enforce-snapshot-only-public-ranking-read-path` + Alcance: backend publico de `ranking`. + Objetivo: eliminar fallback runtime en `/api/ranking` para weekly/monthly. + +4. `TASK-218-enforce-read-model-only-public-player-search-and-profile` + Alcance: `player_search_index`, `player_period_stats`, runners, payloads. + Objetivo: que search y profile no caigan a runtime en request publico. + +5. `TASK-219-create-live-public-current-match-read-model` + Alcance: current match. + Objetivo: sacar RCON directo del endpoint publico. + +6. `TASK-220-add-public-request-performance-observability` + Alcance: backend + frontend instrumentation minima. + Objetivo: medir request, DB, payload, render y fallback. + +7. `TASK-221-run-explain-analyze-for-public-runtime-and-read-model-queries` + Alcance: SQL audit operativa. + Objetivo: validar indices y predicados temporales. +