CLI

Рецепты

Готовые к запуску файлы-примеры для каждого движка лежат в examples/.

Smoke-тест API перед мёрджем

# smoke.test.yaml
steps:
  - name: health
    use: std/http@v1
    with: { url: "https://staging.example.com/health" }
    check: { status: 200, duration_ms_lt: 300 }
# smoke.config.yaml
vus: 5
duration: 30s
perfscale run -f smoke.test.yaml -c smoke.config.yaml

Логин → авторизованный запрос (цепочка шагов)

steps:
  - name: login
    use: std/http@v1
    with:
      method: POST
      url: https://api.example.com/login
      body: { user: demo, password: demo }
    check: { status: 200 }
    outputs: login

  - name: profile
    use: std/http@v1
    with:
      url: https://api.example.com/me
      headers:
        authorization: "Bearer ${{ login.body }}"
    check: { status: 200, body_contains: "demo" }

  - use: std/sleep@v1
    with: { ms: 500 }

Нагрузочный тест WebSocket-эндпоинта

examples/websocket.test.yaml показывает оба стиля — живое соединение, удерживаемое между шагами, и разовую сессию. В живом стиле соединение адресуется по id, который вернул std/ws-connect@v1:

steps:
  - name: open feed
    use: std/ws-connect@v1
    with: { url: ws://127.0.0.1:9222 }
    outputs: feed

  - name: subscribe
    use: std/ws-send@v1
    with:
      id: "${{ feed.id }}"
      send: '{"op":"subscribe","id":"sub-${seq}"}'

  - name: await echo
    use: std/ws-recv@v1
    with: { id: "${{ feed.id }}", until_contains: "sub-1", timeout: 5000 }
    check: { message_contains: "subscribe" }

  - name: hang up
    use: std/ws-close@v1
    with: { id: "${{ feed.id }}" }

Запускайте против любого echo-сервера (npx wscat --listen 9222). Концепции — в руководстве по WebSocket, а полный справочник параметров и метрик — в Actions → WebSocket.

Нагрузочный тест gRPC-эндпоинта

examples/grpc.test.yaml выполняет унарные вызовы и двунаправленный (bidi) стрим через один живой канал. Без кодогенерации по protobuf: схема загружается во время выполнения через server reflection (или base64 descriptor_set), а полезные данные — обычный JSON, преобразуемый по правилам protobuf-JSON. Канал и его схема оплачиваются один раз за итерацию — вызовы и стримы идут по тому же HTTP/2-соединению:

steps:
  - name: open channel
    use: std/grpc-connect@v1
    with:
      url: grpc://127.0.0.1:50051
      reflection: true
    outputs: conn

  - name: unary echo
    use: std/grpc-call@v1
    with:
      id: "${{ conn.id }}"
      method: "perfscale.test.v1.Echo/Unary"
      payload: { message: "ping-${seq}" }
    check:
      duration_ms_lt: 250

  - name: open bidi stream
    use: std/grpc-stream-open@v1
    with:
      id: "${{ conn.id }}"
      method: "perfscale.test.v1.Echo/Bidi"
    outputs: stream

  - name: send events
    use: std/grpc-stream-send@v1
    with:
      id: "${{ stream.id }}"
      payload: { message: "evt-${seq}" }
      repeat: 5
      interval_ms: 20

  - name: await echoes
    use: std/grpc-stream-recv@v1
    with:
      id: "${{ stream.id }}"
      until_contains: "evt-5"
      timeout: 5000
    check:
      messages_count_gte: 5

  - name: close stream
    use: std/grpc-stream-close@v1
    with: { id: "${{ stream.id }}" }

Запускайте против встроенного echo-сервера (cargo run -p perfscale-core --example grpc_echo_server). Для разовых проб есть также one-shot-вариант std/grpc@v1 (подключение → схема → вызов → закрытие одним шагом). Концепции — в руководстве по gRPC, а полный справочник параметров и метрик — в Actions → gRPC.

Нагрузочный тест базы данных

examples/db-sqlite.test.yaml работает с in-memory SQLite — сервер не нужен. PostgreSQL и MySQL/MariaDB работают так же; меняются только driver и dsn:

steps:
  - name: open db
    use: std/db-connect@v1
    with:
      driver: postgres
      dsn: "${{ vars.db_dsn }}"        # keep the password out of the file
    outputs: db

  - name: write
    use: std/db-query@v1
    with:
      id: "${{ db.id }}"
      # SQL is never interpolated — values move through bound params.
      query: INSERT INTO hits (path, status) VALUES ($1, $2)
      params: ["/api/checkout", 200]

  - name: hang up
    use: std/db-close@v1
    with: { id: "${{ db.id }}" }

Транзакции (std/db-tx-begin@v1 / commit / rollback) обрамляют группы запросов на постоянном соединении; mode: per-query, наоборот, подключается заново для каждого запроса (измеряя подключение + запрос). Метрики попадают в db_query_duration, db_rows и классифицированные db_errors. Полный справочник параметров и метрик — в Actions → Database.

Переиспользование существующего скрипта k6

perfscale run --k6 load-tests/checkout.js

Конфигурация нагрузки (VU, стадии, пороги) остаётся в блоке options скрипта — perfscale стримит вывод и сохраняет семантику кодов выхода k6.

Переиспользование существующего locustfile

perfscale run --locust locustfile.py --host https://target.example.com -c load.config.yaml

vus/duration из конфига мапятся на --users/--spawn-rate/--run-time. После прогона CSV-статистика locust преобразуется в тот же summary-блок, который выводят остальные движки.

Сбор результатов с нескольких терминалов / машин

# terminal 1 — collector
perfscale serve --port 7999

# terminals 2..N — each run reports in
perfscale run -f test.yaml -c config.yaml --report http://collector-host:7999

Или зашейте коллектор в конфиг, чтобы флаг не был нужен:

# config.yaml
vus: 10
duration: 5m
report:
  url: http://collector-host:7999

CI (GitHub Actions)

Perfscale/github-action устанавливает perfscale, запускает тест, выводит таблицу метрик в сводку джоба и записывает машиночитаемую JSON-сводку:

- uses: Perfscale/github-action@v1
  id: loadtest
  with:
    file: smoke.test.yaml
    config: smoke.config.yaml

- name: Gate on error rate and p95
  run: |
    jq -e '.summary.error_rate < 0.01 and .summary.p95_ms < 500' \
      "${{ steps.loadtest.outputs.summary-json }}"

Без action тот же гейт работает из любого CI через --summary-export:

perfscale run -f smoke.test.yaml -c smoke.config.yaml --summary-export result.json
jq -e '.summary.error_rate < 0.01' result.json

Сам прогон завершается с кодом 0, даже когда проверки падают (см. семантику кодов выхода) — если падения должны ломать сборку, гейтите по экспортированной сводке, как показано выше.