Files
SGF/docs/MODULE_ASSOCIATION_LOAD_REVIEW.md
2026-11-02 20:29:34 +02:00

12 KiB

Avaliação sintética de carga e paginação da fila

Seguimento: optimização local, comparação final A/B/C e limitações. 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 340f3bb5fa, 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 · resumo e hashes · catálogo de índices. 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, consulta 2. 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.