22 KiB
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.
Actualizacion TASK-225, 2026-06-10: la deuda P1 de stats search y stats player profile fue corregida en codigo para que los GET publicos usen player_search_index y player_period_stats en modo read-only estricto, sin inicializar storage ni caer a runtime fallback pesado. La medicion HTTP final requiere redeploy. current-match sigue pendiente porque depende de RCON live y debe tratarse como hardening/degradacion sin cambiar hosts, puertos ni configuracion RCON.
Conclusiones principales:
- El backend de
rankingya no muestra el cuello de botella grave del ranking anual. La evidencia mas fuerte es el testbackend/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.jsyfrontend/assets/js/stats.jsbloquean la carga principal detras de/health, yranking.jsno tiene proteccion contra request race ni limpieza robusta del estado de carga. historico.jses hoy la referencia mas sana del frontend publico: usa snapshots, cache TTL, deduplicacion de peticiones yrequestIdpara ignorar respuestas obsoletas.partida-actual.jsno bloquea por/health, pero hace polling agresivo y paralelo a tres endpoints (/api/current-match,/api/current-match/kills,/api/current-match/players) sinAbortController, 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 searchystats 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 sobrercon_match_player_stats, lo que aumenta riesgo de scans y de uso parcial de indices. current-matchsigue 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:
- Procesos internos leen RCON, AdminLog o scoreboard publico cuando aplica.
- Procesos internos refrescan snapshots y read models en PostgreSQL.
- Endpoints publicos leen solo read models publicos, sin recalculo runtime ni inicializacion de storage.
- Frontend pide datos principales sin bloquearlos por checks tecnicos no esenciales.
Flujo observado:
rankingystatsmezclan frontend secuencial con backend que aun puede caer a runtime sobre tablas materializadas si falta snapshot o read model.historicoya prioriza snapshots y fallback controlado.current-matchexpone 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-statses/api/current-match/players. - El equivalente real de
historicono 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 llamaloadRanking(). - Si
/healthtarda, falla o llega fuera de orden respecto a cambios del usuario, la UI puede permanecer enCargando ranking global...o en estado offline aunque/api/rankingsea rapido. - No hay
AbortControllernicurrentRequestId. - Cada cambio de filtro dispara
loadRanking()sin proteccion contra respuestas antiguas. - No hay
finallydedicado 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
/healthantes de cargar el ranking anual. searchPlayers()hace una sola request, pero cualquier error marca backend offline globalmente.loadPlayerProfile()usaPromise.allSettled()para semanal y mensual, lo cual es correcto, pero sigue dependiendo del flag globalisBackendOnline.- 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
activeServerRequestIdyactiveLeaderboardRequestId. - 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
innerHTMLcompleto en bloques grandes. - El endpoint llamado como
snapshots/leaderboarden 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
/healthprevio. - 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 noAbortController. - Si el usuario abandona la pagina sin descargar el documento, el polling sigue hasta destruir el contexto.
current-matchre-renderiza bloques completos coninnerHTML.kill feedyplayer statsreducen re-render convisibleSignature, 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 ahydrateTrailer()yrefreshServers(), 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.pyconfirma queget_annual_ranking_snapshot()no llamainitialize_rcon_materialized_storage()en lectura PostgreSQL. Ese fix elimina el cuello de botella mas grave ya conocido del ranking anual.routes.pysepara parseo/validacion de parametros y builders por endpoint de forma clara.main.pyya serializadateydatetimepara 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 alist_rcon_materialized_leaderboard()si falta snapshot y el flag runtime fallback sigue activo. list_rcon_materialized_leaderboard()hace agregacion runtime sobrercon_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()usaplayer_search_indexprimero, 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 yservers_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()usaplayer_period_statsprimero, 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()ybuild_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_activityconfirma 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_indexplayer_period_statsranking_snapshotsranking_snapshot_itemsrcon_annual_ranking_snapshotsrcon_annual_ranking_snapshot_items
Tablas grandes o candidatas a crecimiento
rcon_materialized_matchesrcon_match_player_stats
Indices confirmados por codigo
idx_ranking_snapshots_lookupidx_ranking_snapshot_items_snapshotidx_ranking_snapshot_items_playeridx_rcon_annual_ranking_snapshots_yearidx_rcon_annual_ranking_snapshots_statusidx_player_search_index_nameidx_player_search_index_last_seenidx_player_search_index_playeridx_player_period_stats_player_period_serveridx_player_period_stats_server_periodidx_player_period_stats_last_seenidx_player_period_stats_updatedidx_rcon_materialized_matches_recent- indices textuales sobre
COALESCE(CAST(ended_at AS TEXT), CAST(started_at AS TEXT)) idx_rcon_match_player_stats_matchidx_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. LIKEsobreLOWER(player_name)puede degradarse sin estrategia de indice orientada a busqueda parcial.- Agregaciones publicas sobre
COUNT(DISTINCT stats.match_key)ySUM(...)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_indexporserver_id + normalized_player_name - Lectura de
player_period_statsporplayer_id + period_type + server_id
Locks y refresh
- Los runners usan
backend_writer_lockpara 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/rankingweekly/monthly puede recalcular agregados runtime sobre tablas materializadas./api/stats/players/searchpuede escanear runtime si faltaplayer_search_index./api/stats/players/{player_id}puede recalcular runtime si faltaplayer_period_stats./api/current-matchconsulta RCON directo en request publico.historicosnapshot en modo rcon puede servir runtime fast path en lugar de snapshot real.
Observabilidad Recomendada
Medicion backend por request:
endpointstatus_codetotal_duration_msdb_duration_mspayload_build_duration_msquery_countread_modelfallback_usedfallback_reasonsnapshot_statuspayload_bytestimeframeserver_idmetriclimit
Medicion frontend por vista:
pagerequest_started_atresponse_received_atrender_completed_atperceived_latency_mshealth_blocked_main_databooleanrequest_abortedrequest_supersededrender_errorstale_response_ignored
Puntos concretos:
performance.mark()/performance.measure()enranking.js,stats.js,partida-actual.js- Logging estructurado por endpoint en
main.pyo wrapper de builders - Incluir
read_model,fallback_used,snapshot_statusypayload_bytesen 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
-
TASK-215-decouple-public-frontend-data-load-from-health-checksAlcance:ranking.js,stats.js. Objetivo: no bloquear datos principales por/health. -
TASK-216-add-request-race-protection-to-public-ranking-and-statsAlcance:ranking.js,stats.js. Objetivo:AbortControllerocurrentRequestId, cleanup robusto de loading. -
TASK-217-enforce-snapshot-only-public-ranking-read-pathAlcance: backend publico deranking. Objetivo: eliminar fallback runtime en/api/rankingpara weekly/monthly. -
TASK-218-enforce-read-model-only-public-player-search-and-profileAlcance:player_search_index,player_period_stats, runners, payloads. Objetivo: que search y profile no caigan a runtime en request publico. -
TASK-219-create-live-public-current-match-read-modelAlcance: current match. Objetivo: sacar RCON directo del endpoint publico. -
TASK-220-add-public-request-performance-observabilityAlcance: backend + frontend instrumentation minima. Objetivo: medir request, DB, payload, render y fallback. -
TASK-221-run-explain-analyze-for-public-runtime-and-read-model-queriesAlcance: SQL audit operativa. Objetivo: validar indices y predicados temporales.