Skip to content

Testes de Carga

O example_server inclui uma suíte de testes de carga com k6 em packages/example_server/k6/. Os testes cobrem desde latência de baseline até geração concorrente de relatórios e compartilhamento público.

Pré-requisitos

bash
brew install k6

Os testes não exigem banco de dados externo, email real nem API externa. Tudo roda localmente.

Início rápido

bash
cd packages/example_server

# 1. Popula o SQLite local (5.000 vendas, 100 produtos, 500 clientes)
fvm dart run bin/seed_sqlite.dart /tmp/k6_test.db

# 2. Inicia o servidor sem email real
RESEND_API_TOKEN="" RESEND_FROM_EMAIL="" \
SQLITE_DB_PATH="/tmp/k6_test.db" \
fvm dart run bin/example_server.dart

# 3. Em outro terminal, rode todos os testes
./k6/run.sh

Rodar um teste específico

bash
./k6/run.sh 01        # baseline
./k6/run.sh danfe     # DANFE isolado
./k6/run.sh sqlite    # relatórios SQLite
./k6/run.sh pokemon   # Duelo Pokémon (mock)

# Ou diretamente com k6:
k6 run --env BASE_URL=http://localhost:8090 k6/scripts/test_danfe.js
k6 run --env MAX_VUS=20 k6/scripts/03_generate.js

Scripts disponíveis

ScriptO que testaVUsDuração
01_baseline.js/health + /info — latência HTTP puraaté 100~75s
02_reports.jsListagem e detalhe de relatórios (in-memory)30~60s
03_generate.jsGeração mista PDF/HTML/CSV (só relatórios sem deps externas)~90s
04_sharing.jsCriação de shares + acesso público + fail2ban3+10~70s
05_auth.jsTokens, refresh e detecção de reuso20~60s
test_danfe.jsDANFE (NF-e) — relatório com múltiplas bandas, barcode e cálculos, sem datasource30¹~3m20s
test_pokemon_duel.jsDuelo Pokémon com mock /demo/pokeapi + dart_eval20–120¹~2m20s
test_sqlite.jsRelatórios SQLite: vendas agrupadas e catálogo de produtos10¹~4m20s
saturation.jsRampa agressiva 5→60 VUs — encontra ponto de rupturaaté 60¹~3m

¹ Ajustável com --env MAX_VUS=N

05_auth.js — autenticação

Requer o servidor iniciado com AUTH_SECRET:

bash
AUTH_SECRET=test-k6-secret fvm dart run bin/example_server.dart

k6 run --env AUTH_SECRET=test-k6-secret k6/scripts/05_auth.js

Dependências mockadas

Todos os testes rodam sem serviços externos:

DependênciaSubstituição
Email (Resend/MailerSend)LogEmailProvider — emails vão para stdout
PokeAPI (pokeapi.co)/demo/pokeapi/pokemon/:name — mock local
Supabase / PostgreSQLSQLite local em /tmp/k6_test.db
Assinatura de relatóriosDesabilitada (sem ENCRYPTION_KEY)

Para garantir que o .env não ative email real, o run.sh seta RESEND_API_TOKEN="" via Platform.environment, que tem prioridade sobre o arquivo .env.

Resultados de referência — Mac Air M1 8 GB

Medições realizadas com servidor e k6 no mesmo host (macOS 15, Dart 3.x, Relic).

01 — Baseline /health + /info

MétricaValor
Throughput1.163 req/s
p(95) /health3,4 ms
p(95) /info3,3 ms
Erros0%
Spike 100 VUsestável, sem degradação

02 — Listagem de relatórios

MétricaValor
Throughput486 req/s (30 VUs)
p(95) list3,6 ms
p(95) detail4,5 ms
Erros0%

03 — Geração mista (PDF/HTML/CSV, 8 VUs)

O setup() descarta automaticamente relatórios com dependências externas e testa apenas os que respondem com 200. Resultado típico: ~19/28 relatórios aprovados.

MétricaPDFHTMLCSV
p(95)2,0 s150 ms1,9 s
p(99)< 10 s
Erros de geração0%

Throughput de geração: ~3,5 iter/s com 8 VUs. O gargalo é o IsolateEngine (um worker Dart por request concorrente).

04 — Sharing (3 criadores + 10 visitantes)

MétricaValor
Shares criados~87 em 60s
p(95) criação1,4 s (envolve geração HTML)
p(95) acesso /s/:slug2,8 ms (bytes estáticos)
Fail2ban ativadosim — 330 hits (comportamento esperado em localhost)
Erros de criação0%

O fail2ban dispara porque todos os VUs partilham o mesmo IP (localhost). Em produção com IPs distribuídos, o comportamento é diferente. O contador fail2ban_triggered confirma que a proteção funciona.

test_danfe — DANFE NF-e (30 VUs)

O DANFE usa dados inline sem datasource externo. O relatório inclui múltiplas bandas, barcode, layout posicionado e cálculos de totais.

MétricaPDFHTMLCSV
p(95)41 ms42 ms41 ms
Throughput140 req/s
Erros0%

test_pokemon_duel — Duelo Pokémon com dart_eval (saturação)

O Duelo Pokémon exercita o pipeline completo: REST datasource (mock), script Dart de transformação via dart_eval, layout com tabelas e gráficos.

Resultados por VUs — saturação progressiva

VUsThroughputPDF p(95)HTML p(95)Erros
2020,0 req/s1,30 s110 ms0%
4023,4 req/s2,83 s510 ms0%
6024,8 req/s4,34 s880 ms0% ← throughput plateau
8025,0 req/s6,04 s1,32 s0%
10024,7 req/s7,93 s1,70 s0%
12024,8 req/s9,55 s ⚠️1,83 s0% ← threshold violado

Análise

  • Throughput teto: ~25 req/s a partir de 60 VUs — IsolateEngine saturou em CPU, fila cresce mas não recusa conexões
  • Zero erros até 120 VUs: o servidor enfileira as requisições e as serve até o timeout de 30s; a degradação é graceful
  • HTML ~5–10× mais rápido que PDF em todos os níveis de carga — a diferença é o layout de página
  • Ponto de ruptura do threshold: ~110–120 VUs (PDF p95 cruza 8 s)
  • dart_eval não é o gargalo dominante — a renderização PDF consome mais CPU do que a execução do script Dart

test_sqlite — Relatórios SQLite (10 VUs)

Dois cenários sequenciais: vendas agrupadas por região (5.000 linhas, GROUP BY) e catálogo de produtos (100 itens).

MétricaValor
Throughput65.4 req/s
p(50) (mediana)14.5 ms
p(95)29.2 ms
Erros0% (17.013 requests)

O SQLite local elimina variabilidade de rede. A latência reflete apenas renderização + I/O de disco local. Esses números foram obtidos com o RateLimiterStore assíncrono e o servidor compilado via AOT.

Nota sobre AOT Ao executar com 10+ VUs em relatórios com tabelas e gráficos, recomenda-se compilar o servidor nativamente (dart compile exe bin/example_server.dart). Em modo JIT (dart run), sob taxa elevada de exceptions internas no layout (ex: package:pdf), o OSR/Unwinder da VM Dart pode gerar crashes de segmentação no M1.

Interpretação dos resultados

Gargalos identificados

CenárioGargaloEvidência
Geração PDF/HTMLIsolateEngine (CPU)~3,5 iter/s com 8 VUs, sem erros
Duelo PokémonRenderização PDF (não dart_eval)throughput teto ~25 req/s a partir de 60 VUs
Sharing (criação)Geração do output do sharep(95) = 1,4s
BaselineEvent loop purop(95) = 3,4ms em 100 VUs

Saturação — comportamento sob carga extrema

TesteComportamento ao saturarPonto de ruptura
Geração mista (60 VUs)Timeouts (45 s) → 94,7% de erros~10–15 VUs para PDF pesado
Duelo Pokémon (120 VUs)Fila cresce, latência sobe, zero erros~60 VUs (throughput plateau), threshold p95 violado em ~120 VUs

O IsolateEngine não recusa conexões — ele enfileira. O comportamento de degradação depende do timeout do cliente:

  • Timeout curto (< latência da fila) → erros visíveis
  • Timeout generoso (30 s+) → zero erros, latência alta até ~120 VUs concorrentes

Fail2ban em testes locais

O fail2ban (15 req/min por IP em /s/) sempre dispara em testes locais porque todos os VUs usam 127.0.0.1. Em produção, com IPs de clientes distintos, o comportamento é correto. Para desabilitar nos testes, não inicialize o BannedIpStore no buildApp() ou aumente o threshold via configuração.

Escalabilidade

O Dart executa em um único isolate por default. O IsolateEngine distribui geração entre um pool de workers (IsolatedObject do Relic), utilizando os núcleos disponíveis para escalabilidade vertical.

Para escalabilidade horizontal (múltiplas instâncias atrás de um load balancer), o RateLimiterStore tem suporte a backends externos (PostgreSQL e Redis), permitindo que instâncias paralelas compartilhem estado de rate limiting, sessões OTP e proteção contra ataques.

Comportamento sob saturação de I/O de rede

Em ciclos ininterruptos do k6/reproduce.sh com o backend AOT, a ~65 req/s gerando relatórios pesados, o limite de saturação foi atingido no subsistema de I/O de rede (Unix Sockets), não em CPU ou memória. Com clientes cancelando conexões forçadamente sob alta concorrência, o kernel do macOS colocou os processos do servidor em Uninterruptible Sleep, tornando-os não responsivos a sinais externos (kill -9). Esse comportamento é do sistema operacional, não do servidor em si.

Adicionando novos testes

javascript
// k6/scripts/meu_teste.js
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    ramp: {
      executor: 'ramping-vus',
      stages: [
        { duration: '15s', target: 10 },
        { duration: '60s', target: 10 },
        { duration: '15s', target: 0 },
      ],
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<2000'],
    http_req_failed:   ['rate<0.01'],
  },
};

const BASE = __ENV.BASE_URL || 'http://localhost:8090';

export function setup() {
  // Descobre IDs dinâmicamente — nunca hardcode
  const res = http.get(`${BASE}/api/v1/reports`);
  const reports = res.json();
  return { id: reports[0].id };
}

export default function ({ id }) {
  const res = http.post(
    `${BASE}/api/v1/reports/${id}/generate?format=html`,
    '{}',
    { headers: { 'Content-Type': 'application/json' } },
  );
  check(res, { 'status 200': (r) => r.status === 200 });
}

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro