# Avaliação sintética de carga e paginação da fila Seguimento: [optimização local, comparação final A/B/C e limitações](MODULE_ASSOCIATION_OPTIMIZATION_DELIVERY.md). A avaliação abaixo permanece a linha de base histórica; a comparação nova usa 1 000/5 000/10 000 registos em **cada** área e mede separadamente código e índices exclusivamente sintéticos. Não se considera validada a capacidade de implantação. 02/11/2026. Avaliação do código funcional aceite em **340f3bb5fa0e72d659abe43f3498b49e25ca3809**, sem o modificar. A paginação e os filtros devolveram os registos esperados nos casos concluídos, mas a leitura repetida da auditoria torna o tempo elevado com mais histórico. **Não se considera resolvida a capacidade de implantação.** ## Método e isolamento Novo ensaio explícito em `scripts/benchmarks/ModuleAssociationLoadTest.php`, fora das pastas da suite habitual. Executa pedidos autenticados pelo kernel HTTP no processo de testes, com sessões em memória e PostgreSQL real em loopback. Não mede transporte HTTP externo, login, TLS, navegador ou capacidade com utilizadores simultâneos. Não foram repetidos os testes válidos da entrega aceite nem o build. PHP **8.4.27**, Node fixado **24.22.1**, PostgreSQL **17.11**; base **sgf_tempo_test**, role **sgf_test_runner**, cluster **sgf_modular_regression_2576427d1939**, porta **55439**, aplicação de regressão em `C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/app`. Guardas conferidas antes de criar fixtures. A base persistente 8039, a instalação local e os seus seis registos protegidos não foram consultados ou alterados nesta passagem. Cada escala usa empresa/tenant/contas fictícias novas e rollback das fixtures; sem reset, migração, direitos públicos, alteração de índices ou decisões reais. Foram gerados eventos de associação/retirada **apenas como histórico sintético**, sem alterar os registos protegidos. Inclui ruído de outro tenant e uma prestação de unidade não seleccionada. | Clientes | Serviços | Prestações | Rascunhos de facturação | Eventos de associação/retirada | | ---: | ---: | ---: | ---: | ---: | | 1 000 | 100 | 100 | 100 | 7 376 | | 5 000 | 100 | 500 | 500 | 34 570 | | 10 000 | 100 | 1 000 | 1 000 | 68 579 | O estado é distribuído pelo ID sintético: cerca de um terço sem eventos, um terço associado e um terço com associações retiradas. Para os registos com eventos, quatro pares de associação/retirada e uma associação final nos activos. Os rascunhos referenciam cliente, serviço e, na facturação, prestação de origem. Não representa todas as possíveis distribuições de módulos ou histórico. Há aquecimento por escala, depois três amostras por cenário concluído. Limites de diagnóstico: 3 s por instrução SQL, 15 s por pedido observado entre consultas e 2 500 consultas. **Estes limites protegem o ensaio; não são SLA nem limites configurados no produto.** Após atingir um limite, não se repete esse cenário para obter uma mediana artificial. ## Resultados Medianas em segundos, incluindo instrumentação e kernel, excluindo criação das fixtures e `EXPLAIN`. Primeira/última página usam a mesma ordenação por ID. | Cenário | 1 000 clientes | 5 000 clientes | 10 000 clientes | | --- | ---: | ---: | ---: | | Clientes, 25, primeira página | 0,396 | 1,673 | 4,811 | | Clientes, 25, última página | 0,412 | 1,900 | 4,611 | | Clientes, 100, primeira página | 1,611 | 7,772 | Limite de 15 s atingido | | Clientes sem eventos, 25 | 0,458 | 2,142 | 6,365 | | Clientes associados, 25 | 0,447 | 2,017 | 4,249 | | Clientes com associações retiradas, 25 | 0,491 | 2,495 | 5,208 | | Pesquisa que selecciona um cliente | 0,027 | 0,088 | 0,202 | | Prestações com dependências, 25 | 0,803 | 3,189 | 9,365 | | Facturação com dependências, 25 | 1,018 | 4,189 | 11,503 | **26 de 27 cenários concluíram as três amostras: 78 pedidos medidos completos.** O outro cenário atingiu o orçamento e não tem mediana nem confirmação de resultado completo. Não houve timeout SQL nos cenários finais; o limite observado foi o do pedido instrumentado. A conclusão do ensaio não equivale a aprovação desse cenário. O novo teste terminou aprovado: **1 teste, 2 439 asserções**. Verificou totais/IDs exactos nos casos completos, primeira/última página, estados, pesquisa selectiva, ausência de dependências inacessíveis e exclusão do ruído fora do âmbito. Na escala de 1 000 houve ainda percurso integral de dez páginas de 100, com cobertura ordenada exacta sem repetição ou omissão. Nas escalas maiores não se percorreu cada página intermédia. | Resposta | Consultas por pedido completo | Tamanho observado da resposta na maior escala | | --- | ---: | ---: | | 25 clientes | 185 | cerca de 3,7 KB | | 100 clientes | 710 nas escalas concluídas | cerca de 13,8 KB em 5 000 | | Pesquisa de um cliente | 17 | 448 bytes | | 25 prestações | 435 | cerca de 7,2 KB | | 25 rascunhos de facturação | 560 | cerca de 9,2 KB | Pico PHP observado: 44/48/54 MiB por escala. Inclui kernel, PHPUnit e alocações de fixtures/instrumentação; **não é memória exclusiva da listagem nem memória PostgreSQL**. Não se infere capacidade de concorrência ou consumo por utilizador a partir destes valores. ## Gargalos sustentados pelos planos Na maior escala, 25 clientes provocam **50 chamadas `DISTINCT ON` à auditoria e 50 chamadas `MAX(id)`**: o estado é pedido na listagem e novamente na descrição do grafo. No terceiro pedido medido da primeira página, essas duas formas acumularam aproximadamente **3,87 s + 0,83 s** de tempo SQL. Nos rascunhos de facturação sobem para **125 chamadas de cada forma**, aproximadamente **9,86 s + 1,62 s** naquele pedido medido. Os planos amostrados mostram varrimento sequencial de `identity_audit` para `DISTINCT ON`, com extracção de chaves JSON, ordenação e, na maior escala, trabalhadores paralelos. `MAX(id)` pode percorrer a chave primária filtrando dezenas de milhares de eventos até encontrar o registo, ou percorrê-los sem encontrar associação. O catálogo conferido mostra apenas **identity_audit_pkey** nessa tabela; não há índice por empresa/área/registo/módulo. O filtro de pendentes também é relevante: o plano de página amostrado em 10 000 contém `Nested Loop`/`Materialize`, **75 iterações** da entrada materializada e **27 818 blocos temporários lidos**, além da leitura posterior do estado de cada registo. Não somar blocos repetidos de pais/filhos do plano como se fossem páginas distintas. A primeira e a última página de 25 ficaram próximas neste ensaio; os dados não sustentam que o OFFSET seja o gargalo dominante actual. A pesquisa selectiva é mais rápida porque descreve um registo em vez de 25. A resposta HTTP mantém-se pequena: os principais custos observados estão na leitura SQL repetida e nos filtros, não no volume de JSON devolvido. ## Tratamento técnico proposto, ainda não implementado 1. Construir o conjunto de registos/dependências autorizados de cada página e ler os eventos correspondentes **em conjunto**, reutilizando resultados dentro do pedido. Eliminar a dupla descrição do estado do registo e consultas repetidas das mesmas dependências. Não manter cache entre pedidos que possa esconder revogações ou mudanças. 2. Separar correctamente «última associação/retirada por módulo» de «última revisão de qualquer decisão». `module.record.reviewed` afecta a revisão/idempotência, mas não deve inventar associação activa ou transformar um registo sem eventos de associação num associado. 3. Ensaiar em base sintética um índice composto/de expressão compatível com tenant, empresa, área, registo, módulo e ordem de evento. Medir a consulta em conjunto e os filtros de estado; não assumir que a chave primária basta ou que um índice isolado resolve as consultas por registo. 4. Rever a consulta dos estados para evitar materializar/repercorrer o histórico repetidamente. Manter filtros e ordenação no servidor e as verificações de consulta originais antes de apresentar dependências. 5. Repetir apenas as medições e provas afectadas pela futura optimização, comparar com esta linha de base e definir critérios de latência/carga antes de homologar implantação. Validar também distribuição de histórico mais longa, várias empresas, decisões concorrentes, custos de escrita e recuperação do índice. A confirmação individual deve continuar a reler/bloquear o estado, sem confiar na fotografia da listagem. Não se propõe aumentar o tamanho de página como solução. A interface mantém 25; o limite de 100 da API não deve ser interpretado como capacidade validada em todos os volumes. Não se alterou o código funcional, um índice, um direito ou configuração de cliente nesta avaliação. ## Evidência, recuperação e limites [Medições e planos](proofs/module-association-load-20261102.json) · [resumo e hashes](proofs/module-association-load-summary-20261102.json) · [catálogo de índices](proofs/module-association-load-catalog-20261102.json). Os planos publicados omitem bindings e filtros com valores: não exportam nomes, documentos ou credenciais. As fixtures foram desfeitas por rollback. Duas consultas READ ONLY posteriores confirmaram vazias as quatro tabelas de registos, auditoria, utilizadores, concessões e pertenças da base descartável: [consulta 1](proofs/module-association-load-postcheck1-20261102.json), [consulta 2](proofs/module-association-load-postcheck2-20261102.json). **Ambas são posteriores ao ensaio**, não uma comparação com impressão digital anterior. As sequências avançam com a preparação sintética e não foram repostas. Estatísticas `ANALYZE` e disposição física/cache pertencem à base de provas; não são alterações da implantação. As escalas correram sucessivamente na mesma base, com rollback e `ANALYZE`, sem recriação ou `VACUUM` entre escalas. Cache, páginas físicas e estatísticas podem influenciar as diferenças; não se infere uma curva pura de cardinalidade. São três amostras por caso, sem percentis de cauda ou throughput. O cluster é isolado da instalação, mas partilhado com a demonstração sintética noutro banco; não se ensaiou saturação de recursos ou multiutilizador. Uma primeira tentativa de preparação falhou por parâmetro JSON sem tipo PostgreSQL (`42P18`). Corrigiu-se apenas o gerador sintético com cast explícito; a transacção falhada foi desfeita. A execução final acima não apresentou erros. Não houve correcção de produto, repetição da suite aceite, novo checkpoint, publicação ou activação de Compras. A cache bloqueada ficou intacta. ## Repetição deliberada O launcher é `scripts/module-association-load.mjs`, invocado pelo Node fixado do SGF; usa explicitamente PHP 8.4.27 e confere o host isolado. Antes de uma nova avaliação autorizada, `scripts/modular-regression-run.mjs sync` actualiza apenas a cópia de regressão. Não executar `prepare` sobre o cluster existente nem apontar PHPUnit à demonstração persistente. O relatório/JUnit ficam no directório privado dessa execução; os artefactos publicados acima contêm apenas métricas/planos sem bindings. `scripts/module-association-load-state.php` é um diagnóstico READ ONLY exclusivo da base de regressão, com guarda e identidade física; não serve para consultar dados de cliente. Nesta entrega foi usado apenas depois do ensaio. Avaliação sintética concluída como diagnóstico, **optimização e validação da capacidade de implantação pendentes**. Revisão visual permanece separada. A fila continua tecnicamente aceite no âmbito anterior e os seis registos continuam dependentes de decisões individuais futuras.