Diagnóstico de performance — Relatórios de Emplacamento (medição real em prod)
Investigação feita em 2026-08-01, branch chore/vona/debug-performance-relatorios-emplacamento,
em resposta a relato geral de lentidão navegando no sistema. Objetivo: descobrir se o
gargalo é frontend ou backend/query, com dados reais (não estimativa de leitura de
código). Complementa perf-por-marca.md (que já documentava a
mesma suspeita por leitura estática de código, sem acesso ao DW na época) com números
medidos direto em produção via MCP do Supabase.
Metodologia
- Login real em
novoemplacamento.abracaf.com.br(prod) via browser automation, medindo TTFB/timing do Dashboard KPI (emplacamento/home/[account]/dashboard-kpi/) com Performance API do navegador. mcp__supabase__get_logs(service: postgres)no projeto de prod (tmyxcpfhmupflbwikfcw) — logs reais das últimas 24h, incluindoEXPLAINde queries lentas (Postgres loga automaticamenteduration + plande queries acima de um threshold).mcp__supabase__get_advisors(type: performance)no mesmo projeto.
Pré-requisito confirmado: dw_emplacamento_mart/dw_emplacamento não é um Postgres
externo — é o mesmo projeto Supabase de prod, acessado via postgres.js direto
(Supavisor) em vez de supabase-js (SUPA_DB_HOST em
reports/por-territorio/_lib/server/supabase-dw-client.ts). Por isso os advisors/logs
desse projeto cobrem essas queries.
Resultado — não é frontend
TTFB do shell do Dashboard KPI: 88ms. DOMContentLoaded em ~960ms. O documento HTML
inicial é rápido — o componente é client-rendered com skeleton
(DashboardKpiProvider/DashboardKpiView) e dispara a Server Action getDashboardKpiData
via useEffect; essa chamada, uma vez disparada, completa em 170–466ms nas amostras
capturadas. Não há evidência de gargalo relevante no bundle/hidratação/render.
(Medição de "quanto tempo até a Server Action disparar" ficou contaminada por throttling de aba em background da automação do browser — não é um número confiável e foi descartada. O que importa é que, uma vez a chamada é feita, ela é rápida.)
Resultado — é a query. Confirmado nos logs de prod
Queries reais contra dw_emplacamento_mart.emplacamento_kpi_dia e
dw_emplacamento.fato_emplacamentos, capturadas nos logs do Postgres de prod das
últimas 24h:
| Query | Duração | Plano |
|---|---|---|
SELECT DISTINCT modalidade FROM emplacamento_kpi_dia WHERE modalidade IS NOT NULL | 23,1s e 25,7s (rodou 2× nas últimas 24h) | Parallel Index Only Scan em 1.590.342 linhas do índice emplacamento_kpi_dia_data_fabricante_codigo_grupomodeloveic_key — índice existe mas não cobre bem esse filtro (tem que varrer quase tudo) |
array_agg(DISTINCT municipio/estado/regiao_operacional/regiao_geografica/regiao_metropolitana/area_influencia_nome) sem WHERE | 19,0s e 16,0s | Seq Scan completo em 2.703.582 linhas — sem índice algum ajuda aqui, é scan puro pra montar opções de filtro |
Queries de por-territorio (fato_emplacamentos filtrado por local_sk = ANY(array de 27 ids), GROUP BY local_sk, mes/ano) | 10,7s, 12,1s, 10,9s | Parallel Bitmap/Seq Scan nas partições fato_emplacamentos_2025/_2026 (centenas de milhares a milhões de linhas por partição) |
As duas primeiras são exatamente as funções de popular dropdown de filtro
(getFieldsAreaAbrangencia e a de modalidade, ambas em report-utils.tsx — já citadas
em perf-por-marca.md
como as mais caras das "sem cache"). Rodam pra retornar meia dúzia de valores distintos,
mas pagam scan de milhões de linhas porque não existe estrutura (índice cobrindo a coluna
filtrada, ou tabela de lookup materializada) que evite isso.
Advisors — infraestrutura que agrava
dw_emplacamento_mart.emplacamento_kpi_dianão tem primary key.- Índice
ix_emplacamento_kpi_dia_fabricantena mesma tabela nunca foi usado — não bate com os padrões de query reais (filtro pordata/ano/mes,DISTINCTmodalidade,array_aggde campos de território). - O único índice realmente utilizado (
emplacamento_kpi_dia_data_fabricante_codigo_grupomodeloveic_key, visto no plano acima) serve de índice-cobertura genérico, mas força index-only scan de quase a tabela inteira quando o filtro não é pordata/fabricante/codigo(ex.:modalidade).
Sinal de cache não efetivo em prod
A mesma query de modalidade (SELECT DISTINCT modalidade ...) aparece 2 vezes na
janela de 24h de log, com durações parecidas (23,1s / 25,7s) — ambas pagando o custo
completo do scan. Essas funções (report-utils.tsx) estão marcadas como já cacheadas em
perf-por-marca.md item 1
(cacheService.wrap, tag REPORT_TAGS.MARCA, TTL CACHE_TTL.HISTORICO = 3600s). Rodar
2× em 24h não é necessariamente uma falha de cache (pode ser cache-miss legítimo após
revalidação ou deploy) — mas junto com o fato de a tag MARCA ser reusada por múltiplas
rotas (achado do levantamento de código anterior a este ciclo, via agente Explore), é um
ponto que merece confirmação antes de assumir que o cache está protegendo essas queries
em todos os casos de uso.
Outros achados (não relacionados a query)
- 2 requests de prefetch da sidebar retornaram HTTP 503:
/emplacamento/home/abracaf/usuariose/emplacamento/home/abracaf/chat. Não investigado a fundo neste ciclo — pode ser transiente (prefetch sob carga) ou sintoma relacionado. Vale checarget_logs(service: api)num ciclo futuro se persistir. - Pool do DW client é
max: 3, semstatement_timeout(Supavisor em prod rejeita esse parâmetro de startup — comentário no código confirma). Combinado com queries de 10–26s, cada uma dessas queries lentas prende 1 dos 3 slots do pool por um tempo longo; sob concorrência de poucos usuários simultâneos já dá pra esgotar o pool.
Conclusão
O gargalo de "lentidão geral" é backend/query, não frontend. Especificamente: as
funções que populam opções de filtro (modalidade, área de abrangência) fazem
SELECT DISTINCT/array_agg sem filtro sobre tabelas de 1,6–2,7 milhões de linhas,
sem índice adequado, custando 16–26 segundos por execução. As queries de por-território
somam mais 10–12s cada quando disparadas. Isso bate com a suspeita já registrada em
perf-por-marca.md e nos "Itens que exigem o time de dados"
daquele doc — agora com números reais de prod em vez de estimativa de leitura de código.
Não implementado neste ciclo
Por decisão explícita, este ciclo foi só de diagnóstico — nenhuma mudança de código ou schema foi feita. Candidatos a próximo passo (não iniciados):
- Materializar os valores distintos de filtro (
modalidade,municipio,estado,regiao_*,area_influencia_nome) numa tabela pequena de lookup atualizada pelo ETL, em vez deSELECT DISTINCT/array_aggao vivo sobre a tabela de fatos — elimina o scan de milhões de linhas por completo. - Confirmar com o time de dados se dá pra criar um índice em
dw_emplacamento_mart.emplacamento_kpi_diacobrindomodalidadeisoladamente (ou avaliar se compensa, dado o índice já existente e não usado). - Confirmar se a tag de cache
MARCAcompartilhada entre rotas está de fato evitando reexecução dessas queries em todos os fluxos que as chamam (auditoria de uso de tags, não coberta neste ciclo). - Investigar os 503 de prefetch da sidebar, separadamente (não seria explicado pelas queries lentas do DW).