Pular para o conteúdo principal

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

  1. 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.
  2. mcp__supabase__get_logs(service: postgres) no projeto de prod (tmyxcpfhmupflbwikfcw) — logs reais das últimas 24h, incluindo EXPLAIN de queries lentas (Postgres loga automaticamente duration + plan de queries acima de um threshold).
  3. 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:

QueryDuraçãoPlano
SELECT DISTINCT modalidade FROM emplacamento_kpi_dia WHERE modalidade IS NOT NULL23,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 WHERE19,0s e 16,0sSeq 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,9sParallel 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_dia não tem primary key.
  • Índice ix_emplacamento_kpi_dia_fabricante na mesma tabela nunca foi usado — não bate com os padrões de query reais (filtro por data/ano/mes, DISTINCT modalidade, array_agg de 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 é por data/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/usuarios e /emplacamento/home/abracaf/chat. Não investigado a fundo neste ciclo — pode ser transiente (prefetch sob carga) ou sintoma relacionado. Vale checar get_logs(service: api) num ciclo futuro se persistir.
  • Pool do DW client é max: 3, sem statement_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):

  1. Materializar os valores distintos de filtro (modalidade, municipio, estado, regiao_*, area_influencia_nome) numa tabela pequena de lookup atualizada pelo ETL, em vez de SELECT DISTINCT/array_agg ao vivo sobre a tabela de fatos — elimina o scan de milhões de linhas por completo.
  2. Confirmar com o time de dados se dá pra criar um índice em dw_emplacamento_mart.emplacamento_kpi_dia cobrindo modalidade isoladamente (ou avaliar se compensa, dado o índice já existente e não usado).
  3. Confirmar se a tag de cache MARCA compartilhada 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).
  4. Investigar os 503 de prefetch da sidebar, separadamente (não seria explicado pelas queries lentas do DW).