92 lines
12 KiB
Markdown
92 lines
12 KiB
Markdown
# 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.
|